Browse Source

Updates all datetimes to use UTC on the backend. This includes a DB migration to convert existing dates.

Fixes #316
pull/733/head
Jordan Wright 3 years ago
parent
commit
58a57589bd
11 changed files with 130 additions and 88 deletions
  1. +8
    -8
      controllers/api.go
  2. +16
    -0
      db/db_mysql/migrations/20170828220440_0.4_utc_dates.sql
  3. +15
    -0
      db/db_sqlite3/migrations/20170827141312_0.4_utc_dates.sql
  4. +8
    -5
      models/campaign.go
  5. +1
    -1
      static/js/dist/app/campaign_results.min.js
  6. +1
    -1
      static/js/dist/app/campaigns.min.js
  7. +1
    -1
      static/js/dist/app/dashboard.min.js
  8. +4
    -4
      static/js/src/app/campaign_results.js
  9. +69
    -66
      static/js/src/app/campaigns.js
  10. +6
    -1
      static/js/src/app/dashboard.js
  11. +1
    -1
      worker/worker.go

+ 8
- 8
controllers/api.go View File

@ -203,7 +203,7 @@ func API_Groups(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
return
}
g.ModifiedDate = time.Now()
g.ModifiedDate = time.Now().UTC()
g.UserId = ctx.Get(r, "user_id").(int64)
err = models.PostGroup(&g)
if err != nil {
@ -256,7 +256,7 @@ func API_Groups_Id(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
return
}
g.ModifiedDate = time.Now()
g.ModifiedDate = time.Now().UTC()
g.UserId = ctx.Get(r, "user_id").(int64)
err = models.PutGroup(&g)
if err != nil {
@ -305,7 +305,7 @@ func API_Templates(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: false, Message: "Template name already in use"}, http.StatusConflict)
return
}
t.ModifiedDate = time.Now()
t.ModifiedDate = time.Now().UTC()
t.UserId = ctx.Get(r, "user_id").(int64)
err = models.PostTemplate(&t)
if err == models.ErrTemplateNameNotSpecified {
@ -354,7 +354,7 @@ func API_Templates_Id(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and template_id mismatch"}, http.StatusBadRequest)
return
}
t.ModifiedDate = time.Now()
t.ModifiedDate = time.Now().UTC()
t.UserId = ctx.Get(r, "user_id").(int64)
err = models.PutTemplate(&t)
if err != nil {
@ -390,7 +390,7 @@ func API_Pages(w http.ResponseWriter, r *http.Request) {
Logger.Println(err)
return
}
p.ModifiedDate = time.Now()
p.ModifiedDate = time.Now().UTC()
p.UserId = ctx.Get(r, "user_id").(int64)
err = models.PostPage(&p)
if err != nil {
@ -431,7 +431,7 @@ func API_Pages_Id(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: false, Message: "/:id and /:page_id mismatch"}, http.StatusBadRequest)
return
}
p.ModifiedDate = time.Now()
p.ModifiedDate = time.Now().UTC()
p.UserId = ctx.Get(r, "user_id").(int64)
err = models.PutPage(&p)
if err != nil {
@ -467,7 +467,7 @@ func API_SMTP(w http.ResponseWriter, r *http.Request) {
Logger.Println(err)
return
}
s.ModifiedDate = time.Now()
s.ModifiedDate = time.Now().UTC()
s.UserId = ctx.Get(r, "user_id").(int64)
err = models.PostSMTP(&s)
if err != nil {
@ -513,7 +513,7 @@ func API_SMTP_Id(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
s.ModifiedDate = time.Now()
s.ModifiedDate = time.Now().UTC()
s.UserId = ctx.Get(r, "user_id").(int64)
err = models.PutSMTP(&s)
if err != nil {

+ 16
- 0
db/db_mysql/migrations/20170828220440_0.4_utc_dates.sql View File

@ -0,0 +1,16 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
UPDATE campaigns SET `created_date`=CONVERT_TZ(`created_date`, @@session.time_zone, '+00:00');
UPDATE campaigns SET `completed_date`=CONVERT_TZ(`completed_date`, @@session.time_zone, '+00:00');
UPDATE campaigns SET `launch_date`=CONVERT_TZ(`launch_date`, @@session.time_zone, '+00:00');
UPDATE events SET `time`=CONVERT_TZ(`time`, @@session.time_zone, '+00:00');
UPDATE groups SET `modified_date`=CONVERT_TZ(`modified_date`, @@session.time_zone, '+00:00');
UPDATE templates SET `modified_date`=CONVERT_TZ(`modified_date`, @@session.time_zone, '+00:00');
UPDATE pages SET `modified_date`=CONVERT_TZ(`modified_date`, @@session.time_zone, '+00:00');
UPDATE smtp SET `modified_date`=CONVERT_TZ(`modified_date`, @@session.time_zone, '+00:00');
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

+ 15
- 0
db/db_sqlite3/migrations/20170827141312_0.4_utc_dates.sql View File

@ -0,0 +1,15 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
UPDATE campaigns SET created_date=STRFTIME('%Y-%m-%d %H:%M:%S+00:00', DATETIME(created_date, 'utc'));
UPDATE campaigns SET completed_date=STRFTIME('%Y-%m-%d %H:%M:%S+00:00', DATETIME(completed_date, 'utc'));
UPDATE campaigns SET launch_date=STRFTIME('%Y-%m-%d %H:%M:%S+00:00', DATETIME(launch_date, 'utc'));
UPDATE events SET `time`=STRFTIME('%Y-%m-%d %H:%M:%S+00:00', DATETIME(`time`, 'utc'));
UPDATE groups SET modified_date=STRFTIME('%Y-%m-%d %H:%M:%S+00:00', DATETIME(modified_date, 'utc'));
UPDATE templates SET modified_date=STRFTIME('%Y-%m-%d %H:%M:%S+00:00', DATETIME(modified_date, 'utc'));
UPDATE pages SET modified_date=STRFTIME('%Y-%m-%d %H:%M:%S+00:00', DATETIME(modified_date, 'utc'));
UPDATE smtp SET modified_date=STRFTIME('%Y-%m-%d %H:%M:%S+00:00', DATETIME(modified_date, 'utc'));
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

+ 8
- 5
models/campaign.go View File

@ -140,7 +140,7 @@ func (c *Campaign) UpdateStatus(s string) error {
// AddEvent creates a new campaign event in the database
func (c *Campaign) AddEvent(e Event) error {
e.CampaignId = c.Id
e.Time = time.Now()
e.Time = time.Now().UTC()
return db.Debug().Save(&e).Error
}
@ -226,7 +226,7 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
if err != nil {
return s, err
}
// Every opened email event implies the email was sent
// Every opened email event implies the email was sent
s.EmailsSent += s.OpenedEmail
err = query.Where("status=?", ERROR).Count(&s.Error).Error
return s, err
@ -279,6 +279,7 @@ func GetCampaignSummaries(uid int64) (CampaignSummaries, error) {
return overview, err
}
cs[i].Stats = s
Logger.Println(cs[i].CreatedDate.String())
}
overview.Total = int64(len(cs))
overview.Campaigns = cs
@ -361,11 +362,13 @@ func PostCampaign(c *Campaign, uid int64) error {
}
// Fill in the details
c.UserId = uid
c.CreatedDate = time.Now()
c.CreatedDate = time.Now().UTC()
c.CompletedDate = time.Time{}
c.Status = CAMPAIGN_CREATED
if c.LaunchDate.IsZero() {
c.LaunchDate = time.Now()
c.LaunchDate = time.Now().UTC()
} else {
c.LaunchDate = c.LaunchDate.UTC()
}
// Check to make sure all the groups already exist
for i, g := range c.Groups {
@ -479,7 +482,7 @@ func CompleteCampaign(id int64, uid int64) error {
return nil
}
// Mark the campaign as complete
c.CompletedDate = time.Now()
c.CompletedDate = time.Now().UTC()
c.Status = CAMPAIGN_COMPLETE
err = db.Where("id=? and user_id=?", id, uid).Save(&c).Error
if err != nil {

+ 1
- 1
static/js/dist/app/campaign_results.min.js
File diff suppressed because it is too large
View File


+ 1
- 1
static/js/dist/app/campaigns.min.js
File diff suppressed because it is too large
View File


+ 1
- 1
static/js/dist/app/dashboard.min.js
File diff suppressed because it is too large
View File


+ 4
- 4
static/js/src/app/campaign_results.js View File

@ -282,7 +282,7 @@ function renderTimeline(data) {
' <div class="timeline-icon ' + statuses[event.message].label + '">' +
' <i class="fa ' + statuses[event.message].icon + '"></i></div>' +
' <div class="timeline-message">' + escapeHtml(event.message) +
' <span class="timeline-date">' + moment(event.time).format('MMMM Do YYYY h:mm a') + '</span>'
' <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm a') + '</span>'
if (event.details) {
if (event.message == "Submitted Data") {
results += '<div class="timeline-replay-button"><button onclick="replay(' + i + ')" class="btn btn-success">'
@ -461,7 +461,7 @@ function poll() {
/* Update the timeline */
var timeline_series_data = []
$.each(campaign.timeline, function (i, event) {
var event_date = moment(event.time)
var event_date = moment.utc(event.time).local()
timeline_series_data.push({
email: event.email,
x: event_date.valueOf(),
@ -470,7 +470,7 @@ function poll() {
})
var timeline_series_data = []
$.each(campaign.timeline, function (i, event) {
var event_date = moment(event.time)
var event_date = moment.utc(event.time).local()
timeline_series_data.push({
email: event.email,
message: event.message,
@ -661,7 +661,7 @@ function load() {
});
// Setup the graphs
$.each(campaign.timeline, function (i, event) {
var event_date = moment(event.time)
var event_date = moment.utc(event.time).local()
timeline_series_data.push({
email: event.email,
message: event.message,

+ 69
- 66
static/js/src/app/campaigns.js View File

@ -24,48 +24,49 @@ function launch() {
reverseButtons: true,
allowOutsideClick: false,
showLoaderOnConfirm: true,
preConfirm: function() {
return new Promise(function(resolve, reject) {
preConfirm: function () {
return new Promise(function (resolve, reject) {
groups = []
$("#users").select2("data").forEach(function(group){
groups.push({name: group.text});
$("#users").select2("data").forEach(function (group) {
groups.push({ name: group.text });
})
// Validate our fields
campaign = {
name: $("#name").val(),
template: {
name: $("#template").select2("data")[0].text
},
url: $("#url").val(),
page: {
name: $("#page").select2("data")[0].text
},
smtp: {
name: $("#profile").select2("data")[0].text
},
launch_date: moment($("#launch_date").val(), "MM/DD/YYYY hh:mm a").format(),
groups: groups
}
// Submit the campaign
name: $("#name").val(),
template: {
name: $("#template").select2("data")[0].text
},
url: $("#url").val(),
page: {
name: $("#page").select2("data")[0].text
},
smtp: {
name: $("#profile").select2("data")[0].text
},
launch_date: moment($("#launch_date").val(), "MM/DD/YYYY hh:mm a").utc().format(),
groups: groups
}
console.log("Launching campaign at time: " + campaign.launch_date)
// Submit the campaign
api.campaigns.post(campaign)
.success(function(data) {
.success(function (data) {
resolve()
campaign = data
})
.error(function(data) {
.error(function (data) {
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
swal.close()
})
})
}
}).then(function() {
}).then(function () {
swal(
'Campaign Scheduled!',
'This campaign has been scheduled for launch!',
'success'
);
$('button:contains("OK")').on('click', function() {
$('button:contains("OK")').on('click', function () {
window.location = "/campaigns/" + campaign.id.toString()
})
})
@ -91,14 +92,14 @@ function sendTestEmail() {
}
btnHtml = $("#sendTestModalSubmit").html()
$("#sendTestModalSubmit").html('<i class="fa fa-spinner fa-spin"></i> Sending')
// Send the test email
// Send the test email
api.send_test_email(test_email_request)
.success(function(data) {
.success(function (data) {
$("#sendTestEmailModal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-success\">\
<i class=\"fa fa-check-circle\"></i> Email Sent!</div>")
$("#sendTestModalSubmit").html(btnHtml)
})
.error(function(data) {
.error(function (data) {
$("#sendTestEmailModal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
$("#sendTestModalSubmit").html(btnHtml)
@ -127,24 +128,24 @@ function deleteCampaign(idx) {
confirmButtonColor: "#428bca",
reverseButtons: true,
allowOutsideClick: false,
preConfirm: function() {
return new Promise(function(resolve, reject) {
preConfirm: function () {
return new Promise(function (resolve, reject) {
api.campaignId.delete(campaigns[idx].id)
.success(function(msg) {
.success(function (msg) {
resolve()
})
.error(function(data) {
.error(function (data) {
reject(data.responseJSON.message)
})
})
}
}).then(function() {
}).then(function () {
swal(
'Campaign Deleted!',
'This campaign has been deleted!',
'success'
);
$('button:contains("OK")').on('click', function() {
$('button:contains("OK")').on('click', function () {
location.reload()
})
})
@ -152,12 +153,12 @@ function deleteCampaign(idx) {
function setupOptions() {
api.groups.get()
.success(function(groups) {
.success(function (groups) {
if (groups.length == 0) {
modalError("No groups found!")
return false;
} else {
var group_s2 = $.map(groups, function(obj) {
var group_s2 = $.map(groups, function (obj) {
obj.text = obj.name
return obj
});
@ -168,12 +169,12 @@ function setupOptions() {
}
});
api.templates.get()
.success(function(templates) {
.success(function (templates) {
if (templates.length == 0) {
modalError("No templates found!")
return false
} else {
var template_s2 = $.map(templates, function(obj) {
var template_s2 = $.map(templates, function (obj) {
obj.text = obj.name
return obj
});
@ -184,12 +185,12 @@ function setupOptions() {
}
});
api.pages.get()
.success(function(pages) {
.success(function (pages) {
if (pages.length == 0) {
modalError("No pages found!")
return false
} else {
var page_s2 = $.map(pages, function(obj) {
var page_s2 = $.map(pages, function (obj) {
obj.text = obj.name
return obj
});
@ -200,12 +201,12 @@ function setupOptions() {
}
});
api.SMTP.get()
.success(function(profiles) {
.success(function (profiles) {
if (profiles.length == 0) {
modalError("No profiles found!")
return false
} else {
var profile_s2 = $.map(profiles, function(obj) {
var profile_s2 = $.map(profiles, function (obj) {
obj.text = obj.name
return obj
});
@ -223,9 +224,9 @@ function edit(campaign) {
function copy(idx) {
setupOptions();
// Set our initial values
// Set our initial values
api.campaignId.get(campaigns[idx].id)
.success(function(campaign) {
.success(function (campaign) {
$("#name").val("Copy of " + campaign.name)
if (!campaign.template.id) {
$("#template").select2({
@ -250,29 +251,29 @@ function copy(idx) {
}
$("#url").val(campaign.url)
})
.error(function(data) {
.error(function (data) {
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
})
}
$(document).ready(function() {
$(document).ready(function () {
$("#launch_date").datetimepicker({
"widgetPositioning": {
"vertical": "bottom"
},
"showTodayButton": true,
"defaultDate": moment()
})
// Setup multiple modals
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
$('.modal').on('hidden.bs.modal', function(event) {
"widgetPositioning": {
"vertical": "bottom"
},
"showTodayButton": true,
"defaultDate": moment()
})
// Setup multiple modals
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
$('.modal').on('hidden.bs.modal', function (event) {
$(this).removeClass('fv-modal-stack');
$('body').data('fv_open_modals', $('body').data('fv_open_modals') - 1);
});
$('.modal').on('shown.bs.modal', function(event) {
$('.modal').on('shown.bs.modal', function (event) {
// Keep track of the number of open modals
if (typeof($('body').data('fv_open_modals')) == 'undefined') {
if (typeof ($('body').data('fv_open_modals')) == 'undefined') {
$('body').data('fv_open_modals', 0);
}
// if the z-index of this modal has been set, ignore.
@ -291,11 +292,11 @@ $(document).ready(function() {
$(document).on('hidden.bs.modal', '.modal', function () {
$('.modal:visible').length && $(document.body).addClass('modal-open');
});
$('#modal').on('hidden.bs.modal', function(event) {
$('#modal').on('hidden.bs.modal', function (event) {
dismiss()
});
api.campaigns.summary()
.success(function(data) {
.success(function (data) {
campaigns = data.campaigns
$("#loading").hide()
if (campaigns.length > 0) {
@ -309,23 +310,25 @@ $(document).ready(function() {
[1, "desc"]
]
});
$.each(campaigns, function(i, campaign) {
$.each(campaigns, function (i, campaign) {
console.log(campaign)
console.log(campaign.created_date)
label = labels[campaign.status] || "label-default";
//section for tooltips on the status of a campaign to show some quick stats
//section for tooltips on the status of a campaign to show some quick stats
var launchDate;
if(moment(campaign.launch_date).isAfter(moment())){
launchDate = "Scheduled to start: "+moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate+"<br><br>"+"Number of recipients: "+campaign.stats.total
if (moment(campaign.launch_date).isAfter(moment())) {
launchDate = "Scheduled to start: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total
} else {
launchDate = "Launch Date: "+moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate+"<br><br>"+"Number of recipients: "+campaign.stats.total+"<br><br>"+"Emails opened: "+campaign.stats.opened+"<br><br>"+"Emails clicked: "+campaign.stats.clicked+"<br><br>"+"Submitted Credentials: "+campaign.stats.submitted_data+"<br><br>"+"Errors : "+campaign.stats.error
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error
}
campaignTable.row.add([
escapeHtml(campaign.name),
moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a'),
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\""+quickStats+"\">" + campaign.status + "</span>",
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='left' title='View Results'>\
<i class='fa fa-bar-chart'></i>\
</a>\
@ -342,7 +345,7 @@ $(document).ready(function() {
$("#emptyMessage").show()
}
})
.error(function() {
.error(function () {
$("#loading").hide()
errorFlash("Error fetching campaigns")
})

+ 6
- 1
static/js/src/app/dashboard.js View File

@ -197,7 +197,7 @@ function generateStatsPieCharts(campaigns) {
function generateTimelineChart(campaigns) {
var overview_data = []
$.each(campaigns, function (i, campaign) {
var campaign_date = moment(campaign.created_date)
var campaign_date = moment.utc(campaign.created_date).local()
// Add it to the chart data
campaign.y = 0
// Clicked events also contain our data submitted events
@ -275,6 +275,11 @@ function generateTimelineChart(campaigns) {
}
$(document).ready(function () {
Highcharts.setOptions({
global: {
useUTC: false
}
})
api.campaigns.summary()
.success(function (data) {
$("#loading").hide()

+ 1
- 1
worker/worker.go View File

@ -36,7 +36,7 @@ func New() *Worker {
func (w *Worker) Start() {
Logger.Println("Background Worker Started Successfully - Waiting for Campaigns")
for t := range time.Tick(1 * time.Minute) {
cs, err := models.GetQueuedCampaigns(t)
cs, err := models.GetQueuedCampaigns(t.UTC())
// Not really sure of a clean way to catch errors per campaign...
if err != nil {
Logger.Println(err)

Loading…
Cancel
Save