From 7dddea8ccca601bf2da28960f2e27a223fe60ea6 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Thu, 24 Nov 2022 10:29:24 +0000 Subject: [PATCH] rework aux lock rules, use sorm model, deprecate old class and let name and description be translatable, fixes #1791 Closes #1791 Merge request studip/studip!1177 --- app/controllers/admin/courses.php | 8 +- app/controllers/admin/specification.php | 106 +++++++++----- app/controllers/authenticated_controller.php | 21 ++- .../consultation/consultation_controller.php | 12 -- app/controllers/course/members.php | 3 + app/controllers/course/overview.php | 26 ++-- app/views/admin/courses/aux-select.php | 12 +- app/views/admin/courses/aux_preselect.php | 21 +-- app/views/admin/specification/_field.php | 28 ++-- app/views/admin/specification/edit.php | 129 ++++++++--------- app/views/admin/specification/index.php | 75 +++++----- app/views/course/members/additional_input.php | 54 ++++--- ...3.9_convert_aux_lock_rules_json_fields.php | 56 ++++++++ lib/classes/AuxLockRules.class.php | 3 + lib/classes/Markup.class.php | 2 +- lib/classes/StudipArrayObject.class.php | 8 ++ lib/classes/WidgetContainer.php | 5 +- lib/models/AuxLockRule.php | 133 +++++++++++++----- lib/models/DatafieldEntryModel.class.php | 82 ++++++----- templates/i18n/input.php | 10 ++ 20 files changed, 481 insertions(+), 313 deletions(-) create mode 100644 db/migrations/5.3.9_convert_aux_lock_rules_json_fields.php diff --git a/app/controllers/admin/courses.php b/app/controllers/admin/courses.php index e832628003e..7e78aaea4d5 100644 --- a/app/controllers/admin/courses.php +++ b/app/controllers/admin/courses.php @@ -372,13 +372,7 @@ class Admin_CoursesController extends AuthenticatedController ]], LockRule::findAllByType('sem') )); - $this->aux_lock_rules = array_merge( - [[ - 'name' => '--' . _("keine Zusatzangaben") . '--', - 'lock_id' => 'none' - ]], - AuxLockRules::getAllLockRules() - ); + $this->aux_lock_rules = AuxLockRule::findBySQL('1 ORDER BY name'); //build the sidebar: diff --git a/app/controllers/admin/specification.php b/app/controllers/admin/specification.php index 977ec1830f5..819d59bd82d 100644 --- a/app/controllers/admin/specification.php +++ b/app/controllers/admin/specification.php @@ -17,6 +17,8 @@ */ class Admin_SpecificationController extends AuthenticatedController { + protected $_autobind = true; + /** * Common tasks for all actions. */ @@ -41,25 +43,39 @@ class Admin_SpecificationController extends AuthenticatedController */ public function index_action() { - $this->allrules = AuxLockRules::getAllLockRules(); + $this->rules = AuxLockRule::findBySQL('1 ORDER BY name'); + + Sidebar::Get()->addWidget(new ActionsWidget())->addLink( + _('Neue Regel anlegen'), + $this->editURL(), + Icon::create('add') + ); } /** * Edit or create a rule - * - * @param string $edit_id + * @property AuxLockRule $rule */ - public function edit_action($id = null) + public function edit_action(AuxLockRule $rule = null) { - //get data - $user_field = 'user'; - $semdata_field = 'usersemdata'; - $this->semFields = AuxLockRules::getSemFields(); - $this->entries_user = DataField::getDataFields($user_field); - $this->entries_semdata = DataField::getDataFields($semdata_field); - $this->rule = is_null($id) ? false : AuxLockRules::getLockRuleByID($id); - - if ($GLOBALS['perm']->have_perm('root') && count($this->entries_semdata) == 0) { + $rule->name = Request::i18n('name', $rule->name); + $rule->description = Request::i18n('description', $rule->description); + $rule->attributes = Request::optionArray('fields') ?: $rule->attributes; + $rule->sorting = Request::getArray('order') ?: $rule->sorting; + + if ($GLOBALS['perm']->have_perm('root')) { + Sidebar::Get()->addWidget(new ActionsWidget())->addLink( + _('Datenfelder bearbeiten'), + URLHelper::getURL('dispatch.php/admin/datafields'), + Icon::create('edit') + ); + } + + $this->semFields = $this->getSemFields(); + $this->entries_user = DataField::getDataFields('user'); + $this->entries_semdata = DataField::getDataFields('usersemdata'); + + if ($GLOBALS['perm']->have_perm('root') && count($this->entries_semdata) === 0) { PageLayout::postWarning(sprintf( _('Sie müssen zuerst im Bereich %sDatenfelder%s in der Kategorie ' . '<em>Datenfelder für Personenzusatzangaben in Veranstaltungen</em> ' @@ -74,51 +90,63 @@ class Admin_SpecificationController extends AuthenticatedController * Store or edit Rule * @param string $id */ - public function store_action($id = '') + public function store_action(AuxLockRule $rule = null) { - CSRFProtection::verifyRequest(); + CSRFProtection::verifyUnsafeRequest(); $errors = []; - if (!Request::get('rulename')) { + if (!trim(Request::get('name'))) { $errors[] = _('Bitte geben Sie der Regel mindestens einen Namen!'); } - if (!AuxLockRules::checkLockRule(Request::getArray('fields'))) { + + if (!AuxLockRule::validateFields(Request::optionArray('fields'))) { $errors[] = _('Bitte wählen Sie mindestens ein Feld aus der Kategorie "Zusatzinformationen" aus!'); } - if (empty($errors)) { - if (!$id) { - //new - AuxLockRules::createLockRule(Request::get('rulename'), Request::get('description'), Request::getArray('fields'), Request::getArray('order')); - } else { - //edit - AuxLockRules::updateLockRule($id, Request::get('rulename'), Request::get('description'), Request::getArray('fields'), Request::getArray('order')); - } - PageLayout::postSuccess(sprintf( - _('Die Regel "%s" wurde erfolgreich gespeichert!'), - htmlReady(Request::get('rulename')) - )); - } else { + if ($errors) { PageLayout::postError(_('Ihre Eingaben sind ungültig.'), $errors); - } + $this->keepRequest(); + $this->redirect($this->editURL($rule)); + } else { + $rule->name = Request::i18n('name'); + $rule->description = Studip\Markup::purifyHtml(Request::i18n('description')); + $rule->attributes = Request::optionArray('fields') ?? []; + $rule->sorting = Request::getArray('order') ?? []; - $this->redirect('admin/specification'); + if ($rule->store()) { + PageLayout::postSuccess(sprintf( + _('Die Regel "%s" wurde erfolgreich gespeichert!'), + htmlReady($rule->name) + )); + } + $this->redirect('admin/specification'); + } } /** * Delete a rule, using a modal dialog - * - * @param string $rule_id */ - public function delete_action($rule_id) + public function delete_action(AuxLockRule $rule) { CSRFProtection::verifyUnsafeRequest(); - if (AuxLockRules::deleteLockRule($rule_id)) { - PageLayout::postSuccess(_('Die Regel wurde erfolgreich gelöscht!')); - } else { + + $result = $rule->delete(); + if ($result === false) { PageLayout::postError(_('Es können nur nicht verwendete Regeln gelöscht werden!')); + } elseif ($result > 0) { + PageLayout::postSuccess(_('Die Regel wurde erfolgreich gelöscht!')); } - $this->redirect('admin/specification'); + $this->redirect($this->indexURL()); + } + + private function getSemFields(): array + { + return [ + 'vasemester' => _('Semester'), + 'vanr' => _('Veranstaltungsnummer'), + 'vatitle' => _('Veranstaltungstitel'), + 'vadozent' => _('Dozent'), + ]; } } diff --git a/app/controllers/authenticated_controller.php b/app/controllers/authenticated_controller.php index f50d478c9fa..e051ffa7156 100644 --- a/app/controllers/authenticated_controller.php +++ b/app/controllers/authenticated_controller.php @@ -1,8 +1,4 @@ <?php -# Lifter007: TODO -# Lifter003: TODO -# Lifter010: TODO - /* * Copyright (C) 2009 - Marcus Lunzenauer <mlunzena@uos.de> * @@ -16,4 +12,21 @@ class AuthenticatedController extends StudipController { protected $with_session = true; //we do need to have a session for this controller protected $allow_nobody = false; //nobody is not allowed and always gets a login-screen + + public function before_filter(&$action, &$args) + { + parent::before_filter($action, $args); + + // Restore request if present + if (isset($this->flash['request'])) { + foreach ($this->flash['request'] as $key => $value) { + Request::set($key, $value); + } + } + } + + protected function keepRequest() + { + $this->flash['request'] = Request::getInstance()->getIterator()->getArrayCopy(); + } } diff --git a/app/controllers/consultation/consultation_controller.php b/app/controllers/consultation/consultation_controller.php index eba81ff4967..dc7fd6e6a90 100644 --- a/app/controllers/consultation/consultation_controller.php +++ b/app/controllers/consultation/consultation_controller.php @@ -26,13 +26,6 @@ abstract class ConsultationController extends AuthenticatedController URLHelper::addLinkParam('cid', $this->range->id); } - // Restore request if present - if (isset($this->flash['request'])) { - foreach ($this->flash['request'] as $key => $value) { - Request::set($key, $value); - } - } - // This defines the function to display a note. Not really a partial, // not a controller method. This has no real place... $this->displayNote = function ($what, $length = 40, $position = 'above') { @@ -72,11 +65,6 @@ abstract class ConsultationController extends AuthenticatedController return $this->range->getConfiguration()->CONSULTATION_TAB_TITLE; } - protected function keepRequest() - { - $this->flash['request'] = Request::getInstance()->getIterator()->getArrayCopy(); - } - /** * @param $block_id * diff --git a/app/controllers/course/members.php b/app/controllers/course/members.php index 16c59a3ed27..ad7d6dacf93 100644 --- a/app/controllers/course/members.php +++ b/app/controllers/course/members.php @@ -1330,6 +1330,9 @@ class Course_MembersController extends AuthenticatedController $course = Course::findCurrent(); $member = $course->members->findOneBy('user_id', $GLOBALS['user']->id); $this->datafields = $member ? $course->aux->getMemberData($member) : []; + + $this->editable = false; + // We need aux data in the view $this->aux = $course->aux; diff --git a/app/controllers/course/overview.php b/app/controllers/course/overview.php index 0994d547374..96e7f5abefc 100644 --- a/app/controllers/course/overview.php +++ b/app/controllers/course/overview.php @@ -87,24 +87,14 @@ class Course_OverviewController extends AuthenticatedController $this->show_dozenten = $show_dozenten; // Check lock rules - if (!$GLOBALS["perm"]->have_studip_perm('dozent', $this->course_id)) { - $rule = AuxLockRules::getLockRuleBySemId($this->course_id); - if (isset($rule)) { - $show = false; - foreach ((array) $rule['attributes'] as $val) { - if ($val == 1) { - // Es gibt also Zusatzangaben. Nun noch überprüfen ob der Nutzer diese Angaben schon gemacht hat... - $count = DataField::countBySql("LEFT JOIN datafields_entries USING (datafield_id) WHERE object_type = ? AND sec_range_id = ? AND range_id = ?", - ['usersemdata', $this->course_id, $GLOBALS['user']->id] - ); - if (!$count) { - $show = true; - } - break; - } - } - - if ($show) { + if (!$GLOBALS['perm']->have_studip_perm('dozent', $this->course_id)) { + $rule = AuxLockRule::findOneByCourse($this->course); + if ($rule && count($rule->attributes) > 0) { + $count = DataField::countBySql("LEFT JOIN datafields_entries USING (datafield_id) WHERE object_type = ? AND sec_range_id = ? AND range_id = ?", + ['usersemdata', $this->course_id, $GLOBALS['user']->id] + ); + + if ($count === 0) { PageLayout::postInfo( _("Sie haben noch nicht die für diese Veranstaltung benötigten Zusatzinformationen eingetragen."), [ diff --git a/app/views/admin/courses/aux-select.php b/app/views/admin/courses/aux-select.php index 1786598a0b2..950120e081f 100644 --- a/app/views/admin/courses/aux-select.php +++ b/app/views/admin/courses/aux-select.php @@ -1,13 +1,17 @@ <?php /** * @var Course $course - * @var array $aux_lock_rules + * @var AuxLockRule[] $aux_lock_rules + * @var array $values */ ?> <select name="lock_sem[<?= htmlReady($course->id) ?>]" style="max-width: 200px"> -<? foreach ($aux_lock_rules as $id => $rule) : ?> - <option value="<?= $id ?>" <?= $values['aux_lock_rule'] == $id ? 'selected' : '' ?>> - <?= htmlReady($rule['name']) ?> + <option value="none"> + --<?= _('keine Zusatzangaben') ?>-- + </option> +<? foreach ($aux_lock_rules as $rule) : ?> + <option value="<?= htmlReady($rule->id) ?>" <? if ($values['aux_lock_rule'] === $rule->id) echo 'selected'; ?>> + <?= htmlReady($rule->name) ?> </option> <? endforeach ?> </select> diff --git a/app/views/admin/courses/aux_preselect.php b/app/views/admin/courses/aux_preselect.php index e44e378973f..73a2036577e 100644 --- a/app/views/admin/courses/aux_preselect.php +++ b/app/views/admin/courses/aux_preselect.php @@ -1,21 +1,24 @@ <?php /** * @var array $values - * @var array $aux_lock_rules + * @var AuxLockRule[] $aux_lock_rules */ ?> <label><?= _('Für alle Veranstaltungen') ?> <select name="lock_sem_all" style="max-width: 200px"> - <? foreach ($aux_lock_rules as $id => $rule) : ?> - <option value="<?= $id ?>" - <?= ($values['aux_lock_rule'] == $id) ? 'selected' : '' ?>> - <?= htmlReady($rule["name"]) ?> - </option> - <? endforeach ?> + <option value="none"> + --<?= _('keine Zusatzangaben') ?>-- + </option> + <? foreach ($aux_lock_rules as $rule) : ?> + <option value="<?= htmlReady($rule->id) ?>" + <? if ($values['aux_lock_rule'] === $rule->id) echo 'selected'; ?>> + <?= htmlReady($rule->name) ?> + </option> + <? endforeach ?> </select> </label> <label> -<input type="checkbox" value="1" name="aux_all_forced"> -<?=_("Erzwungen")?> + <input type="checkbox" value="1" name="aux_all_forced"> + <?=_('Erzwungen')?> </label> <?= \Studip\Button::createAccept(_('Speichern'), 'all'); ?> diff --git a/app/views/admin/specification/_field.php b/app/views/admin/specification/_field.php index 05ea09f6073..3d2703b7571 100644 --- a/app/views/admin/specification/_field.php +++ b/app/views/admin/specification/_field.php @@ -2,40 +2,36 @@ /** * @var string $name * @var string $id - * @var array $rule + * @var AuxLockRule $rule */ -$fields = Request::getArray('fields'); -$order = Request::getArray('order'); ?> <section> - <? if (!empty($required)) : ?> +<? if (!empty($required)) : ?> <span class="required"> <?= htmlReady($name) ?> </span> - <? else : ?> +<? else: ?> <?= htmlReady($name) ?> - <? endif ?> - +<? endif ?> <div class="hgroup"> <label class="col-2"> <?= _('Sortierung') ?> - <input id="order_<?= $id ?>" min="0" type="number" size="3" name="order[<?= $id ?>]" - value="<?= (int)(($order && isset($order[$id])) ? $order[$id] : @$rule['order'][$id]) ?>"> - <input type="hidden" name="fields[<?= $id ?>]" value="0"> + <input id="order_<?= htmlReady($id) ?>" min="0" type="number" size="3" name="order[<?= htmlReady($id) ?>]" + value="<?= ($rule->sorting[$id] ?? 0) ?>"> </label> <label class="col-2"> <input type="checkbox" - name="fields[<?= $id ?>]" - value="1" - <?= (($fields && isset($fields[$id])) ? $fields[$id] : @$rule['attributes'][$id]) ? 'checked="checked"' : '' ?>> + name="fields[]" + value="<?= htmlReady($id) ?>" + <? if ($rule->attributes->contains($id)) echo 'checked'; ?>> <?= _('Aktivieren') ?> </label> + <? if (!empty($institution)) : ?> <label class="col-1"> - <? if (!empty($institution)) : ?> - <?= htmlReady($institution->name)?> - <? endif; ?> + <?= htmlReady($institution->name )?> </label> + <? endif; ?> </div> </section> diff --git a/app/views/admin/specification/edit.php b/app/views/admin/specification/edit.php index 30bee490d5d..452c39b8e2c 100644 --- a/app/views/admin/specification/edit.php +++ b/app/views/admin/specification/edit.php @@ -3,95 +3,86 @@ /** * @var Admin_SpecificationController $controller - * @var AuxLockRules $rule + * @var AuxLockRule $rule * @var array $semFields * @var DataField[] $entries_semdata * @var DataField[] $entries_user */ use Studip\Button, Studip\LinkButton; ?> -<? if (isset($flash['error'])) : ?> - <?= MessageBox::error($flash['error'], $flash['error_detail']) ?> -<? elseif (isset($flash['info'])): ?> - <?= MessageBox::info($flash['info']) ?> -<? endif ?> - -<form action="<?= $controller->url_for('admin/specification/store' . ($rule ? '/' . $rule['lock_id'] : '')) ?>" - method="post" class="default"> +<form action="<?= $controller->store($rule) ?>" method="post" class="default"> <?= CSRFProtection::tokenTag() ?> <fieldset> <legend> - <? if ($rule) : ?> - <?= sprintf(_('Regel "%s" editieren'), htmlReady($rule['name'])) ?> - <? else : ?> - <?= _('Eine neue Regel definieren') ?> - <? endif ?> + <? if ($rule->isNew()) : ?> + <?= _('Eine neue Regel definieren') ?> + <? else : ?> + <?= sprintf(_('Regel "%s" editieren'), htmlReady($rule['name'])) ?> + <? endif ?> </legend> <label> <span class="required"> - <?= _('Name der Regel:') ?> + <?= _('Name der Regel') ?> </span> - <input type="text" name="rulename" value="<?= htmlReady(Request::get('rulename', $rule ? $rule['name'] : '')) ?>" - required="required"> + <?= I18N::input('name', $rule->name, [ + 'required' => '', + ]) ?> </label> <label> <?= _('Beschreibung') ?> - <textarea cols="60" rows="5" - name="description"><?= htmlReady(Request::get('description', $rule ? $rule['description'] : '')) ?></textarea> + <?= I18N::textarea('description', $rule->description, [ + 'class' => 'wysiwyg', + ]) ?> </label> </fieldset> - <? if (count($entries_semdata) > 0) : ?> - <fieldset> - <legend> - <?= _('Zusatzinformationen') ?> - </legend> - <? foreach ($entries_semdata as $id => $entry) : ?> - <?= $this->render_partial('admin/specification/_field', array_merge( - compact('rule'), - ['id' => $entry->datafield_id, 'name' => $entry->name], - ['required' => true, 'institution' => $entry->institution] - )) ?> - <? endforeach ?> - </fieldset> - <? endif ?> - <? if (count($semFields) > 0) : ?> - <fieldset> - <legend> - <?= _('Veranstaltungsinformationen') ?> - </legend> - <? foreach ($semFields as $id => $name) : ?> - <?= $this->render_partial('admin/specification/_field', compact('rule', 'id', 'name')) ?> - <? endforeach ?> - </fieldset> - <? endif ?> +<? if (count($entries_semdata) > 0) : ?> + <fieldset> + <legend> + <?= _('Zusatzinformationen') ?> + </legend> + <? foreach ($entries_semdata as $id => $entry) : ?> + <?= $this->render_partial('admin/specification/_field', [ + 'rule' => $rule, + 'id' => $entry->datafield_id, + 'name' => $entry->name, + 'required' => true, + 'institution' => $entry->institution, + ]) ?> + <? endforeach ?> + </fieldset> +<? endif ?> + + <fieldset> + <legend> + <?= _('Veranstaltungsinformationen') ?> + </legend> + <? foreach ($semFields as $id => $name) : ?> + <?= $this->render_partial('admin/specification/_field', compact('rule', 'id', 'name')) ?> + <? endforeach ?> + </fieldset> + +<? if (count($entries_user) > 0) : ?> + <fieldset> + <legend> + <?= _('Personenbezogene Informationen') ?> + </legend> + <? foreach ($entries_user as $id => $entry) : ?> + <?= $this->render_partial('admin/specification/_field', [ + 'rule' => $rule, + 'id' => $entry->datafield_id, + 'name' => $entry->name, + ]) ?> + <? endforeach ?> + </fieldset> +<? endif ?> - <? if (count($entries_user) > 0) : ?> - <fieldset> - <legend> - <?= _('Personenbezogene Informationen') ?> - </legend> - <? foreach ($entries_user as $id => $entry) : ?> - <?= $this->render_partial('admin/specification/_field', - array_merge(compact('rule'), ['id' => $entry->datafield_id, 'name' => $entry->name])) ?> - <? endforeach ?> - </fieldset> - <? endif ?> <footer> - <? if ($rule) : ?> - <?= Button::createAccept(_('Übernehmen'), 'uebernehmen', ['title' => _('Änderungen übernehmen')]) ?> - <? else : ?> - <?= Button::createAccept(_('Erstellen'), 'erstellen', ['title' => _('Neue Regel erstellen')]) ?> - <? endif ?> - <?= LinkButton::createCancel(_('Abbrechen'), $controller->url_for('admin/specification'), ['title' => _('Zurück zur Übersicht')]) ?> + <? if ($rule->isNew()) : ?> + <?= Button::createAccept(_('Erstellen'), 'erstellen', ['title' => _('Neue Regel erstellen')]) ?> + <? else : ?> + <?= Button::createAccept(_('Übernehmen'), 'uebernehmen', ['title' => _('Änderungen übernehmen')]) ?> + <? endif ?> + <?= LinkButton::createCancel(_('Abbrechen'), $controller->indexURL(), ['title' => _('Zurück zur Übersicht')]) ?> </footer> </form> - -<? -$sidebar = Sidebar::Get(); -if ($GLOBALS['perm']->have_perm('root')) { - $actions = new ActionsWidget(); - $actions->addLink(_('Datenfelder bearbeiten'), URLHelper::getLink('dispatch.php/admin/datafields'), Icon::create('add', 'clickable')); - $sidebar->addWidget($actions); -} -?> diff --git a/app/views/admin/specification/index.php b/app/views/admin/specification/index.php index b2b42ea5886..4538b6a0bca 100644 --- a/app/views/admin/specification/index.php +++ b/app/views/admin/specification/index.php @@ -1,62 +1,63 @@ <?php /** * @var Admin_SpecificationController $controller - * @var AuxLockRules[] $allrules + * @var AuxLockRule[] $rules */ ?> <form method="post" class="default"> <?= CSRFProtection::tokenTag() ?> - <table class="default"> + <table class="default <? if (count($rules) > 0) echo 'sortable-table'; ?>" data-sortlist="[[0, 0]]"> <caption> <?= _('Verwaltung von Zusatzangaben') ?> </caption> <colgroup> - <col style="width: 45%"> - <col style="width: 45%"> - <col style="width: 10%"> + <col style="width: 40%"> + <col> + <col style="width: 10ex"> + <col style="width: 8ex"> </colgroup> <thead> <tr> - <th><?= _('Name') ?></th> - <th><?= _('Beschreibung') ?></th> - <th><?= _('Aktionen') ?></th> + <th data-sort="text"><?= _('Name') ?></th> + <th data-sort="text"><?= _('Beschreibung') ?></th> + <th data-sort="htmldata"> + <abbr title="<?= _('Anzahl der zugeordneten Veranstaltungen') ?>">#</abbr> + </th> + <th class="actions" data-sort="false"><?= _('Aktionen') ?></th> </tr> </thead> <tbody> - <? if (!empty($allrules)): ?> - <? foreach ($allrules as $index => $rule) : ?> - <tr> - <td> - <?= htmlReady($rule['name']) ?> - </td> - <td> - <?= htmlReady($rule['description']) ?> - </td> - <td class="actions"> - <a href="<?=$controller->url_for('admin/specification/edit/'.$rule['lock_id']) ?>" style="vertical-align: bottom"> - <?= Icon::create('edit', 'clickable', ['title' => _('Regel bearbeiten')])->asImg() ?> - </a> - <?=Icon::create('trash', 'clickable', tooltip2(_('Regel löschen')))->asInput([ - 'formaction' => $controller->url_for('admin/specification/delete/' . $rule['lock_id']), - 'data-confirm' => sprintf(_('Wollen Sie die Regel "%s" wirklich löschen?'), $rule['name']), - 'style' => 'vertical-align: bottom' - ])?> - </td> - </tr> - <? endforeach ?> - <? else : ?> + <? if (count($rules) === 0): ?> <tr> - <td colspan="3" style="text-align: center"> + <td colspan="4" style="text-align: center"> <?= _('Es wurden noch keine Zusatzangaben definiert.') ?> </td> </tr> <? endif ?> + <? foreach ($rules as $index => $rule) : ?> + <tr> + <td><?= htmlReady($rule->name) ?></td> + <td><?= htmlReady(Studip\Markup::removeHtml($rule->description)) ?></td> + <td data-sort-value="<?= count($rule->courses) ?>"> + <?= number_format(count($rule->courses), 0, ',', '.') ?> + </td> + <td class="actions"> + <a href="<?= $controller->edit($rule) ?>"> + <?= Icon::create('edit')->asImg(['title' => _('Regel bearbeiten')]) ?> + </a> + <? if (count($rule->courses) > 0): ?> + <?= Icon::create('trash', Icon::ROLE_INACTIVE)->asImg( + tooltip2(_('Die Regel kann nicht gelöscht werden, da sie noch verwendet wird.')) + ) ?> + <? else: ?> + <?= Icon::create('trash')->asInput(tooltip2(_('Regel löschen')) + [ + 'formaction' => $controller->deleteURL($rule), + 'data-confirm' => sprintf(_('Wollen Sie die Regel "%s" wirklich löschen?'), $rule->name), + ]) ?> + <? endif; ?> + </td> + </tr> + <? endforeach ?> </tbody> </table> </form> -<? - -$sidebar = Sidebar::Get(); -$actions = new ActionsWidget(); -$actions->addLink(_('Neue Regel anlegen'), $controller->url_for('admin/specification/edit'), Icon::create('add', 'clickable')); -$sidebar->addWidget($actions); diff --git a/app/views/course/members/additional_input.php b/app/views/course/members/additional_input.php index 89df041a6b2..50fd3b09156 100644 --- a/app/views/course/members/additional_input.php +++ b/app/views/course/members/additional_input.php @@ -1,30 +1,38 @@ +<?php +/** + * @var DataFieldEntryModel[] $datafields + * @var AuxLockRule $aux + * @var bool $editable + */ +$editable = false; +?> <form class="default" method="post"> - <? if ($datafields) : ?> - <fieldset> - <legend> - <?= htmlReady($aux->name) ?> - </legend> +<? if ($datafields) : ?> + <fieldset> + <legend> + <?= htmlReady($aux->name) ?> + </legend> - <p><?= formatReady($aux->description) ?></p> + <p><?= formatReady($aux->description) ?></p> - <? foreach ($datafields as $field): ?> - <? if ($field->getTypedDatafield()->isVisible()): ?> - <? if ($field->getTypedDatafield()->isEditable()) : ?> - <? $editable = true; ?> - <? endif ?> - <?= $field->getTypedDatafield()->getHTML('aux'); ?> - <? endif; ?> - <? endforeach; ?> - </fieldset> + <? foreach ($datafields as $field): ?> + <? if ($field->getTypedDatafield()->isVisible()): ?> + <? if ($field->getTypedDatafield()->isEditable()) : ?> + <? $editable = true; ?> + <? endif ?> + <?= $field->getTypedDatafield()->getHTML('aux'); ?> + <? endif; ?> + <? endforeach; ?> + </fieldset> - <? if ($editable): ?> - <footer> - <?= \Studip\Button::create(_('Speichern'), 'save') ?> - </footer> - <? else: ?> - <?= MessageBox::info(_('Keine einstellbaren Zusatzdaten vorhanden')) ?> - <? endif; ?> - <? else : ?> + <? if ($editable): ?> + <footer> + <?= Studip\Button::create(_('Speichern'), 'save') ?> + </footer> + <? else: ?> <?= MessageBox::info(_('Keine einstellbaren Zusatzdaten vorhanden')) ?> <? endif; ?> +<? else : ?> + <?= MessageBox::info(_('Keine einstellbaren Zusatzdaten vorhanden')) ?> +<? endif; ?> </form> diff --git a/db/migrations/5.3.9_convert_aux_lock_rules_json_fields.php b/db/migrations/5.3.9_convert_aux_lock_rules_json_fields.php new file mode 100644 index 00000000000..56b1c2b571a --- /dev/null +++ b/db/migrations/5.3.9_convert_aux_lock_rules_json_fields.php @@ -0,0 +1,56 @@ +<?php +final class ConvertAuxLockRulesJsonFields extends Migration +{ + public function description() + { + return parent::description(); // TODO: Change the autogenerated stub + } + + protected function up() + { + $query = "SELECT `lock_id`, `attributes`, `sorting` FROM `aux_lock_rules`"; + $rows = DBManager::get()->fetchAll($query); + + $query = "UPDATE `aux_lock_rules` + SET `attributes` = :attributes, + `sorting` = :sorting + WHERE `lock_id` = :id"; + $statement = DBManager::get()->prepare($query); + + foreach ($rows as $row) { + $attributes = json_decode($row['attributes'], true) ?: []; + $attributes = array_filter($attributes); + $attributes = array_keys($attributes); + + $sorting = json_decode($row['sorting'], true) ?: []; + $sorting = array_filter($sorting, function ($id) use ($attributes) { + return in_array($id, $attributes); + }, ARRAY_FILTER_USE_KEY); + + $statement->bindValue(':id', $row['lock_id']); + $statement->bindValue(':attributes', json_encode($attributes)); + $statement->bindValue(':sorting', json_encode($sorting)); + $statement->execute(); + } + } + + protected function down() + { + $query = "SELECT `lock_id`, `attributes` FROM `aux_lock_rules`"; + $rows = DBManager::get()->fetchAll($query); + + $query = "UPDATE `aux_lock_rules` + SET `attributes` = :attributes + WHERE `lock_id` = :id"; + $statement = DBManager::get()->prepare($query); + + foreach ($rows as $row) { + $attributes = json_decode($row['attributes'], true) ?: []; + $attributes = array_fill_keys($attributes, '1'); + + $statement->bindValue(':id', $row['lock_id']); + $statement->bindValue(':attributes', json_encode($attributes)); + $statement->execute(); + } + } +} diff --git a/lib/classes/AuxLockRules.class.php b/lib/classes/AuxLockRules.class.php index 6a6dfd9e0dd..4d06ca2d9d2 100644 --- a/lib/classes/AuxLockRules.class.php +++ b/lib/classes/AuxLockRules.class.php @@ -23,6 +23,9 @@ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +/** + * @deprecated since Stud.IP 5.3 + */ class AuxLockRules { diff --git a/lib/classes/Markup.class.php b/lib/classes/Markup.class.php index af298beaffa..0f5a5c87232 100644 --- a/lib/classes/Markup.class.php +++ b/lib/classes/Markup.class.php @@ -233,7 +233,7 @@ class Markup * Create HTML purifier instance with Stud.IP-specific configuration. * * @param boolean $autoformat Apply the AutoFormat rules - * @return HTMLPurifier A new instance of the HTML purifier. + * @return \HTMLPurifier A new instance of the HTML purifier. */ private static function createPurifier($autoformat) { diff --git a/lib/classes/StudipArrayObject.class.php b/lib/classes/StudipArrayObject.class.php index bb5444c6188..f025ffbb8b8 100644 --- a/lib/classes/StudipArrayObject.class.php +++ b/lib/classes/StudipArrayObject.class.php @@ -436,4 +436,12 @@ class StudipArrayObject implements IteratorAggregate, ArrayAccess, Serializable, throw new InvalidArgumentException("{$key} is a protected property, use a different key"); } } + + /** + * Returns whether the given value is in the underlying array. + */ + public function contains($value): bool + { + return in_array($value, $this->storage); + } } diff --git a/lib/classes/WidgetContainer.php b/lib/classes/WidgetContainer.php index b4aaa8cdd05..108477a3844 100644 --- a/lib/classes/WidgetContainer.php +++ b/lib/classes/WidgetContainer.php @@ -46,10 +46,11 @@ abstract class WidgetContainer /** * Add a widget to the container. * - * @param Widget $widget The actual widget + * @template W of Widget + * @param W $widget The actual widget * @param String $index Optional index/name of the widget, defaults to * class name without "widget" - * @return Widget The added widget to allow for easier handling + * @return W The added widget to allow for easier handling */ public function addWidget(Widget $widget, $index = null) { diff --git a/lib/models/AuxLockRule.php b/lib/models/AuxLockRule.php index d0686a9ab99..4bf8e8bcf25 100644 --- a/lib/models/AuxLockRule.php +++ b/lib/models/AuxLockRule.php @@ -13,31 +13,75 @@ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * @category Stud.IP * @since 3.0 + * * @property string lock_id database column * @property string id alias column for lock_id * @property string name database column * @property string description database column - * @property string attributes database column - * @property string sorting database column + * @property JSONArrayObject attributes database column + * @property JSONArrayObject sorting database column * @property array datafields computed column * @property string order computed column - * @property Course course belongs_to Course + * @property Course[]|SimpleORMapCollection courses has_and_belongs_to_many Courses */ class AuxLockRule extends SimpleORMap { protected static function configure($config = []) { $config['db_table'] = 'aux_lock_rules'; - $config['belongs_to']['course'] = [ - 'class_name' => Course::class, - 'foreign_key' => 'lock_id', - 'assoc_foreign_key' => 'aux_lock_rule', + + $config['has_many'] = [ + 'courses' => [ + 'class_name' => Course::class, + 'foreign_key' => 'lock_id', + 'assoc_foreign_key' => 'aux_lock_rule', + ], + ]; + + $config['additional_fields'] = [ + 'datafields' => true + ]; + + $config['serialized_fields'] = [ + 'attributes' => JSONArrayObject::class, + 'sorting' => JSONArrayObject::class, ]; - $config['additional_fields']['datafields'] = true; - $config['additional_fields']['order'] = true; + + $config['i18n_fields'] = [ + 'name' => true, + 'description' => true, + ]; + + $config['registered_callbacks'] = [ + 'before_store' => [ + function (AuxLockRule $rule) { + $rule->sorting = array_filter($rule->sorting->getArrayCopy(), function ($id) use ($rule) { + return $rule->attributes->contains($id); + }, ARRAY_FILTER_USE_KEY); + }, + ], + 'before_delete' => [ + function (AuxLockRule $rule) { + return count($rule->courses) === 0; + }, + ] + ]; + parent::configure($config); } + public static function findOneByCourse(Course $course): ?AuxLockRule + { + return self::findOneByCourseId($course->id); + } + + public static function findOneByCourseId(string $course_id): ?AuxLockRule + { + $condition = "JOIN seminare ON lock_id = aux_lock_rule + WHERE Seminar_id = ?"; + return self::findOneBySQL($condition, [$course_id]); + } + /** * Cache to avoid loading datafields for a user more than once */ @@ -50,8 +94,8 @@ class AuxLockRule extends SimpleORMap */ public function getDatafields() { - $attributes = json_decode($this->attributes, true) ?: []; - $sorting = json_decode($this->sorting, true) ?: []; + $attributes = $this->attributes->getArrayCopy(); + $sorting = $this->sorting->getArrayCopy(); foreach ($attributes as $key => $attr) { if (!$attr) { @@ -64,11 +108,8 @@ class AuxLockRule extends SimpleORMap /** * Updates a datafield of a courseMember by the given data - * - * @param object $member - * @param object $data */ - public function updateMember($member, $data) + public function updateMember(CourseMember $member, array $data) { foreach ($data as $key => $value) { $datafield = current($this->getDatafield($member, $key)); @@ -88,23 +129,22 @@ class AuxLockRule extends SimpleORMap */ public function getCourseData($course = null, $display_only = false) { - // set course if (!$course) { $course = $this->course; } $mapping = [ - 'vadozent' => join(', ', $course->members->findBy('status', 'dozent')->getUserFullname()), + 'vadozent' => join(', ', $course->members->findBy('status', 'dozent')->getUserFullname()), 'vasemester' => $course->start_semester->name, - 'vatitle' => $course->name, - 'vanr' => $course->VeranstaltungsNummer, + 'vatitle' => $course->name, + 'vanr' => $course->veranstaltungsnummer, ]; $head_mapping = [ - 'vadozent' => _('Dozenten'), + 'vadozent' => _('Dozenten'), 'vasemester' => _('Semester'), - 'vatitle' => _('Veranstaltungstitel'), - 'vanr' => _('Veranstaltungsnummer'), + 'vatitle' => _('Veranstaltungstitel'), + 'vanr' => _('Veranstaltungsnummer'), ]; // start collecting entries @@ -145,38 +185,44 @@ class AuxLockRule extends SimpleORMap return $result; } - public function getMemberData($member) + public function getMemberData(CourseMember $member) { $datafields = SimpleCollection::createFromArray(DatafieldEntryModel::findByModel($member)); $result = []; - foreach (array_keys($this->datafields) as $field) { + foreach ($this->attributes as $field) { // since we have no only datafields we have to filter! - if ($new = $datafields->findOneBy('datafield_id', $field)) { + $new = $datafields->findOneBy('datafield_id', $field); + if ($new) { $result[] = $new; } } + + usort($result, function (DatafieldEntryModel $a, DatafieldEntryModel $b) { + $a_order = $this->sorting[$a->datafield_id] ?? 0; + $b_order = $this->sorting[$b->datafield_id] ?? 0; + return $a_order - $b_order; + }); + return $result; } /** * Caching for the datafields - * @param type $member - * @param type $fieldID - * @return null */ - private function getDatafield($member, $fieldID) + private function getDatafield(CourseMember $member, $field_id): ?array { - if (mb_strlen($fieldID) == 32) { - if (!array_key_exists($fieldID, $this->datafieldCache)) { - $this->datafieldCache[$fieldID] = DataField::find($fieldID); + if (mb_strlen($field_id) === 32) { + if (!array_key_exists($field_id, $this->datafieldCache)) { + $this->datafieldCache[$field_id] = DataField::find($field_id); } - if (isset($this->datafieldCache[$fieldID])) { - if ($this->datafieldCache[$fieldID]->object_type == 'usersemdata') { - $field = current(DatafieldEntryModel::findByModel($member, $fieldID)); + if (isset($this->datafieldCache[$field_id])) { + $field = null; + if ($this->datafieldCache[$field_id]->object_type === 'usersemdata') { + $field = current(DatafieldEntryModel::findByModel($member, $field_id)); } - if ($this->datafieldCache[$fieldID]->object_type == 'user') { - $field = current(DatafieldEntryModel::findByModel(User::find($member->user_id), $fieldID)); + if ($this->datafieldCache[$field_id]->object_type === 'user') { + $field = current(DatafieldEntryModel::findByModel(User::find($member->user_id), $field_id)); } if ($field) { $range_id = $field->sec_range_id ? [$field->range_id, $field->sec_range_id] : $field->range_id; @@ -185,6 +231,19 @@ class AuxLockRule extends SimpleORMap } } } + + return null; } + public static function validateFields(array $fields): bool + { + $entries = DataField::getDataFields('usersemdata'); + foreach ($entries as $entry) { + if (in_array($entry->id, $fields)) { + return true; + } + } + + return false; + } } diff --git a/lib/models/DatafieldEntryModel.class.php b/lib/models/DatafieldEntryModel.class.php index fa7a878cad1..882b37ed22c 100644 --- a/lib/models/DatafieldEntryModel.class.php +++ b/lib/models/DatafieldEntryModel.class.php @@ -48,37 +48,44 @@ class DatafieldEntryModel extends SimpleORMap implements PrivacyObject */ public static function findByModel(SimpleORMap $model, $datafield_id = null) { - $mask = ["user" => 1, "autor" => 2, "tutor" => 4, "dozent" => 8, "admin" => 16, "root" => 32]; + $mask = [ + 'user' => 1, + 'autor' => 2, + 'tutor' => 4, + 'dozent' => 8, + 'admin' => 16, + 'root' => 32, + ]; $sec_range_id = null; - if (is_a($model, "Course")) { + if ($model instanceof Course) { $params[':institution_ids'] = $model->institutes->pluck('institut_id'); $object_class = SeminarCategories::GetByTypeId($model->status)->id; $object_type = 'sem'; - $range_id = $model->getId(); - } elseif(is_a($model, "Institute")) { - $params[':institution_ids'] = [$model->Institut_id]; + $range_id = $model->id; + } elseif ($model instanceof Institute) { + $params[':institution_ids'] = [$model->id]; $object_class = $model->type; $object_type = 'inst'; - $range_id = $model->getId(); - } elseif(is_a($model, "User")) { + $range_id = $model->id; + } elseif ($model instanceof User) { $params[':institution_ids'] = $model->institute_memberships->pluck('institut_id'); $object_class = $mask[$model->perms]; $object_type = 'user'; - $range_id = $model->getId(); - } elseif(is_a($model, "CourseMember")) { + $range_id = $model->id; + } elseif($model instanceof CourseMember) { $params[':institution_ids'] = $model->course->institutes->pluck('institut_id'); $object_class = $mask[$model->status]; $object_type = 'usersemdata'; $range_id = $model->user_id; $sec_range_id = $model->seminar_id; - } elseif(is_a($model, "InstituteMember")) { + } elseif($model instanceof InstituteMember) { $params[':institution_ids'] = [$model->institut_id]; $object_class = $mask[$model->inst_perms]; $object_type = 'userinstrole'; $range_id = $model->user_id; $sec_range_id = $model->institut_id; - } elseif (is_a($model, 'ModulDeskriptor')) { + } elseif ($model instanceof ModulDeskriptor) { $params[':institution_ids'] = ''; if (!empty($model->modul->responsible_institute->institut_id)) { $params[':institution_ids'] = [$model->modul->responsible_institute->institut_id]; @@ -86,7 +93,7 @@ class DatafieldEntryModel extends SimpleORMap implements PrivacyObject $object_class = $model->getVariant(); $object_type = 'moduldeskriptor'; $range_id = $model->deskriptor_id; - } elseif (is_a($model, 'ModulteilDeskriptor')) { + } elseif ($model instanceof ModulteilDeskriptor) { $params[':institution_ids'] = [$model->modulteil->modul->responsible_institute->institut_id]; $object_class = $model->getVariant(); $object_type = 'modulteildeskriptor'; @@ -106,46 +113,50 @@ class DatafieldEntryModel extends SimpleORMap implements PrivacyObject $object_class = $model->getVariant(); $object_type = 'studycourse'; $range_id = $model->studiengang_id; - } - - if (!$object_type) { + } else { throw new InvalidArgumentException('Wrong type of model: ' . get_class($model)); } - $one_datafield = ''; + + $query = "SELECT a.*, b.*, a.datafield_id, b.datafield_id AS isset_content + FROM datafields a + LEFT JOIN datafields_entries b + ON (a.datafield_id=b.datafield_id AND range_id = :range_id AND sec_range_id = :sec_range_id) + WHERE object_type = :object_type + AND (lang IS NULL OR lang = '') + AND (a.institut_id IS NULL OR a.institut_id IN (:institution_ids))"; + if ($datafield_id !== null) { - $one_datafield = ' AND a.datafield_id = ' . DBManager::get()->quote($datafield_id); - } else { - $one_datafield = ''; + $query .= ' AND a.datafield_id = :one_datafield_id'; + $params[':one_datafield_id'] = $datafield_id; } - $query = "SELECT a.*, b.*,a.datafield_id,b.datafield_id as isset_content "; - $query .= "FROM datafields a LEFT JOIN datafields_entries b ON (a.datafield_id=b.datafield_id AND range_id = :range_id AND sec_range_id = :sec_range_id) "; - $query .= "WHERE object_type = :object_type AND (ISNULL(lang) OR lang = '') AND (a.institut_id IS NULL OR a.institut_id IN (:institution_ids))"; - if ($object_type === 'studycourse') { - $query .= "AND (LOCATE(:object_class, object_class) OR LOCATE('all', object_class)) $one_datafield ORDER BY priority"; + $query .= " AND (LOCATE(:object_class, object_class) OR LOCATE('all', object_class)) ORDER BY priority"; $params = array_merge($params,[ - ':range_id' => (string) $range_id, + ':range_id' => (string) $range_id, ':sec_range_id' => (string) $sec_range_id, - ':object_type' => $object_type, - ':object_class' => (string) $object_class]); + ':object_type' => $object_type, + ':object_class' => (string) $object_class, + ]); } elseif ($object_type === 'moduldeskriptor' || $object_type === 'modulteildeskriptor') { // find datafields by language (string) - $query .= "AND (LOCATE(:object_class, object_class) OR object_class IS NULL) $one_datafield ORDER BY priority"; + $query .= " AND (LOCATE(:object_class, object_class) OR object_class IS NULL) ORDER BY priority"; $params = array_merge($params,[ - ':range_id' => (string) $range_id, + ':range_id' => (string) $range_id, ':sec_range_id' => (string) $sec_range_id, - ':object_type' => $object_type, - ':object_class' => (string) $object_class]); + ':object_type' => $object_type, + ':object_class' => (string) $object_class, + ]); } else { // find datafields by perms or status (int) - $query .= "AND ((object_class & :object_class) OR object_class IS NULL) $one_datafield ORDER BY priority"; + $query .= " AND ((object_class & :object_class) OR object_class IS NULL) ORDER BY priority"; $params = array_merge($params, [ - ':range_id' => (string) $range_id, + ':range_id' => (string) $range_id, ':sec_range_id' => (string) $sec_range_id, - ':object_type' => $object_type, - ':object_class' => (int) $object_class]); + ':object_type' => $object_type, + ':object_class' => (int) $object_class, + ]); } $st = DBManager::get()->prepare($query); @@ -181,6 +192,7 @@ class DatafieldEntryModel extends SimpleORMap implements PrivacyObject } return $ret; } + public function setContentLanguage($language) { if (!Config::get()->CONTENT_LANGUAGES[$language]) { diff --git a/templates/i18n/input.php b/templates/i18n/input.php index 9c9a456ad49..3ab92a5b6c7 100644 --- a/templates/i18n/input.php +++ b/templates/i18n/input.php @@ -1,3 +1,13 @@ +<?php +/** + * @var array $languages + * @var array $attributes + * @var string $base_lang + * @var string $name + * @var I18NString $value + * + */ +?> <? foreach ($languages as $locale => $lang): ?> <? $attr = $attributes; -- GitLab