From 29cb4fe2eb37b3b5517ab282983281b2f80ac0ac Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Tue, 30 Aug 2022 19:51:00 +0200 Subject: [PATCH 01/24] feat: backend support for statistics custom date ranges --- .../src/Api/Controller/ShowStatisticsData.php | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index bbe4718d52..5eae588eb6 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -9,6 +9,7 @@ namespace Flarum\Statistics\Api\Controller; +use Carbon\Carbon; use DateTime; use Flarum\Discussion\Discussion; use Flarum\Http\RequestUtil; @@ -69,18 +70,34 @@ public function handle(ServerRequestInterface $request): ResponseInterface // control panel. $actor->assertAdmin(); - $reportingPeriod = Arr::get($request->getQueryParams(), 'period'); - $model = Arr::get($request->getQueryParams(), 'model'); + $query = $request->getQueryParams(); - return new JsonResponse($this->getResponse($model, $reportingPeriod)); + $reportingPeriod = Arr::get($query, 'period'); + $model = Arr::get($query, 'model'); + $customDateRange = Arr::get($query, 'dateRange'); + + return new JsonResponse($this->getResponse($model, $reportingPeriod, $customDateRange)); } - private function getResponse(?string $model, ?string $period): array + private function getResponse(?string $model, ?string $period, ?array $customDateRange): array { if ($period === 'lifetime') { return $this->getLifetimeStatistics(); } + if ($period === 'custom') { + if (! $customDateRange) { + throw new InvalidParameterException('A custom date range must be specified'); + } + + // We use ms-based timestamp because this is what JS uses, so what the frontend will provide + $startRange = Carbon::createFromTimestampMsUTC($customDateRange['start'])->toDateTime(); + $endRange = Carbon::createFromTimestampMsUTC($customDateRange['end'])->toDateTime(); + + // We can't really cache this + return $this->getTimedCounts($this->entities[$model][0], $this->entities[$model][1], $startRange, $endRange); + } + if (! Arr::exists($this->entities, $model)) { throw new InvalidParameterException(); } @@ -104,7 +121,7 @@ private function getTimedStatistics(string $model) }); } - private function getTimedCounts(Builder $query, $column) + private function getTimedCounts(Builder $query, string $column, DateTime $startDate = new DateTime('-365 days'), DateTime $endDate = new DateTime()) { $results = $query ->selectRaw( @@ -115,7 +132,8 @@ private function getTimedCounts(Builder $query, $column) [new DateTime('-25 hours')] ) ->selectRaw('COUNT(id) as count') - ->where($column, '>', new DateTime('-365 days')) + ->where($column, '>', $startDate) + ->where($column, '<', $endDate) ->groupBy('time_group') ->pluck('count', 'time_group'); From c6571381196b3bd69775d9d62ed79f1fd04f4b9c Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Tue, 30 Aug 2022 22:32:14 +0200 Subject: [PATCH 02/24] feat: use seconds-based timestamps on backend instead --- .../src/Api/Controller/ShowStatisticsData.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index 5eae588eb6..1ec9502cd0 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -85,23 +85,23 @@ private function getResponse(?string $model, ?string $period, ?array $customDate return $this->getLifetimeStatistics(); } + if (! Arr::exists($this->entities, $model)) { + throw new InvalidParameterException('A model must be specified'); + } + if ($period === 'custom') { if (! $customDateRange) { throw new InvalidParameterException('A custom date range must be specified'); } - // We use ms-based timestamp because this is what JS uses, so what the frontend will provide - $startRange = Carbon::createFromTimestampMsUTC($customDateRange['start'])->toDateTime(); - $endRange = Carbon::createFromTimestampMsUTC($customDateRange['end'])->toDateTime(); + // Seconds-based timestamps + $startRange = Carbon::createFromTimestampUTC($customDateRange['start'])->toDateTime(); + $endRange = Carbon::createFromTimestampUTC($customDateRange['end'])->toDateTime(); // We can't really cache this return $this->getTimedCounts($this->entities[$model][0], $this->entities[$model][1], $startRange, $endRange); } - if (! Arr::exists($this->entities, $model)) { - throw new InvalidParameterException(); - } - return $this->getTimedStatistics($model); } From 82ebf32028aa5cc4753e27e430c11a526c9306eb Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sat, 3 Sep 2022 17:23:18 +0100 Subject: [PATCH 03/24] feat: add frontend date selection option --- .../src/admin/components/StatisticsWidget.tsx | 175 +++++++++++++++--- .../StatisticsWidgetDateSelectionModal.tsx | 126 +++++++++++++ extensions/statistics/less/admin.less | 4 + extensions/statistics/locale/en.yml | 13 +- 4 files changed, 296 insertions(+), 22 deletions(-) create mode 100644 extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx index 22dc445f4b..15b42bf4ae 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx @@ -3,11 +3,15 @@ import app from 'flarum/admin/app'; import SelectDropdown from 'flarum/common/components/SelectDropdown'; import Button from 'flarum/common/components/Button'; import abbreviateNumber from 'flarum/common/utils/abbreviateNumber'; +import extractText from 'flarum/common/utils/extractText'; import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; +import Placeholder from 'flarum/common/components/Placeholder'; import icon from 'flarum/common/helpers/icon'; import DashboardWidget, { IDashboardWidgetAttrs } from 'flarum/admin/components/DashboardWidget'; +import StatisticsWidgetDateSelectionModal, { IDateSelection, IStatisticsWidgetDateSelectionModalAttrs } from './StatisticsWidgetDateSelectionModal'; + import type Mithril from 'mithril'; // @ts-expect-error No typings available @@ -25,14 +29,23 @@ export default class StatisticsWidget extends DashboardWidget { chart: any; + customPeriod: IDateSelection | null = null; + timedData: Record = {}; lifetimeData: any; + customPeriodData: Record = {}; + + noData: boolean = false; loadingLifetime = true; loadingTimed: Record = this.entities.reduce((acc, curr) => { acc[curr] = 'unloaded'; return acc; }, {} as Record); + loadingCustom: Record = this.entities.reduce((acc, curr) => { + acc[curr] = 'unloaded'; + return acc; + }, {} as Record); selectedEntity = 'users'; selectedPeriod: undefined | string; @@ -105,17 +118,74 @@ export default class StatisticsWidget extends DashboardWidget { m.redraw(); } + async loadCustomRangeData(model: string): Promise { + this.loadingCustom[model] = 'loading'; + m.redraw(); + + // We clone so we can check that the same period is still selected + // once the HTTP request is complete and the data is to be displayed + const range = { ...this.customPeriod }; + try { + const data = await app.request({ + method: 'GET', + url: app.forum.attribute('apiUrl') + '/statistics', + params: { + period: 'custom', + model, + dateRange: { + start: range.start, + end: range.end, + }, + }, + }); + + if (JSON.stringify(range) !== JSON.stringify(this.customPeriod)) { + // The range this method was called with is no longer the selected. + // Bail out here. + return; + } + + this.customPeriodData[model] = data; + this.loadingCustom[model] = 'loaded'; + + m.redraw(); + } catch (e) { + if (JSON.stringify(range) !== JSON.stringify(this.customPeriod)) { + // The range this method was called with is no longer the selected. + // Bail out here. + return; + } + + console.error(e); + this.loadingCustom[model] = 'fail'; + } + } + className() { return 'StatisticsWidget'; } content() { - const loadingSelectedEntity = this.loadingTimed[this.selectedEntity] !== 'loaded'; - - const thisPeriod = loadingSelectedEntity ? null : this.periods![this.selectedPeriod!]; + const loadingSelectedEntity = (this.selectedPeriod === 'custom' ? this.loadingCustom : this.loadingTimed)[this.selectedEntity] !== 'loaded'; + + const thisPeriod = loadingSelectedEntity + ? null + : this.selectedPeriod === 'custom' + ? { + start: this.customPeriod?.end!, + end: this.customPeriod?.end!, + step: 86400, + } + : this.periods![this.selectedPeriod!]; - if (!this.timedData[this.selectedEntity] && this.loadingTimed[this.selectedEntity] === 'unloaded') { - this.loadTimedData(this.selectedEntity); + if (this.selectedPeriod === 'custom') { + if (!this.customPeriodData[this.selectedEntity] && this.loadingCustom[this.selectedEntity] === 'unloaded') { + this.loadCustomRangeData(this.selectedEntity); + } + } else { + if (!this.timedData[this.selectedEntity] && this.loadingTimed[this.selectedEntity] === 'unloaded') { + this.loadTimedData(this.selectedEntity); + } } return ( @@ -128,16 +198,56 @@ export default class StatisticsWidget extends DashboardWidget { ) : ( - {Object.keys(this.periods!).map((period) => ( - - ))} + {Object.keys(this.periods!) + .map((period) => ( + + )) + .concat([ + , + ])} )} @@ -148,11 +258,14 @@ export default class StatisticsWidget extends DashboardWidget { const thisPeriodCount = loadingSelectedEntity ? app.translator.trans('flarum-statistics.admin.statistics.loading') : this.getPeriodCount(entity, thisPeriod!); - const lastPeriodCount = loadingSelectedEntity - ? app.translator.trans('flarum-statistics.admin.statistics.loading') - : this.getPeriodCount(entity, this.getLastPeriod(thisPeriod!)); + const lastPeriodCount = + this.selectedPeriod === 'custom' + ? null + : loadingSelectedEntity + ? app.translator.trans('flarum-statistics.admin.statistics.loading') + : this.getPeriodCount(entity, this.getLastPeriod(thisPeriod!)); const periodChange = - loadingSelectedEntity || lastPeriodCount === 0 + loadingSelectedEntity || lastPeriodCount === 0 || lastPeriodCount === null ? 0 : (((thisPeriodCount as number) - (lastPeriodCount as number)) / (lastPeriodCount as number)) * 100; @@ -197,6 +310,8 @@ export default class StatisticsWidget extends DashboardWidget { /> )} + + {this.noData && } ); } @@ -206,7 +321,16 @@ export default class StatisticsWidget extends DashboardWidget { return; } - const period = this.periods![this.selectedPeriod!]; + debugger; + + const period = + this.selectedPeriod === 'custom' + ? { + start: this.customPeriod?.start!, + end: this.customPeriod?.end!, + step: 86400, + } + : this.periods![this.selectedPeriod!]; const periodLength = period.end - period.start; const labels = []; const thisPeriod = []; @@ -231,6 +355,15 @@ export default class StatisticsWidget extends DashboardWidget { lastPeriod.push(this.getPeriodCount(this.selectedEntity, { start: i - periodLength, end: i - periodLength + period.step })); } + if (thisPeriod.length === 0) { + this.noData = true; + m.redraw(); + return; + } else { + this.noData = false; + m.redraw(); + } + const datasets = [{ values: lastPeriod }, { values: thisPeriod }]; const data = { labels, @@ -275,7 +408,7 @@ export default class StatisticsWidget extends DashboardWidget { } getPeriodCount(entity: string, period: { start: number; end: number }) { - const timed: Record = this.timedData[entity]; + const timed: Record = (this.selectedPeriod === 'custom' ? this.customPeriodData : this.timedData)[entity]; let count = 0; for (const t in timed) { diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx new file mode 100644 index 0000000000..bb5757c147 --- /dev/null +++ b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx @@ -0,0 +1,126 @@ +import app from 'flarum/admin/app'; +import ItemList from 'flarum/common/utils/ItemList'; +import generateElementId from 'flarum/admin/utils/generateElementId'; +import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal'; + +import Mithril from 'mithril'; +import Button from 'flarum/common/components/Button'; + +export interface IDateSelection { + /** + * Timestamp (seconds, not ms) for start date + */ + start: number; + /** + * Timestamp (seconds, not ms) for end date + */ + end: number; +} + +export interface IStatisticsWidgetDateSelectionModalAttrs extends IInternalModalAttrs { + onModalSubmit: (dates: IDateSelection) => void; + value?: IDateSelection; +} + +interface IStatisticsWidgetDateSelectionModalState { + inputs: { + startDateVal: string; + endDateVal: string; + }; + ids: { + startDate: string; + endDate: string; + }; +} + +export default class StatisticsWidgetDateSelectionModal extends Modal { + /* @ts-expect-error core typings don't allow us to set the type of the state attr :( */ + state: IStatisticsWidgetDateSelectionModalState = { + inputs: { + startDateVal: dayjs().format('YYYY-MM-DD'), + endDateVal: dayjs().format('YYYY-MM-DD'), + }, + ids: { + startDate: generateElementId(), + endDate: generateElementId(), + }, + }; + + oninit(vnode) { + super.oninit(vnode); + + if (this.attrs.value) { + this.state.inputs = { + startDateVal: dayjs(this.attrs.value.start * 1000).format('YYYY-MM-DD'), + endDateVal: dayjs(this.attrs.value.end * 1000).format('YYYY-MM-DD'), + }; + } + } + + className(): string { + return 'StatisticsWidgetDateSelectionModal'; + } + + title(): Mithril.Children { + return app.translator.trans('flarum-statistics.admin.date_selection_modal.title'); + } + + content(): Mithril.Children { + return ; + } + + items(): ItemList { + const items = new ItemList(); + + items.add('intro',

{app.translator.trans('flarum-statistics.admin.date_selection_modal.description')}

, 100); + + items.add( + 'date_start', +
+ + +
, + 90 + ); + + items.add( + 'date_end', +
+ + +
, + 80 + ); + + items.add( + 'submit', + , + 0 + ); + + return items; + } + + updateState(field: keyof IStatisticsWidgetDateSelectionModalState['inputs']): (e: InputEvent) => void { + return (e: InputEvent) => { + this.state.inputs[field] = (e.currentTarget as HTMLInputElement).value; + }; + } + + submitData(): IDateSelection { + // We force 'zulu' time (UTC) + return { + start: Math.floor(new Date(this.state.inputs.startDateVal + 'Z').getTime() / 1000), + end: Math.floor(new Date(this.state.inputs.endDateVal + 'Z').getTime() / 1000), + }; + } + + onsubmit(e: SubmitEvent): void { + e.preventDefault(); + + this.attrs.onModalSubmit(this.submitData()); + this.hide(); + } +} diff --git a/extensions/statistics/less/admin.less b/extensions/statistics/less/admin.less index be1d66405a..3b850896be 100644 --- a/extensions/statistics/less/admin.less +++ b/extensions/statistics/less/admin.less @@ -109,6 +109,10 @@ padding: 12px 16px; text-align: center; } + + .Placeholder { + padding-bottom: 32px; + } } /*! diff --git a/extensions/statistics/locale/en.yml b/extensions/statistics/locale/en.yml index 04fb4eadae..d3200a1185 100644 --- a/extensions/statistics/locale/en.yml +++ b/extensions/statistics/locale/en.yml @@ -1,11 +1,19 @@ flarum-statistics: - ## # UNIQUE KEYS - The following keys are used in only one location each. ## # Translations in this namespace are used by the admin interface. admin: + # These translations are used in the date selection modal. + date_selection_modal: + description: | + Pick a custom date range to display statistics for. Loading data may take + multiple minutes on forums with a lot of activity. + end_date: End date (inclusive) + start_date: Start date (inclusive) + submit_button: Confirm date range + title: Choose custom date range # These translations are used in the Statistics dashboard widget. statistics: @@ -16,9 +24,12 @@ flarum-statistics: mini_heading: Forum statistics previous_28_days_label: Previous 28 days previous_7_days_label: Previous 7 days + custom_label: Choose custom range... + custom_label_specified: "{fromDate} to {toDate}" loading: => core.ref.loading posts_heading: => core.ref.posts today_label: Today total_label: Total users_heading: => core.ref.users view_full: View more statistics + no_data: There is no data available for this date range. From e18130cda1e8e48f6ac1451ef7e9e422873f1cbd Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 04:44:35 +0100 Subject: [PATCH 04/24] feat: add tests for lifetime and timed stats --- .../api/CanRequestLifetimeStatisticsTest.php | 75 ++++++++++++++++ .../api/CanRequestTimedStatisticsTest.php | 89 +++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php create mode 100644 extensions/statistics/tests/integration/api/CanRequestTimedStatisticsTest.php diff --git a/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php b/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php new file mode 100644 index 0000000000..8da9eb4e5a --- /dev/null +++ b/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php @@ -0,0 +1,75 @@ +nowTime = Carbon::now(); + + $this->extension('flarum-statistics'); + + $this->prepareDatabase($this->getDatabaseData()); + } + + protected function getDatabaseData(): array + { + return [ + 'users' => [ + ['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1], + ['id' => 2, 'username' => 'normal', 'email' => 'normal@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->subDays(1)], + ], + 'discussions' => [ + ['id' => 1, 'title' => __CLASS__, 'created_at' => $this->nowTime, 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ['id' => 2, 'title' => __CLASS__, 'created_at' => $this->nowTime->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ['id' => 3, 'title' => __CLASS__, 'created_at' => $this->nowTime->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ['id' => 4, 'title' => __CLASS__, 'created_at' => $this->nowTime->subDays(2), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ], + 'posts' => [ + ['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1], + ['id' => 2, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1], + ['id' => 3, 'discussion_id' => 3, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1], + ['id' => 4, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1], + ['id' => 5, 'discussion_id' => 1, 'user_id' => 2, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 2], + ], + ]; + } + + /** + * @test + */ + public function can_request_lifetime_stats() + { + $response = $this->send( + $this->request('GET', '/api/statistics', ['authenticatedAs' => 1])->withQueryParams([ + 'period' => 'lifetime', + ]) + ); + + $body = json_decode($response->getBody()->getContents(), true); + + $db = $this->getDatabaseData(); + + $this->assertEquals(200, $response->getStatusCode()); + + $this->assertEqualsCanonicalizing( + [ + 'users' => count($db['users']), + 'discussions' => count($db['discussions']), + 'posts' => count($db['posts']), + ], + $body + ); + } +} diff --git a/extensions/statistics/tests/integration/api/CanRequestTimedStatisticsTest.php b/extensions/statistics/tests/integration/api/CanRequestTimedStatisticsTest.php new file mode 100644 index 0000000000..bd7650c8e3 --- /dev/null +++ b/extensions/statistics/tests/integration/api/CanRequestTimedStatisticsTest.php @@ -0,0 +1,89 @@ +nowTime = Carbon::now()->subDays(10); + + $this->extension('flarum-statistics'); + + $this->prepareDatabase($this->getDatabaseData()); + } + + protected function getDatabaseData(): array + { + return [ + 'users' => [ + ['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->copy()], + ['id' => 2, 'username' => 'normal', 'email' => 'normal@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->copy()->subDays(1)], + ], + 'discussions' => [ + ['id' => 1, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ['id' => 2, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ['id' => 3, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ['id' => 4, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(2), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ], + 'posts' => [ + ['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()], + ['id' => 2, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()->subDays(1)], + ['id' => 3, 'discussion_id' => 3, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()->subDays(1)], + ['id' => 4, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()->subDays(2)], + ['id' => 5, 'discussion_id' => 1, 'user_id' => 2, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 2, 'created_at' => $this->nowTime->copy()], + ], + ]; + } + + /** + * @test + */ + public function can_request_timed_stats() + { + $time = $this->nowTime->copy(); + $time->setTime(0, 0, 0, 0); + + $models = [ + "users" => [ + $time->copy()->getTimestamp() => 1, + $time->copy()->subDays(1)->getTimestamp() => 1, + ], "discussions" => [ + $time->copy()->getTimestamp() => 1, + $time->copy()->subDays(1)->getTimestamp() => 2, + $time->copy()->subDays(2)->getTimestamp() => 1, + ], "posts" => [ + $time->copy()->getTimestamp() => 2, + $time->copy()->subDays(1)->getTimestamp() => 2, + $time->copy()->subDays(2)->getTimestamp() => 1, + ] + ]; + + foreach ($models as $model => $data) { + $response = $this->send( + $this->request('GET', '/api/statistics', ['authenticatedAs' => 1])->withQueryParams([ + 'model' => $model, + ]) + ); + + $body = json_decode($response->getBody()->getContents(), true); + + $this->assertEquals(200, $response->getStatusCode()); + + $this->assertEqualsCanonicalizing( + $data, + $body + ); + } + } +} From 555f865a9ab3eca3c975d82db2a9adb3f1017355 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 4 Sep 2022 03:52:12 +0000 Subject: [PATCH 05/24] Apply fixes from StyleCI --- .../api/CanRequestLifetimeStatisticsTest.php | 9 ++++++++- .../api/CanRequestTimedStatisticsTest.php | 15 +++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php b/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php index 8da9eb4e5a..aa35e07717 100644 --- a/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php +++ b/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php @@ -1,5 +1,12 @@ setTime(0, 0, 0, 0); $models = [ - "users" => [ + 'users' => [ $time->copy()->getTimestamp() => 1, $time->copy()->subDays(1)->getTimestamp() => 1, - ], "discussions" => [ + ], 'discussions' => [ $time->copy()->getTimestamp() => 1, $time->copy()->subDays(1)->getTimestamp() => 2, $time->copy()->subDays(2)->getTimestamp() => 1, - ], "posts" => [ + ], 'posts' => [ $time->copy()->getTimestamp() => 2, $time->copy()->subDays(1)->getTimestamp() => 2, $time->copy()->subDays(2)->getTimestamp() => 1, From 3a177415714ade5443415608aada46c7739b6969 Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 16:55:40 +0100 Subject: [PATCH 06/24] fix: add error alert when end date is after start date --- .../StatisticsWidgetDateSelectionModal.tsx | 12 +++++++++++- extensions/statistics/locale/en.yml | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx index bb5757c147..127de92f92 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx @@ -120,7 +120,17 @@ export default class StatisticsWidgetDateSelectionModal extends Modal Date: Sun, 4 Sep 2022 16:55:50 +0100 Subject: [PATCH 07/24] fix: wrong label --- .../src/admin/components/StatisticsWidgetDateSelectionModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx index 127de92f92..7046df6225 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx @@ -86,7 +86,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal - + , 80 From 247060b2012508a0d99c1bc9f24b9e1cfd73fb9b Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 16:56:14 +0100 Subject: [PATCH 08/24] fix: no data when start and end date are same day --- .../StatisticsWidgetDateSelectionModal.tsx | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx index 7046df6225..97867dc450 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx @@ -6,6 +6,11 @@ import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal'; import Mithril from 'mithril'; import Button from 'flarum/common/components/Button'; +import dayjsUtc from 'dayjs/plugin/utc'; + +// @ts-expect-error dayjs plugin typings not available +dayjs.extend(dayjsUtc); + export interface IDateSelection { /** * Timestamp (seconds, not ms) for start date @@ -51,8 +56,10 @@ export default class StatisticsWidgetDateSelectionModal extends Modal Date: Sun, 4 Sep 2022 16:56:36 +0100 Subject: [PATCH 09/24] fix: use utc dayjs for formatting custom date range on widget --- .../src/admin/components/StatisticsWidget.tsx | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx index 15b42bf4ae..e882940314 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx @@ -14,6 +14,14 @@ import StatisticsWidgetDateSelectionModal, { IDateSelection, IStatisticsWidgetDa import type Mithril from 'mithril'; +import dayjsUtc from 'dayjs/plugin/utc'; +import dayjsLocalizedFormat from 'dayjs/plugin/localizedFormat'; + +// @ts-expect-error dayjs plugin typings not available +dayjs.extend(dayjsUtc); +// @ts-expect-error dayjs plugin typings not available +dayjs.extend(dayjsLocalizedFormat); + // @ts-expect-error No typings available import { Chart } from 'frappe-charts'; @@ -241,8 +249,10 @@ export default class StatisticsWidget extends DashboardWidget { {this.selectedPeriod === 'custom' ? extractText( app.translator.trans(`flarum-statistics.admin.statistics.custom_label_specified`, { - fromDate: dayjs(this.customPeriod!.start! * 1000).format('DD MMM YYYY'), - toDate: dayjs(this.customPeriod!.end! * 1000).format('DD MMM YYYY'), + // @ts-expect-error dayjs plugin typings not available + fromDate: dayjs.utc(this.customPeriod!.start! * 1000).format('ll'), + // @ts-expect-error dayjs plugin typings not available + toDate: dayjs.utc(this.customPeriod!.end! * 1000).format('ll'), }) ) : app.translator.trans(`flarum-statistics.admin.statistics.custom_label`)} @@ -321,8 +331,6 @@ export default class StatisticsWidget extends DashboardWidget { return; } - debugger; - const period = this.selectedPeriod === 'custom' ? { @@ -340,12 +348,20 @@ export default class StatisticsWidget extends DashboardWidget { let label; if (period.step < 86400) { - label = dayjs.unix(i).format('h A'); + // @ts-expect-error dayjs plugin typings not available + label = dayjs.unix(i).utc().format('h A'); } else { - label = dayjs.unix(i).format('D MMM'); + // @ts-expect-error dayjs plugin typings not available + label = dayjs.unix(i).utc().format('D MMM'); if (period.step > 86400) { - label += ' - ' + dayjs.unix(i + period.step - 1).format('D MMM'); + // @ts-expect-error dayjs plugin typings not available + label += + ' - ' + + dayjs + .unix(i + period.step - 1) + .utc() + .format('D MMM'); } } From b053661cf40c79c57c5be3788300b66513b9f92e Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 16:57:05 +0100 Subject: [PATCH 10/24] chore: add dayjs as project dep --- extensions/statistics/js/package.json | 13 +++++++------ yarn.lock | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/extensions/statistics/js/package.json b/extensions/statistics/js/package.json index 0dcbc07994..79fa7715f4 100644 --- a/extensions/statistics/js/package.json +++ b/extensions/statistics/js/package.json @@ -4,18 +4,19 @@ "version": "0.0.0", "prettier": "@flarum/prettier-config", "dependencies": { + "dayjs": "^1.11.5", "frappe-charts": "^1.6.2" }, "devDependencies": { - "@types/mithril": "^2.0.11", - "prettier": "^2.7.1", - "flarum-webpack-config": "^2.0.0", - "webpack": "^5.73.0", - "webpack-cli": "^4.10.0", "@flarum/prettier-config": "^1.0.0", + "@types/mithril": "^2.0.11", "flarum-tsconfig": "^1.0.2", + "flarum-webpack-config": "^2.0.0", + "prettier": "^2.7.1", "typescript": "^4.7.4", - "typescript-coverage-report": "^0.6.4" + "typescript-coverage-report": "^0.6.4", + "webpack": "^5.73.0", + "webpack-cli": "^4.10.0" }, "scripts": { "dev": "webpack --mode development --watch", diff --git a/yarn.lock b/yarn.lock index 8c19fbdb86..4b630bbd1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1739,6 +1739,11 @@ dayjs@^1.10.4, dayjs@^1.10.7: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41" integrity sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow== +dayjs@^1.11.5: + version "1.11.5" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93" + integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA== + debug@^4.1.0, debug@^4.1.1: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" From 5802c60903fc066981e86424c69233cefcf0815d Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 17:43:36 +0100 Subject: [PATCH 11/24] fix: make end date inclusive --- extensions/statistics/src/Api/Controller/ShowStatisticsData.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index 1ec9502cd0..52fd777c97 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -133,7 +133,7 @@ private function getTimedCounts(Builder $query, string $column, DateTime $startD ) ->selectRaw('COUNT(id) as count') ->where($column, '>', $startDate) - ->where($column, '<', $endDate) + ->where($column, '<=', $endDate) ->groupBy('time_group') ->pluck('count', 'time_group'); From 04068b98ecded712c4171e5007f7eb63b8a1b4ab Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 17:43:47 +0100 Subject: [PATCH 12/24] feat: add integration test for custom date period --- .../CanRequestCustomTimedStatisticsTest.php | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php diff --git a/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php b/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php new file mode 100644 index 0000000000..30f17c3185 --- /dev/null +++ b/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php @@ -0,0 +1,97 @@ +nowTime = Carbon::now()->subDays(10); + + $this->extension('flarum-statistics'); + + $this->prepareDatabase($this->getDatabaseData()); + } + + protected function getDatabaseData(): array + { + return [ + 'users' => [ + ['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->copy()], + ['id' => 2, 'username' => 'normal', 'email' => 'normal@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->copy()->subDays(1)], + ['id' => 3, 'username' => 'normal2', 'email' => 'normal2@machine.local', 'is_email_confirmed' => 1, 'joined_at' => $this->nowTime->copy()->subDays(2)], + ], + 'discussions' => [ + ['id' => 1, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ['id' => 2, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ['id' => 3, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(1), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ['id' => 4, 'title' => __CLASS__, 'created_at' => $this->nowTime->copy()->subDays(2), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1], + ], + 'posts' => [ + ['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()], + ['id' => 2, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()->subDays(1)], + ['id' => 3, 'discussion_id' => 3, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()->subDays(1)], + ['id' => 4, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 1, 'created_at' => $this->nowTime->copy()->subDays(2)], + ['id' => 5, 'discussion_id' => 1, 'user_id' => 2, 'type' => 'comment', 'content' => '

Text

', 'is_private' => 0, 'number' => 2, 'created_at' => $this->nowTime->copy()], + ], + ]; + } + + /** + * @test + */ + public function can_request_timed_stats() + { + $time = $this->nowTime->copy(); + + $start = $time->copy()->subDays(1)->startOfDay()->getTimestamp(); + $end = $time->copy()->endOfDay()->getTimestamp(); + + $timeStart = $time->copy()->startOfDay(); + + $models = [ + "users" => [ + $timeStart->copy()->getTimestamp() => 1, + $timeStart->copy()->subDays(1)->getTimestamp() => 1, + ], "discussions" => [ + $timeStart->copy()->getTimestamp() => 1, + $timeStart->copy()->subDays(1)->getTimestamp() => 2, + ], "posts" => [ + $timeStart->copy()->getTimestamp() => 2, + $timeStart->copy()->subDays(1)->getTimestamp() => 2, + ] + ]; + + foreach ($models as $model => $data) { + $response = $this->send( + $this->request('GET', '/api/statistics', ['authenticatedAs' => 1])->withQueryParams([ + 'model' => $model, + 'period' => 'custom', + 'dateRange' => [ + 'start' => $start, + 'end' => $end, + ], + ]) + ); + + $body = json_decode($response->getBody()->getContents(), true); + + $this->assertEquals(200, $response->getStatusCode()); + + $this->assertEquals( + $data, + $body, + ); + } + } +} From 9f1ad48133af88c8d85f8e0e0cfbf1b9b1f688bf Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 4 Sep 2022 16:44:26 +0000 Subject: [PATCH 13/24] Apply fixes from StyleCI --- .../api/CanRequestCustomTimedStatisticsTest.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php b/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php index 30f17c3185..ca87dc5b46 100644 --- a/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php +++ b/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php @@ -1,5 +1,12 @@ copy()->startOfDay(); $models = [ - "users" => [ + 'users' => [ $timeStart->copy()->getTimestamp() => 1, $timeStart->copy()->subDays(1)->getTimestamp() => 1, - ], "discussions" => [ + ], 'discussions' => [ $timeStart->copy()->getTimestamp() => 1, $timeStart->copy()->subDays(1)->getTimestamp() => 2, - ], "posts" => [ + ], 'posts' => [ $timeStart->copy()->getTimestamp() => 2, $timeStart->copy()->subDays(1)->getTimestamp() => 2, ] From b9e9dc8553e8495637b014b03e17481fdee9d58a Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 18:01:07 +0100 Subject: [PATCH 14/24] fix: incorrect ts expect error comment --- .../statistics/js/src/admin/components/StatisticsWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx index e882940314..650feaba24 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx @@ -355,11 +355,11 @@ export default class StatisticsWidget extends DashboardWidget { label = dayjs.unix(i).utc().format('D MMM'); if (period.step > 86400) { - // @ts-expect-error dayjs plugin typings not available label += ' - ' + dayjs .unix(i + period.step - 1) + // @ts-expect-error dayjs plugin typings not available .utc() .format('D MMM'); } From f6d9b9dbf0168e2cdbaa7e1f700f36e3a500fbfe Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 18:01:14 +0100 Subject: [PATCH 15/24] fix: add missing type --- .../src/admin/components/StatisticsWidgetDateSelectionModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx index 97867dc450..9e109b8f9f 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx @@ -51,7 +51,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal) { super.oninit(vnode); if (this.attrs.value) { From 05ecfa0ca6f9ee09c57458f17432384d67969264 Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 18:05:54 +0100 Subject: [PATCH 16/24] fix: typing errors --- extensions/statistics/js/package.json | 1 - .../js/src/admin/components/StatisticsWidget.tsx | 8 +------- .../components/StatisticsWidgetDateSelectionModal.tsx | 6 +----- yarn.lock | 5 ----- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/extensions/statistics/js/package.json b/extensions/statistics/js/package.json index 79fa7715f4..42295cba2b 100644 --- a/extensions/statistics/js/package.json +++ b/extensions/statistics/js/package.json @@ -4,7 +4,6 @@ "version": "0.0.0", "prettier": "@flarum/prettier-config", "dependencies": { - "dayjs": "^1.11.5", "frappe-charts": "^1.6.2" }, "devDependencies": { diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx index 650feaba24..ae01b9b150 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx @@ -14,12 +14,11 @@ import StatisticsWidgetDateSelectionModal, { IDateSelection, IStatisticsWidgetDa import type Mithril from 'mithril'; +import dayjs from 'dayjs'; import dayjsUtc from 'dayjs/plugin/utc'; import dayjsLocalizedFormat from 'dayjs/plugin/localizedFormat'; -// @ts-expect-error dayjs plugin typings not available dayjs.extend(dayjsUtc); -// @ts-expect-error dayjs plugin typings not available dayjs.extend(dayjsLocalizedFormat); // @ts-expect-error No typings available @@ -249,9 +248,7 @@ export default class StatisticsWidget extends DashboardWidget { {this.selectedPeriod === 'custom' ? extractText( app.translator.trans(`flarum-statistics.admin.statistics.custom_label_specified`, { - // @ts-expect-error dayjs plugin typings not available fromDate: dayjs.utc(this.customPeriod!.start! * 1000).format('ll'), - // @ts-expect-error dayjs plugin typings not available toDate: dayjs.utc(this.customPeriod!.end! * 1000).format('ll'), }) ) @@ -348,10 +345,8 @@ export default class StatisticsWidget extends DashboardWidget { let label; if (period.step < 86400) { - // @ts-expect-error dayjs plugin typings not available label = dayjs.unix(i).utc().format('h A'); } else { - // @ts-expect-error dayjs plugin typings not available label = dayjs.unix(i).utc().format('D MMM'); if (period.step > 86400) { @@ -359,7 +354,6 @@ export default class StatisticsWidget extends DashboardWidget { ' - ' + dayjs .unix(i + period.step - 1) - // @ts-expect-error dayjs plugin typings not available .utc() .format('D MMM'); } diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx index 9e109b8f9f..12b87c9feb 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx @@ -6,9 +6,9 @@ import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal'; import Mithril from 'mithril'; import Button from 'flarum/common/components/Button'; +import dayjs from 'dayjs'; import dayjsUtc from 'dayjs/plugin/utc'; -// @ts-expect-error dayjs plugin typings not available dayjs.extend(dayjsUtc); export interface IDateSelection { @@ -56,9 +56,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal Date: Sun, 4 Sep 2022 19:04:24 +0100 Subject: [PATCH 17/24] fix(tests): remove type from class attribute definition --- .../integration/api/CanRequestCustomTimedStatisticsTest.php | 5 ++++- .../integration/api/CanRequestLifetimeStatisticsTest.php | 5 ++++- .../tests/integration/api/CanRequestTimedStatisticsTest.php | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php b/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php index ca87dc5b46..e1fbd40d0b 100644 --- a/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php +++ b/extensions/statistics/tests/integration/api/CanRequestCustomTimedStatisticsTest.php @@ -17,7 +17,10 @@ class CanRequestCustomTimedStatisticsTest extends TestCase { use RetrievesAuthorizedUsers; - protected Carbon $nowTime; + /** + * @var Carbon + */ + protected $nowTime; protected function setUp(): void { diff --git a/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php b/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php index aa35e07717..3a7e01746f 100644 --- a/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php +++ b/extensions/statistics/tests/integration/api/CanRequestLifetimeStatisticsTest.php @@ -17,7 +17,10 @@ class CanRequestLifetimeStatisticsTest extends TestCase { use RetrievesAuthorizedUsers; - protected Carbon $nowTime; + /** + * @var Carbon + */ + protected $nowTime; protected function setUp(): void { diff --git a/extensions/statistics/tests/integration/api/CanRequestTimedStatisticsTest.php b/extensions/statistics/tests/integration/api/CanRequestTimedStatisticsTest.php index a6c28d7750..8dabfc6e7c 100644 --- a/extensions/statistics/tests/integration/api/CanRequestTimedStatisticsTest.php +++ b/extensions/statistics/tests/integration/api/CanRequestTimedStatisticsTest.php @@ -17,7 +17,10 @@ class CanRequestTimedStatisticsTest extends TestCase { use RetrievesAuthorizedUsers; - protected Carbon $nowTime; + /** + * @var Carbon + */ + protected $nowTime; protected function setUp(): void { From a5ad687479d8c38fa5fe47dc35748ca1ba0f5ab3 Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 19:31:16 +0100 Subject: [PATCH 18/24] fix: extract default values to function body --- .../src/Api/Controller/ShowStatisticsData.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index 52fd777c97..0f51c6faac 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -85,12 +85,12 @@ private function getResponse(?string $model, ?string $period, ?array $customDate return $this->getLifetimeStatistics(); } - if (! Arr::exists($this->entities, $model)) { + if (!Arr::exists($this->entities, $model)) { throw new InvalidParameterException('A model must be specified'); } if ($period === 'custom') { - if (! $customDateRange) { + if (!$customDateRange) { throw new InvalidParameterException('A custom date range must be specified'); } @@ -121,12 +121,20 @@ private function getTimedStatistics(string $model) }); } - private function getTimedCounts(Builder $query, string $column, DateTime $startDate = new DateTime('-365 days'), DateTime $endDate = new DateTime()) + private function getTimedCounts(Builder $query, string $column, ?DateTime $startDate = null, ?DateTime $endDate = null) { + if (!isset($startDate)) { + $startDate = new DateTime('-365 days'); + } + + if (!isset($endDate)) { + $startDate = new DateTime(); + } + $results = $query ->selectRaw( 'DATE_FORMAT( - @date := '.$column.', + @date := ' . $column . ', IF(@date > ?, \'%Y-%m-%d %H:00:00\', \'%Y-%m-%d\') -- if within the last 24 hours, group by hour ) as time_group', [new DateTime('-25 hours')] From 84c9a671f94920917fb7787ea1faba13bcdeb976 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 4 Sep 2022 18:31:31 +0000 Subject: [PATCH 19/24] Apply fixes from StyleCI --- .../src/Api/Controller/ShowStatisticsData.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index 0f51c6faac..ec25e24f98 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -85,12 +85,12 @@ private function getResponse(?string $model, ?string $period, ?array $customDate return $this->getLifetimeStatistics(); } - if (!Arr::exists($this->entities, $model)) { + if (! Arr::exists($this->entities, $model)) { throw new InvalidParameterException('A model must be specified'); } if ($period === 'custom') { - if (!$customDateRange) { + if (! $customDateRange) { throw new InvalidParameterException('A custom date range must be specified'); } @@ -123,18 +123,18 @@ private function getTimedStatistics(string $model) private function getTimedCounts(Builder $query, string $column, ?DateTime $startDate = null, ?DateTime $endDate = null) { - if (!isset($startDate)) { + if (! isset($startDate)) { $startDate = new DateTime('-365 days'); } - if (!isset($endDate)) { + if (! isset($endDate)) { $startDate = new DateTime(); } $results = $query ->selectRaw( 'DATE_FORMAT( - @date := ' . $column . ', + @date := '.$column.', IF(@date > ?, \'%Y-%m-%d %H:00:00\', \'%Y-%m-%d\') -- if within the last 24 hours, group by hour ) as time_group', [new DateTime('-25 hours')] From 362e4cebd287c52513e71e7d61f567a7735cfbb1 Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Sun, 4 Sep 2022 19:39:30 +0100 Subject: [PATCH 20/24] fix: typo --- extensions/statistics/src/Api/Controller/ShowStatisticsData.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index ec25e24f98..1682aba7bf 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -128,7 +128,7 @@ private function getTimedCounts(Builder $query, string $column, ?DateTime $start } if (! isset($endDate)) { - $startDate = new DateTime(); + $endDate = new DateTime(); } $results = $query From 7ea4ca642e7d3b83a9a18f0363798a532f5c8695 Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Tue, 20 Sep 2022 14:32:15 +0100 Subject: [PATCH 21/24] chore: use small modal Co-authored-by: Sami Mazouz --- .../src/admin/components/StatisticsWidgetDateSelectionModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx index 12b87c9feb..3f7688daea 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx @@ -63,7 +63,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal Date: Tue, 20 Sep 2022 14:32:42 +0100 Subject: [PATCH 22/24] fix: add missing `FormControl` class Co-authored-by: Sami Mazouz --- .../admin/components/StatisticsWidgetDateSelectionModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx index 3f7688daea..809eecb6ee 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx @@ -83,7 +83,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal - + , 90 ); @@ -92,7 +92,7 @@ export default class StatisticsWidgetDateSelectionModal extends Modal - + , 80 ); From 6cc357b7d10e5e4181aa34d8d419dc35f0281f06 Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Tue, 20 Sep 2022 14:33:29 +0100 Subject: [PATCH 23/24] fix: cast url params to int to enforce type Co-authored-by: Sami Mazouz --- .../statistics/src/Api/Controller/ShowStatisticsData.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php index 1682aba7bf..0040c80c51 100644 --- a/extensions/statistics/src/Api/Controller/ShowStatisticsData.php +++ b/extensions/statistics/src/Api/Controller/ShowStatisticsData.php @@ -90,13 +90,16 @@ private function getResponse(?string $model, ?string $period, ?array $customDate } if ($period === 'custom') { - if (! $customDateRange) { + $start = (int) $customDateRange['start']; + $end = (int) $customDateRange['end']; + + if (! $customDateRange || ! $start || ! $end) { throw new InvalidParameterException('A custom date range must be specified'); } // Seconds-based timestamps - $startRange = Carbon::createFromTimestampUTC($customDateRange['start'])->toDateTime(); - $endRange = Carbon::createFromTimestampUTC($customDateRange['end'])->toDateTime(); + $startRange = Carbon::createFromTimestampUTC($start)->toDateTime(); + $endRange = Carbon::createFromTimestampUTC($end)->toDateTime(); // We can't really cache this return $this->getTimedCounts($this->entities[$model][0], $this->entities[$model][1], $startRange, $endRange); From 28ebeea78b938b30711a04770774ec9db291b258 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Thu, 29 Sep 2022 11:52:35 +0100 Subject: [PATCH 24/24] chore: `yarn format` Signed-off-by: Sami Mazouz --- .../StatisticsWidgetDateSelectionModal.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx index 809eecb6ee..86d5f251f5 100644 --- a/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx +++ b/extensions/statistics/js/src/admin/components/StatisticsWidgetDateSelectionModal.tsx @@ -83,7 +83,13 @@ export default class StatisticsWidgetDateSelectionModal extends Modal - + , 90 ); @@ -92,7 +98,13 @@ export default class StatisticsWidgetDateSelectionModal extends Modal - + , 80 );