xref: /webtrees/app/Module/StoriesModule.php (revision 6c822b73268a069742aad33f74ffa3b5e1d2ba76)
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\Bootstrap4;
20use Fisharebest\Webtrees\Controller\PageController;
21use Fisharebest\Webtrees\Database;
22use Fisharebest\Webtrees\Filter;
23use Fisharebest\Webtrees\Functions\FunctionsEdit;
24use Fisharebest\Webtrees\Html;
25use Fisharebest\Webtrees\I18N;
26use Fisharebest\Webtrees\Individual;
27use Fisharebest\Webtrees\Menu;
28use Fisharebest\Webtrees\Module;
29use Fisharebest\Webtrees\Tree;
30use stdClass;
31use Symfony\Component\HttpFoundation\RedirectResponse;
32use Symfony\Component\HttpFoundation\Request;
33use Symfony\Component\HttpFoundation\Response;
34
35/**
36 * Class StoriesModule
37 */
38class StoriesModule extends AbstractModule implements ModuleTabInterface, ModuleConfigInterface, ModuleMenuInterface {
39	/** {@inheritdoc} */
40	public function getTitle() {
41		return /* I18N: Name of a module */
42			I18N::translate('Stories');
43	}
44
45	/** {@inheritdoc} */
46	public function getDescription() {
47		return /* I18N: Description of the “Stories” module */
48			I18N::translate('Add narrative stories to individuals in the family tree.');
49	}
50
51	/** {@inheritdoc} */
52	public function getConfigLink() {
53		return route('module', ['module' => $this->getName(), 'action' => 'Admin']);
54	}
55
56	/** {@inheritdoc} */
57	public function defaultTabOrder() {
58		return 55;
59	}
60
61	/** {@inheritdoc} */
62	public function getTabContent(Individual $individual) {
63		return view('modules/stories/tab', [
64			'is_admin'   => Auth::isAdmin(),
65			'individual' => $individual,
66			'stories'    => $this->getStoriesForIndividual($individual),
67		]);
68	}
69
70	/** {@inheritdoc} */
71	public function hasTabContent(Individual $individual) {
72		return Auth::isManager($individual->getTree()) || !empty($this->getStoriesForIndividual($individual));
73	}
74
75	/** {@inheritdoc} */
76	public function isGrayedOut(Individual $individual) {
77		return !empty($this->getStoriesForIndividual($individual));
78	}
79
80	/** {@inheritdoc} */
81	public function canLoadAjax() {
82		return false;
83	}
84
85	/**
86	 * @param Individual $individual
87	 *
88	 * @return stdClass[]
89	 */
90	private function getStoriesForIndividual(Individual $individual): array {
91		$block_ids =
92			Database::prepare(
93				"SELECT SQL_CACHE block_id" .
94				" FROM `##block`" .
95				" WHERE module_name = :module_name" .
96				" AND xref          = :xref" .
97				" AND gedcom_id     = :tree_id"
98			)->execute([
99				'module_name' => $this->getName(),
100				'xref'        => $individual->getXref(),
101				'tree_id'     => $individual->getTree()->getTreeId(),
102			])->fetchOneColumn();
103
104		$stories = [];
105		foreach ($block_ids as $block_id) {
106			// Only show this block for certain languages
107			$languages = $this->getBlockSetting($block_id, 'languages', '');
108			if ($languages === '' || in_array(WT_LOCALE, explode(',', $languages))) {
109				$stories[] = (object) [
110					'block_id'   => $block_id,
111					'title'      => $this->getBlockSetting($block_id, 'title'),
112					'story_body' => $this->getBlockSetting($block_id, 'story_body'),
113				];
114			}
115		}
116
117		return $stories;
118	}
119
120	/**
121	 * The admin view - list, create, edit, delete stories.
122	 */
123	private function config() {
124		global $WT_TREE;
125
126		$controller = new PageController;
127		$controller
128			->restrictAccess(Auth::isAdmin())
129			->setPageTitle($this->getTitle())
130			->pageHeader()
131			->addInlineJavascript('
132				$("#story_table").dataTable({
133					' . I18N::datatablesI18N() . ',
134					autoWidth: false,
135					paging: true,
136					pagingType: "full_numbers",
137					lengthChange: true,
138					filter: true,
139					info: true,
140					sorting: [[0,"asc"]],
141					columns: [
142						/* 0-name */ null,
143						/* 1-NAME */ null,
144						/* 2-NAME */ { sortable:false },
145						/* 3-NAME */ { sortable:false }
146					]
147				});
148			');
149
150		$stories = Database::prepare(
151			"SELECT block_id, xref" .
152			" FROM `##block` b" .
153			" WHERE module_name=?" .
154			" AND gedcom_id=?" .
155			" ORDER BY xref"
156		)->execute([$this->getName(), $WT_TREE->getTreeId()])->fetchAll();
157
158		echo Bootstrap4::breadcrumbs([
159			route('admin-control-panel') => I18N::translate('Control panel'),
160			route('admin-modules')       => I18N::translate('Module administration'),
161		], $controller->getPageTitle());
162		?>
163
164		<h1><?= $controller->getPageTitle() ?></h1>
165
166		<form class="form form-inline">
167			<label for="ged" class="sr-only">
168				<?= I18N::translate('Family tree') ?>
169			</label>
170			<input type="hidden" name="mod" value="<?= $this->getName() ?>">
171			<input type="hidden" name="mod_action" value="admin_config">
172			<?= Bootstrap4::select(Tree::getNameList(), $WT_TREE->getName(), ['id' => 'ged', 'name' => 'ged']) ?>
173			<input type="submit" class="btn btn-primary" value="<?= I18N::translate('show') ?>">
174		</form>
175
176		<p>
177			<a href="module.php?mod=<?= $this->getName() ?>&amp;mod_action=admin_edit" class="btn btn-default">
178				<i class="fas fa-plus"></i>
179				<?= I18N::translate('Add a story') ?>
180			</a>
181		</p>
182
183		<table class="table table-bordered table-sm">
184			<thead>
185				<tr>
186					<th><?= I18N::translate('Story title') ?></th>
187					<th><?= I18N::translate('Individual') ?></th>
188					<th><?= I18N::translate('Edit') ?></th>
189					<th><?= I18N::translate('Delete') ?></th>
190				</tr>
191			</thead>
192			<tbody>
193				<?php foreach ($stories as $story): ?>
194					<tr>
195						<td>
196							<?= e($this->getBlockSetting($story->block_id, 'title')) ?>
197						</td>
198						<td>
199							<?php $individual = Individual::getInstance($story->xref, $WT_TREE) ?>
200							<?php if ($individual): ?>
201								<a href="<?= e($individual->url()) ?>#tab-stories">
202									<?= $individual->getFullName() ?>
203								</a>
204							<?php else: ?>
205								<?= $story->xref ?>
206							<?php endif ?>
207						</td>
208						<td>
209							<a href="module.php?mod=<?= $this->getName() ?>&amp;mod_action=admin_edit&amp;block_id=<?= $story->block_id ?>">
210								<i class="fas fa-pencil-alt"></i> <?= I18N::translate('Edit') ?>
211							</a>
212						</td>
213						<td>
214							<a
215								href="module.php?mod=<?= $this->getName() ?>&amp;mod_action=admin_delete&amp;block_id=<?= $story->block_id ?>" data-confirm="<?= I18N::translate('Are you sure you want to delete “%s”?', e($this->getBlockSetting($story->block_id, 'title'))) ?>" onclick="return confirm(this.dataset.confirm);">
216								<i class="fas fa-trash-alt"></i> <?= I18N::translate('Delete') ?>
217							</a>
218						</td>
219					</tr>
220				<?php endforeach ?>
221			</tbody>
222		</table>
223		<?php
224	}
225
226	/**
227	 * The user can re-order menus. Until they do, they are shown in this order.
228	 *
229	 * @return int
230	 */
231	public function defaultMenuOrder() {
232		return 30;
233	}
234
235	/**
236	 * What is the default access level for this module?
237	 *
238	 * Some modules are aimed at admins or managers, and are not generally shown to users.
239	 *
240	 * @return int
241	 */
242	public function defaultAccessLevel() {
243		return Auth::PRIV_HIDE;
244	}
245
246	/**
247	 * A menu, to be added to the main application menu.
248	 *
249	 * @return Menu|null
250	 */
251	public function getMenu() {
252		$menu = new Menu($this->getTitle(), e(route('module', ['module' => $this->getName(), 'action' => 'ShowList'])), 'menu-story');
253
254		return $menu;
255	}
256
257	/**
258	 * @param Request $request
259	 *
260	 * @return Response
261	 */
262	public function getAdminAction(Request $request): Response {
263		/** @var Tree $tree */
264		$tree = $request->attributes->get('tree');
265
266		$this->layout = 'layouts/administration';
267
268		$stories = Database::prepare(
269			"SELECT block_id, xref, gedcom_id" .
270			" FROM `##block` b" .
271			" WHERE module_name = :module_name" .
272			" AND gedcom_id = :tree_id" .
273			" ORDER BY gedcom_id, xref"
274		)->execute([
275			'tree_id'     => $tree->getTreeId(),
276			'module_name' => $this->getName(),
277		])->fetchAll();
278
279		foreach ($stories as $story) {
280			$story->individual = Individual::getInstance($story->xref, $tree);
281			$story->title      = $this->getBlockSetting($story->block_id, 'title');
282			$story->languages  = $this->getBlockSetting($story->block_id, 'languages');
283		}
284
285		return $this->viewResponse('modules/stories/config', [
286			'stories'    => $stories,
287			'title'      => $this->getTitle() . ' — ' . $tree->getTitle(),
288			'tree'       => $tree,
289			'tree_names' => Tree::getNameList(),
290		]);
291	}
292
293	/**
294	 * @param Request $request
295	 *
296	 * @return Response
297	 */
298	public function getAdminEditAction(Request $request): Response {
299		/** @var Tree $tree */
300		$tree = $request->attributes->get('tree');
301
302		$this->layout = 'layouts/administration';
303
304		$block_id = (int) $request->get('block_id');
305
306		if ($block_id === 0) {
307			// Creating a new story
308			$individual  = Individual::getInstance($request->get('xref', ''), $tree);
309			$story_title = '';
310			$story_body  = '';
311			$languages   = [];
312
313			$title = I18N::translate('Add a story') . ' — ' . e($tree->getTitle());
314		} else {
315			// Editing an existing story
316			$xref = Database::prepare(
317				"SELECT xref FROM `##block` WHERE block_id = :block_id"
318			)->execute([
319				'block_id' => $block_id,
320			])->fetchOne();
321
322			$individual  = Individual::getInstance($xref, $tree);
323			$story_title = $this->getBlockSetting($block_id, 'title', '');
324			$story_body  = $this->getBlockSetting($block_id, 'story_body', '');
325			$languages   = explode(',', $this->getBlockSetting($block_id, 'languages'));
326
327			$title = I18N::translate('Edit the story') . ' — ' . e($tree->getTitle());
328		}
329
330		return $this->viewResponse('modules/stories/edit', [
331			'block_id'    => $block_id,
332			'languages'   => $languages,
333			'story_body'  => $story_body,
334			'story_title' => $story_title,
335			'title'       => $title,
336			'tree'        => $tree,
337			'individual'  => $individual,
338		]);
339	}
340
341	/**
342	 * @param Request $request
343	 *
344	 * @return RedirectResponse
345	 */
346	public function postAdminEditAction(Request $request): RedirectResponse {
347		/** @var Tree $tree */
348		$tree = $request->attributes->get('tree');
349
350		$block_id    = (int) $request->get('block_id');
351		$xref        = $request->get('xref', '');
352		$story_body  = $request->get('story_body', '');
353		$story_title = $request->get('story_title', '');
354		$languages   = $request->get('languages', []);
355
356		if ($block_id !== 0) {
357			Database::prepare(
358				"UPDATE `##block` SET gedcom_id = :tree_id, xref = :xref WHERE block_id = :block_id"
359			)->execute([
360				'tree_id'  => $tree->getTreeId(),
361				'xref'     => $xref,
362				'block_id' => $block_id,
363			]);
364		} else {
365			Database::prepare(
366				"INSERT INTO `##block` (gedcom_id, xref, module_name, block_order) VALUES (:tree_id, :xref, 'stories', 0)"
367			)->execute([
368				'tree_id' => $tree->getTreeId(),
369				'xref'    => $xref,
370			]);
371
372			$block_id = Database::getInstance()->lastInsertId();
373		}
374
375		$this->setBlockSetting($block_id, 'story_body', $story_body);
376		$this->setBlockSetting($block_id, 'title', $story_title);
377		$this->setBlockSetting($block_id, 'languages', implode(',', $languages));
378
379		$url = route('module', ['module' => 'stories', 'action' => 'Admin', 'ged' => $tree->getName()]);
380
381		return new RedirectResponse($url);
382	}
383
384	/**
385	 * @param Request $request
386	 *
387	 * @return Response
388	 */
389	public function postAdminDeleteAction(Request $request): Response {
390		/** @var Tree $tree */
391		$tree = $request->attributes->get('tree');
392
393		$block_id = (int) $request->get('block_id');
394
395		Database::prepare(
396			"DELETE FROM `##block_setting` WHERE block_id = :block_id"
397		)->execute([
398			'block_id' => $block_id,
399		]);
400
401		Database::prepare(
402			"DELETE FROM `##block` WHERE block_id = :block_id"
403		)->execute([
404			'block_id' => $block_id,
405		]);
406
407		$url = route('module', ['module' => 'stories', 'action' => 'Admin', 'ged' => $tree->getName()]);
408
409		return new RedirectResponse($url);
410	}
411
412	/**
413	 * @param Request $request
414	 *
415	 * @return Response
416	 */
417	public function getShowListAction(Request $request): Response {
418		/** @var Tree $tree
419		 */
420		$tree = $request->attributes->get('tree');
421
422		$stories = Database::prepare(
423			"SELECT block_id, xref" .
424			" FROM `##block` b" .
425			" WHERE module_name = :module_name" .
426			" AND gedcom_id = :tree_id" .
427			" ORDER BY xref"
428		)->execute([
429			'module_name' => $this->getName(),
430			'tree_id'     => $tree->getTreeId(),
431		])->fetchAll();
432
433		foreach ($stories as $story) {
434			$story->individual = Individual::getInstance($story->xref, $tree);
435			$story->title      = $this->getBlockSetting($story->block_id, 'title');
436			$story->languages  = $this->getBlockSetting($story->block_id, 'languages');
437		}
438
439		// Filter non-existant and private individuals.
440		$stories = array_filter($stories, function (stdClass $story) {
441			return $story->individual !== null && $story->individual->canShow();
442		});
443
444		// Filter foreign languages.
445		$stories = array_filter($stories, function (stdClass $story) {
446			return $story->language === '' || in_array(WT_LOCALE, explode(',', $story->language));
447		});
448
449		return $this->viewResponse('modules/stories/list', [
450			'stories' => $stories,
451			'title'   => $this->getTitle(),
452		]);
453	}
454}
455