commit
01fe8d0dbc
14 changed files with 446 additions and 66 deletions
|
@ -1,5 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
## 1.8.0 2016-05-13
|
||||
|
||||
* Show details about subscribers who clicked on a specific link
|
||||
|
||||
## 1.7.0 2016-05-11
|
||||
|
||||
* Updated API, added new option **REQUIRE_CONFIRMATION** for subscriptions to send confirmation email before subscribing
|
||||
|
|
|
@ -127,6 +127,73 @@ module.exports.filter = (request, parent, callback) => {
|
|||
}
|
||||
};
|
||||
|
||||
module.exports.filterClickedSubscribers = (campaign, linkId, request, columns, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT COUNT(`subscription__' + campaign.list + '`.`id`) AS total FROM `subscription__' + campaign.list + '` JOIN `campaign_tracker__' + campaign.id + '` ON `campaign_tracker__' + campaign.id + '`.`list`=? AND `campaign_tracker__' + campaign.id + '`.`subscriber`=`subscription__' + campaign.list + '`.`id` AND `campaign_tracker__' + campaign.id + '`.`link`=?';
|
||||
let values = [campaign.list, linkId];
|
||||
|
||||
connection.query(query, values, (err, total) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
total = total && total[0] && total[0].total || 0;
|
||||
|
||||
let ordering = [];
|
||||
|
||||
if (request.order && request.order.length) {
|
||||
|
||||
request.order.forEach(order => {
|
||||
let orderField = columns[Number(order.column)];
|
||||
let orderDirection = (order.dir || '').toString().toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
||||
if (orderField) {
|
||||
ordering.push('`' + orderField + '` ' + orderDirection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!ordering.length) {
|
||||
ordering.push('`email` ASC');
|
||||
}
|
||||
|
||||
let args = [Number(request.length) || 50, Number(request.start) || 0];
|
||||
|
||||
if (request.search && request.search.value) {
|
||||
query = 'SELECT SQL_CALC_FOUND_ROWS * FROM `subscription__' + campaign.list + '` JOIN `campaign_tracker__' + campaign.id + '` ON `campaign_tracker__' + campaign.id + '`.`list`=? AND `campaign_tracker__' + campaign.id + '`.`subscriber`=`subscription__' + campaign.list + '`.`id` AND `campaign_tracker__' + campaign.id + '`.`link`=? WHERE email LIKE ? OR first_name LIKE ? OR last_name LIKE ? ORDER BY ' + ordering.join(', ') + ' LIMIT ? OFFSET ?';
|
||||
|
||||
let searchVal = '%' + request.search.value.replace(/\\/g, '\\\\').replace(/([%_])/g, '\\$1') + '%';
|
||||
args = values.concat([searchVal, searchVal, searchVal]).concat(args);
|
||||
} else {
|
||||
query = 'SELECT SQL_CALC_FOUND_ROWS * FROM `subscription__' + campaign.list + '` JOIN `campaign_tracker__' + campaign.id + '` ON `campaign_tracker__' + campaign.id + '`.`list`=? AND `campaign_tracker__' + campaign.id + '`.`subscriber`=`subscription__' + campaign.list + '`.`id` AND `campaign_tracker__' + campaign.id + '`.`link`=? ORDER BY ' + ordering.join(', ') + ' LIMIT ? OFFSET ?';
|
||||
args = values.concat(args);
|
||||
}
|
||||
|
||||
connection.query(query, args, (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT FOUND_ROWS() AS total', (err, filteredTotal) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let subscriptions = rows.map(row => tools.convertKeys(row));
|
||||
|
||||
filteredTotal = filteredTotal && filteredTotal[0] && filteredTotal[0].total || 0;
|
||||
return callback(null, subscriptions, total, filteredTotal);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports.getByCid = (cid, callback) => {
|
||||
cid = (cid || '').toString().trim();
|
||||
if (!cid) {
|
||||
|
@ -226,8 +293,14 @@ module.exports.get = (id, withSegment, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
module.exports.getLinks = (id, callback) => {
|
||||
module.exports.getLinks = (id, linkId, callback) => {
|
||||
if (!callback && typeof linkId === 'function') {
|
||||
callback = linkId;
|
||||
linkId = false;
|
||||
}
|
||||
|
||||
id = Number(id) || 0;
|
||||
linkId = Number(linkId) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Campaign ID'));
|
||||
|
@ -238,8 +311,18 @@ module.exports.getLinks = (id, callback) => {
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT `id`, `url`, `clicks` FROM links WHERE `campaign`=? LIMIT 1000';
|
||||
connection.query(query, [id], (err, rows) => {
|
||||
let query;
|
||||
let values;
|
||||
|
||||
if (!linkId) {
|
||||
query = 'SELECT `id`, `url`, `clicks` FROM links WHERE `campaign`=? LIMIT 1000';
|
||||
values = [id];
|
||||
} else {
|
||||
query = 'SELECT `id`, `url`, `clicks` FROM links WHERE `id`=? AND `campaign`=? LIMIT 1';
|
||||
values = [linkId, id];
|
||||
}
|
||||
|
||||
connection.query(query, values, (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
|
|
@ -429,7 +429,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
};
|
||||
}).filter(subField => subField)
|
||||
};
|
||||
item.value = item.options.filter(subField => showAll || subField.visible && subField.value).map(subField => subField.name).join(', ');
|
||||
item.value = item.options.filter(subField => (showAll || subField.visible) && subField.value).map(subField => subField.name).join(', ');
|
||||
item.mergeValue = item.value || field.defaultValue;
|
||||
row.push(item);
|
||||
break;
|
||||
|
|
|
@ -178,7 +178,7 @@ function formatMessage(serviceUrl, campaign, list, subscription, message, filter
|
|||
return message.replace(/\[([a-z0-9_]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
|
||||
identifier = identifier.toUpperCase();
|
||||
let value = (getValue(identifier) || fallback || '').trim();
|
||||
return value ? filter(value) : match;
|
||||
return value ? filter(value) : '';
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"schemaVersion": 9
|
||||
"schemaVersion": 10
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "mailtrain",
|
||||
"private": true,
|
||||
"version": "1.7.0",
|
||||
"version": "1.8.0",
|
||||
"description": "Self hosted email newsletter app",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let lists = require('../lib/models/lists');
|
||||
let fields = require('../lib/models/fields');
|
||||
let templates = require('../lib/models/templates');
|
||||
let campaigns = require('../lib/models/campaigns');
|
||||
let settings = require('../lib/models/settings');
|
||||
|
@ -157,7 +158,56 @@ router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
|
|||
view = 'campaigns/edit';
|
||||
}
|
||||
|
||||
res.render(view, campaign);
|
||||
lists.get(campaign.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
if (!list) {
|
||||
req.flash('danger', 'Selected list does not exist');
|
||||
res.render(view, campaign);
|
||||
return;
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
let mergeTags = [
|
||||
// indent
|
||||
{
|
||||
key: 'LINK_UNSUBSCRIBE',
|
||||
value: 'URL that points to the preferences page of the subscriber'
|
||||
}, {
|
||||
key: 'LINK_PREFERENCES',
|
||||
value: 'URL that points to the unsubscribe page'
|
||||
}, {
|
||||
key: 'LINK_BROWSER',
|
||||
value: 'URL to preview the message in a browser'
|
||||
}, {
|
||||
key: 'FIRST_NAME',
|
||||
value: 'First name'
|
||||
}, {
|
||||
key: 'LAST_NAME',
|
||||
value: 'Last name'
|
||||
}, {
|
||||
key: 'FULL_NAME',
|
||||
value: 'Full name (first and last name combined)'
|
||||
}
|
||||
];
|
||||
|
||||
fieldList.forEach(field => {
|
||||
mergeTags.push({
|
||||
key: field.key,
|
||||
value: field.name
|
||||
});
|
||||
});
|
||||
|
||||
campaign.mergeTags = mergeTags;
|
||||
res.render(view, campaign);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -268,6 +318,9 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => {
|
|||
|
||||
campaign.isScheduled = campaign.scheduled && campaign.scheduled > new Date();
|
||||
|
||||
// show only messages that weren't bounced as delivered
|
||||
campaign.delivered = campaign.delivered - campaign.bounced;
|
||||
|
||||
campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 100) : 0;
|
||||
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
|
||||
|
||||
|
@ -286,7 +339,8 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => {
|
|||
}
|
||||
return link;
|
||||
});
|
||||
campaign.showOverview = true;
|
||||
campaign.showOverview = !req.query.tab || req.query.tab === 'overview';
|
||||
campaign.showLinks = req.query.tab === 'links';
|
||||
res.render('campaigns/view', campaign);
|
||||
});
|
||||
|
||||
|
@ -294,6 +348,86 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
router.get('/clicked/:id/:linkId', passport.csrfProtection, (req, res) => {
|
||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || 'Could not find campaign with specified ID');
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
lists.get(campaign.list, (err, list) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
campaign.csrfToken = req.csrfToken();
|
||||
campaign.list = list;
|
||||
|
||||
// show only messages that weren't bounced as delivered
|
||||
campaign.delivered = campaign.delivered - campaign.bounced;
|
||||
|
||||
campaigns.getLinks(campaign.id, req.params.linkId, (err, links) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
let index = 0;
|
||||
campaign.link = (links || []).map(link => {
|
||||
link.index = ++index;
|
||||
link.totalPercentage = campaign.delivered ? Math.round(((link.clicks / campaign.delivered) * 100) * 1000) / 1000 : 0;
|
||||
link.relPercentage = campaign.clicks ? Math.round(((link.clicks / campaign.clicks) * 100) * 1000) / 1000 : 0;
|
||||
link.short = link.url.replace(/^https?:\/\/(www.)?/i, '');
|
||||
if (link.short > 63) {
|
||||
link.short = link.short.substr(0, 60) + '…';
|
||||
}
|
||||
return link;
|
||||
}).shift();
|
||||
campaign.showOverview = true;
|
||||
res.render('campaigns/clicked', campaign);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/clicked/ajax/:id/:linkId', (req, res) => {
|
||||
let linkId = Number(req.params.linkId) || 0;
|
||||
|
||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
return res.json({
|
||||
error: err && err.message || err || 'Campaign not found',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '`.`created', 'count'];
|
||||
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
(Number(req.body.start) || 0) + 1 + i,
|
||||
htmlescape(row.email || ''),
|
||||
htmlescape(row.firstName || ''),
|
||||
htmlescape(row.lastName || ''),
|
||||
row.created && row.created.toISOString ? '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>' : 'N/A',
|
||||
row.count,
|
||||
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + campaign.list + '/edit/' + row.cid + '">Edit</a>'
|
||||
])
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
|
|
|
@ -151,7 +151,7 @@ function formatMessage(message, callback) {
|
|||
if (field.options) {
|
||||
field.options.forEach(subField => {
|
||||
if (subField.mergeTag) {
|
||||
message.subscription.mergeTags[subField.mergeTag] = subField.mergeValue || '';
|
||||
message.subscription.mergeTags[subField.mergeTag] = subField.value && subField.mergeValue || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,7 +23,9 @@ CREATE TABLE `campaign_tracker` (
|
|||
`ip` varchar(100) CHARACTER SET ascii DEFAULT NULL,
|
||||
`country` varchar(2) CHARACTER SET ascii DEFAULT NULL,
|
||||
`count` int(11) unsigned NOT NULL DEFAULT '1',
|
||||
PRIMARY KEY (`list`,`subscriber`,`link`)
|
||||
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`list`,`subscriber`,`link`),
|
||||
KEY `created_index` (`created`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE `campaigns` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
|
@ -181,7 +183,7 @@ CREATE TABLE `settings` (
|
|||
`value` text NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `key` (`key`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4;
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','465');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','TLS');
|
||||
|
@ -198,7 +200,7 @@ INSERT INTO `settings` (`id`, `key`, `value`) VALUES (13,'default_from','My Awes
|
|||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','http://localhost:3000/');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','8');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','10');
|
||||
CREATE TABLE `subscription` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||
|
@ -242,6 +244,7 @@ CREATE TABLE `users` (
|
|||
`username` varchar(255) NOT NULL DEFAULT '',
|
||||
`password` varchar(255) NOT NULL DEFAULT '',
|
||||
`email` varchar(255) CHARACTER SET utf8 DEFAULT NULL,
|
||||
`access_token` varchar(40) DEFAULT NULL,
|
||||
`reset_token` varchar(255) CHARACTER SET ascii DEFAULT NULL,
|
||||
`reset_expire` timestamp NULL DEFAULT NULL,
|
||||
`created` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
@ -249,9 +252,10 @@ CREATE TABLE `users` (
|
|||
UNIQUE KEY `email` (`email`),
|
||||
KEY `username` (`username`(191)),
|
||||
KEY `reset` (`reset_token`),
|
||||
KEY `check_reset` (`username`(191),`reset_token`,`reset_expire`)
|
||||
KEY `check_reset` (`username`(191),`reset_token`,`reset_expire`),
|
||||
KEY `token_index` (`access_token`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
|
||||
INSERT INTO `users` (`id`, `username`, `password`, `email`, `reset_token`, `reset_expire`, `created`) VALUES (1,'admin','$2a$10$mzKU71G62evnGB2PvQA4k..Wf9jASk.c7a8zRMHh6qQVjYJ2r/g/K','admin@example.com',NULL,NULL,NOW());
|
||||
INSERT INTO `users` (`id`, `username`, `password`, `email`, `access_token`, `reset_token`, `reset_expire`, `created`) VALUES (1,'admin','$2a$10$mzKU71G62evnGB2PvQA4k..Wf9jASk.c7a8zRMHh6qQVjYJ2r/g/K','admin@example.com',NULL,NULL,NULL,NOW());
|
||||
|
||||
SET UNIQUE_CHECKS=1;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
|
17
setup/sql/upgrade-00010.sql
Normal file
17
setup/sql/upgrade-00010.sql
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Header section
|
||||
# Define incrementing schema version number
|
||||
SET @schema_version = '10';
|
||||
|
||||
-- {{#each tables.campaign_tracker}}
|
||||
|
||||
# Adds new column 'created' to campaign tracker table
|
||||
# Indicates when a subscriber first clicked a link or opened the message
|
||||
ALTER TABLE `{{this}}` ADD COLUMN `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `count`;
|
||||
CREATE INDEX created_index ON `{{this}}` (`created`);
|
||||
|
||||
-- {{/each}}
|
||||
|
||||
# Footer section
|
||||
LOCK TABLES `settings` WRITE;
|
||||
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
|
||||
UNLOCK TABLES;
|
86
views/campaigns/clicked.hbs
Normal file
86
views/campaigns/clicked.hbs
Normal file
|
@ -0,0 +1,86 @@
|
|||
<ol class="breadcrumb">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/campaigns">Campaigns</a></li>
|
||||
{{#if parent}}
|
||||
<li><a href="/campaigns/view/{{parent.id}}">{{parent.name}}</a></li>
|
||||
{{/if}}
|
||||
<li><a href="/campaigns/view/{{id}}">{{name}}</a></li>
|
||||
<li class="active">Link info</li>
|
||||
</ol>
|
||||
|
||||
<h2><span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> {{name}} <small>Link info</small> <a class="btn btn-default btn-xs" href="/campaigns/view/{{id}}?tab=links" role="button"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> View campaign</a></h2>
|
||||
|
||||
<hr>
|
||||
|
||||
{{#if description}}
|
||||
<div class="well well-sm">{{{description}}}</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<th>
|
||||
URL
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
Clicks
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
% of clicks
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
% of messages
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="success">
|
||||
<td>
|
||||
<a href="{{link.url}}">{{link.short}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{link.clicks}}
|
||||
</td>
|
||||
<td>
|
||||
{{link.relPercentage}}
|
||||
</td>
|
||||
<td>
|
||||
{{link.totalPercentage}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="panel panel-info">
|
||||
<!-- Default panel contents -->
|
||||
<div class="panel-heading">Subscribers who clicked on this link:</div>
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table data-topic-url="/campaigns/clicked" data-topic-id="{{id}}/{{link.id}}" data-sort-column="1" data-sort-order="asc" class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1,1,1,0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-1">
|
||||
#
|
||||
</th>
|
||||
<th>
|
||||
Address
|
||||
</th>
|
||||
<th>
|
||||
First Name
|
||||
</th>
|
||||
<th>
|
||||
Last Name
|
||||
</th>
|
||||
<th>
|
||||
First click
|
||||
</th>
|
||||
<th>
|
||||
Click count
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -71,6 +71,52 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<a class="btn btn-default" role="button" data-toggle="collapse" href="#mergeReference" aria-expanded="false" aria-controls="mergeReference">Merge tag reference</a>
|
||||
<div class="collapse" id="mergeReference">
|
||||
<p>
|
||||
Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: <code>[TAG_NAME]</code> or <code>[TAG_NAME/fallback]</code> where <code>fallback</code> is an optional text value
|
||||
used when <code>TAG_NAME</code> is empty.
|
||||
</p>
|
||||
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Merge tag
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
[RSS_ENTRY]
|
||||
</th>
|
||||
<td>
|
||||
content from an RSS entry
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{#each mergeTags}}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
[{{key}}]
|
||||
</th>
|
||||
<td>
|
||||
{{value}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="template" class="col-sm-2 control-label">RSS Feed Url</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
|
@ -129,29 +129,30 @@
|
|||
text value used when <code>TAG_NAME</code> is empty.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<code>[FIRST_NAME]</code> – first name of the subscriber
|
||||
</li>
|
||||
<li>
|
||||
<code>[LAST_NAME]</code> – last name of the subscriber
|
||||
</li>
|
||||
<li>
|
||||
<code>[FULL_NAME]</code> – first and last names of the subscriber joined
|
||||
</li>
|
||||
<li>
|
||||
<code>[LINK_UNSUBSCRIBE]</code> – URL that points to the preferences page of the subscriber
|
||||
</li>
|
||||
<li>
|
||||
<code>[LINK_PREFERENCES]</code> – URL that points to the unsubscribe page
|
||||
</li>
|
||||
<li>
|
||||
<code>[LINK_BROWSER]</code> – URL to preview the message in a browser
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
In addition to that any custom field can have its own merge tag.
|
||||
</p>
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Merge tag
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each mergeTags}}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
[{{key}}]
|
||||
</th>
|
||||
<td>
|
||||
{{value}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -294,6 +294,9 @@
|
|||
<a href="{{url}}">{{short}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="pull-right">
|
||||
<a href="/campaigns/clicked/{{../id}}/{{id}}" title="List subscribers who clicked this link"><span class="glyphicon glyphicon-circle-arrow-right" aria-hidden="true"></span></a>
|
||||
</div>
|
||||
{{clicks}}
|
||||
</td>
|
||||
<td>
|
||||
|
@ -339,32 +342,34 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if isRss}}
|
||||
<div class="table-responsive">
|
||||
<div class="well text-info">
|
||||
If a new entry is found from campaign feed a new subcampaign is created of that entry and it will be listed here
|
||||
</div>
|
||||
<table data-topic-url="/campaigns" data-sort-column="4" data-sort-order="desc" class="table table-bordered table-hover data-table-ajax display nowrap" data-topic-args="parent={{id}}" width="100%" data-row-sort="0,1,0,1,1,0">
|
||||
<thead>
|
||||
<th class="col-md-1">
|
||||
#
|
||||
</th>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
<th>
|
||||
Status
|
||||
</th>
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
|
||||
</th>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{#if isRss}}
|
||||
<div class="table-responsive">
|
||||
<div class="well text-info">
|
||||
If a new entry is found from campaign feed a new subcampaign is created of that entry and it will be listed here
|
||||
</div>
|
||||
{{/if}}
|
||||
<table data-topic-url="/campaigns" data-sort-column="4" data-sort-order="desc" class="table table-bordered table-hover data-table-ajax display nowrap" data-topic-args="parent={{id}}" width="100%" data-row-sort="0,1,0,1,1,0">
|
||||
<thead>
|
||||
<th class="col-md-1">
|
||||
#
|
||||
</th>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
<th>
|
||||
Status
|
||||
</th>
|
||||
<th>
|
||||
Created
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
|
||||
</th>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
Loading…
Reference in a new issue