diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 92bddfb4..60a1b927 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,10 @@ # Changelog +## 1.5.0 2016-05-05 + + * Fixed a bug in unsubscribing through the admin interface + * Added individual link click stats + ## 1.4.1 2016-05-04 * Added support for RSS templates diff --git a/lib/models/campaigns.js b/lib/models/campaigns.js index 6c16c27d..52e80c4d 100644 --- a/lib/models/campaigns.js +++ b/lib/models/campaigns.js @@ -177,29 +177,89 @@ module.exports.get = (id, withSegment, callback) => { let campaign = tools.convertKeys(rows[0]); - if (!campaign.segment || !withSegment) { - return callback(null, campaign); - } else { - segments.get(campaign.segment, (err, segment) => { - if (err || !segment) { - // ignore - return callback(null, campaign); - } - segments.subscribers(segment.id, true, (err, subscribers) => { - if (err || !subscribers) { - segment.subscribers = 0; - } else { - segment.subscribers = subscribers; + let handleSegment = () => { + + if (!campaign.segment || !withSegment) { + return callback(null, campaign); + } else { + segments.get(campaign.segment, (err, segment) => { + if (err || !segment) { + // ignore + return callback(null, campaign); } - campaign.segment = segment; - return callback(null, campaign); + segments.subscribers(segment.id, true, (err, subscribers) => { + if (err || !subscribers) { + segment.subscribers = 0; + } else { + segment.subscribers = subscribers; + } + campaign.segment = segment; + return callback(null, campaign); + }); }); - }); + } + }; + + if (!campaign.parent) { + return handleSegment(); } + + db.getConnection((err, connection) => { + if (err) { + return callback(err); + } + connection.query('SELECT `id`, `cid`, `name` FROM campaigns WHERE id=?', [campaign.parent], (err, rows) => { + connection.release(); + if (err) { + return callback(err); + } + + if (!rows || !rows.length) { + return handleSegment(); + } + + campaign.parent = tools.convertKeys(rows[0]); + return handleSegment(); + }); + }); }); }); }; +module.exports.getLinks = (id, callback) => { + id = Number(id) || 0; + + if (id < 1) { + return callback(new Error('Missing Campaign ID')); + } + + db.getConnection((err, connection) => { + if (err) { + return callback(err); + } + + let query = 'SELECT `id`, `url`, `clicks` FROM links WHERE `campaign`=? LIMIT 1000'; + connection.query(query, [id], (err, rows) => { + connection.release(); + if (err) { + return callback(err); + } + + if (!rows || !rows.length) { + return callback(null, []); + } + + let links = rows.map( + row => tools.convertKeys(row) + ).sort((a, b) => ( + a.url.replace(/^https?:\/\/(www.)?/, '').toLowerCase()).localeCompare(b.url.replace(/^https?:\/\/(www.)?/, '').toLowerCase())); + + return callback(null, links); + }); + + }); +}; + module.exports.create = (campaign, opts, callback) => { campaign = tools.convertKeys(campaign); diff --git a/package.json b/package.json index 552d751e..bb8d893e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mailtrain", "private": true, - "version": "1.4.1", + "version": "1.5.0", "description": "Self hosted email newsletter app", "main": "index.js", "scripts": { diff --git a/public/images/img01.png b/public/images/img01.png new file mode 100644 index 00000000..fc03cdfb Binary files /dev/null and b/public/images/img01.png differ diff --git a/public/images/img01_lists.png b/public/images/img01_lists.png deleted file mode 100644 index f7ff88c5..00000000 Binary files a/public/images/img01_lists.png and /dev/null differ diff --git a/public/images/img02.png b/public/images/img02.png new file mode 100644 index 00000000..7e48f04f Binary files /dev/null and b/public/images/img02.png differ diff --git a/public/images/img02_list.png b/public/images/img02_list.png deleted file mode 100644 index bcd1b312..00000000 Binary files a/public/images/img02_list.png and /dev/null differ diff --git a/public/images/img03.png b/public/images/img03.png new file mode 100644 index 00000000..f9c50e25 Binary files /dev/null and b/public/images/img03.png differ diff --git a/public/images/img03_fields.png b/public/images/img03_fields.png deleted file mode 100644 index 32bc7808..00000000 Binary files a/public/images/img03_fields.png and /dev/null differ diff --git a/public/images/img04.png b/public/images/img04.png new file mode 100644 index 00000000..ba36dc2f Binary files /dev/null and b/public/images/img04.png differ diff --git a/public/images/img04_segment.png b/public/images/img04_segment.png deleted file mode 100644 index 22c6103b..00000000 Binary files a/public/images/img04_segment.png and /dev/null differ diff --git a/public/images/img05.png b/public/images/img05.png new file mode 100644 index 00000000..a5c72c2f Binary files /dev/null and b/public/images/img05.png differ diff --git a/public/images/img05_subscribe.png b/public/images/img05_subscribe.png deleted file mode 100644 index 852939bb..00000000 Binary files a/public/images/img05_subscribe.png and /dev/null differ diff --git a/public/images/img06.png b/public/images/img06.png new file mode 100644 index 00000000..1fdb8c63 Binary files /dev/null and b/public/images/img06.png differ diff --git a/public/images/img07.png b/public/images/img07.png new file mode 100644 index 00000000..a69230cf Binary files /dev/null and b/public/images/img07.png differ diff --git a/public/images/img08.png b/public/images/img08.png new file mode 100644 index 00000000..6096a23d Binary files /dev/null and b/public/images/img08.png differ diff --git a/public/images/img09.png b/public/images/img09.png new file mode 100644 index 00000000..0eb3df83 Binary files /dev/null and b/public/images/img09.png differ diff --git a/routes/campaigns.js b/routes/campaigns.js index e443e9ea..ed5819a2 100644 --- a/routes/campaigns.js +++ b/routes/campaigns.js @@ -271,7 +271,25 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => { campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 100) : 0; campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0; - res.render('campaigns/view', campaign); + 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 = true; + res.render('campaigns/view', campaign); + }); + }); }); }); diff --git a/routes/lists.js b/routes/lists.js index 3cf12fe5..a48cfcb4 100644 --- a/routes/lists.js +++ b/routes/lists.js @@ -378,7 +378,7 @@ router.post('/subscription/unsubscribe', passport.parseForm, passport.csrfProtec return res.redirect('/lists/view/' + list.id); } - subscriptions.unsubscribe(list.id, subscription.email, err => { + subscriptions.unsubscribe(list.id, subscription.email, false, err => { if (err) { req.flash('danger', err && err.message || err || 'Could not unsubscribe user'); return res.redirect('/lists/subscription/' + list.id + '/edit/' + subscription.cid); diff --git a/services/feedcheck.js b/services/feedcheck.js index 4ffae847..42640b9a 100644 --- a/services/feedcheck.js +++ b/services/feedcheck.js @@ -139,8 +139,7 @@ function checkEntries(parent, entries, callback) { from: parent.from, address: parent.address, subject: entry.title || parent.subject, - list: parent.list, - segment: parent.segment, + list: parent.segment ? parent.list + ':' + parent.segment : parent.list, html }; diff --git a/views/campaigns/edit-rss.hbs b/views/campaigns/edit-rss.hbs index 69e39371..4f57aac9 100644 --- a/views/campaigns/edit-rss.hbs +++ b/views/campaigns/edit-rss.hbs @@ -1,6 +1,9 @@
diff --git a/views/campaigns/edit.hbs b/views/campaigns/edit.hbs index fa656c7f..863cea32 100644 --- a/views/campaigns/edit.hbs +++ b/views/campaigns/edit.hbs @@ -1,6 +1,9 @@ diff --git a/views/campaigns/view.hbs b/views/campaigns/view.hbs index 5ecbb8e2..fbe3ffd1 100644 --- a/views/campaigns/view.hbs +++ b/views/campaigns/view.hbs @@ -1,6 +1,9 @@ @@ -16,250 +19,352 @@- # - | -- Name - | -- Description - | -- Status - | -- Created - | -- - | - -
---|
+ # + | ++ URL + | ++ Clicks + | ++ % of clicks + | ++ % of messages + | + + + {{#if links}} + {{#each links}} +
---|---|---|---|---|
+ {{index}} + | ++ {{short}} + | ++ {{clicks}} + | ++ {{relPercentage}} + | ++ {{totalPercentage}} + | +
+ No data available in table + | +||||
+ | + Aggregated clicks + | ++ {{clicks}} + | ++ – + | ++ CTR {{clicksRate}}% + | +
+ Clicks are counted as unique subscribers that clicked on a specific link or on any link (in aggregated view) +
+ ++ # + | ++ Name + | ++ Description + | ++ Status + | ++ Created + | ++ + | + +
---|
Setup Mailtrain to track RSS feeds and if a new entry is detected in a feed then Mailtrain auto-generates a new campaign using entry data as message contents and sends it to selected subscribers.
+If a list includes a custom field for a GPG Public Key then subscribers can upload their GPG public keys when signing up or managing preferences. Subscriber that have a key set get all messages from the list in an encrypted form.
+After a campaign is sent, check individual click statistics for every link included in the message.
+