list clicked subscribers
This commit is contained in:
		
							parent
							
								
									a9d6c1a666
								
							
						
					
					
						commit
						0d038f8a06
					
				
					 13 changed files with 439 additions and 66 deletions
				
			
		| 
						 | 
				
			
			@ -127,6 +127,74 @@ 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 +294,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 +312,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,85 @@ 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 || ''),
 | 
			
		||||
                    '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>',
 | 
			
		||||
                    row.count
 | 
			
		||||
                ])
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
							
								
								
									
										83
									
								
								views/campaigns/clicked.hbs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								views/campaigns/clicked.hbs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,83 @@
 | 
			
		|||
<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>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <a href="{{link.url}}">{{link.short}}</a>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    {{link.clicks}}
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    {{link.relPercentage}}
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    {{link.totalPercentage}}
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
 | 
			
		||||
    <h3>Subscribers that clicked on <a href="{{link.url}}">{{link.short}}</a></h3>
 | 
			
		||||
    <hr />
 | 
			
		||||
 | 
			
		||||
    <div class="table-responsive">
 | 
			
		||||
        <table data-topic-url="/campaigns/clicked" data-topic-id="{{id}}/{{list.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">
 | 
			
		||||
            <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>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
        </table>
 | 
			
		||||
    </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…
	
	Add table
		Add a link
		
	
		Reference in a new issue