diff --git a/app/controllers/course/wiki.php b/app/controllers/course/wiki.php index d817ce01a942983483fe3ebc121169f86032bd27..2420f1d7ecd2984cd290881545090db3f79da230 100644 --- a/app/controllers/course/wiki.php +++ b/app/controllers/course/wiki.php @@ -18,6 +18,9 @@ class Course_WikiController extends AuthenticatedController parent::before_filter($action, $args); object_set_visit_module('wiki'); $this->range = Context::get(); + $this->plugin = PluginManager::getInstance()->getPlugin('CoreWiki'); + + PageLayout::setTitle(Navigation::getItem('/course/wiki')->getTitle()); } public function page_action($page_id = null) @@ -26,7 +29,6 @@ class Course_WikiController extends AuthenticatedController $page_id = $this->range->getConfiguration()->WIKI_STARTPAGE_ID; } Navigation::activateItem('/course/wiki/start'); - PageLayout::setTitle(Navigation::getItem('/course/wiki')->getTitle()); $this->page = new WikiPage($page_id); @@ -547,60 +549,51 @@ class Course_WikiController extends AuthenticatedController Navigation::activateItem('/course/wiki/listnew'); $this->limit = Config::get()->ENTRIES_PER_PAGE; + $this->last_visit = object_get_visit($this->range->id, $this->plugin->getPluginId()); $statement = DBManager::get()->prepare(" - SELECT COUNT(*) FROM ( - SELECT `wiki_pages`.`page_id` AS `id`, - 0 AS `is_version`, - `wiki_pages`.`chdate` AS `timestamp` - FROM `wiki_pages` - WHERE `wiki_pages`.`range_id` = :range_id - - UNION - - SELECT `wiki_versions`.`version_id` AS `id`, - 1 AS `is_version`, - `wiki_versions`.`mkdate` AS `timestamp` - FROM `wiki_versions` - JOIN `wiki_pages` USING (`page_id`) - WHERE `wiki_pages`.`range_id` = :range_id - ) AS `all_entries` - "); - $statement->execute([ - 'range_id' => $this->range->id - ]); - $this->num_entries = $statement->fetch(PDO::FETCH_COLUMN); - $this->page = Request::int('page', 0); - - $statement = DBManager::get()->prepare(" - SELECT `wiki_pages`.`page_id` AS `id`, - 0 AS `is_version`, - `wiki_pages`.`chdate` AS `timestamp` + SELECT COUNT(*) FROM `wiki_pages` WHERE `wiki_pages`.`range_id` = :range_id - - UNION - - SELECT `wiki_versions`.`version_id` AS `id`, - 1 AS `is_version`, - `wiki_versions`.`mkdate` AS `timestamp` - FROM `wiki_versions` - JOIN `wiki_pages` USING (`page_id`) - WHERE `wiki_pages`.`range_id` = :range_id - ORDER BY `timestamp` DESC - LIMIT :offset, :limit + AND `wiki_pages`.`chdate` > :threshold + AND `wiki_pages`.`user_id` != :me "); $statement->execute([ 'range_id' => $this->range->id, - 'offset' => Request::int('page', 0) * $this->limit, - 'limit' => $this->limit + 'threshold' => $this->last_visit, + 'me' => User::findCurrent()->id ]); - $this->versions = []; - foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $row) { - if ($row['is_version']) { - $this->versions[] = WikiVersion::find($row['id']); - } else { - $this->versions[] = WikiPage::find($row['id']); - } + $this->num_entries = $statement->fetch(PDO::FETCH_COLUMN); + $this->pagenumber = Request::int('page', 0); + $this->sort = Request::option('sort', 'chdate'); + if (!in_array($this->sort, ['name', 'chdate'])) { + $this->sort = 'chdate'; + } + $this->sort_asc = Request::bool('sort_asc', $this->sort === 'name'); + + + if ($this->num_entries > 0) { + $statement = DBManager::get()->prepare(" + SELECT `wiki_pages`.* + FROM `wiki_pages` + WHERE `wiki_pages`.`range_id` = :range_id + AND `wiki_pages`.`chdate` > :threshold + AND `wiki_pages`.`user_id` != :me + ORDER BY `wiki_pages`.`{$this->sort}` " . ($this->sort_asc ? 'ASC' : 'DESC') . " + LIMIT :offset, :limit + "); + $statement->execute([ + 'range_id' => $this->range->id, + 'threshold' => $this->last_visit, + 'offset' => $this->pagenumber * $this->limit, + 'limit' => $this->limit, + 'me' => User::findCurrent()->id + ]); + $this->pages = array_map( + fn($p) => WikiPage::buildExisting($p), + $statement->fetchAll(PDO::FETCH_ASSOC) + ); + } else { + $this->pages = []; } } @@ -1113,4 +1106,24 @@ class Course_WikiController extends AuthenticatedController } } } + + /** + * @see https://stackoverflow.com/a/7475502/982902 + */ + public function findLongestCommonSubstring(string $str0, string $str1, bool $from_end = false): int + { + if ($from_end) { + $str0 = implode('', array_reverse(mb_str_split($str0, 1))); + $str1 = implode('', array_reverse(mb_str_split($str1, 1))); + } + $length = mb_strlen( + mb_strcut( + $str0, + 0, + strspn($str0 ^ $str1, "\0") + ) + ); + + return $from_end ? mb_strlen($str0) - $length : $length; + } } diff --git a/app/views/course/wiki/newpages.php b/app/views/course/wiki/newpages.php index 0220ac4dcde6843ba632eb86e56750c847dbf14f..d9e0dceea899ae4e9580d7d592548dbf20da382c 100644 --- a/app/views/course/wiki/newpages.php +++ b/app/views/course/wiki/newpages.php @@ -1,25 +1,113 @@ -<table class="default sortable-table" data-sortlist="[[3, 1]]"> +<?php +/** + * @var Course_WikiController $controller + * @var string $sort + * @var bool $sort_asc + * @var WikiPage[]|null $pages + * @var int $last_visit + * + * @var int $num_entries + * @var int $limit + * @var int $pagenumber + */ +?> +<table class="default"> <caption> <?= _('Letzte Änderungen') ?> </caption> + <colgroup> + <col style="min-width: 120px;"> + <col> + <col style="min-width: 150px;"> + <col> + </colgroup> <thead> - <tr> - <th data-sort="text"><?= _('Seitenname') ?></th> - <th data-sort="false"><?= _('Text') ?></th> - <th data-sort="text"><?= _('Autor/-in') ?></th> - <th data-sort="text"><?= _('Datum') ?></th> + <tr class="sortable"> + <th <? if ($sort === 'name') echo 'class="' . ($sort_asc ? 'sortasc' : 'sortdesc') . '"'; ?>> + <a href="<?= $controller->newpages(['sort' => 'name', 'sort_asc' => $sort !== 'name' || !$sort_asc ? 1 : 0]) ?>"> + <?= _('Seitenname') ?> + </a> + </th> + <th><?= _('Text') ?></th> + <th><?= _('Autor/-in') ?></th> + <th <? if ($sort === 'chdate') echo 'class="' . ($sort_asc ? 'sortasc' : 'sortdesc') . '"'; ?>> + <a href="<?= $controller->newpages(['sort' => 'chdate', 'sort_asc' => $sort === 'chdate' && !$sort_asc ? 1 : 0]) ?>"> + <?= _('Datum') ?> + </a> + </th> </tr> </thead> <tbody> - <? foreach (array_reverse($versions) as $version) : ?> - <?= $this->render_partial('course/wiki/versioncompare', ['version' => $version]) ?> - <? endforeach ?> + <? if (count($pages) === 0): ?> + <tr> + <td colspan="4"> + <?= _('Keine Seiten wurden seit Ihrem letzten Besuch verändert.') ?> + </td> + </tr> + <? endif ?> + <? foreach ($pages as $page) : ?> + <tr> + <td> + <a href="<?= $controller->page($page) ?>"> + <?= htmlReady($page->name) ?> + </a> + </td> + <td> + <? + $authors = [$page->user_id => $page->user]; + $oldcontent = ""; + $oldversion = $page; + while ($oldversion = $oldversion->predecessor) { + if ($oldversion->mkdate >= $last_visit && $oldversion->user_id !== User::findCurrent()->id) { + $oldcontent = $oldversion->content; + if (!isset($authors[$oldversion->user_id])) { + $authors[$oldversion->user_id] = $oldversion->user; + } + } else { + break; + } + } + $oldcontent = strip_tags(wikiReady($oldcontent)); + $content = strip_tags(wikiReady($page->content)); + + $commonFromStart = $controller->findLongestCommonSubstring($content, $oldcontent); + $commonFromEnd = $controller->findLongestCommonSubstring($content, $oldcontent, true); + + $content = mb_substr($content, $commonFromStart, $commonFromEnd); + $oldcontent = mb_substr($oldcontent, $commonFromStart, $commonFromEnd); + if ($content) { + echo htmlReady(mila($content, 300), true, true); + } elseif ($oldcontent) { + echo _('Gelöscht') . ': ' . htmlReady($oldcontent, true, true); + } + ?> + </td> + <td> + <ul class="wiki_authors"> + <? foreach ($authors as $user) : ?> + <li> + <? if ($user): ?> + <a href="<?= URLHelper::getLink('dispatch.php/profile', ['username' => $user->username]) ?>" + style="background-image: url(<?= Avatar::getAvatar($user->id)->getURL(Avatar::SMALL) ?>)" + > + <?= htmlReady($user->getFullName()) ?> + </a> + <? else: ?> + <?= _('unbekannt') ?> + <? endif; ?> + </li> + <? endforeach ?> + </ul> + </td> + <td><?= strftime('%x %X', $page->chdate) ?></td> + </tr> + <? endforeach ?> </tbody> <? if ($num_entries > $limit) : ?> <tfoot> <tr> <td colspan="4" class="actions"> - <?= Pagination::create($num_entries, $page, $limit)->asLinks() ?> + <?= Pagination::create($num_entries, $pagenumber, $limit)->asLinks() ?> </td> </tr> </tfoot> diff --git a/app/views/course/wiki/versioncompare.php b/app/views/course/wiki/versioncompare.php deleted file mode 100644 index 9c98ce4a27c6ece1bf49c12087c35d1dddb73729..0000000000000000000000000000000000000000 --- a/app/views/course/wiki/versioncompare.php +++ /dev/null @@ -1,44 +0,0 @@ -<tr> - <td data-sort-value="<?= htmlReady(is_a($version, WikiPage::class) ? $version->name : $version->page->name) ?>"> - <a href="<?= is_a($version, WikiPage::class) ? $controller->page($version) : $controller->version($version) ?>"> - <?= htmlReady(is_a($version, WikiPage::class) ? $version->name : $version->page->name) ?> - </a> - </td> - <td> - <? - $oldversion = $version->predecessor ? $version->predecessor->content : ''; - $oldcontent = strip_tags(wikiReady($oldversion)); - $content = strip_tags(wikiReady($version->content)); - while ($content && $oldcontent && $content[0] == $oldcontent[0]) { - $content = substr($content, 1); - $oldcontent = substr($oldcontent, 1); - } - while ($content && $oldcontent && $content[strlen($content) - 1] == $oldcontent[strlen($oldcontent) - 1]) { - $content = substr($content, 0, -1); - $oldcontent = substr($oldcontent, 0, -1); - } - if ($content) { - echo nl2br(htmlReady(mila($content, 300))); - } elseif ($oldcontent) { - echo _('Gelöscht') . ': ' . nl2br(htmlReady($oldcontent)); - } else { - echo nl2br(strip_tags(wikiReady(mila($version->content, 300)))); - } - - ?></td> - <? $user = User::find($version->user_id) ?> - <td data-sort-value="<?= htmlReady($user ? $user->getFullName() : _('unbekannt')) ?>"> - <? - if ($user) { - echo Avatar::getAvatar($user->id)->getImageTag(Avatar::SMALL); - echo ' '; - echo htmlReady($user->getFullName()); - } else { - echo _('unbekannt'); - } - ?></td> - <td data-sort-value="<?= htmlReady(is_a($version, WikiPage::class) ? $version->chdate : $version->mkdate) ?>"> - <? $chdate = is_a($version, WikiPage::class) ? $version->chdate : $version->mkdate ?> - <?= $chdate > 0 ? date('d.m.Y H:i:s', $chdate) : _('unbekannt') ?> - </td> -</tr> diff --git a/app/views/course/wiki/versiondiff.php b/app/views/course/wiki/versiondiff.php index 49ac6f4827c412b46234aceb0387a1b94e19fe64..d621446143f42a9a7de686d195e3896a840d2246 100644 --- a/app/views/course/wiki/versiondiff.php +++ b/app/views/course/wiki/versiondiff.php @@ -2,16 +2,17 @@ /** * @var WikiPage|WikiVersion $version * @var Course_WikiController $controller + * @var string $diff */ ?> <h3> <a href="<?= is_a($version, WikiPage::class) ? $controller->page($version) : $controller->version($version) ?>"> <? $chdate = is_a($version, WikiPage::class) ? $version->chdate : $version->mkdate ?> <?= sprintf( - _('Version %1$s, geändert von %2$s am %3$s.'), - htmlReady($version->versionnumber), + _('Version %1$u, geändert von %2$s am %3$s.'), + $version->versionnumber, htmlReady($version->user ? $version->user->getFullName() : _('unbekannt')), - $chdate > 0 ? date('d.m.Y H:i:s', $chdate) : _('unbekannt')) ?> + $chdate ? strftime('%x %X', $chdate) : _('unbekannt')) ?> </a> </h3> <div class="wiki_diffs"> diff --git a/lib/models/WikiPage.class.php b/lib/models/WikiPage.class.php index a5392b49dc4e6d9e926beca13b261b3b41711a32..3011a26595c2c03e94a394d0e04ef29abed291a3 100644 --- a/lib/models/WikiPage.class.php +++ b/lib/models/WikiPage.class.php @@ -22,9 +22,12 @@ * @property int|null $mkdate database column * @property User|null $user belongs_to User * @property Course $course belongs_to Course - * @property-read mixed $parent additional field - * @property-read mixed $children additional field - * @property-read mixed $config additional field + * @property WikiVersion[]|SimpleORMapCollection $versions + * @property WikiOnlineEditingUser[]|SimpleORMapCollection $onlineeditingusers + * @property-read WikiPage $parent additional field + * @property-read WikiPage[] $children additional field + * @property-read WikiVersion|null $predecessor additional field + * @property-read int $versionnumber additional field */ class WikiPage extends SimpleORMap implements PrivacyObject { @@ -57,25 +60,25 @@ class WikiPage extends SimpleORMap implements PrivacyObject ]; $config['additional_fields']['parent'] = [ - 'get' => function ($page) { - return \WikiPage::find($page->parent_id); + 'get' => function (WikiPage $page): ?WikiPage { + return self::find($page->parent_id); } ]; $config['additional_fields']['children'] = [ - 'get' => function ($page) { + 'get' => function (WikiPage $page): array { return self::findBySQL('parent_id = ?', [ $page->id ]); } ]; $config['additional_fields']['predecessor'] = [ - 'get' => function ($page) { + 'get' => function (WikiPage $page): ?WikiVersion { return $page->versions ? $page->versions[0] : null; } ]; $config['additional_fields']['versionnumber'] = [ - 'get' => function ($page) { + 'get' => function (WikiPage $page): int { return count($page->versions) + 1; } ]; @@ -92,7 +95,7 @@ class WikiPage extends SimpleORMap implements PrivacyObject $this->user_id = User::findCurrent()->id; if ( !$this->isNew() - && $this->content['content'] !== $this->content_db['content'] + && $this->content['content'] !== $this->content_db['content'] && ( $this->content_db['user_id'] !== $this->content['user_id'] || $this->content_db['chdate'] < time() - 60 * 30 @@ -121,7 +124,7 @@ class WikiPage extends SimpleORMap implements PrivacyObject /** * Returns whether this page is visible to the given user. - * @param mixed $user User object or id + * @param string|null $user_id User id * @return boolean indicating whether the page is visible */ public function isReadable(?string $user_id = null): bool @@ -166,7 +169,7 @@ class WikiPage extends SimpleORMap implements PrivacyObject /** * Returns whether this page is editable to the given user. - * @param string $user_id the ID of the user + * @param string|null $user_id the ID of the user * @return boolean indicating whether the page is editable */ public function isEditable(?string $user_id = null): bool @@ -203,7 +206,7 @@ class WikiPage extends SimpleORMap implements PrivacyObject * @param string $range_id Course id * @return WikiPage */ - public static function getStartPage($range_id) + public static function getStartPage($range_id): WikiPage { $page_id = CourseConfig::get($range_id)->WIKI_STARTPAGE_ID; @@ -212,7 +215,6 @@ class WikiPage extends SimpleORMap implements PrivacyObject } $page = new WikiPage(); - $pagename = _('Startseite'); $page->content = _('Dieses Wiki ist noch leer.'); if ($page->isEditable()) { $page->content .= ' ' . _("Bearbeiten Sie es!\nNeue Seiten oder Links werden einfach durch Eingeben von [nop][[Wikinamen]][/nop] in doppelten eckigen Klammern angelegt."); @@ -244,11 +246,11 @@ class WikiPage extends SimpleORMap implements PrivacyObject /** * Tests if a given Wikipage name (keyword) is a valid ancestor for this page. * - * @param string ancestor Wikipage name to be tested to be an ancestor + * @param string $ancestor Wikipage name to be tested to be an ancestor * @return boolean true if ok, false if not * */ - public function isValidAncestor($ancestor) + public function isValidAncestor($ancestor): bool { if ($this->name === 'WikiWikiWeb' || $this->name === $ancestor) { return false; @@ -267,10 +269,10 @@ class WikiPage extends SimpleORMap implements PrivacyObject /** * Retrieve an array of all descending WikiPages (recursive). * - * @return array Array of all descendant WikiPages + * @return WikiPage[] Array of all descendant WikiPages * */ - public function getDescendants() + public function getDescendants(): array { $descendants = []; @@ -281,6 +283,9 @@ class WikiPage extends SimpleORMap implements PrivacyObject return $descendants; } + /** + * @return array + */ public function getOnlineUsers(): array { $users = []; diff --git a/resources/assets/stylesheets/scss/variables.scss b/resources/assets/stylesheets/scss/variables.scss index a915e766f6a0b25edc8fc2e1653f052d132b6c20..a2d90b58ef2e0aa9432c353cba82407fdc2b04c7 100644 --- a/resources/assets/stylesheets/scss/variables.scss +++ b/resources/assets/stylesheets/scss/variables.scss @@ -171,6 +171,10 @@ $grid-gap: 0; #{"--"}group-color-7: $petrol; #{"--"}group-color-8: $brown; + #{"--"}avatar-small: $avatar-small; + #{"--"}avatar-medium: $avatar-medium; + #{"--"}avatar-normal: $avatar-normal; + #{"--"}transition-duration: $transition-duration; #{"--"}transition-duration-slow: $transition-duration-slow; diff --git a/resources/assets/stylesheets/scss/wiki.scss b/resources/assets/stylesheets/scss/wiki.scss index 12cf63b3bd579f41189df3897611ad1a1c199ed5..a139eb524c3c89c7a4c312623b3a66eb4191bf3e 100644 --- a/resources/assets/stylesheets/scss/wiki.scss +++ b/resources/assets/stylesheets/scss/wiki.scss @@ -185,3 +185,18 @@ article.studip.wiki { .wiki_highlight { background-color: var(--yellow); } + ul.wiki_authors { + list-style-type: none; + padding: 0; + li { + margin-bottom: 5px; + } + a { + background-position: left top; + background-repeat: no-repeat; + background-size: var(--avatar-small); + display: block; + min-height: var(--avatar-small); + padding-left: calc(var(--avatar-small) + 1ex); + } + }