v1.11.0
This commit is contained in:
parent
3fa0e109af
commit
9bd6db2624
13 changed files with 201 additions and 51 deletions
|
@ -1,5 +1,10 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.11.0 2016-05-31
|
||||||
|
|
||||||
|
* Retry transactional mail if failed with soft error (4xx)
|
||||||
|
* New feature to preview campaigns using selected test users
|
||||||
|
|
||||||
## 1.10.1 2016-05-26
|
## 1.10.1 2016-05-26
|
||||||
|
|
||||||
* Fix a bug with SMTP transport instance where campaign sending stalled until server was restarted
|
* Fix a bug with SMTP transport instance where campaign sending stalled until server was restarted
|
||||||
|
|
|
@ -72,7 +72,24 @@ module.exports.sendMail = (mail, template, callback) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.transport.sendMail(mail, callback);
|
let tryCount = 0;
|
||||||
|
let trySend = () => {
|
||||||
|
tryCount++;
|
||||||
|
|
||||||
|
module.exports.transport.sendMail(mail, (err, info) => {
|
||||||
|
if (err) {
|
||||||
|
log.error('Mail', err.stack);
|
||||||
|
if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
|
||||||
|
// temporary error, try again
|
||||||
|
log.verbose('Mail', 'Retrying after %s sec. ...', tryCount);
|
||||||
|
return setTimeout(trySend, tryCount * 1000);
|
||||||
|
}
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
return callback(null, info);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
setImmediate(trySend);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,6 +40,44 @@ module.exports.list = (listId, start, limit, callback) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.listTestUsers = (listId, callback) => {
|
||||||
|
listId = Number(listId) || 0;
|
||||||
|
|
||||||
|
if (listId < 1) {
|
||||||
|
return callback(new Error('Missing List ID'));
|
||||||
|
}
|
||||||
|
|
||||||
|
db.getConnection((err, connection) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.query('SELECT id, cid, email, first_name, last_name FROM `subscription__' + listId + '` WHERE is_test=1 LIMIT 100', (err, rows) => {
|
||||||
|
connection.release();
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows || !rows.length) {
|
||||||
|
return callback(null, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscribers = rows.map(subscriber => {
|
||||||
|
subscriber = tools.convertKeys(subscriber);
|
||||||
|
let fullName = [].concat(subscriber.firstName || []).concat(subscriber.lastName || []).join(' ');
|
||||||
|
if (fullName) {
|
||||||
|
subscriber.displayName = fullName + ' <' + subscriber.email + '>';
|
||||||
|
} else {
|
||||||
|
subscriber.displayName = subscriber.email;
|
||||||
|
}
|
||||||
|
return subscriber;
|
||||||
|
});
|
||||||
|
return callback(null, subscribers);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports.filter = (listId, request, columns, segmentId, callback) => {
|
module.exports.filter = (listId, request, columns, segmentId, callback) => {
|
||||||
listId = Number(listId) || 0;
|
listId = Number(listId) || 0;
|
||||||
segmentId = Number(segmentId) || 0;
|
segmentId = Number(segmentId) || 0;
|
||||||
|
@ -291,13 +329,16 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
||||||
let keys = [];
|
let keys = [];
|
||||||
let values = [];
|
let values = [];
|
||||||
|
|
||||||
let allowedKeys = ['first_name', 'last_name', 'tz'];
|
let allowedKeys = ['first_name', 'last_name', 'tz', 'is_test'];
|
||||||
Object.keys(subscription).forEach(key => {
|
Object.keys(subscription).forEach(key => {
|
||||||
let value = subscription[key];
|
let value = subscription[key];
|
||||||
key = tools.toDbKey(key);
|
key = tools.toDbKey(key);
|
||||||
if (key === 'tz') {
|
if (key === 'tz') {
|
||||||
value = (value || '').toString().toLowerCase().trim();
|
value = (value || '').toString().toLowerCase().trim();
|
||||||
}
|
}
|
||||||
|
if (key === 'is_test') {
|
||||||
|
value = value ? '1' : '0';
|
||||||
|
}
|
||||||
if (allowedKeys.indexOf(key) >= 0) {
|
if (allowedKeys.indexOf(key) >= 0) {
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
values.push(value);
|
values.push(value);
|
||||||
|
@ -551,7 +592,7 @@ module.exports.update = (listId, cid, updates, allowEmail, callback) => {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
let allowedKeys = ['first_name', 'last_name', 'tz'];
|
let allowedKeys = ['first_name', 'last_name', 'tz', 'is_test'];
|
||||||
|
|
||||||
if (allowEmail) {
|
if (allowEmail) {
|
||||||
allowedKeys.unshift('email');
|
allowedKeys.unshift('email');
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": 13
|
"schemaVersion": 14
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "mailtrain",
|
"name": "mailtrain",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.10.1",
|
"version": "1.11.0",
|
||||||
"description": "Self hosted email newsletter app",
|
"description": "Self hosted email newsletter app",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -36,8 +36,8 @@
|
||||||
"config": "^1.20.4",
|
"config": "^1.20.4",
|
||||||
"connect-flash": "^0.1.1",
|
"connect-flash": "^0.1.1",
|
||||||
"connect-redis": "^3.0.2",
|
"connect-redis": "^3.0.2",
|
||||||
"cookie-parser": "^1.4.2",
|
"cookie-parser": "^1.4.3",
|
||||||
"csurf": "^1.8.3",
|
"csurf": "^1.9.0",
|
||||||
"csv-parse": "^1.1.0",
|
"csv-parse": "^1.1.0",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"express": "^4.13.4",
|
"express": "^4.13.4",
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
"humanize": "0.0.9",
|
"humanize": "0.0.9",
|
||||||
"is-url": "^1.2.1",
|
"is-url": "^1.2.1",
|
||||||
"isemail": "^2.1.2",
|
"isemail": "^2.1.2",
|
||||||
"jsdom": "^9.2.0",
|
"jsdom": "^9.2.1",
|
||||||
"juice": "^2.0.0",
|
"juice": "^2.0.0",
|
||||||
"moment-timezone": "^0.5.4",
|
"moment-timezone": "^0.5.4",
|
||||||
"morgan": "^1.7.0",
|
"morgan": "^1.7.0",
|
||||||
|
|
|
@ -59,7 +59,7 @@ router.get('/:campaign/:list/:subscription', (req, res, next) => {
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mail) {
|
if (!mail && !req.user) {
|
||||||
err = new Error('Not Found');
|
err = new Error('Not Found');
|
||||||
err.status = 404;
|
err.status = 404;
|
||||||
return next(err);
|
return next(err);
|
||||||
|
|
|
@ -6,6 +6,7 @@ let lists = require('../lib/models/lists');
|
||||||
let fields = require('../lib/models/fields');
|
let fields = require('../lib/models/fields');
|
||||||
let templates = require('../lib/models/templates');
|
let templates = require('../lib/models/templates');
|
||||||
let campaigns = require('../lib/models/campaigns');
|
let campaigns = require('../lib/models/campaigns');
|
||||||
|
let subscriptions = require('../lib/models/subscriptions');
|
||||||
let settings = require('../lib/models/settings');
|
let settings = require('../lib/models/settings');
|
||||||
let tools = require('../lib/tools');
|
let tools = require('../lib/tools');
|
||||||
let striptags = require('striptags');
|
let striptags = require('striptags');
|
||||||
|
@ -306,51 +307,70 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => {
|
||||||
campaign.csrfToken = req.csrfToken();
|
campaign.csrfToken = req.csrfToken();
|
||||||
campaign.list = list;
|
campaign.list = list;
|
||||||
|
|
||||||
campaign.isIdling = campaign.status === 1;
|
subscriptions.listTestUsers(list.id, (err, testUsers) => {
|
||||||
campaign.isSending = campaign.status === 2;
|
if (err || !testUsers) {
|
||||||
campaign.isFinished = campaign.status === 3;
|
testUsers = [];
|
||||||
campaign.isPaused = campaign.status === 4;
|
|
||||||
campaign.isInactive = campaign.status === 5;
|
|
||||||
campaign.isActive = campaign.status === 6;
|
|
||||||
|
|
||||||
campaign.isNormal = campaign.type === 1 || campaign.type === 3;
|
|
||||||
campaign.isRss = campaign.type === 2;
|
|
||||||
|
|
||||||
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) * 10000)/100 : 0;
|
|
||||||
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 10000)/100 : 0;
|
|
||||||
campaign.bounceRate = campaign.delivered ? Math.round((campaign.bounced / campaign.delivered) * 10000)/100 : 0;
|
|
||||||
campaign.complaintRate = campaign.delivered ? Math.round((campaign.complained / campaign.delivered) * 10000)/100 : 0;
|
|
||||||
campaign.unsubscribeRate = campaign.delivered ? Math.round((campaign.unsubscribed / campaign.delivered) * 10000)/100 : 0;
|
|
||||||
|
|
||||||
campaigns.getLinks(campaign.id, (err, links) => {
|
|
||||||
if (err) {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
let index = 0;
|
|
||||||
campaign.links = (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;
|
|
||||||
});
|
|
||||||
campaign.showOverview = !req.query.tab || req.query.tab === 'overview';
|
|
||||||
campaign.showLinks = req.query.tab === 'links';
|
|
||||||
res.render('campaigns/view', campaign);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
campaign.testUsers = testUsers;
|
||||||
|
campaign.isIdling = campaign.status === 1;
|
||||||
|
campaign.isSending = campaign.status === 2;
|
||||||
|
campaign.isFinished = campaign.status === 3;
|
||||||
|
campaign.isPaused = campaign.status === 4;
|
||||||
|
campaign.isInactive = campaign.status === 5;
|
||||||
|
campaign.isActive = campaign.status === 6;
|
||||||
|
|
||||||
|
campaign.isNormal = campaign.type === 1 || campaign.type === 3;
|
||||||
|
campaign.isRss = campaign.type === 2;
|
||||||
|
|
||||||
|
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) * 10000) / 100 : 0;
|
||||||
|
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 10000) / 100 : 0;
|
||||||
|
campaign.bounceRate = campaign.delivered ? Math.round((campaign.bounced / campaign.delivered) * 10000) / 100 : 0;
|
||||||
|
campaign.complaintRate = campaign.delivered ? Math.round((campaign.complained / campaign.delivered) * 10000) / 100 : 0;
|
||||||
|
campaign.unsubscribeRate = campaign.delivered ? Math.round((campaign.unsubscribed / campaign.delivered) * 10000) / 100 : 0;
|
||||||
|
|
||||||
|
campaigns.getLinks(campaign.id, (err, links) => {
|
||||||
|
if (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
let index = 0;
|
||||||
|
campaign.links = (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;
|
||||||
|
});
|
||||||
|
campaign.showOverview = !req.query.tab || req.query.tab === 'overview';
|
||||||
|
campaign.showLinks = req.query.tab === 'links';
|
||||||
|
res.render('campaigns/view', campaign);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/preview/:id', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||||
|
let campaign = req.body.campaign;
|
||||||
|
let list = req.body.list;
|
||||||
|
let listId = req.body.listId;
|
||||||
|
let subscription = req.body.subscriber;
|
||||||
|
|
||||||
|
if (subscription === '_create') {
|
||||||
|
return res.redirect('/lists/subscription/' + encodeURIComponent(listId) + '/add/?is-test=true');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('/archive/' + encodeURIComponent(campaign) + '/' + encodeURIComponent(list) + '/' + encodeURIComponent(subscription));
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/opened/:id', passport.csrfProtection, (req, res) => {
|
router.get('/opened/:id', passport.csrfProtection, (req, res) => {
|
||||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||||
if (err || !campaign) {
|
if (err || !campaign) {
|
||||||
|
|
|
@ -410,6 +410,7 @@ router.post('/subscription/delete', passport.parseForm, passport.csrfProtection,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/subscription/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
router.post('/subscription/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||||
|
req.body['is-test'] = req.body['is-test'] ? '1' : '0';
|
||||||
subscriptions.update(req.body.list, req.body.cid, req.body, true, (err, updated) => {
|
subscriptions.update(req.body.list, req.body.cid, req.body, true, (err, updated) => {
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -185,7 +185,7 @@ CREATE TABLE `settings` (
|
||||||
`value` text NOT NULL,
|
`value` text NOT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `key` (`key`)
|
UNIQUE KEY `key` (`key`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4;
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost');
|
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 (2,'smtp_port','465');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','TLS');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','TLS');
|
||||||
|
@ -202,7 +202,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 (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 (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 (16,'default_homepage','http://localhost:3000/');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','13');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','14');
|
||||||
CREATE TABLE `subscription` (
|
CREATE TABLE `subscription` (
|
||||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||||
|
@ -212,6 +212,7 @@ CREATE TABLE `subscription` (
|
||||||
`tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,
|
`tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,
|
||||||
`imported` int(11) unsigned DEFAULT NULL,
|
`imported` int(11) unsigned DEFAULT NULL,
|
||||||
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
||||||
|
`is_test` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
||||||
`status_change` timestamp NULL DEFAULT NULL,
|
`status_change` timestamp NULL DEFAULT NULL,
|
||||||
`latest_open` timestamp NULL DEFAULT NULL,
|
`latest_open` timestamp NULL DEFAULT NULL,
|
||||||
`latest_click` timestamp NULL DEFAULT NULL,
|
`latest_click` timestamp NULL DEFAULT NULL,
|
||||||
|
@ -224,7 +225,8 @@ CREATE TABLE `subscription` (
|
||||||
KEY `status` (`status`),
|
KEY `status` (`status`),
|
||||||
KEY `first_name` (`first_name`(191)),
|
KEY `first_name` (`first_name`(191)),
|
||||||
KEY `last_name` (`last_name`(191)),
|
KEY `last_name` (`last_name`(191)),
|
||||||
KEY `subscriber_tz` (`tz`)
|
KEY `subscriber_tz` (`tz`),
|
||||||
|
KEY `is_test` (`is_test`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
CREATE TABLE `templates` (
|
CREATE TABLE `templates` (
|
||||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
|
17
setup/sql/upgrade-00014.sql
Normal file
17
setup/sql/upgrade-00014.sql
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Header section
|
||||||
|
# Define incrementing schema version number
|
||||||
|
SET @schema_version = '14';
|
||||||
|
|
||||||
|
-- {{#each tables.subscription}}
|
||||||
|
|
||||||
|
# Adds new column 'tz' to subscriptions table
|
||||||
|
# Indicates subscriber time zone, use UTC as default
|
||||||
|
ALTER TABLE `{{this}}` ADD COLUMN `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0' AFTER `status`;
|
||||||
|
CREATE INDEX is_test ON `{{this}}` (`is_test`);
|
||||||
|
|
||||||
|
-- {{/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;
|
|
@ -87,6 +87,31 @@
|
||||||
|
|
||||||
{{#if isNormal}}
|
{{#if isNormal}}
|
||||||
|
|
||||||
|
<dt>Preview campaign as</dt>
|
||||||
|
<dd>
|
||||||
|
<form method="post" action="/campaigns/preview/{{id}}" class="form-inline">
|
||||||
|
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||||
|
<input type="hidden" name="campaign" value="{{cid}}">
|
||||||
|
<input type="hidden" name="list" value="{{list.cid}}">
|
||||||
|
<input type="hidden" name="listId" value="{{list.id}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<select name="subscriber" class="form-control" required>
|
||||||
|
{{#each testUsers}}
|
||||||
|
<option value="{{cid}}">{{displayName}}</option>
|
||||||
|
{{/each}}
|
||||||
|
{{#if testUsers}}
|
||||||
|
<optgroup label="Actions">
|
||||||
|
<option value="_create">Add new test user ...</option>
|
||||||
|
</optgroup>
|
||||||
|
{{else}}
|
||||||
|
<option value="_create">No test users yet, create one here ...</option>
|
||||||
|
{{/if}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-default">Go</button>
|
||||||
|
</form>
|
||||||
|
</dd>
|
||||||
|
|
||||||
{{#unless isIdling}}
|
{{#unless isIdling}}
|
||||||
<dt>Delivered <a href="/campaigns/status/{{id}}/delivered" title="List subscribers who received this message"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
<dt>Delivered <a href="/campaigns/status/{{id}}/delivered" title="List subscribers who received this message"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
||||||
<dd>{{delivered}}</dd>
|
<dd>{{delivered}}</dd>
|
||||||
|
|
|
@ -131,6 +131,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="is-test" {{#if isTest}} checked {{/if}}> Test user?
|
||||||
|
<span class="help-block">If checked then this subscription can be used for previewing campaign messages</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
@ -144,6 +144,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="is-test" {{#if isTest}} checked {{/if}}> Test user?
|
||||||
|
<span class="help-block">If checked then this subscription can be used for previewing campaign messages</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
Loading…
Reference in a new issue