class FluidbookStatsController extends Controller
{
-
- public static function getMatomoTokens() {
- // Each stats server has a different instance of Matamo, so we need to provide different API tokens for each
- // Normally this information would be stored in the .env but there's no good way to do that with an array, so
- // it is simpler to keep it here. These are also stored in the shared Bitwarden entry for Matomo.
- return [
- 'stats3.fluidbook.com' => '9df722a0bd30878ddc4d737352427502',
- 'stats4.fluidbook.com' => '3ffdbe052ae625f065573df9fa9515df',
- 'stats5.fluidbook.com' => '85e9cc307b6e5083249949e9472a80b8',
- ];
- }
-
- private function _getReporting($fluidbook_id)
- {
- // Get the appropriate server / API token based on the Fluidbook ID
- // Stats are split across different servers depending on the ID:
- // ID < 21210 = stats3.fluidbook.com
- // ID >= 21210 (even numbers) = stats4.fluidbook.com
- // ID >= 21211 (odd numbers) = stats5.fluidbook.com
-
- $fluidbook_id = intval($fluidbook_id);
-
- if ($fluidbook_id < 21210) {
- $server = 'stats3.fluidbook.com';
- } elseif ($fluidbook_id >= 21210 && $fluidbook_id % 2 === 0) {
- $server = 'stats4.fluidbook.com';
- } else {
- $server = 'stats5.fluidbook.com';
- }
-
- //dump("Server is $server");
-
- $matomo_tokens = self::getMatomoTokens();
-
- return new Reporting("https://{$server}/", $matomo_tokens[$server]);
- }
-
-
- private function _parseDate($date) {
- // Match possible date strings:
- // - YYYY
- // - YYYY-MM
- // - YYYY-MM-DD
- // - YYYY-MM-DD,YYYY-MM-DD
- // https://regex101.com/r/BLrqm0/1
- $regex = '/^(?<start_date>(?<start_year>2\d{3})-?(?<start_month>0[1-9]|1[012])?-?(?<start_day>0[1-9]|[12][0-9]|3[01])?),?(?<end_date>2\d{3}-(?>0[1-9]|1[012])-(?>0[1-9]|[12][0-9]|3[01]))?/';
-
- preg_match($regex, $date, $date_matches);
-
- return $date_matches;
- }
-
- protected function summary($fluidbook_id, $date = null)
- {
- $dates = $date ? $this->_parseDate($date) : false;
-
- $fluidbook = FluidbookPublication::findOrFail($fluidbook_id);
-
-
- // TODO: year(s)? view like the old version: https://workshop.fluidbook.com/stats/10003_ab3cacc39ebbf2478e0931629d114e74
- // Need to calculate all the available dates, probably based on creation date of the Fluidbook
-
- // TODO: month view, breakdown of individual day stats: https://workshop.fluidbook.com/stats/10003_ab3cacc39ebbf2478e0931629d114e74/2017/10
- // These would be linked from the "Year(s)" view above...
-
- // Matomo API
- // We need to pass it a date (eg. "2022-01-01") or date range (eg. "2022-03-01,2022-05-15")
- // We can then specify the granularity of stats by specifying the period:
- // - range = aggregated summary of stats for the specified date range
- // - year = summary of stats broken down by year(s)
- // - month = summary of stats broken down by month(s)
- // - day = summary of stats broken down by day(s)
-
- // Which mode are we in?
- if (isset($dates['start_date']) && isset($dates['end_date'])) { // Custom date range
- $mode = 'range';
- $date_range = "{$dates['start_date']},{$dates['end_date']}";
- $period = 'day'; // Segregate stats by day
-
- } elseif (isset($dates['start_year']) && isset($dates['start_month'])) { // Month view
- $mode = 'month';
- $month = $dates['start_month'];
- $year = $dates['start_year'];
- $last_day_of_month = cal_days_in_month(CAL_GREGORIAN, $month, $year);
- $date_range = "{$year}-{$month}-01,{$year}-{$month}-{$last_day_of_month}";
- $period = 'day'; // Segregate stats by day
-
- } elseif (isset($dates['start_year'])) {
- $mode = 'year';
- $year = $dates['start_year'];
- $end_date = $year == date('Y') ? date('Y-m-d') : "{$year}-12-31"; // If it's the current year, don't get future dates
- $date_range = "{$year}-01-01,{$end_date}"; // Full range of specified year
- $period = 'month'; // Segregate stats by month
-
- } else { // No valid dates specified, display the full data set
- $mode = 'overview';
- $start_date = $fluidbook->created_at->isoFormat('YYYY-MM-DD');
- $date_range = $start_date . ',' . date('Y-m-d');
- $period = 'month'; // Segregate stats by month
- }
-
- // TODO: support the ability to specify a date range from a date-picker and also maybe choose the breakdown (by year/month/day/range)
-
- $report = $this->_getReporting($fluidbook_id);
-
- echo "Getting stats for date range $date_range, segregated by $period";
- //dump(collect($report->getVisits($fluidbook_id, $date_range, $period))->sum('nb_visits'));
- //dump($report->getVisits($fluidbook_id, $date_range, 'range'));
- // dd($report->getVisits($fluidbook_id, $date_range, $period));
- $visits = collect($report->getVisits($fluidbook_id, $date_range, $period));
- $pageviews = collect($report->getPageViews($fluidbook_id, $date_range, $period));
-
- // Get the search keywords as a range because we don't need to display them by date
- $searches = collect($report->getSearchKeywords($fluidbook_id, $date_range, 'range'));
-
- // Format dates for display as labels on the x-axis
- $labels = $visits->keys()->map(function($label, $index) use ($period) {
- return match ($period) {
- 'day' => Carbon::parse($label)->isoFormat('DD'), // Convert YYYY-MM-DD string from API into zero-padded day alone
- 'month' => Carbon::parse($label)->isoFormat('MMM'), // Convert to abbreviated month name
- default => $label,
- };
- })->toArray();
-
- // Format dates for display in the tooltip title
- $formatted_dates = $visits->keys()->map(function($label, $index) use ($period) {
- return match ($period) {
- 'day' => Carbon::parse($label)->isoFormat('dddd Do MMMM YYYY'),
- 'month' => Carbon::parse($label)->isoFormat('MMMM YYYY'),
- default => $label,
- };
- })->toArray();
-
- $chart = Chartisan::build()
- ->labels($labels)
- ->extra(['tooltip_labels' => array_combine($labels, $formatted_dates)])
- ->dataset('Visits', $visits->pluck('nb_visits')->toArray())
- ->dataset('Page Views', $pageviews->pluck('nb_pageviews')->toArray())
- ->toJSON();
-
- // header('Content-Type: application/json; charset=utf-8');
- // return $chart;
-
- return view('fluidbook_stats.summary', compact('fluidbook', 'visits', 'pageviews', 'searches', 'chart', 'mode', 'period', 'dates'));
-
-
- }
-
- protected function API() {
-
- if (!can('superadmin')) {
- // Only allow superadmin access because this is a dev tool and it exposes the API tokens
- return response(null)->setStatusCode('403');
- }
-
- $matomo_tokens = json_encode(static::getMatomoTokens());
-
- return view('fluidbook_stats.API', compact('matomo_tokens'));
- }
-
+ // @see App\Http\Controllers\Admin\Operations\FluidbookPublication\StatsOperation
}
--- /dev/null
+<?php
+
+namespace App\Http\Controllers\Admin\Operations\FluidbookPublication;
+
+use App\Http\Middleware\CheckIfAdmin;
+use App\Models\FluidbookPublication;
+use Carbon\Carbon;
+use Cubist\Matomo\Reporting;
+use Illuminate\Support\Facades\Route;
+
+trait StatsOperation
+{
+ protected function setupStatsRoutes($segment, $routeName, $controller)
+ {
+ Route::get($segment . '/stats/API', $controller . '@statsAPI');
+ // Route is only secured by hash
+ Route::get($segment . '/{id}_{hash}/stats/{date?}', $controller . '@statsSummary')->withoutMiddleware([CheckIfAdmin::class]);
+ }
+
+ protected function setupStatsDefaults()
+ {
+ $this->crud->addButtonFromView('line', 'stats', 'fluidbook_publication.stats', 'end');
+ }
+
+
+ public static function getMatomoTokens()
+ {
+ // Each stats server has a different instance of Matamo, so we need to provide different API tokens for each
+ // Normally this information would be stored in the .env but there's no good way to do that with an array, so
+ // it is simpler to keep it here. These are also stored in the shared Bitwarden entry for Matomo.
+ return [
+ 'stats3.fluidbook.com' => '9df722a0bd30878ddc4d737352427502',
+ 'stats4.fluidbook.com' => '3ffdbe052ae625f065573df9fa9515df',
+ 'stats5.fluidbook.com' => '85e9cc307b6e5083249949e9472a80b8',
+ ];
+ }
+
+ private function _getReporting($fluidbook_id)
+ {
+ // Get the appropriate server / API token based on the Fluidbook ID
+ // Stats are split across different servers depending on the ID:
+ // ID < 21210 = stats3.fluidbook.com
+ // ID >= 21210 (even numbers) = stats4.fluidbook.com
+ // ID >= 21211 (odd numbers) = stats5.fluidbook.com
+
+ $fluidbook_id = intval($fluidbook_id);
+
+ if ($fluidbook_id < 21210) {
+ $server = 'stats3.fluidbook.com';
+ } elseif ($fluidbook_id >= 21210 && $fluidbook_id % 2 === 0) {
+ $server = 'stats4.fluidbook.com';
+ } else {
+ $server = 'stats5.fluidbook.com';
+ }
+
+ //dump("Server is $server");
+
+ $matomo_tokens = self::getMatomoTokens();
+
+ return new Reporting("https://{$server}/", $matomo_tokens[$server]);
+ }
+
+
+ private function _parseDate($date)
+ {
+ // Match possible date strings:
+ // - YYYY
+ // - YYYY-MM
+ // - YYYY-MM-DD
+ // - YYYY-MM-DD,YYYY-MM-DD
+ // https://regex101.com/r/BLrqm0/1
+ $regex = '/^(?<start_date>(?<start_year>2\d{3})-?(?<start_month>0[1-9]|1[012])?-?(?<start_day>0[1-9]|[12][0-9]|3[01])?),?(?<end_date>2\d{3}-(?>0[1-9]|1[012])-(?>0[1-9]|[12][0-9]|3[01]))?/';
+
+ preg_match($regex, $date, $date_matches);
+
+ return $date_matches;
+ }
+
+ protected function statsSummary($fluidbook_id, $hash, $date = null)
+ {
+ $dates = $date ? $this->_parseDate($date) : false;
+
+ $fluidbook = FluidbookPublication::where('id', $fluidbook_id)->where('hash', $hash)->first();
+ if (null === $fluidbook) {
+ abort(404);
+ }
+
+ // TODO: year(s)? view like the old version: https://workshop.fluidbook.com/stats/10003_ab3cacc39ebbf2478e0931629d114e74
+ // Need to calculate all the available dates, probably based on creation date of the Fluidbook
+
+ // TODO: month view, breakdown of individual day stats: https://workshop.fluidbook.com/stats/10003_ab3cacc39ebbf2478e0931629d114e74/2017/10
+ // These would be linked from the "Year(s)" view above...
+
+ // Matomo API
+ // We need to pass it a date (eg. "2022-01-01") or date range (eg. "2022-03-01,2022-05-15")
+ // We can then specify the granularity of stats by specifying the period:
+ // - range = aggregated summary of stats for the specified date range
+ // - year = summary of stats broken down by year(s)
+ // - month = summary of stats broken down by month(s)
+ // - day = summary of stats broken down by day(s)
+
+ // Which mode are we in?
+ if (isset($dates['start_date']) && isset($dates['end_date'])) { // Custom date range
+ $mode = 'range';
+ $date_range = "{$dates['start_date']},{$dates['end_date']}";
+ $period = 'day'; // Segregate stats by day
+
+ } elseif (isset($dates['start_year']) && isset($dates['start_month'])) { // Month view
+ $mode = 'month';
+ $month = $dates['start_month'];
+ $year = $dates['start_year'];
+ $last_day_of_month = cal_days_in_month(CAL_GREGORIAN, $month, $year);
+ $date_range = "{$year}-{$month}-01,{$year}-{$month}-{$last_day_of_month}";
+ $period = 'day'; // Segregate stats by day
+
+ } elseif (isset($dates['start_year'])) {
+ $mode = 'year';
+ $year = $dates['start_year'];
+ $end_date = $year == date('Y') ? date('Y-m-d') : "{$year}-12-31"; // If it's the current year, don't get future dates
+ $date_range = "{$year}-01-01,{$end_date}"; // Full range of specified year
+ $period = 'month'; // Segregate stats by month
+
+ } else { // No valid dates specified, display the full data set
+ $mode = 'overview';
+ $start_date = $fluidbook->created_at->isoFormat('YYYY-MM-DD');
+ $date_range = $start_date . ',' . date('Y-m-d');
+ $period = 'month'; // Segregate stats by month
+ }
+
+ // TODO: support the ability to specify a date range from a date-picker and also maybe choose the breakdown (by year/month/day/range)
+
+ $report = $this->_getReporting($fluidbook_id);
+
+ echo "Getting stats for date range $date_range, segregated by $period";
+ //dump(collect($report->getVisits($fluidbook_id, $date_range, $period))->sum('nb_visits'));
+ //dump($report->getVisits($fluidbook_id, $date_range, 'range'));
+ // dd($report->getVisits($fluidbook_id, $date_range, $period));
+ $visits = collect($report->getVisits($fluidbook_id, $date_range, $period));
+ $pageviews = collect($report->getPageViews($fluidbook_id, $date_range, $period));
+
+ // Get the search keywords as a range because we don't need to display them by date
+ $searches = collect($report->getSearchKeywords($fluidbook_id, $date_range, 'range'));
+
+ // Format dates for display as labels on the x-axis
+ $labels = $visits->keys()->map(function ($label, $index) use ($period) {
+ return match ($period) {
+ 'day' => Carbon::parse($label)->isoFormat('DD'), // Convert YYYY-MM-DD string from API into zero-padded day alone
+ 'month' => Carbon::parse($label)->isoFormat('MMM'), // Convert to abbreviated month name
+ default => $label,
+ };
+ })->toArray();
+
+ // Format dates for display in the tooltip title
+ $formatted_dates = $visits->keys()->map(function ($label, $index) use ($period) {
+ return match ($period) {
+ 'day' => Carbon::parse($label)->isoFormat('dddd Do MMMM YYYY'),
+ 'month' => Carbon::parse($label)->isoFormat('MMMM YYYY'),
+ default => $label,
+ };
+ })->toArray();
+
+// $chart = Chartisan::build()
+// ->labels($labels)
+// ->extra(['tooltip_labels' => array_combine($labels, $formatted_dates)])
+// ->dataset('Visits', $visits->pluck('nb_visits')->toArray())
+// ->dataset('Page Views', $pageviews->pluck('nb_pageviews')->toArray())
+// ->toJSON();
+
+ // header('Content-Type: application/json; charset=utf-8');
+ // return $chart;
+
+ return view('fluidbook_stats.summary', compact('fluidbook', 'visits', 'pageviews', 'searches', /*'chart',*/ 'mode', 'period', 'dates'));
+
+
+ }
+
+ protected function statsAPI()
+ {
+
+ if (!can('superadmin')) {
+ // Only allow superadmin access because this is a dev tool and it exposes the API tokens
+ return response(null)->setStatusCode('403');
+ }
+
+ $matomo_tokens = json_encode(static::getMatomoTokens());
+
+ return view('fluidbook_stats.API', compact('matomo_tokens'));
+ }
+
+}
@section('content')
<h1>{{ __('Statistiques') }}</h1>
- <div class="chart-wrapper" style="position:relative; height: 30vh;">
- <div id="chart"></div>
- </div>
+{{-- <div class="chart-wrapper" style="position:relative; height: 30vh;">--}}
+{{-- <div id="chart"></div>--}}
+{{-- </div>--}}
@dump($fluidbook, $visits, $pageviews, $searches)
@endsection
-@push('after_scripts')
- {{-- Charting library --}}
- <script src="https://unpkg.com/chart.js@^2.9.3/dist/Chart.min.js"></script>
- {{-- Chartisan --}}
- <script src="https://unpkg.com/@chartisan/chartjs@^2.1.0/dist/chartisan_chartjs.umd.js"></script>
+{{--@push('after_scripts')--}}
+{{-- --}}{{-- Charting library --}}
+{{-- <script src="https://unpkg.com/chart.js@^2.9.3/dist/Chart.min.js"></script>--}}
+{{-- --}}{{-- Chartisan --}}
+{{-- <script src="https://unpkg.com/@chartisan/chartjs@^2.1.0/dist/chartisan_chartjs.umd.js"></script>--}}
- <script>
- const bar_offset = 2; // How much to offset each overlapping bar (applied as a -/+ depending on which dataset)
- const chart = new Chartisan({
- el: '#chart',
- data: {!! $chart !!},
- hooks: new ChartisanHooks()
- .colors(['#9ec400', '#888'])
- .tooltip({
- mode: 'index',
- position: 'nearest',
- callbacks: {
- title: function(context) {
- return chart.options.data.chart.extra.tooltip_labels[context[0].label];
- }
- },
- })
- .datasets([{
- barPercentage: 0.9,
- // categoryPercentage: 0.8,
- // maxBarThickness: 50,
- }])
- // Complex options (ref: https://github.com/Chartisan/Chartisan/issues/7#issuecomment-774745067)
- .custom(function({ data, merge, server }) {
- // data -> Contains the current chart configuration
- // data that will be passed to the chart instance.
- // merge -> Contains a function that can be called to merge
- // two javascript objects and returns its merge.
- // server -> Contains the server information in case you need
- // to access the raw information provided by the server.
- // This is mostly used to access the `extra` field.
+{{-- <script>--}}
+{{-- const bar_offset = 2; // How much to offset each overlapping bar (applied as a -/+ depending on which dataset)--}}
+{{-- const chart = new Chartisan({--}}
+{{-- el: '#chart',--}}
+{{-- data: {!! $chart !!},--}}
+{{-- hooks: new ChartisanHooks()--}}
+{{-- .colors(['#9ec400', '#888'])--}}
+{{-- .tooltip({--}}
+{{-- mode: 'index',--}}
+{{-- position: 'nearest',--}}
+{{-- callbacks: {--}}
+{{-- title: function(context) {--}}
+{{-- return chart.options.data.chart.extra.tooltip_labels[context[0].label];--}}
+{{-- }--}}
+{{-- },--}}
+{{-- })--}}
+{{-- .datasets([{--}}
+{{-- barPercentage: 0.9,--}}
+{{-- // categoryPercentage: 0.8,--}}
+{{-- // maxBarThickness: 50,--}}
+{{-- }])--}}
+{{-- // Complex options (ref: https://github.com/Chartisan/Chartisan/issues/7#issuecomment-774745067)--}}
+{{-- .custom(function({ data, merge, server }) {--}}
+{{-- // data -> Contains the current chart configuration--}}
+{{-- // data that will be passed to the chart instance.--}}
+{{-- // merge -> Contains a function that can be called to merge--}}
+{{-- // two javascript objects and returns its merge.--}}
+{{-- // server -> Contains the server information in case you need--}}
+{{-- // to access the raw information provided by the server.--}}
+{{-- // This is mostly used to access the `extra` field.--}}
- //console.log('data', data, 'server', server);
+{{-- //console.log('data', data, 'server', server);--}}
- let merged = merge(data, {
- options: {
- maintainAspectRatio: false,
- // responsive: true,
- scales: {
- xAxes: [{ stacked: true }],
- yAxes: [{ stacked: false }], // Don't stack y-axis: prevents datasets from adding
- },
- },
- plugins: [{
- afterUpdate: function(chart) {
- // Create an offset between the bars
- // Ref: https://stackoverflow.com/questions/62731182/chart-js-creating-a-barchart-with-overlaying-and-offset-bars
- chart.config.data.datasets.forEach(function(dataset, index) {
- // First dataset is moved left (-), second is moved right (+)
- let offset = index % 2 === 0 ? 0 - bar_offset : bar_offset;
+{{-- let merged = merge(data, {--}}
+{{-- options: {--}}
+{{-- maintainAspectRatio: false,--}}
+{{-- // responsive: true,--}}
+{{-- scales: {--}}
+{{-- xAxes: [{ stacked: true }],--}}
+{{-- yAxes: [{ stacked: false }], // Don't stack y-axis: prevents datasets from adding--}}
+{{-- },--}}
+{{-- },--}}
+{{-- plugins: [{--}}
+{{-- afterUpdate: function(chart) {--}}
+{{-- // Create an offset between the bars--}}
+{{-- // Ref: https://stackoverflow.com/questions/62731182/chart-js-creating-a-barchart-with-overlaying-and-offset-bars--}}
+{{-- chart.config.data.datasets.forEach(function(dataset, index) {--}}
+{{-- // First dataset is moved left (-), second is moved right (+)--}}
+{{-- let offset = index % 2 === 0 ? 0 - bar_offset : bar_offset;--}}
- dataset._meta[0].data.forEach(function(entry) {
- let model = entry._model;
- model.x += offset;
- model.controlPointNextX = offset;
- model.controlPointPreviousX = offset;
- });
- });
- }
- }]
- });
+{{-- dataset._meta[0].data.forEach(function(entry) {--}}
+{{-- let model = entry._model;--}}
+{{-- model.x += offset;--}}
+{{-- model.controlPointNextX = offset;--}}
+{{-- model.controlPointPreviousX = offset;--}}
+{{-- });--}}
+{{-- });--}}
+{{-- }--}}
+{{-- }]--}}
+{{-- });--}}
- //console.log("MERGED!", merged)
+{{-- //console.log("MERGED!", merged)--}}
- return merged;
+{{-- return merged;--}}
- // The function must always return the new chart configuration.
- })
- })
- </script>
+{{-- // The function must always return the new chart configuration.--}}
+{{-- })--}}
+{{-- })--}}
+{{-- </script>--}}
-@endpush
+{{--@endpush--}}
--- /dev/null
+@if($entry->stats)
+ <a class="btn btn-sm btn-link" href="{{$crud->route}}/{{$entry->id}}_{{$entry->hash}}/stats"
+ data-toggle="tooltip"
+ title="{{__('Consulter les statistiques')}}"><i class="la la-chart-bar"></i> {{__('Stats')}}
+ </a>
+@endif
Route::get('fluidbookthemepreview/{id}-burger.jpg', 'FluidbookThemePreviewController@previewBurger');
Route::get('fluidbookthemepreview/{id}-menu.jpg', 'FluidbookThemePreviewController@previewMenu');
Route::get('fluidbookthemepreview/{id}.jpg', 'FluidbookThemePreviewController@preview');
- Route::get('fluidbook-stats/API', 'FluidbookStatsController@API');
- Route::get('fluidbook-stats/{fluidbook_id}/{date?}', 'FluidbookStatsController@summary');
});