Test Failed
Branch master (4a3c5b)
by Greg
12:31
created

RecentChangesModule::getDescription()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2017 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
 */
16
namespace Fisharebest\Webtrees\Module;
17
18
use Fisharebest\Webtrees\Auth;
19
use Fisharebest\Webtrees\Bootstrap4;
20
use Fisharebest\Webtrees\Database;
21
use Fisharebest\Webtrees\Filter;
22
use Fisharebest\Webtrees\FontAwesome;
23
use Fisharebest\Webtrees\Functions\FunctionsEdit;
24
use Fisharebest\Webtrees\GedcomRecord;
25
use Fisharebest\Webtrees\GedcomTag;
26
use Fisharebest\Webtrees\Html;
27
use Fisharebest\Webtrees\I18N;
28
use Fisharebest\Webtrees\Individual;
29
use Fisharebest\Webtrees\Tree;
30
use Fisharebest\Webtrees\View;
31
use Ramsey\Uuid\Uuid;
32
33
/**
34
 * Class RecentChangesModule
35
 */
36
class RecentChangesModule extends AbstractModule implements ModuleBlockInterface {
37
	const DEFAULT_BLOCK      = '1';
38
	const DEFAULT_DAYS       = 7;
39
	const DEFAULT_HIDE_EMPTY = '0';
40
	const DEFAULT_SHOW_USER  = '1';
41
	const DEFAULT_SORT_STYLE = 'date_desc';
42
	const DEFAULT_INFO_STYLE = 'table';
43
	const MAX_DAYS           = 90;
44
45
	/** {@inheritdoc} */
46
	public function getTitle() {
47
		return /* I18N: Name of a module */ I18N::translate('Recent changes');
48
	}
49
50
	/** {@inheritdoc} */
51
	public function getDescription() {
52
		return /* I18N: Description of the “Recent changes” module */ I18N::translate('A list of records that have been updated recently.');
53
	}
54
55
	/** {@inheritdoc} */
56
	public function getBlock($block_id, $template = true, $cfg = []): string {
57
		global $ctype, $WT_TREE;
58
59
		$days      = $this->getBlockSetting($block_id, 'days', self::DEFAULT_DAYS);
60
		$infoStyle = $this->getBlockSetting($block_id, 'infoStyle', self::DEFAULT_INFO_STYLE);
61
		$sortStyle = $this->getBlockSetting($block_id, 'sortStyle', self::DEFAULT_SORT_STYLE);
62
		$show_user = $this->getBlockSetting($block_id, 'show_user', self::DEFAULT_SHOW_USER);
63
64 View Code Duplication
		foreach (['days', 'infoStyle', 'sortStyle', 'show_user'] as $name) {
65
			if (array_key_exists($name, $cfg)) {
66
				$$name = $cfg[$name];
67
			}
68
		}
69
70
		$records = $this->getRecentChanges($WT_TREE, $days);
0 ignored issues
show
Bug introduced by
$days of type string is incompatible with the type integer expected by parameter $days of Fisharebest\Webtrees\Mod...ule::getRecentChanges(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

70
		$records = $this->getRecentChanges($WT_TREE, /** @scrutinizer ignore-type */ $days);
Loading history...
71
72
		$content = '';
73
		// Print block content
74
		if (empty($records)) {
75
			$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));
0 ignored issues
show
Bug introduced by
$days of type string is incompatible with the type double expected by parameter $n of Fisharebest\Webtrees\I18N::number(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

75
			$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(/** @scrutinizer ignore-type */ $days));
Loading history...
76
		} else {
77
			switch ($infoStyle) {
78
			case 'list':
79
				$content .= $this->changesList($records, $sortStyle, $show_user);
0 ignored issues
show
Bug introduced by
$show_user of type string is incompatible with the type boolean expected by parameter $show_user of Fisharebest\Webtrees\Mod...esModule::changesList(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

79
				$content .= $this->changesList($records, $sortStyle, /** @scrutinizer ignore-type */ $show_user);
Loading history...
80
				break;
81
			case 'table':
82
				$content .= $this->changesTable($records, $sortStyle, $show_user);
0 ignored issues
show
Bug introduced by
$show_user of type string is incompatible with the type boolean expected by parameter $show_user of Fisharebest\Webtrees\Mod...sModule::changesTable(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

82
				$content .= $this->changesTable($records, $sortStyle, /** @scrutinizer ignore-type */ $show_user);
Loading history...
83
				break;
84
			}
85
		}
86
87
		if ($template) {
88
			if ($ctype === 'gedcom' && Auth::isManager($WT_TREE) || $ctype === 'user' && Auth::check()) {
89
				$config_url = Html::url('block_edit.php', ['block_id' => $block_id, 'ged' => $WT_TREE->getName()]);
90
			} else {
91
				$config_url = '';
92
			}
93
94
			return View::make('blocks/template', [
95
				'block'      => str_replace('_', '-', $this->getName()),
96
				'id'         => $block_id,
97
				'config_url' => $config_url,
98
				'title'      => I18N::plural('Changes in the last %s day', 'Changes in the last %s days', $days, I18N::number($days)),
99
				'content'    => $content,
100
			]);
101
		} else {
102
			return $content;
103
		}
104
	}
105
106
	/** {@inheritdoc} */
107
	public function loadAjax(): bool {
108
		return true;
109
	}
110
111
	/** {@inheritdoc} */
112
	public function isUserBlock(): bool {
113
		return true;
114
	}
115
116
	/** {@inheritdoc} */
117
	public function isGedcomBlock(): bool {
118
		return true;
119
	}
120
121
	/** {@inheritdoc} */
122
	public function configureBlock($block_id): void {
123
		if (Filter::postBool('save') && Filter::checkCsrf()) {
124
			$this->setBlockSetting($block_id, 'days', Filter::postInteger('days', 1, self::MAX_DAYS));
125
			$this->setBlockSetting($block_id, 'infoStyle', Filter::post('infoStyle', 'list|table'));
126
			$this->setBlockSetting($block_id, 'sortStyle', Filter::post('sortStyle', 'name|date_asc|date_desc'));
127
			$this->setBlockSetting($block_id, 'show_user', Filter::postBool('show_user'));
128
		}
129
130
		$days      = $this->getBlockSetting($block_id, 'days', self::DEFAULT_DAYS);
131
		$infoStyle = $this->getBlockSetting($block_id, 'infoStyle', self::DEFAULT_INFO_STYLE);
132
		$sortStyle = $this->getBlockSetting($block_id, 'sortStyle', self::DEFAULT_SORT_STYLE);
133
		$show_user = $this->getBlockSetting($block_id, 'show_user', self::DEFAULT_SHOW_USER);
134
135
		echo '<div class="form-group row"><label class="col-sm-3 col-form-label" for="days">';
136
		echo I18N::translate('Number of days to show');
137
		echo '</div><div class="col-sm-9">';
138
		echo '<input type="text" name="days" size="2" value="', $days, '">';
139
		echo ' ' . I18N::plural('maximum %s day', 'maximum %s days', I18N::number(self::MAX_DAYS), I18N::number(self::MAX_DAYS));
140
		echo '</div></div>';
141
142
		echo '<div class="form-group row"><label class="col-sm-3 col-form-label" for="infoStyle">';
143
		echo I18N::translate('Presentation style');
144
		echo '</div><div class="col-sm-9">';
145
		echo Bootstrap4::select(['list' => I18N::translate('list'), 'table' => I18N::translate('table')], $infoStyle, ['id' => 'infoStyle', 'name' => 'infoStyle']);
146
		echo '</div></div>';
147
148
		echo '<div class="form-group row"><label class="col-sm-3 col-form-label" for="sortStyle">';
149
		echo I18N::translate('Sort order');
150
		echo '</div><div class="col-sm-9">';
151
		echo Bootstrap4::select([
152
			'name'      => /* I18N: An option in a list-box */ I18N::translate('sort by name'),
153
			'date_asc'  => /* I18N: An option in a list-box */ I18N::translate('sort by date, oldest first'),
154
			'date_desc' => /* I18N: An option in a list-box */ I18N::translate('sort by date, newest first'),
155
		], $sortStyle, ['id' => 'sortStyle', 'name' => 'sortStyle']);
156
		echo '</div></div>';
157
158
		echo '<div class="form-group row"><label class="col-sm-3 col-form-label" for="show_usere">';
159
		echo /* I18N: label for a yes/no option */ I18N::translate('Show the user who made the change');
160
		echo '</div><div class="col-sm-9">';
161
		echo Bootstrap4::radioButtons('show_user', FunctionsEdit::optionsNoYes(), $show_user, true);
162
		echo '</div></div>';
163
	}
164
165
	/**
166
	 * Find records that have changed since a given julian day
167
	 *
168
	 * @param Tree $tree Changes for which tree
169
	 * @param int  $days Number of days
170
	 *
171
	 * @return GedcomRecord[] List of records with changes
172
	 */
173
	private function getRecentChanges(Tree $tree, $days) {
174
		$sql =
175
			"SELECT xref FROM `##change`" .
176
			" WHERE new_gedcom != '' AND change_time > DATE_SUB(NOW(), INTERVAL :days DAY) AND gedcom_id = :tree_id" .
177
			" GROUP BY xref" .
178
			" ORDER BY MAX(change_id) DESC";
179
180
		$vars = [
181
			'days'    => $days,
182
			'tree_id' => $tree->getTreeId(),
183
		];
184
185
		$xrefs = Database::prepare($sql)->execute($vars)->fetchOneColumn();
186
187
		$records = [];
188
		foreach ($xrefs as $xref) {
189
			$record = GedcomRecord::getInstance($xref, $tree);
190
			if ($record && $record->canShow()) {
191
				$records[] = $record;
192
			}
193
		}
194
195
		return $records;
196
	}
197
198
	/**
199
	 * Format a table of events
200
	 *
201
	 * @param GedcomRecord[] $records
202
	 * @param string         $sort
203
	 * @param bool           $show_user
204
	 *
205
	 * @return string
206
	 */
207
	private function changesList(array $records, $sort, $show_user) {
208
		switch ($sort) {
209
		case 'name':
210
			uasort($records, ['self', 'sortByNameAndChangeDate']);
211
			break;
212
		case 'date_asc':
213
			uasort($records, ['self', 'sortByChangeDateAndName']);
214
			$records = array_reverse($records);
215
			break;
216
		case 'date_desc':
217
			uasort($records, ['self', 'sortByChangeDateAndName']);
218
		}
219
220
		$html = '';
221
		foreach ($records as $record) {
222
			$html .= '<a href="' . $record->getHtmlUrl() . '" class="list_item name2">' . $record->getFullName() . '</a>';
223
			$html .= '<div class="indent" style="margin-bottom: 5px;">';
224
			if ($record instanceof Individual) {
225
				if ($record->getAddName()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $record->getAddName() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
226
					$html .= '<a href="' . $record->getHtmlUrl() . '" class="list_item">' . $record->getAddName() . '</a>';
227
				}
228
			}
229
230
			// The timestamp may be missing or private.
231
			$timestamp = $record->lastChangeTimestamp();
232
			if ($timestamp !== '') {
233
				if ($show_user) {
234
					$html .= /* I18N: [a record was] Changed on <date/time> by <user> */
235
						I18N::translate('Changed on %1$s by %2$s', $timestamp, Html::escape($record->lastChangeUser()));
236
				} else {
237
					$html .= /* I18N: [a record was] Changed on <date/time> */
238
						I18N::translate('Changed on %1$s', $timestamp);
239
				}
240
			}
241
			$html .= '</div>';
242
		}
243
244
		return $html;
245
	}
246
247
	/**
248
	 * Format a table of events
249
	 *
250
	 * @param GedcomRecord[] $records
251
	 * @param string         $sort
252
	 * @param bool           $show_user
253
	 *
254
	 * @return string
255
	 */
256
	private function changesTable($records, $sort, $show_user) {
257
		global $controller;
258
259
		$table_id = 'table-chan-' . Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
0 ignored issues
show
Bug introduced by
Are you sure Ramsey\Uuid\Uuid::uuid4() of type Ramsey\Uuid\UuidInterface can be used in concatenation? Consider adding a __toString()-method. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

259
		$table_id = 'table-chan-' . /** @scrutinizer ignore-type */ Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page
Loading history...
260
261
		switch ($sort) {
262
		case 'name':
263
		default:
264
			$aaSorting = "[1,'asc'], [2,'desc']";
265
			break;
266
		case 'date_asc':
267
			$aaSorting = "[2,'asc'], [1,'asc']";
268
			break;
269
		case 'date_desc':
270
			$aaSorting = "[2,'desc'], [1,'asc']";
271
			break;
272
		}
273
274
		$html = '';
275
		$controller
276
			->addInlineJavascript('
277
				$("#' . $table_id . '").dataTable({
278
					dom: \'t\',
279
					paging: false,
280
					autoWidth:false,
281
					lengthChange: false,
282
					filter: false,
283
					' . I18N::datatablesI18N() . ',
284
					sorting: [' . $aaSorting . '],
285
					columns: [
286
						{ sortable: false, class: "center" },
287
						null,
288
						null,
289
						{ visible: ' . ($show_user ? 'true' : 'false') . ' }
290
					]
291
				});
292
			');
293
294
		$html .= '<table id="' . $table_id . '" class="width100">';
295
		$html .= '<thead><tr>';
296
		$html .= '<th></th>';
297
		$html .= '<th>' . I18N::translate('Record') . '</th>';
298
		$html .= '<th>' . I18N::translate('Last change') . '</th>';
299
		$html .= '<th>' . GedcomTag::getLabel('_WT_USER') . '</th>';
300
		$html .= '</tr></thead><tbody>';
301
302
		foreach ($records as $record) {
303
			$html .= '<tr><td>';
304
			switch ($record::RECORD_TYPE) {
305
			case 'INDI':
306
				$html .= FontAwesome::semanticIcon('individual', I18N::translate('Individual'));
307
				break;
308
			case 'FAM':
309
				$html .= FontAwesome::semanticicon('family', I18N::translate('Family'));
310
				break;
311
			case 'OBJE':
312
				$html .= FontAwesome::semanticIcon('media', I18N::translate('Media'));
313
				break;
314
			case 'NOTE':
315
				$html .= FontAwesome::semanticIcon('note', I18N::translate('Note'));
316
				break;
317
			case 'SOUR':
318
				$html .= FontAwesome::semanticIcon('source', I18N::translate('Source'));
319
				break;
320
			case 'SUBM':
321
				$html .= FontAwesome::semanticIcon('submitter', I18N::translate('Submitter'));
322
				break;
323
			case 'REPO':
324
				$html .= FontAwesome::semanticIcon('repository', I18N::translate('Repository'));
325
				break;
326
			}
327
			$html .= '</td>';
328
			$html .= '<td data-sort="' . Html::escape($record->getSortName()) . '">';
329
			$html .= '<a href="' . $record->getHtmlUrl() . '">' . $record->getFullName() . '</a>';
330
			$addname = $record->getAddName();
331
			if ($addname) {
332
				$html .= '<div class="indent"><a href="' . $record->getHtmlUrl() . '">' . $addname . '</a></div>';
333
			}
334
			$html .= '</td>';
335
			$html .= '<td data-sort="' . $record->lastChangeTimestamp(true) . '">' . $record->lastChangeTimestamp() . '</td>';
336
			$html .= '<td>' . Html::escape($record->lastChangeUser()) . '</td>';
337
			$html .= '</tr>';
338
		}
339
340
		$html .= '</tbody></table>';
341
342
		return $html;
343
	}
344
345
	/**
346
	 * Sort the records by (1) last change date and (2) name
347
	 *
348
	 * @param GedcomRecord $a
349
	 * @param GedcomRecord $b
350
	 *
351
	 * @return int
352
	 */
353
	private static function sortByChangeDateAndName(GedcomRecord $a, GedcomRecord $b) {
354
		return $b->lastChangeTimestamp(true) - $a->lastChangeTimestamp(true) ?: GedcomRecord::compare($a, $b);
355
	}
356
357
	/**
358
	 * Sort the records by (1) name and (2) last change date
359
	 *
360
	 * @param GedcomRecord $a
361
	 * @param GedcomRecord $b
362
	 *
363
	 * @return int
364
	 */
365
	private static function sortByNameAndChangeDate(GedcomRecord $a, GedcomRecord $b) {
366
		return GedcomRecord::compare($a, $b) ?: $b->lastChangeTimestamp(true) - $a->lastChangeTimestamp(true);
367
	}
368
}
369