diff options
| author | Finn Schneider <schneider@data-quest.de> | 2025-12-16 10:28:32 +0000 |
|---|---|---|
| committer | Murtaza Sultani <sultani@data-quest.de> | 2025-12-16 11:28:32 +0100 |
| commit | 61b2e2ba3271a911308f80fcbe2073852312bf80 (patch) | |
| tree | e85e7f9829a104bbfc5d29437b3691ca25abc04d /app | |
| parent | 7ace20d44455006de508db7a82b8038960b7fae7 (diff) | |
Resolve "Bulk-Aktions in der Themenansicht"
Closes #10
Merge request studip/studip!4514
Diffstat (limited to 'app')
| -rw-r--r-- | app/controllers/course/topics.php | 113 | ||||
| -rw-r--r-- | app/views/course/topics/_dates.php | 13 | ||||
| -rw-r--r-- | app/views/course/topics/_material.php | 34 | ||||
| -rw-r--r-- | app/views/course/topics/details.php | 28 | ||||
| -rw-r--r-- | app/views/course/topics/index.php | 236 |
5 files changed, 284 insertions, 140 deletions
diff --git a/app/controllers/course/topics.php b/app/controllers/course/topics.php index e955ba1..026c2bf 100644 --- a/app/controllers/course/topics.php +++ b/app/controllers/course/topics.php @@ -21,7 +21,10 @@ class Course_TopicsController extends AuthenticatedController $this->forum_activated = $course->isToolActive(CoreForum::class); $this->documents_activated = $course->isToolActive(CoreDocuments::class); - if ($action !== 'index' && !$GLOBALS['perm']->have_studip_perm('tutor', Context::getId())) { + if ( + !in_array($action, ['index', 'details']) + && !User::findCurrent()->hasPermissionLevel('tutor', Context::get()) + ) { throw new AccessDeniedException(); } @@ -37,9 +40,7 @@ class Course_TopicsController extends AuthenticatedController public function delete_action(CourseTopic $topic) { - if (!Request::isPost()) { - throw new MethodNotAllowedException(); - } + CSRFProtection::verifyUnsafeRequest(); if ($topic->seminar_id && ($topic->seminar_id !== Context::getId())) { throw new AccessDeniedException(); @@ -102,7 +103,7 @@ class Course_TopicsController extends AuthenticatedController } PageLayout::postSuccess(_('Thema gespeichert.')); - $this->redirect($this->indexURL(['open' => $topic->id])); + $this->redirect($this->indexURL()); } public function swap_action(CourseTopic $a, CourseTopic $b) @@ -123,7 +124,7 @@ class Course_TopicsController extends AuthenticatedController $a->store(); $b->store(); - $this->redirect($this->indexURL(['open' => $a->id])); + $this->redirect($this->indexURL()); } public function allow_public_action() @@ -219,25 +220,62 @@ class Course_TopicsController extends AuthenticatedController $this->render_json($output); } + public function bulk_action(string $action): void + { + CSRFProtection::verifyUnsafeRequest(); + $topic_ids = Request::optionArray('topics'); + + [$callback, $success, $failure] = match ($action) { + 'ftopic' => [ + fn(CourseTopic $topic) => $topic->connectWithForumThread(), + _('Forumsthemen erfolgreich angelegt.'), + _('Fehler beim Anlegen von Forumsthemen zu:'), + ], + 'folder' => [ + fn(CourseTopic $topic) => $topic->connectWithDocumentFolder(), + _('Ordner erfolgreich angelegt.'), + _('Fehler beim Anlegen von Ordnern zu:'), + ], + 'delete' => [ + fn(CourseTopic $topic) => $topic->delete(), + _('Erfolgreich gelöscht.'), + _('Fehler beim Löschen von:'), + ], + default => throw new InvalidArgumentException('Unknown action'), + }; + + $errors = []; + CourseTopic::findEachMany( + function (CourseTopic $topic) use ($callback, &$errors) { + if (!$callback($topic)) { + $errors[] = $topic->title; + } + }, + $topic_ids + ); + if (count($errors) === 0) { + PageLayout::postSuccess($success); + } else { + PageLayout::postError( + $failure, + array_map('htmlReady', $errors) + ); + } + + $this->redirect($this->indexURL()); + } + + public function details_action(CourseTopic $topic) + { + PageLayout::setTitle(sprintf(_('Details: %s'), htmlReady($topic->title))); + $this->render_template('course/topics/details'); + } + private function setupSidebar($action) { $sidebar = Sidebar::get(); $actions = $sidebar->addWidget(new ActionsWidget()); - if ($action === 'index') { - $actions->addLink( - _('Alle Themen aufklappen'), - $this->url_for('course/topics/show'), - Icon::create('arr_1down'), - ['onclick' => "jQuery('table.withdetails > tbody > tr:not(.details):not(.open) > :first-child a').click(); return false;"] - ); - $actions->addLink( - _('Alle Themen zuklappen'), - $this->url_for('course/topics/hide'), - Icon::create('arr_1right'), - ['onclick' => "jQuery('table.withdetails > tbody > tr:not(.details).open > :first-child a').click(); return false;"] - ); - } if ($GLOBALS['perm']->have_studip_perm('tutor', Context::getId())) { $actions->addLink( _('Neues Thema erstellen'), @@ -286,4 +324,39 @@ class Course_TopicsController extends AuthenticatedController return $links; } + + public function getActionMenu(CourseTopic $topic): ActionMenu + { + $actions = ActionMenu::get(); + $actions->setContext(htmlReady($topic->title)); + + if (User::findCurrent()->hasPermissionLevel('tutor', Context::get())) { + $actions->addLink( + $this->editURL($topic), + _('Bearbeiten'), + Icon::create('edit') + )->asDialog(); + $actions->addButton( + 'delete', + _('Löschen'), + Icon::create('trash'), + [ + 'formaction' => $this->delete($topic), + 'data-confirm' => _('Das Thema wirklich löschen?'), + ] + ); + if (!$this->cancelled_dates_locked && count($topic->dates) > 0) { + $actions->addLink( + URLHelper::getURL( + 'dispatch.php/course/cancel_dates', + ['issue_id' => $topic->id] + ), + _('Alle Termine ausfallen lassen'), + Icon::create('date') + )->asDialog(); + } + } + + return $actions; + } } diff --git a/app/views/course/topics/_dates.php b/app/views/course/topics/_dates.php new file mode 100644 index 0000000..4fc37f6 --- /dev/null +++ b/app/views/course/topics/_dates.php @@ -0,0 +1,13 @@ +<ul class="clean"> + <? foreach ($topic->dates as $date) : ?> + <li> + <a href="<?= URLHelper::getLink('dispatch.php/course/dates/details/' . $date->id) ?>" + data-dialog="size=auto" + style="white-space: nowrap" + > + <?= Icon::create('date')->asSvg(['class' => 'text-bottom']) ?> + <?= htmlReady($date->getFullName()) ?> + </a> + </li> + <? endforeach ?> +</ul> diff --git a/app/views/course/topics/_material.php b/app/views/course/topics/_material.php new file mode 100644 index 0000000..5e853ce --- /dev/null +++ b/app/views/course/topics/_material.php @@ -0,0 +1,34 @@ +<ul class="clean"> + <? $folder = $topic->folders->first() ?> + <? if ($documents_activated && $folder) : ?> + <li> + <? $folder_label = sprintf(_('Zu Dateiordner von Thema %s navigieren'), htmlReady($topic->title)) ?> + <a + href="<?= URLHelper::getLink('dispatch.php/course/files/index/' . $folder->id) ?>" + aria-label="<?= $folder_label ?>" + title="<?= $folder_label ?>" + > + <?= $folder->getTypedFolder()->getIcon('clickable')->asSvg(['class' => 'text-bottom']) ?> + <span class="<?= $always_show ? '' : 'responsive-hidden' ?>"> + <?= _('Dateiordner') ?> + </span> + </a> + </li> + <? endif ?> + + <? if ($forum_activated && $topic->forum_thread_url) : ?> + <li> + <? $ftopic_label = sprintf(_('Zu Forumsthema von Thema %s navigieren'), htmlReady($topic->title)) ?> + <a + href="<?= URLHelper::getLink($topic->forum_thread_url) ?>" + aria-label="<?= $ftopic_label ?>" + title="<?= $ftopic_label ?>" + > + <?= Icon::create('forum')->asSvg(['class' => 'text-bottom']) ?> + <span class="<?= $always_show ? '' : 'responsive-hidden' ?>"> + <?= _('Thema im Forum') ?> + </span> + </a> + </li> + <? endif ?> +</ul> diff --git a/app/views/course/topics/details.php b/app/views/course/topics/details.php new file mode 100644 index 0000000..df72454 --- /dev/null +++ b/app/views/course/topics/details.php @@ -0,0 +1,28 @@ +<div> + <h2><?= htmlReady($topic->title) ?></h2> + <? $has_content = false ?> + <? if ( + ($documents_activated && $topic->folders->first()) + || ($forum_activated && $topic->forum_thread_url) + ) : ?> + <h3><?= _('Materialien') ?></h3> + <?= $this->render_partial('course/topics/_material.php', ['always_show' => true]) ?> + <? $has_content = true ?> + <? endif ?> + + <? if (count($topic->dates) > 0) : ?> + <h3><?= _('Termine') ?></h3> + <?= $this->render_partial('course/topics/_dates.php') ?> + <? $has_content = true ?> + <? endif ?> + + <? if ($topic->description) : ?> + <h3><?= _('Beschreibung') ?></h3> + <?= formatReady($topic->description) ?> + <? $has_content = true ?> + <? endif ?> + + <? if (!$has_content) : ?> + <?= _('Keine weiteren Informationen verfügbar.') ?> + <? endif ?> +</div> diff --git a/app/views/course/topics/index.php b/app/views/course/topics/index.php index e538542..29a9d98 100644 --- a/app/views/course/topics/index.php +++ b/app/views/course/topics/index.php @@ -4,130 +4,126 @@ * @var Course_TopicsController $controller * @var array<array{next: ?CourseTopic, previous: ?CourseTopic}> $topic_links */ + +use Studip\Button; + ?> <? if (count($topics) > 0) : ?> -<table class="default withdetails"> - <colgroup> - <col width="50%"> - <col> - </colgroup> - <thead> - <tr> - <th><?= _('Thema') ?></th> - <th><?= _('Termine') ?></th> - </tr> - </thead> - <tbody> - <? foreach ($topics as $key => $topic) : ?> - <tr class="<?= Request::get("open") === $topic->getId() ? "open" : "" ?>"> - <td> - <a href="#" name="<?=$topic->getId()?>" onClick="jQuery(this).closest('tr').toggleClass('open'); return false;"> - <? if ($topic->paper_related): ?> - <?= Icon::create('glossary')->asSvg(array_merge( - ['class' => 'text-top'], - tooltip2(_('Thema behandelt eine Hausarbeit oder ein Referat')) - )) ?> - <? endif; ?> - <?= htmlReady($topic['title']) ?> - </a> - </td> - <td> - <ul class="clean"> - <? foreach ($topic->dates as $date) : ?> - <li> - <a href="<?= URLHelper::getLink("dispatch.php/course/dates/details/".$date->getId()) ?>" data-dialog="size=auto"> - <?= Icon::create('date', 'clickable')->asSvg(['class' => 'text-bottom']) ?> - <?= htmlReady($date->getFullName()) ?> - </a> - </li> - <? endforeach ?> - </ul> - </td> - </tr> - <tr class="details nohover"> - <td colspan="2"> - <div class="detailscontainer"> - <table class="default nohover"> - <tbody> - <tr> - <td><strong><?= _('Beschreibung') ?></strong></td> - <td><?= formatReady($topic['description']) ?></td> - </tr> - <tr> - <td><strong><?= _('Materialien') ?></strong></td> - <td> - <? $material = false ?> - <ul class="clean"> - <? $folder = $topic->folders->first() ?> - <? if ($documents_activated && $folder) : ?> - <li> - <a href="<?= URLHelper::getLink( - 'dispatch.php/course/files/index/' . $folder->id - ) ?>"> - <?= $folder->getTypedFolder()->getIcon('clickable')->asSvg(['class' => 'text-bottom']) ?> - <?= _('Dateiordner') ?> - </a> - </li> - <? $material = true ?> - <? endif ?> - - <? if ($forum_activated && ($link_to_thread = $topic->forum_thread_url)) : ?> - <li> - <a href="<?= URLHelper::getLink($link_to_thread) ?>"> - <?= Icon::create('forum', 'clickable')->asSvg(['class' => 'text-bottom']) ?> - <?= _('Thema im Forum') ?> - </a> - </li> - <? $material = true ?> - <? endif ?> - </ul> - <? if (!$material) : ?> - <?= _('Keine Materialien zu dem Thema vorhanden') ?> + <form method="POST"> + <?= CSRFProtection::tokenTag() ?> + <table class="default" id="topics_index_table"> + <thead> + <tr> + <th scope="col" style="width: 20px"> + <input + type="checkbox" + data-proxyfor="#topics_index_table tbody input[type=checkbox]" + data-activates="#topics_index_table tfoot button" + aria-label="<?= _('Alle Themen auswählen') ?>" + > + </th> + <th scope="col"><?= _('Thema') ?></th> + <th scope="col" class="responsive-hidden"><?= _('Termine') ?></th> + <th scope="col"><?= _('Materialien') ?></th> + <th scope="col" class="responsive-hidden"><?= _('Beschreibung') ?></th> + <? if ($is_tutor = User::findCurrent()->hasPermissionLevel('tutor', Context::get())) : ?> + <th scope="col" class="actions"><?= _('Aktionen') ?></th> + <? endif ?> + </tr> + </thead> + <tbody> + <? foreach ($topics as $topic) : ?> + <tr> + <td> + <input type="checkbox" value="<?= htmlReady($topic->id) ?>" name="topics[]" + aria-label="<?= sprintf(_('Thema %s auswählen'), htmlReady($topic->title)) ?>"> + </td> + <td> + <a + href="<?= URLHelper::getLink('dispatch.php/course/topics/details/' . $topic->id) ?>" + title=" <?= sprintf(_('Thema %s öffnen'), htmlReady($topic->title)) ?>" + aria-label="<?= sprintf(_('Thema %s öffnen'), htmlReady($topic->title)) ?>" + data-dialog="size=auto" + > + <?= htmlReady($topic['title']) ?> + </a> + <? if ($topic->paper_related): ?> + <?= Icon::create('info-circle')->asSvg(array_merge( + tooltip2(_('Thema behandelt eine Hausarbeit oder ein Referat')) + )) ?> + <? endif ?> + </td> + <td class="responsive-hidden"> + <?= $this->render_partial('course/topics/_dates.php', ['topic' => $topic]) ?> + </td> + <td> + <?= $this->render_partial('course/topics/_material.php', ['topic' => $topic]) ?> + </td> + <td class="responsive-hidden"> + <?= formatReady($topic['description']) ?> + </td> + <? if ($is_tutor) : ?> + <td class="actions"> + <div> + <? $move_up_label = sprintf(_('%s nach oben verschieben'), htmlReady($topic->title)); + if ($topic_links[$topic->id]['previous']) : ?> + <button + class="as-link" + formaction="<?= $controller->swap($topic, $topic_links[$topic->id]['previous']) ?>" + aria-label="<?= $move_up_label ?>" + title="<?= $move_up_label ?>" + > + <?= Icon::create('arr_2up') ?> + </button> + <? else : ?> + <?= Icon::create('arr_2up', Icon::ROLE_INACTIVE) ?> <? endif ?> - </td> - </tr> - </tbody> - </table> - <div style="text-align: center;"> - <? if ($GLOBALS['perm']->have_studip_perm("tutor", Context::getId())) : ?> - <?= Studip\LinkButton::createEdit( - _('Bearbeiten'), - $controller->editURL($topic), - ['data-dialog' => ''] - ) ?> - - <form action="<?= $controller->delete($topic) ?>" method="post" style="display: inline"> - <?= Studip\Button::create( - _('Löschen'), - 'delete', - ['data-confirm' => _('Das Thema wirklich löschen?')] - ) ?> - </form> - - <? if (!$cancelled_dates_locked && $topic->dates->count()) : ?> - <?= \Studip\LinkButton::create(_('Alle Termine ausfallen lassen'), URLHelper::getURL("dispatch.php/course/cancel_dates", ['issue_id' => $topic->getId()]), ['data-dialog' => '']) ?> - <? endif ?> - - <span class="button-group"> - <? if ($topic_links[$topic->id]['previous']) : ?> - <form action="<?= $controller->swap($topic, $topic_links[$topic->id]['previous']) ?>" method="post" style="display: inline;"> - <?= Studip\Button::createMoveUp(_('Nach oben verschieben')) ?> - </form> - <? endif ?> - <? if ($topic_links[$topic->id]['next']) : ?> - <form action="<?= $controller->swap($topic, $topic_links[$topic->id]['next']) ?>" method="post" style="display: inline;"> - <?= Studip\Button::createMoveDown(_('Nach unten verschieben')) ?> - </form> - <? endif ?> - </span> + <? $move_down_label = sprintf(_('%s nach unten verschieben'), htmlReady($topic->title)); + if ($topic_links[$topic->id]['next']) : ?> + <button + class="as-link" + formaction="<?= $controller->swap($topic, $topic_links[$topic->id]['next']) ?>" + aria-label="<?= $move_down_label ?>" + title="<?= $move_down_label ?>" + > + <?= Icon::create('arr_2down') ?> + </button> + <? else : ?> + <?= Icon::create('arr_2down', Icon::ROLE_INACTIVE) ?> + <? endif ?> + <?= $controller->getActionMenu($topic) ?> + </div> + </td> + <? endif ?> + </tr> + <? endforeach ?> + </tbody> + <? if ($is_tutor) : ?> + <tfoot> + <tr> + <td colspan="6"> + <? if ($documents_activated) : ?> + <?= Button::create(_('Dateiordner anlegen'), 'bulk_folder', [ + 'formaction' => $controller->bulkURL('folder'), + 'data-confirm' => _('Sind Sie sicher, dass Sie für Ihre Auswahl je einen Dateiordner anlegen wollen?'), + ]) ?> + <? endif ?> + <? if ($forum_activated) : ?> + <?= Button::create(_('Forumsthema anlegen'), 'bulk_ftopic', [ + 'formaction' => $controller->bulkURL('ftopic'), + 'data-confirm' => _('Sind Sie sicher, dass Sie für Ihre Auswahl je ein Forumsthema anlegen wollen?'), + ]) ?> <? endif ?> - </div> - </div> - </td> - </tr> - <? endforeach ?> - </tbody> -</table> + <?= Button::create(_('Löschen'), 'bulk_delete', [ + 'formaction' => $controller->bulkURL('delete'), + 'data-confirm' => _('Sind Sie sicher, dass Sie Ihre Auswahl löschen wollen?'), + ]) ?> + </td> + </tr> + </tfoot> + <? endif ?> + </table> + </form> <? else : ?> <?= MessageBox::info(_('Keine Themen vorhanden.')) ?> <? endif ?> |
