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() ?>&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() ?>&mod_action=admin_edit&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() ?>&mod_action=admin_delete&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