xref: /webtrees/app/Module/RecentChangesModule.php (revision 0dcd9387ba2c042491548d2300fa2bf341235d8d)
18c2e8227SGreg Roach<?php
23976b470SGreg Roach
38c2e8227SGreg Roach/**
48c2e8227SGreg Roach * webtrees: online genealogy
5*0dcd9387SGreg Roach * Copyright (C) 2020 webtrees development team
68c2e8227SGreg Roach * This program is free software: you can redistribute it and/or modify
78c2e8227SGreg Roach * it under the terms of the GNU General Public License as published by
88c2e8227SGreg Roach * the Free Software Foundation, either version 3 of the License, or
98c2e8227SGreg Roach * (at your option) any later version.
108c2e8227SGreg Roach * This program is distributed in the hope that it will be useful,
118c2e8227SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
128c2e8227SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
138c2e8227SGreg Roach * GNU General Public License for more details.
148c2e8227SGreg Roach * You should have received a copy of the GNU General Public License
158c2e8227SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
168c2e8227SGreg Roach */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
2076692c8bSGreg Roachnamespace Fisharebest\Webtrees\Module;
2176692c8bSGreg Roach
224459dc9aSGreg Roachuse Fisharebest\Webtrees\Carbon;
236e45321fSGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
240e62c4b8SGreg Roachuse Fisharebest\Webtrees\I18N;
25bd77bf38SGreg Roachuse Fisharebest\Webtrees\Services\UserService;
266e45321fSGreg Roachuse Fisharebest\Webtrees\Tree;
2777654037SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
28bd77bf38SGreg Roachuse Illuminate\Database\Query\Expression;
29bd77bf38SGreg Roachuse Illuminate\Support\Collection;
301e7a7a28SGreg Roachuse Illuminate\Support\Str;
316ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
32bd77bf38SGreg Roachuse stdClass;
33bd77bf38SGreg Roach
34bd77bf38SGreg Roachuse function extract;
35bd77bf38SGreg Roachuse function view;
36bd77bf38SGreg Roach
37bd77bf38SGreg Roachuse const EXTR_OVERWRITE;
388c2e8227SGreg Roach
398c2e8227SGreg Roach/**
408c2e8227SGreg Roach * Class RecentChangesModule
418c2e8227SGreg Roach */
4237eb8894SGreg Roachclass RecentChangesModule extends AbstractModule implements ModuleBlockInterface
43c1010edaSGreg Roach{
4449a243cbSGreg Roach    use ModuleBlockTrait;
4549a243cbSGreg Roach
4616d6367aSGreg Roach    private const DEFAULT_DAYS       = '7';
4716d6367aSGreg Roach    private const DEFAULT_SHOW_USER  = '1';
4816d6367aSGreg Roach    private const DEFAULT_SORT_STYLE = 'date_desc';
4916d6367aSGreg Roach    private const DEFAULT_INFO_STYLE = 'table';
5016d6367aSGreg Roach    private const MAX_DAYS           = 90;
518c2e8227SGreg Roach
5201221f27SGreg Roach    // Pagination
5301221f27SGreg Roach    private const LIMIT_LOW  = 10;
5401221f27SGreg Roach    private const LIMIT_HIGH = 20;
5501221f27SGreg Roach
56bd77bf38SGreg Roach    /** @var UserService */
57bd77bf38SGreg Roach    private $user_service;
58bd77bf38SGreg Roach
59bd77bf38SGreg Roach    /**
60bd77bf38SGreg Roach     * RecentChangesModule constructor.
61bd77bf38SGreg Roach     *
62bd77bf38SGreg Roach     * @param UserService $user_service
63bd77bf38SGreg Roach     */
64bd77bf38SGreg Roach    public function __construct(UserService $user_service)
65bd77bf38SGreg Roach    {
66bd77bf38SGreg Roach        $this->user_service = $user_service;
67bd77bf38SGreg Roach    }
68bd77bf38SGreg Roach
69961ec755SGreg Roach    /**
700cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
71961ec755SGreg Roach     *
72961ec755SGreg Roach     * @return string
73961ec755SGreg Roach     */
7449a243cbSGreg Roach    public function title(): string
75c1010edaSGreg Roach    {
76bbb76c12SGreg Roach        /* I18N: Name of a module */
77bbb76c12SGreg Roach        return I18N::translate('Recent changes');
788c2e8227SGreg Roach    }
798c2e8227SGreg Roach
80961ec755SGreg Roach    /**
81961ec755SGreg Roach     * A sentence describing what this module does.
82961ec755SGreg Roach     *
83961ec755SGreg Roach     * @return string
84961ec755SGreg Roach     */
8549a243cbSGreg Roach    public function description(): string
86c1010edaSGreg Roach    {
87bbb76c12SGreg Roach        /* I18N: Description of the “Recent changes” module */
88bbb76c12SGreg Roach        return I18N::translate('A list of records that have been updated recently.');
898c2e8227SGreg Roach    }
908c2e8227SGreg Roach
91*0dcd9387SGreg Roach    /**
92*0dcd9387SGreg Roach     * @param Tree   $tree
93*0dcd9387SGreg Roach     * @param int    $block_id
94*0dcd9387SGreg Roach     * @param string $context
95*0dcd9387SGreg Roach     * @param array  $config
96*0dcd9387SGreg Roach     *
97*0dcd9387SGreg Roach     * @return string
98*0dcd9387SGreg Roach     */
993caaa4d2SGreg Roach    public function getBlock(Tree $tree, int $block_id, string $context, array $config = []): string
100c1010edaSGreg Roach    {
10155664801SGreg Roach        $days      = (int) $this->getBlockSetting($block_id, 'days', self::DEFAULT_DAYS);
1026e45321fSGreg Roach        $infoStyle = $this->getBlockSetting($block_id, 'infoStyle', self::DEFAULT_INFO_STYLE);
1036e45321fSGreg Roach        $sortStyle = $this->getBlockSetting($block_id, 'sortStyle', self::DEFAULT_SORT_STYLE);
104047cb287SGreg Roach        $show_user = (bool) $this->getBlockSetting($block_id, 'show_user', self::DEFAULT_SHOW_USER);
1058c2e8227SGreg Roach
1063caaa4d2SGreg Roach        extract($config, EXTR_OVERWRITE);
1078c2e8227SGreg Roach
108bd77bf38SGreg Roach        $rows = $this->getRecentChanges($tree, $days);
1098c2e8227SGreg Roach
1100280f44aSGreg Roach        switch ($sortStyle) {
1110280f44aSGreg Roach            case 'name':
112bd77bf38SGreg Roach                $rows = $rows->sort(static function (stdClass $x, stdClass $y): int {
113bd77bf38SGreg Roach                    return GedcomRecord::nameComparator()($x->record, $y->record);
114bd77bf38SGreg Roach                });
115fbd9d3f8SGreg Roach                $order = [[1, 'asc']];
1168c2e8227SGreg Roach                break;
11780e23601SGreg Roach
1180280f44aSGreg Roach            case 'date_asc':
119bd77bf38SGreg Roach                $rows = $rows->sort(static function (stdClass $x, stdClass $y): int {
120bd77bf38SGreg Roach                    return $x->time <=> $y->time;
121bd77bf38SGreg Roach                });
122fbd9d3f8SGreg Roach                $order = [[2, 'asc']];
1238c2e8227SGreg Roach                break;
12480e23601SGreg Roach
125fbd9d3f8SGreg Roach            default:
1260280f44aSGreg Roach            case 'date_desc':
127bd77bf38SGreg Roach                $rows = $rows->sort(static function (stdClass $x, stdClass $y): int {
128bd77bf38SGreg Roach                    return $y->time <=> $x->time;
129bd77bf38SGreg Roach                });
1301be6d38bSGreg Roach                $order = [[2, 'desc']];
131fbd9d3f8SGreg Roach                break;
1328c2e8227SGreg Roach        }
1330280f44aSGreg Roach
134bd77bf38SGreg Roach        if ($rows->isEmpty()) {
1350280f44aSGreg Roach            $content = I18N::plural('There have been no changes within the last %s day.', 'There have been no changes within the last %s days.', $days, I18N::number($days));
1360280f44aSGreg Roach        } elseif ($infoStyle === 'list') {
1374d2d7d1aSGreg Roach            $content = view('modules/recent_changes/changes-list', [
138e24053e5SGreg Roach                'id'         => $block_id,
13901221f27SGreg Roach                'limit_low'  => self::LIMIT_LOW,
14001221f27SGreg Roach                'limit_high' => self::LIMIT_HIGH,
141e24053e5SGreg Roach                'rows'       => $rows->values(),
1424d2d7d1aSGreg Roach                'show_user'  => $show_user,
1434d2d7d1aSGreg Roach            ]);
1440280f44aSGreg Roach        } else {
1454d2d7d1aSGreg Roach            $content = view('modules/recent_changes/changes-table', [
14601221f27SGreg Roach                'limit_low'  => self::LIMIT_LOW,
14701221f27SGreg Roach                'limit_high' => self::LIMIT_HIGH,
148bd77bf38SGreg Roach                'rows'       => $rows,
1490280f44aSGreg Roach                'show_user'  => $show_user,
150fbd9d3f8SGreg Roach                'order'      => $order,
1510280f44aSGreg Roach            ]);
1528c2e8227SGreg Roach        }
1538c2e8227SGreg Roach
1543caaa4d2SGreg Roach        if ($context !== self::CONTEXT_EMBED) {
155147e99aaSGreg Roach            return view('modules/block-template', [
1561e7a7a28SGreg Roach                'block'      => Str::kebab($this->name()),
1579c6524dcSGreg Roach                'id'         => $block_id,
1583caaa4d2SGreg Roach                'config_url' => $this->configUrl($tree, $context, $block_id),
1598cbbfdceSGreg Roach                'title'      => I18N::plural('Changes in the last %s day', 'Changes in the last %s days', $days, I18N::number($days)),
1609c6524dcSGreg Roach                'content'    => $content,
1619c6524dcSGreg Roach            ]);
1628c2e8227SGreg Roach        }
163b2ce94c6SRico Sonntag
164b2ce94c6SRico Sonntag        return $content;
1658c2e8227SGreg Roach    }
1668c2e8227SGreg Roach
1673caaa4d2SGreg Roach    /**
1683caaa4d2SGreg Roach     * Should this block load asynchronously using AJAX?
1693caaa4d2SGreg Roach     *
1703caaa4d2SGreg Roach     * Simple blocks are faster in-line, more complex ones can be loaded later.
1713caaa4d2SGreg Roach     *
1723caaa4d2SGreg Roach     * @return bool
1733caaa4d2SGreg Roach     */
174c1010edaSGreg Roach    public function loadAjax(): bool
175c1010edaSGreg Roach    {
1768c2e8227SGreg Roach        return true;
1778c2e8227SGreg Roach    }
1788c2e8227SGreg Roach
1793caaa4d2SGreg Roach    /**
1803caaa4d2SGreg Roach     * Can this block be shown on the user’s home page?
1813caaa4d2SGreg Roach     *
1823caaa4d2SGreg Roach     * @return bool
1833caaa4d2SGreg Roach     */
184c1010edaSGreg Roach    public function isUserBlock(): bool
185c1010edaSGreg Roach    {
1868c2e8227SGreg Roach        return true;
1878c2e8227SGreg Roach    }
1888c2e8227SGreg Roach
1893caaa4d2SGreg Roach    /**
1903caaa4d2SGreg Roach     * Can this block be shown on the tree’s home page?
1913caaa4d2SGreg Roach     *
1923caaa4d2SGreg Roach     * @return bool
1933caaa4d2SGreg Roach     */
19463276d8fSGreg Roach    public function isTreeBlock(): bool
195c1010edaSGreg Roach    {
1968c2e8227SGreg Roach        return true;
1978c2e8227SGreg Roach    }
1988c2e8227SGreg Roach
19920ac4041SGreg Roach    /**
200a45f9889SGreg Roach     * Update the configuration for a block.
201a45f9889SGreg Roach     *
2026ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
203a45f9889SGreg Roach     * @param int     $block_id
204a45f9889SGreg Roach     *
205a45f9889SGreg Roach     * @return void
206a45f9889SGreg Roach     */
2076ccdf4f0SGreg Roach    public function saveBlockConfiguration(ServerRequestInterface $request, int $block_id): void
208a45f9889SGreg Roach    {
209b46c87bdSGreg Roach        $params = (array) $request->getParsedBody();
210af2b6902SGreg Roach
211eb235819SGreg Roach        $this->setBlockSetting($block_id, 'days', $params['days']);
212eb235819SGreg Roach        $this->setBlockSetting($block_id, 'infoStyle', $params['infoStyle']);
213eb235819SGreg Roach        $this->setBlockSetting($block_id, 'sortStyle', $params['sortStyle']);
214eb235819SGreg Roach        $this->setBlockSetting($block_id, 'show_user', $params['show_user']);
215a45f9889SGreg Roach    }
216a45f9889SGreg Roach
217a45f9889SGreg Roach    /**
21820ac4041SGreg Roach     * An HTML form to edit block settings
21920ac4041SGreg Roach     *
22020ac4041SGreg Roach     * @param Tree $tree
22120ac4041SGreg Roach     * @param int  $block_id
22220ac4041SGreg Roach     *
2233caaa4d2SGreg Roach     * @return string
22420ac4041SGreg Roach     */
2253caaa4d2SGreg Roach    public function editBlockConfiguration(Tree $tree, int $block_id): string
226c1010edaSGreg Roach    {
22755664801SGreg Roach        $days      = (int) $this->getBlockSetting($block_id, 'days', self::DEFAULT_DAYS);
2286e45321fSGreg Roach        $infoStyle = $this->getBlockSetting($block_id, 'infoStyle', self::DEFAULT_INFO_STYLE);
2296e45321fSGreg Roach        $sortStyle = $this->getBlockSetting($block_id, 'sortStyle', self::DEFAULT_SORT_STYLE);
2306e45321fSGreg Roach        $show_user = $this->getBlockSetting($block_id, 'show_user', self::DEFAULT_SHOW_USER);
2318c2e8227SGreg Roach
232c385536dSGreg Roach        $info_styles = [
233bbb76c12SGreg Roach            /* I18N: An option in a list-box */
234bbb76c12SGreg Roach            'list'  => I18N::translate('list'),
235bbb76c12SGreg Roach            /* I18N: An option in a list-box */
236bbb76c12SGreg Roach            'table' => I18N::translate('table'),
237c385536dSGreg Roach        ];
2388c2e8227SGreg Roach
239c385536dSGreg Roach        $sort_styles = [
240bbb76c12SGreg Roach            /* I18N: An option in a list-box */
241bbb76c12SGreg Roach            'name'      => I18N::translate('sort by name'),
242bbb76c12SGreg Roach            /* I18N: An option in a list-box */
243bbb76c12SGreg Roach            'date_asc'  => I18N::translate('sort by date, oldest first'),
244bbb76c12SGreg Roach            /* I18N: An option in a list-box */
245bbb76c12SGreg Roach            'date_desc' => I18N::translate('sort by date, newest first'),
246c385536dSGreg Roach        ];
2478c2e8227SGreg Roach
2483caaa4d2SGreg Roach        return view('modules/recent_changes/config', [
249c385536dSGreg Roach            'days'        => $days,
250c385536dSGreg Roach            'infoStyle'   => $infoStyle,
251c385536dSGreg Roach            'info_styles' => $info_styles,
252c385536dSGreg Roach            'max_days'    => self::MAX_DAYS,
253c385536dSGreg Roach            'sortStyle'   => $sortStyle,
254c385536dSGreg Roach            'sort_styles' => $sort_styles,
255c385536dSGreg Roach            'show_user'   => $show_user,
256c385536dSGreg Roach        ]);
2578c2e8227SGreg Roach    }
2588c2e8227SGreg Roach
2596e45321fSGreg Roach    /**
2606e45321fSGreg Roach     * Find records that have changed since a given julian day
2616e45321fSGreg Roach     *
2626e45321fSGreg Roach     * @param Tree $tree Changes for which tree
26324319d9dSGreg Roach     * @param int  $days Number of days
2646e45321fSGreg Roach     *
265bd77bf38SGreg Roach     * @return Collection<stdClass> List of records with changes
2666e45321fSGreg Roach     */
267bd77bf38SGreg Roach    private function getRecentChanges(Tree $tree, int $days): Collection
268c1010edaSGreg Roach    {
269bd77bf38SGreg Roach        $subquery = DB::table('change')
27077654037SGreg Roach            ->where('gedcom_id', '=', $tree->id())
27177654037SGreg Roach            ->where('status', '=', 'accepted')
27277654037SGreg Roach            ->where('new_gedcom', '<>', '')
27377654037SGreg Roach            ->where('change_time', '>', Carbon::now()->subDays($days))
2747f5c2944SGreg Roach            ->groupBy(['xref'])
275bd77bf38SGreg Roach            ->select(new Expression('MAX(change_id) AS recent_change_id'));
276bd77bf38SGreg Roach
277bd77bf38SGreg Roach        $query = DB::table('change')
278bd77bf38SGreg Roach            ->joinSub($subquery, 'recent', 'recent_change_id', '=', 'change_id')
279bd77bf38SGreg Roach            ->select(['change.*']);
280bd77bf38SGreg Roach
281bd77bf38SGreg Roach        return $query
282bd77bf38SGreg Roach            ->get()
283bd77bf38SGreg Roach            ->map(function (stdClass $row) use ($tree): stdClass {
284bd77bf38SGreg Roach                return (object) [
28501221f27SGreg Roach                    'record' => GedcomRecord::getInstance($row->xref, $tree, $row->new_gedcom),
286bd77bf38SGreg Roach                    'time'   => Carbon::create($row->change_time)->local(),
287bd77bf38SGreg Roach                    'user'   => $this->user_service->find($row->user_id),
288bd77bf38SGreg Roach                ];
28977654037SGreg Roach            })
290bd77bf38SGreg Roach            ->filter(static function (stdClass $row): bool {
291bd77bf38SGreg Roach                return $row->record instanceof GedcomRecord && $row->record->canShow();
292bd77bf38SGreg Roach            });
2936e45321fSGreg Roach    }
2948c2e8227SGreg Roach}
295