xref: /webtrees/app/Module/RecentChangesModule.php (revision 771e86bf6fe88015a121927a182011cbc19da40e)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2018 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16namespace Fisharebest\Webtrees\Module;
17
18use Fisharebest\Webtrees\Auth;
19use Fisharebest\Webtrees\Database;
20use Fisharebest\Webtrees\Filter;
21use Fisharebest\Webtrees\FontAwesome;
22use Fisharebest\Webtrees\GedcomRecord;
23use Fisharebest\Webtrees\GedcomTag;
24use Fisharebest\Webtrees\I18N;
25use Fisharebest\Webtrees\Individual;
26use Fisharebest\Webtrees\Tree;
27use Ramsey\Uuid\Uuid;
28
29/**
30 * Class RecentChangesModule
31 */
32class RecentChangesModule extends AbstractModule implements ModuleBlockInterface {
33	const DEFAULT_BLOCK      = '1';
34	const DEFAULT_DAYS       = 7;
35	const DEFAULT_HIDE_EMPTY = '0';
36	const DEFAULT_SHOW_USER  = '1';
37	const DEFAULT_SORT_STYLE = 'date_desc';
38	const DEFAULT_INFO_STYLE = 'table';
39	const MAX_DAYS           = 90;
40
41	/** {@inheritdoc} */
42	public function getTitle() {
43		return /* I18N: Name of a module */ I18N::translate('Recent changes');
44	}
45
46	/** {@inheritdoc} */
47	public function getDescription() {
48		return /* I18N: Description of the “Recent changes” module */ I18N::translate('A list of records that have been updated recently.');
49	}
50
51	/** {@inheritdoc} */
52	public function getBlock($block_id, $template = true, $cfg = []): string {
53		global $ctype, $WT_TREE;
54
55		$days      = $this->getBlockSetting($block_id, 'days', self::DEFAULT_DAYS);
56		$infoStyle = $this->getBlockSetting($block_id, 'infoStyle', self::DEFAULT_INFO_STYLE);
57		$sortStyle = $this->getBlockSetting($block_id, 'sortStyle', self::DEFAULT_SORT_STYLE);
58		$show_user = $this->getBlockSetting($block_id, 'show_user', self::DEFAULT_SHOW_USER);
59
60		extract($cfg, EXTR_OVERWRITE);
61
62		$records = $this->getRecentChanges($WT_TREE, $days);
63
64		$content = '';
65		// Print block content
66		if (empty($records)) {
67			$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));
68		} else {
69			switch ($infoStyle) {
70				case 'list':
71					$content .= $this->changesList($records, $sortStyle, $show_user);
72					break;
73				case 'table':
74					$content .= $this->changesTable($records, $sortStyle, $show_user);
75					break;
76			}
77		}
78
79		if ($template) {
80			if ($ctype === 'gedcom' && Auth::isManager($WT_TREE)) {
81				$config_url = route('tree-page-block-edit', ['block_id' => $block_id, 'ged' => $WT_TREE->getName()]);
82			} elseif ($ctype === 'user' && Auth::check()) {
83				$config_url = route('user-page-block-edit', ['block_id' => $block_id, 'ged' => $WT_TREE->getName()]);
84			} else {
85				$config_url = '';
86			}
87
88			return view('blocks/template', [
89				'block'      => str_replace('_', '-', $this->getName()),
90				'id'         => $block_id,
91				'config_url' => $config_url,
92				'title'      => I18N::plural('Changes in the last %s day', 'Changes in the last %s days', $days, I18N::number($days)),
93				'content'    => $content,
94			]);
95		} else {
96			return $content;
97		}
98	}
99
100	/** {@inheritdoc} */
101	public function loadAjax(): bool {
102		return true;
103	}
104
105	/** {@inheritdoc} */
106	public function isUserBlock(): bool {
107		return true;
108	}
109
110	/** {@inheritdoc} */
111	public function isGedcomBlock(): bool {
112		return true;
113	}
114
115	/** {@inheritdoc} */
116	public function configureBlock($block_id) {
117		if ($_SERVER['REQUEST_METHOD'] === 'POST') {
118			$this->setBlockSetting($block_id, 'days', Filter::postInteger('days', 1, self::MAX_DAYS));
119			$this->setBlockSetting($block_id, 'infoStyle', Filter::post('infoStyle', 'list|table'));
120			$this->setBlockSetting($block_id, 'sortStyle', Filter::post('sortStyle', 'name|date_asc|date_desc'));
121			$this->setBlockSetting($block_id, 'show_user', Filter::postBool('show_user'));
122
123			return;
124		}
125
126		$days      = $this->getBlockSetting($block_id, 'days', self::DEFAULT_DAYS);
127		$infoStyle = $this->getBlockSetting($block_id, 'infoStyle', self::DEFAULT_INFO_STYLE);
128		$sortStyle = $this->getBlockSetting($block_id, 'sortStyle', self::DEFAULT_SORT_STYLE);
129		$show_user = $this->getBlockSetting($block_id, 'show_user', self::DEFAULT_SHOW_USER);
130
131		$info_styles = [
132			'list'  => /* I18N: An option in a list-box */ I18N::translate('list'),
133			'table' => /* I18N: An option in a list-box */ I18N::translate('table'),
134		];
135
136		$sort_styles = [
137			'name'      => /* I18N: An option in a list-box */ I18N::translate('sort by name'),
138			'date_asc'  => /* I18N: An option in a list-box */ I18N::translate('sort by date, oldest first'),
139			'date_desc' => /* I18N: An option in a list-box */ I18N::translate('sort by date, newest first'),
140		];
141
142		echo view('blocks/recent-changes-config', [
143			'days'        => $days,
144			'infoStyle'   => $infoStyle,
145			'info_styles' => $info_styles,
146			'max_days'    => self::MAX_DAYS,
147			'sortStyle'   => $sortStyle,
148			'sort_styles' => $sort_styles,
149			'show_user'   => $show_user,
150		]);
151	}
152
153	/**
154	 * Find records that have changed since a given julian day
155	 *
156	 * @param Tree $tree Changes for which tree
157	 * @param int  $days Number of days
158	 *
159	 * @return GedcomRecord[] List of records with changes
160	 */
161	private function getRecentChanges(Tree $tree, $days) {
162		$sql =
163			"SELECT xref FROM `##change`" .
164			" WHERE new_gedcom != '' AND change_time > DATE_SUB(NOW(), INTERVAL :days DAY) AND gedcom_id = :tree_id" .
165			" GROUP BY xref" .
166			" ORDER BY MAX(change_id) DESC";
167
168		$vars = [
169			'days'    => $days,
170			'tree_id' => $tree->getTreeId(),
171		];
172
173		$xrefs = Database::prepare($sql)->execute($vars)->fetchOneColumn();
174
175		$records = [];
176		foreach ($xrefs as $xref) {
177			$record = GedcomRecord::getInstance($xref, $tree);
178			if ($record && $record->canShow()) {
179				$records[] = $record;
180			}
181		}
182
183		return $records;
184	}
185
186	/**
187	 * Format a table of events
188	 *
189	 * @param GedcomRecord[] $records
190	 * @param string         $sort
191	 * @param bool           $show_user
192	 *
193	 * @return string
194	 */
195	private function changesList(array $records, $sort, $show_user) {
196		switch ($sort) {
197			case 'name':
198				uasort($records, ['self', 'sortByNameAndChangeDate']);
199				break;
200			case 'date_asc':
201				uasort($records, ['self', 'sortByChangeDateAndName']);
202				$records = array_reverse($records);
203				break;
204			case 'date_desc':
205				uasort($records, ['self', 'sortByChangeDateAndName']);
206		}
207
208		$html = '';
209		foreach ($records as $record) {
210			$html .= '<a href="' . e($record->url()) . '" class="list_item name2">' . $record->getFullName() . '</a>';
211			$html .= '<div class="indent" style="margin-bottom: 5px;">';
212			if ($record instanceof Individual) {
213				if ($record->getAddName()) {
214					$html .= '<a href="' . e($record->url()) . '" class="list_item">' . $record->getAddName() . '</a>';
215				}
216			}
217
218			// The timestamp may be missing or private.
219			$timestamp = $record->lastChangeTimestamp();
220			if ($timestamp !== '') {
221				if ($show_user) {
222					$html .= /* I18N: [a record was] Changed on <date/time> by <user> */
223						I18N::translate('Changed on %1$s by %2$s', $timestamp, e($record->lastChangeUser()));
224				} else {
225					$html .= /* I18N: [a record was] Changed on <date/time> */
226						I18N::translate('Changed on %1$s', $timestamp);
227				}
228			}
229			$html .= '</div>';
230		}
231
232		return $html;
233	}
234
235	/**
236	 * Format a table of events
237	 *
238	 * @param GedcomRecord[] $records
239	 * @param string         $sort
240	 * @param bool           $show_user
241	 *
242	 * @return string
243	 */
244	private function changesTable($records, $sort, $show_user) {
245		global $controller;
246
247		$table_id = 'table-chan-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
248
249		switch ($sort) {
250			case 'name':
251			default:
252				$aaSorting = "[1,'asc'], [2,'desc']";
253				break;
254			case 'date_asc':
255				$aaSorting = "[2,'asc'], [1,'asc']";
256				break;
257			case 'date_desc':
258				$aaSorting = "[2,'desc'], [1,'asc']";
259				break;
260		}
261
262		$html = '';
263		$controller
264			->addInlineJavascript('
265				$("#' . $table_id . '").dataTable({
266					dom: \'t\',
267					paging: false,
268					autoWidth:false,
269					lengthChange: false,
270					filter: false,
271					' . I18N::datatablesI18N() . ',
272					sorting: [' . $aaSorting . '],
273					columns: [
274						{ sortable: false, class: "center" },
275						null,
276						null,
277						{ visible: ' . ($show_user ? 'true' : 'false') . ' }
278					]
279				});
280			');
281
282		$html .= '<table id="' . $table_id . '" class="width100">';
283		$html .= '<thead><tr>';
284		$html .= '<th></th>';
285		$html .= '<th>' . I18N::translate('Record') . '</th>';
286		$html .= '<th>' . I18N::translate('Last change') . '</th>';
287		$html .= '<th>' . GedcomTag::getLabel('_WT_USER') . '</th>';
288		$html .= '</tr></thead><tbody>';
289
290		foreach ($records as $record) {
291			$html .= '<tr><td>';
292			switch ($record::RECORD_TYPE) {
293				case 'INDI':
294					$html .= FontAwesome::semanticIcon('individual', I18N::translate('Individual'));
295					break;
296				case 'FAM':
297					$html .= FontAwesome::semanticicon('family', I18N::translate('Family'));
298					break;
299				case 'OBJE':
300					$html .= FontAwesome::semanticIcon('media', I18N::translate('Media'));
301					break;
302				case 'NOTE':
303					$html .= FontAwesome::semanticIcon('note', I18N::translate('Note'));
304					break;
305				case 'SOUR':
306					$html .= FontAwesome::semanticIcon('source', I18N::translate('Source'));
307					break;
308				case 'SUBM':
309					$html .= FontAwesome::semanticIcon('submitter', I18N::translate('Submitter'));
310					break;
311				case 'REPO':
312					$html .= FontAwesome::semanticIcon('repository', I18N::translate('Repository'));
313					break;
314			}
315			$html .= '</td>';
316			$html .= '<td data-sort="' . e($record->getSortName()) . '">';
317			$html .= '<a href="' . e($record->url()) . '">' . $record->getFullName() . '</a>';
318			$addname = $record->getAddName();
319			if ($addname) {
320				$html .= '<div class="indent"><a href="' . e($record->url()) . '">' . $addname . '</a></div>';
321			}
322			$html .= '</td>';
323			$html .= '<td data-sort="' . $record->lastChangeTimestamp(true) . '">' . $record->lastChangeTimestamp() . '</td>';
324			$html .= '<td>' . e($record->lastChangeUser()) . '</td>';
325			$html .= '</tr>';
326		}
327
328		$html .= '</tbody></table>';
329
330		return $html;
331	}
332
333	/**
334	 * Sort the records by (1) last change date and (2) name
335	 *
336	 * @param GedcomRecord $a
337	 * @param GedcomRecord $b
338	 *
339	 * @return int
340	 */
341	private static function sortByChangeDateAndName(GedcomRecord $a, GedcomRecord $b) {
342		return $b->lastChangeTimestamp(true) - $a->lastChangeTimestamp(true) ?: GedcomRecord::compare($a, $b);
343	}
344
345	/**
346	 * Sort the records by (1) name and (2) last change date
347	 *
348	 * @param GedcomRecord $a
349	 * @param GedcomRecord $b
350	 *
351	 * @return int
352	 */
353	private static function sortByNameAndChangeDate(GedcomRecord $a, GedcomRecord $b) {
354		return GedcomRecord::compare($a, $b) ?: $b->lastChangeTimestamp(true) - $a->lastChangeTimestamp(true);
355	}
356}
357