diff --git a/lib/models/campaigns.js b/lib/models/campaigns.js index c5e0df85..84c48389 100644 --- a/lib/models/campaigns.js +++ b/lib/models/campaigns.js @@ -297,7 +297,7 @@ module.exports.delete = (id, callback) => { }); }; -module.exports.send = (id, callback) => { +module.exports.send = (id, scheduled, callback) => { module.exports.get(id, false, (err, campaign) => { if (err) { return callback(err); @@ -312,8 +312,18 @@ module.exports.send = (id, callback) => { return callback(err); } + let query; + let values; + if (scheduled) { + query = 'UPDATE campaigns SET `status`=2, `scheduled`=?, `status_change`=NOW() WHERE id=? LIMIT 1'; + values = [scheduled, id]; + } else { + query = 'UPDATE campaigns SET `status`=2, `status_change`=NOW() WHERE id=? LIMIT 1'; + values = [id]; + } + // campaigns marked as status=2 should be picked up by the sending processes - connection.query('UPDATE campaigns SET `status`=2, `status_change`=NOW() WHERE id=? LIMIT 1', [id], err => { + connection.query(query, values, err => { connection.release(); if (err) { return callback(err); @@ -357,7 +367,7 @@ module.exports.reset = (id, callback) => { return callback(err); } - if (campaign.status !== 3) { + if (campaign.status !== 3 && !(campaign.status === 2 && campaign.scheduled > new Date())) { return callback(null, false); } diff --git a/meta.json b/meta.json index 5d571180..18fc9078 100644 --- a/meta.json +++ b/meta.json @@ -1,3 +1,3 @@ { - "schemaVersion": 2 + "schemaVersion": 3 } diff --git a/routes/campaigns.js b/routes/campaigns.js index a52c6d02..3165ef38 100644 --- a/routes/campaigns.js +++ b/routes/campaigns.js @@ -38,7 +38,11 @@ router.get('/', (req, res) => { row.statusText = 'Idling'; break; case 2: - row.statusText = 'Sending'; + if (row.scheduled && row.scheduled > new Date()) { + row.statusText = 'Scheduled'; + } else { + row.statusText = 'Sending'; + } break; case 3: row.statusText = 'Finished'; @@ -228,6 +232,8 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => { campaign.isFinished = campaign.status === 3; campaign.isPaused = campaign.status === 4; + campaign.isScheduled = campaign.scheduled && campaign.scheduled > new Date(); + campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 100) : 0; campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0; @@ -251,7 +257,11 @@ router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) = }); router.post('/send', passport.parseForm, passport.csrfProtection, (req, res) => { - campaigns.send(req.body.id, (err, scheduled) => { + let delayHours = Math.max(Number(req.body['delay-hours']) || 0, 0); + let delayMinutes = Math.max(Number(req.body['delay-minutes']) || 0, 0); + let scheduled = new Date(Date.now() + delayHours * 3600 * 1000 + delayMinutes * 60 * 1000); + + campaigns.send(req.body.id, scheduled, (err, scheduled) => { if (err) { req.flash('danger', err && err.message || err); } else if (scheduled) { @@ -264,6 +274,20 @@ router.post('/send', passport.parseForm, passport.csrfProtection, (req, res) => }); }); +router.post('/resume', passport.parseForm, passport.csrfProtection, (req, res) => { + campaigns.send(req.body.id, false, (err, scheduled) => { + if (err) { + req.flash('danger', err && err.message || err); + } else if (scheduled) { + req.flash('success', 'Sending resumed'); + } else { + req.flash('info', 'Could not resume sending'); + } + + return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id)); + }); +}); + router.post('/reset', passport.parseForm, passport.csrfProtection, (req, res) => { campaigns.reset(req.body.id, (err, reset) => { if (err) { diff --git a/services/sender.js b/services/sender.js index 289a3956..ff1202bb 100644 --- a/services/sender.js +++ b/services/sender.js @@ -20,7 +20,7 @@ function findUnsent(callback) { return callback(err); } - let query = 'SELECT id, list, segment FROM campaigns WHERE status=? LIMIT 1'; + let query = 'SELECT `id`, `list`, `segment` FROM `campaigns` WHERE `status`=? AND (`scheduled` IS NULL OR `scheduled` <= NOW()) LIMIT 1'; connection.query(query, [2], (err, rows) => { if (err) { connection.release(); diff --git a/setup/sql/upgrade-00003.sql b/setup/sql/upgrade-00003.sql new file mode 100644 index 00000000..897ecb7b --- /dev/null +++ b/setup/sql/upgrade-00003.sql @@ -0,0 +1,12 @@ +# Header section +# Define incrementing schema version number +SET @schema_version = '3'; + +# Adds new column 'scheduled' to campaigns table. Indicates when the sending should actually start +ALTER TABLE `campaigns` ADD COLUMN `scheduled` timestamp NULL DEFAULT NULL AFTER `status`; +CREATE INDEX schedule_index ON `campaigns` (`scheduled`); + +# 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; diff --git a/views/campaigns/view.hbs b/views/campaigns/view.hbs index 621b35f0..04c2b805 100644 --- a/views/campaigns/view.hbs +++ b/views/campaigns/view.hbs @@ -85,10 +85,28 @@
{{#if isIdling}} -
+ +
+
+

Delay sending

+
+
+
+ +
hours
+
+
+
+
+ +
minutes
+
+
+
+ -
-
-

Sending…

+ + +
+

Sending scheduled {{scheduled}}

+ {{else}} +
+
+ + + + +
+
+

Sending…

+ {{/if}} {{/if}} {{#if isPaused}}
-
+