From d56b792a9d598ccba852c8cfb0389c8ea9325055 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 11 Dec 2017 22:42:51 +1030 Subject: [PATCH] I think we're finally good with the timezone stuff now? MySQL's UNIX_TIMESTAMP function interprets the date using MySQL's configured timezone, which we don't want (may be misconfigured etc). Instead, now we do the conversion to a timestamp on the PHP side. Then JavaScript is given the offset between UTC and the configure timezone so it can work out how to display the data. --- .../statistics/js/admin/dist/extension.js | 27 ++++++--- .../admin/src/components/StatisticsWidget.js | 27 ++++++--- .../src/Listener/AddStatisticsData.php | 57 +++++++++++++------ 3 files changed, 77 insertions(+), 34 deletions(-) diff --git a/extensions/statistics/js/admin/dist/extension.js b/extensions/statistics/js/admin/dist/extension.js index 8208b88fe..32a6db4aa 100644 --- a/extensions/statistics/js/admin/dist/extension.js +++ b/extensions/statistics/js/admin/dist/extension.js @@ -36,7 +36,15 @@ System.register('flarum/statistics/components/StatisticsWidget', ['flarum/compon value: function init() { babelHelpers.get(StatisticsWidget.prototype.__proto__ || Object.getPrototypeOf(StatisticsWidget.prototype), 'init', this).call(this); - var today = new Date().setHours(0, 0, 0, 0) / 1000; + // Create a Date object which represents the start of the day in the + // configured timezone. To do this we convert a UTC time into that timezone, + // reset to the first hour of the day, and then convert back into UTC time. + // We'll be working with seconds rather than milliseconds throughout too. + var today = new Date(); + today.setTime(today.getTime() + app.data.statistics.utcOffset * 1000); + today.setUTCHours(0, 0, 0, 0); + today.setTime(today.getTime() - app.data.statistics.utcOffset * 1000); + today = today / 1000; this.entities = ['users', 'discussions', 'posts']; this.periods = { @@ -135,6 +143,7 @@ System.register('flarum/statistics/components/StatisticsWidget', ['flarum/compon return; } + var offset = app.data.statistics.utcOffset; var period = this.periods[this.selectedPeriod]; var periodLength = period.end - period.start; var labels = []; @@ -145,12 +154,12 @@ System.register('flarum/statistics/components/StatisticsWidget', ['flarum/compon var label = void 0; if (period.step < 86400) { - label = moment.unix(i).format('h A'); + label = moment.unix(i + offset).utc().format('h A'); } else { - label = moment.unix(i).format('D MMM'); + label = moment.unix(i + offset).utc().format('D MMM'); if (period.step > 86400) { - label += ' - ' + moment.unix(i + period.step - 1).format('D MMM'); + label += ' - ' + moment.unix(i + offset + period.step - 1).utc().format('D MMM'); } } @@ -173,7 +182,7 @@ System.register('flarum/statistics/components/StatisticsWidget', ['flarum/compon y_axis_mode: 'span', is_series: 1, show_dots: 0, - colors: ['rgba(0, 0, 0, 0.1)', app.forum.attribute('themePrimaryColor')] + colors: ['rgba(127, 127, 127, 0.2)', app.forum.attribute('themePrimaryColor')] }); } else { context.chart.update_values(datasets, labels); @@ -200,12 +209,12 @@ System.register('flarum/statistics/components/StatisticsWidget', ['flarum/compon }, { key: 'getPeriodCount', value: function getPeriodCount(entity, period) { - var daily = app.data.statistics[entity].daily; + var timed = app.data.statistics[entity].timed; var count = 0; - for (var day in daily) { - if (day >= period.start && day < period.end) { - count += daily[day]; + for (var time in timed) { + if (time >= period.start && time < period.end) { + count += timed[time]; } } diff --git a/extensions/statistics/js/admin/src/components/StatisticsWidget.js b/extensions/statistics/js/admin/src/components/StatisticsWidget.js index 8a1856e85..1c05424a0 100644 --- a/extensions/statistics/js/admin/src/components/StatisticsWidget.js +++ b/extensions/statistics/js/admin/src/components/StatisticsWidget.js @@ -19,7 +19,15 @@ export default class StatisticsWidget extends DashboardWidget { init() { super.init(); - const today = new Date().setHours(0, 0, 0, 0) / 1000; + // Create a Date object which represents the start of the day in the + // configured timezone. To do this we convert a UTC time into that timezone, + // reset to the first hour of the day, and then convert back into UTC time. + // We'll be working with seconds rather than milliseconds throughout too. + let today = new Date(); + today.setTime(today.getTime() + app.data.statistics.utcOffset * 1000); + today.setUTCHours(0, 0, 0, 0); + today.setTime(today.getTime() - app.data.statistics.utcOffset * 1000); + today = today / 1000; this.entities = ['users', 'discussions', 'posts']; this.periods = { @@ -91,6 +99,7 @@ export default class StatisticsWidget extends DashboardWidget { return; } + const offset = app.data.statistics.utcOffset; const period = this.periods[this.selectedPeriod]; const periodLength = period.end - period.start; const labels = []; @@ -101,12 +110,12 @@ export default class StatisticsWidget extends DashboardWidget { let label; if (period.step < 86400) { - label = moment.unix(i).format('h A'); + label = moment.unix(i + offset).utc().format('h A'); } else { - label = moment.unix(i).format('D MMM'); + label = moment.unix(i + offset).utc().format('D MMM'); if (period.step > 86400) { - label += ' - ' + moment.unix(i + period.step - 1).format('D MMM'); + label += ' - ' + moment.unix(i + offset + period.step - 1).utc().format('D MMM'); } } @@ -132,7 +141,7 @@ export default class StatisticsWidget extends DashboardWidget { y_axis_mode: 'span', is_series: 1, show_dots: 0, - colors: ['rgba(0, 0, 0, 0.1)', app.forum.attribute('themePrimaryColor')] + colors: ['rgba(127, 127, 127, 0.2)', app.forum.attribute('themePrimaryColor')] }); } else { context.chart.update_values(datasets, labels); @@ -155,12 +164,12 @@ export default class StatisticsWidget extends DashboardWidget { } getPeriodCount(entity, period) { - const daily = app.data.statistics[entity].daily; + const timed = app.data.statistics[entity].timed; let count = 0; - for (const day in daily) { - if (day >= period.start && day < period.end) { - count += daily[day]; + for (const time in timed) { + if (time >= period.start && time < period.end) { + count += timed[time]; } } diff --git a/extensions/statistics/src/Listener/AddStatisticsData.php b/extensions/statistics/src/Listener/AddStatisticsData.php index dbc36adfe..ae0ccef19 100644 --- a/extensions/statistics/src/Listener/AddStatisticsData.php +++ b/extensions/statistics/src/Listener/AddStatisticsData.php @@ -49,7 +49,10 @@ class AddStatisticsData */ public function addStatisticsData(ConfigureWebApp $event) { - $event->view->setVariable('statistics', $this->getStatistics()); + $event->view->setVariable('statistics', array_merge( + $this->getStatistics(), + ['utcOffset' => $this->getUTCOffset()] + )); } private function getStatistics() @@ -63,40 +66,62 @@ class AddStatisticsData return array_map(function ($entity) { return [ 'total' => $entity[0]->count(), - 'daily' => $this->getDailyCounts($entity[0], $entity[1]) + 'timed' => $this->getTimedCounts($entity[0], $entity[1]) ]; }, $entities); } - private function getDailyCounts(Builder $query, $column) + private function getTimedCounts(Builder $query, $column) { // Calculate the offset between the server timezone (which is used for // dates stored in the database) and the user's timezone (set via the - // settings table). We will use this to adjust dates before aggregating - // daily/hourly statistics. + // settings table). We will use this to make sure we aggregate the + // daily/hourly statistics according to the user's timezone. $offset = $this->getTimezoneOffset(); - return $query + $results = $query ->selectRaw( - 'UNIX_TIMESTAMP( - DATE_FORMAT( - @date := DATE_ADD('.$column.', INTERVAL ? SECOND), -- correct for timezone - IF(@date > ?, \'%Y-%m-%d %H:00:00\', \'%Y-%m-%d\') -- if within the last 48 hours, group by hour - ) - ) as period', + 'DATE_FORMAT( + @date := DATE_ADD('.$column.', INTERVAL ? SECOND), -- correct for timezone + IF(@date > ?, \'%Y-%m-%dT%H:00:00\', \'%Y-%m-%dT00:00:00\') -- if within the last 48 hours, group by hour + ) as time_group', [$offset, new DateTime('-48 hours')] ) ->selectRaw('COUNT(id) as count') ->where($column, '>', new DateTime('-24 months')) - ->groupBy('period') - ->lists('count', 'period'); + ->groupBy('time_group') + ->lists('count', 'time_group'); + + // Now that we have the aggregated statistics, convert each point in + // time into a UNIX timestamp . + $displayTimezone = $this->getDisplayTimezone(); + $timed = []; + + $results->each(function ($count, $time) use (&$timed, $displayTimezone) { + $time = new DateTime($time, $displayTimezone); + $timed[$time->getTimestamp()] = $count; + }); + + return $timed; } private function getTimezoneOffset() { $dataTimezone = new DateTimeZone(date_default_timezone_get()); - $displayTimezone = new DateTimeZone($this->settings->get('flarum-statistics.timezone', date_default_timezone_get())); - return $displayTimezone->getOffset(new DateTime('now', $dataTimezone)); + return $this->getDisplayTimezone()->getOffset(new DateTime('now', $dataTimezone)); } + + private function getUTCOffset() + { + $utcTimezone = new DateTimeZone('UTC'); + + return $this->getDisplayTimezone()->getOffset(new DateTime('now', $utcTimezone)); + } + + private function getDisplayTimezone() + { + return new DateTimeZone($this->settings->get('flarum-statistics.timezone', date_default_timezone_get())); + } + }