diff --git a/app/controllers/admission/courseset.php b/app/controllers/admission/courseset.php index e694a2455f26da787f30172c546f3e18282b6089..90ae5f54d084abb34662116f74d4a5817033cf61 100644 --- a/app/controllers/admission/courseset.php +++ b/app/controllers/admission/courseset.php @@ -258,6 +258,65 @@ class Admission_CoursesetController extends AuthenticatedController $tpl->set_attribute('rights', false); } $this->instTpl = $tpl->render(); + + $this->semesters = array_reverse(array_map( + fn ($s) => $s->toArray(), + Semester::getAll() + )); + + if ($GLOBALS['perm']->have_perm('root')) { + $this->isearch = new StandardSearch('Institut_id'); + $this->myinst = []; + } else { + $this->isearch = null; + $this->myinst = array_map( + fn ($i) => [ + 'id' => $i['Institut_id'], + 'name' => $i['Name'], + 'faculty' => $i['is_fak'] ? null : $i['fakultaets_id'] + ], + Institute::getMyInstitutes() + ); + } + + $props = [ + 'all-semesters' => $this->semesters, + 'my-institutes'=> $this->myinst, + 'my-user-lists' => array_values( + array_map( + fn ($list) => [ + 'id' => $list->getId(), + 'name' => $list->getName(), + 'factor' => $list->getFactor(), + 'count' => $list->getUserCount() + ], + $this->myUserlists + ) + ), + 'institute-search' => (string) $this->isearch + ]; + + if ($this->courseset) { + $props['course-set-id'] = $this->courseset->getId(); + } + + $this->render_vue_app( + Studip\VueApp::create('admission/ConfigureCourseSet') + ->withProps($props) + ); + + Helpbar::get()->addPlainText( + _('Regeln'), + _('Hier können Sie die Regeln, Eigenschaften und Zuordnungen des Anmeldesets bearbeiten.') + ); + Helpbar::get()->addPlainText( + _('Info'), + _('Sie können das Anmeldeset allen Einrichtungen zuordnen, an denen Sie mindestens Lehrendenrechte haben.') + ); + Helpbar::get()->addPlainText( + _('Sichtbarkeit'), + _('Alle Veranstaltungen der Einrichtungen, an denen Sie mindestens Lehrendenrechte haben, können zum Anmeldeset hinzugefügt werden.') + ); } /** diff --git a/app/controllers/admission/restricted_courses.php b/app/controllers/admission/restricted_courses.php index 1a4a19149c229bc5231f3574ad341445d15fab4d..eb4cc16c6371644848d883d3527d831e3981cb21 100644 --- a/app/controllers/admission/restricted_courses.php +++ b/app/controllers/admission/restricted_courses.php @@ -50,7 +50,7 @@ class Admission_RestrictedCoursesController extends AuthenticatedController $this->current_institut_id = 'all'; } if (!$this->current_semester_id) { - $this->current_semester_id = $_SESSION['_default_sem']; + $this->current_semester_id = $_SESSION['_default_sem'] ?? Semester::findDefault()->id; } else { $_SESSION['_default_sem'] = $this->current_semester_id; } diff --git a/app/views/admission/courseset/_institute_choose.php b/app/views/admission/courseset/_institute_choose.php index 48af5f89a43b1e170e9ea228505cf6161b0d8dcd..9a68b1aee528ba15fdf37c0c37ee515e11ac6a59 100644 --- a/app/views/admission/courseset/_institute_choose.php +++ b/app/views/admission/courseset/_institute_choose.php @@ -27,17 +27,20 @@ <?=_("Enthaltene Regeln:")?> <div class="hidden-no-js check_actions"> (<?= _('markieren') ?>: - <a onclick="STUDIP.Admission.checkUncheckAll('choose_rule_type', 'check')"> + <button class="as-link" onclick="return STUDIP.Admission.checkUncheckAll('choose_rule_type', 'check')" + title="<?= _('Alle Regeltypen auswählen') ?>"> <?= _('alle') ?> - </a> + </button> | - <a onclick="STUDIP.Admission.checkUncheckAll('choose_rule_type', 'uncheck')"> + <button class="as-link" onclick="return STUDIP.Admission.checkUncheckAll('choose_rule_type', 'uncheck')" + title="<?= _('Keinen Regeltyp auswählen') ?>"> <?= _('keine') ?> - </a> + </button> | - <a onclick="STUDIP.Admission.checkUncheckAll('choose_rule_type', 'invert')"> + <button class="as-link" onclick="return STUDIP.Admission.checkUncheckAll('choose_rule_type', 'invert')" + title="<?= _('Aktuelle Auswahl der Regeltypen umkehren') ?>"> <?= _('Auswahl umkehren') ?> - </a>) + </button>) </div> </section> diff --git a/app/views/admission/courseset/configure.php b/app/views/admission/courseset/configure.php deleted file mode 100644 index cc2209c180e32a539810f4b15f1fef2fb9c8cd5a..0000000000000000000000000000000000000000 --- a/app/views/admission/courseset/configure.php +++ /dev/null @@ -1,268 +0,0 @@ -<?php -/** - * @var CourseSet $courseset - * @var array $flash - * @var Admission_CoursesetController|Course_AdmissionController $controller - * @var bool $instant_course_set_view - * @var array $myInstitutes - * @var array $selectedInstitutes - * @var QuickSearch $instSearch - * @var string $instTpl - * @var string $coursesTpl - * @var string $selectedSemester - * @var AdmissionUserList[] $myUserlists - */ -use Studip\Button, Studip\LinkButton; - -Helpbar::get()->addPlainText(_('Regeln'), _('Hier können Sie die Regeln, Eigenschaften und Zuordnungen des Anmeldesets bearbeiten.')); -Helpbar::get()->addPlainText(_('Info'), _('Sie können das Anmeldeset allen Einrichtungen zuordnen, an denen Sie mindestens Lehrendenrechte haben.')); -Helpbar::get()->addPlainText(_('Sichtbarkeit'), _('Alle Veranstaltungen der Einrichtungen, an denen Sie mindestens Lehrendenrechte haben, können zum Anmeldeset hinzugefügt werden.')); - -// Load assigned course IDs. -$courseIds = $courseset ? $courseset->getCourses() : []; -// Load assigned user list IDs. -$userlistIds = $courseset ? $courseset->getUserlists() : []; - -if (isset($flash['error'])) { - echo MessageBox::error($flash['error']); -} -?> -<div class="hidden-alert" style="display:none"> - <?= MessageBox::info(_("Diese Daten sind noch nicht gespeichert."));?> -</div> -<h1><?= $courseset ? _('Anmeldeset bearbeiten') : _('Anmeldeset anlegen') ?></h1> -<form class="default" id="courseset-form" action="<?= $controller->url_for(!$instant_course_set_view ? - 'admission/courseset/save/' . ($courseset ? $courseset->getId() : '') : - 'course/admission/save_courseset/' . $courseset->getId()) ?>" method="post"> - <fieldset> - <legend><?= _('Grunddaten') ?></legend> - <label> - <span class="required"><?= _('Name des Anmeldesets') ?></span> - <input type="text" maxlength="255" name="name" - value="<?= $courseset ? htmlReady($courseset->getName()) : '' ?>" - required aria-required="true"/> - </label> - <? if (!$courseset || ($courseset->isUserAllowedToEdit($GLOBALS['user']->id) && !$instant_course_set_view)) : ?> - <label for="private"> - <?= _('Sichtbarkeit') ?> - </label> - <input type="checkbox" id="private" name="private"<?= $courseset ? ($courseset->getPrivate() ? ' checked="checked"' : '') : 'checked' ?>/> - <?= _('Dieses Anmeldeset soll nur für mich selbst und alle Administratoren sichtbar und benutzbar sein.') ?> - <? endif ?> - <? if ($courseset) : ?> - <label> - <?= _('Besitzer des Anmeldesets') ?> - </label> - <div> - <? $user = User::find($courseset->getUserId()) ?> - <? if (isset($user)) : ?> - <a target="_blank" href="<?= $controller->url_for('profile', ['username' => $user->username]) ?>" > - <?= htmlReady($user->getFullName()) ?> (<?= htmlReady($user->username) ?>) - </a> - <? else : ?> - <?= _('unbekannt') ?> - <? endif ?> - </div> - <? endif ;?> - <label for="institutes"> - <span class="required"><?= _('Einrichtungszuordnung') ?></span> - </label> - <? if ($GLOBALS['perm']->have_perm('admin') || $GLOBALS['perm']->have_perm('dozent') && Config::get()->ALLOW_DOZENT_COURSESET_ADMIN) : ?> - <div id="institutes"> - <?php if ($myInstitutes) { ?> - <?php if ($instSearch) { ?> - <?= $instTpl ?> - <?php } else { ?> - <?php foreach ($myInstitutes as $institute) { ?> - <?php if (count($myInstitutes) !== 1) { ?> - <input type="checkbox" name="institutes[]" value="<?= $institute['Institut_id'] ?>" - <?= !empty($selectedInstitutes[$institute['Institut_id']]) ? 'checked' : '' ?> - class="institute" onclick="STUDIP.Admission.getCourses( - '<?= $controller->url_for('admission/courseset/instcourses', $courseset ? $courseset->getId() : '') ?>')"/> - <?php } else { ?> - <input type="hidden" name="institutes[]" value="<?= $institute['Institut_id'] ?>"/> - <?php } ?> - <?= htmlReady($institute['Name']) ?> - <br/> - <?php } ?> - <?php } ?> - <?php } else { ?> - <?php if ($instSearch) { ?> - <div id="institutes"> - <?= Icon::create('arr_2down', Icon::ROLE_SORT)->asImg([ - 'title' => _('Einrichtung hinzufügen'), - 'alt' => _('Einrichtung hinzufügen'), - 'onclick' => "STUDIP.Admission.updateInstitutes($('input[name=\"institute_id\"]').val(), '" .$controller->url_for('admission/courseset/institutes',$courseset?$courseset->getId() : '') . "', '" . $controller->url_for('admission/courseset/instcourses',$courseset?$courseset->getId() : '') . "', 'add')" - ]) ?> - <?= $instSearch ?> - <?= Icon::create('search')->asImg(['title' => _("Suche starten")])?> - </div> - <i><?= _('Sie haben noch keine Einrichtung ausgewählt. Benutzen Sie obige Suche, um dies zu tun.') ?></i> - <?php } else { ?> - <i><?= _('Sie sind keiner Einrichtung zugeordnet.') ?></i> - <?php } ?> - <?php } ?> - </div> - <? else : ?> - <? foreach (SimpleCollection::createFromArray($selectedInstitutes)->orderBy('Name') as $institute) : ?> - <?= htmlReady($institute['Name']) ?> - <br> - <? endforeach ?> - <? endif ?> - </fieldset> - <fieldset> - <legend><?= _('Veranstaltungen') ?></legend> - <? if (!$instant_course_set_view) : ?> - <label> - <?= _('Semester') ?> - <select name="semester" onchange="STUDIP.Admission.getCourses('<?= $controller->url_for('admission/courseset/instcourses', $courseset ? $courseset->getId() : '') ?>')"> - <?php foreach(array_reverse(Semester::getAll(), true) as $id => $semester) { ?> - <option value="<?= $id ?>"<?= $id === $selectedSemester ? ' selected' : '' ?>> - <?= htmlReady($semester->name) ?> - </option> - <?php } ?> - </select> - </label> - <label> - <?= _('Filter auf Name/Nummer/Lehrperson') ?><br> - <input style="display:inline-block" type="text" onKeypress="if (event.which==13) return STUDIP.Admission.getCourses('<?= $controller->url_for('admission/courseset/instcourses', $courseset ? $courseset->getId() : '') ?>')" - value="<?= htmlReady($current_course_filter ?? '') ?>" name="course_filter" > - <?= Icon::create('search')->asImg([ - 'title' => _("Veranstaltungen anzeigen"), - 'onClick' => "return STUDIP.Admission.getCourses('" . $controller->url_for('admission/courseset/instcourses', $courseset ? $courseset->getId() : '') ."')" - ]) ?> - </label> - <div id="instcourses"> - <?= $coursesTpl; ?> - </div> - <? if (count($courseIds)) : ?> - <div> - <?= LinkButton::create(_('Ausgewählte Veranstaltungen konfigurieren'), - $controller->url_for('admission/courseset/configure_courses/' . $courseset->getId()), - ['data-dialog' => 'size=big', 'class' => 'autosave'] - ); ?> - <? if ($num_applicants = $courseset->getNumApplicants()) :?> - <?= LinkButton::create(sprintf(_('Liste der Anmeldungen (%s Nutzer)'), $num_applicants), - $controller->url_for('admission/courseset/applications_list/' . $courseset->getId()), - ['data-dialog' => '', 'class' => 'autosave'] - ); ?> - <?= LinkButton::create(_('Nachricht an alle Angemeldeten'), - $controller->url_for('admission/courseset/applicants_message/' . $courseset->getId()), - ['data-dialog' => '', 'class' => 'autosave'] - ); ?> - <? endif ?> - </div> - <? endif ?> - <? else :?> - <? if (count($courseIds) > 100) :?> - <?= sprintf(_("%s zugewiesene Veranstaltungen"), count($courseIds)) ?> - <? else : ?> - <? - Course::findEachBySQL( - function($c) { - echo htmlReady($c->getFullName('number-name-semester')); - echo '<br>'; - }, - "JOIN `semester_courses` - ON `seminare`.`seminar_id` = `semester_courses`.`course_id` - JOIN `semester_data` USING (`semester_id`) - WHERE `seminare`.`seminar_id` IN ( :course_ids ) - ORDER BY `semester_data`.`beginn`, `VeranstaltungsNummer`, `Name`", - ['course_ids' => $courseIds], - ) - ?> - <? endif ?> - <? endif ?> - </fieldset> - <fieldset> - <legend><?= _('Anmelderegeln') ?></legend> - <div id="rules"> - <?php if ($courseset) { ?> - <div id="rulelist"> - <?php foreach ($courseset->getAdmissionRules() as $rule) { ?> - <?= $this->render_partial('admission/rule/save', ['rule' => $rule]) ?> - <?php } ?> - </div> - <?php } else { ?> - <span id="norules"> - <i><?= _('Sie haben noch keine Anmelderegeln festgelegt.') ?></i> - </span> - <br/> - <?php } ?> - <div style="clear: both;"> - <?= LinkButton::create(_('Anmelderegel hinzufügen'), - $controller->url_for('admission/rule/select_type' . ($courseset ? '/'.$courseset->getId() : '')), - [ - 'onclick' => "return STUDIP.Admission.selectRuleType(this)" - ] - ); ?> - </div> - </div> - </fieldset> - <div class="hidden-alert" style="display:none"> - <?= MessageBox::info(_("Diese Daten sind noch nicht gespeichert."));?> - </div> - <fieldset> - <legend><?= _('Weitere Daten') ?></legend> - <? if (!$instant_course_set_view) : ?> - - <? if ($courseset && $courseset->getSeatDistributionTime()) :?> - <label> - <?= _('Personenlisten zuordnen') ?> - </label> - <?php if ($myUserlists) { ?> - <?php - foreach ($myUserlists as $list) { - $checked = ''; - if (in_array($list->getId(), $userlistIds)) { - $checked = ' checked="checked"'; - } - ?> - <input type="checkbox" name="userlists[]" value="<?= $list->getId() ?>"<?= $checked ?>/> <?= $list->getName() ?><br/> - <?php } ?> - - <?php } else { ?> - <i><?= _('Sie haben noch keine Personenlisten angelegt.') ?></i> - <?php - }?> - <div> - <?= LinkButton::create(_('Liste der Nutzer'), - $controller->url_for('admission/courseset/factored_users/' . $courseset->getId()), - ['data-dialog' => ''] - ); ?> - </div> - <?php - // Keep lists that were assigned by other users. - foreach ($userlistIds as $list) { - if (!in_array($list, array_keys($myUserlists))) { - ?> - <input type="hidden" name="userlists[]" value="<?= $list ?>"/> - <?php - } - } - ?> - <? endif ?> - <? endif ?> - <label for="infotext"> - <?= _('Weitere Hinweise für die Teilnehmenden') ?> - </label> - <textarea cols="60" rows="3" name="infotext"><?= $courseset ? htmlReady($courseset->getInfoText()) : '' ?></textarea> - </fieldset> - - <footer class="submit_wrapper" data-dialog-button> - <?= CSRFProtection::tokenTag() ?> - <?= Button::createAccept(_('Speichern'), 'submit', - $instant_course_set_view ? ['data-dialog' => ''] : []) ?> - <?php if (Request::option('is_copy')) : ?> - <?= LinkButton::createCancel(_('Abbrechen'), - URLHelper::getURL('dispatch.php/admission/courseset/delete/' . $courseset->getId(), - ['really' => 1])) ?> - <?php else : ?> - <?= LinkButton::createCancel(_('Abbrechen'), $controller->url_for('admission/courseset')) ?> - <?php endif ?> - </footer> - -</form> -<? if (Request::get('is_copy')) :?> - <script>STUDIP.Admission.toggleNotSavedAlert();</script> -<? endif ?> diff --git a/app/views/admission/rule/configure.php b/app/views/admission/rule/configure.php index b00c27b248afc04fde9baaf911f3d5bde29ea5ce..64908ee93cd45a151a818c09f66f51fb9e11c416 100644 --- a/app/views/admission/rule/configure.php +++ b/app/views/admission/rule/configure.php @@ -8,19 +8,4 @@ use Studip\Button, Studip\LinkButton; */ ?> <div id="errormessage"></div> -<form action="<?= $controller->url_for('admission/rule/save', get_class($rule), $rule->getId()) ?>" - id="ruleform" class="default" - onsubmit="return STUDIP.Admission.checkAndSaveRule( - '<?= $rule->getId() ?>', - 'errormessage', - '<?= $controller->url_for('admission/rule/validate', get_class($rule), $rule->getId()) ?>', - 'rules', - '<?= $controller->url_for('admission/rule/save', get_class($rule), $rule->getId()) ?>' - )"> <?= $ruleTemplate ?> - <footer data-dialog-button> - <input type="hidden" id="action" name="action" value=""> - <?= Button::createAccept(_('Speichern'), 'submit') ?> - <?= LinkButton::createCancel(_('Abbrechen'), 'cancel') ?> - </footer> -</form> diff --git a/lib/admissionrules/conditionaladmission/ConditionalAdmission.php b/lib/admissionrules/conditionaladmission/ConditionalAdmission.php index 640e8006014af0330353d924491093b6a09907d1..ab26cc299484eee8d65479fd2b2a46ad53971e24 100644 --- a/lib/admissionrules/conditionaladmission/ConditionalAdmission.php +++ b/lib/admissionrules/conditionaladmission/ConditionalAdmission.php @@ -225,7 +225,8 @@ class ConditionalAdmission extends AdmissionRule $stmt = DBManager::get()->prepare("SELECT * FROM `conditionaladmissions` WHERE `rule_id`=? LIMIT 1"); $stmt->execute([$this->id]); - if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { + $current = $stmt->fetchOne(); + if ($current) { $this->message = $current['message']; $this->startTime = $current['start_time']; $this->endTime = $current['end_time']; @@ -243,6 +244,8 @@ class ConditionalAdmission extends AdmissionRule $this->ungrouped_conditions[$condition['filter_id']] = $currentCondition; } } + } else { + $this->id = $this->generateId('conditionaladmissions'); } } @@ -352,26 +355,50 @@ class ConditionalAdmission extends AdmissionRule { UserFilterField::getAvailableFilterFields(); parent::setAllData($data); + $this->conditions = []; $this->ungrouped_conditions = []; $this->conditiongroups = []; $this->quota = []; - foreach ($data['conditions'] as $ser_con) { - $condition = ObjectBuilder::build($ser_con, 'UserFilter'); + foreach ($data['conditions'] as $con) { + $condition = new UserFilter(); + foreach ($con['attributes']['fields'] as $field) { + $classname = $field['attributes']['type']; + $obj = !empty($field['attributes']['typeparam']) + ? new $classname($field['attributes']['typeparam']) + : new $classname(); + $obj->setCompareOperator($field['attributes']['compare-operator']); + $obj->setValue($field['attributes']['value']); + $condition->addField($obj); + } $this->addCondition($condition, $data['conditiongroup_'.$condition->getId()], $data['quota_'.$data['conditiongroup_'.$condition->getId()]] ?? 0); } - foreach ($this->getConditiongroups() as $conditiongroup_id => $conditions) { - if (mb_strlen($conditiongroup_id) < 32) { - $group = md5(uniqid('conditiongroups' . microtime(), true)); - $this->conditiongroups[$group] = $this->conditiongroups[$conditiongroup_id]; - unset($this->conditiongroups[$conditiongroup_id]); + foreach ($data['grouped-conditions'] as $group) { + if ($group['id'] === '') { + $id = $group['id'] ?: md5(uniqid('conditiongroups' . microtime(), true)); + } + + $this->conditiongroups[$id] = []; + $this->quota[$id] = $group['quota']; + + foreach ($group['conditions'] as $con) { + $condition = new UserFilter(); + foreach ($con['attributes']['fields'] as $field) { + $classname = $field['attributes']['type']; + $obj = !empty($field['attributes']['typeparam']) + ? new $classname($field['attributes']['typeparam']) + : new $classname(); + $obj->setCompareOperator($field['attributes']['compare-operator']); + $obj->setValue($field['attributes']['value']); + $condition->addField($obj); + } - $this->quota[$group] = $this->quota[$conditiongroup_id]; - unset($this->quota[$conditiongroup_id]); + $this->conditiongroups[$id][] = $condition; } } - if (count($this->getConditiongroups()) && $data['conditiongroups_allowed']) { + + if (count($this->conditiongroups) && $data['conditiongroups_allowed']) { $this->conditiongroups_allowed = true; } @@ -560,4 +587,75 @@ class ConditionalAdmission extends AdmissionRule parent::setSiblings($siblings); $this->conditiongroups_allowed = null; } + + /** + * Get fields and settings defining this admission rule as array. + */ + public function getPayload(): array + { + $ungrouped = []; + foreach ($this->getUngroupedConditions() as $one) { + + $fields = []; + foreach ($one->getFields() as $field) { + $fields[] = [ + 'attributes' => [ + 'type' => get_class($field), + 'id' => $field->getId(), + 'compare-operator' => $field->getCompareOperator(), + 'value' => $field->getValue() + ] + ]; + } + + $ungrouped[] = [ + 'attributes' => [ + 'text' => $one->toString(), + 'fields' => $fields + ] + ]; + } + + $groups = []; + + foreach ($this->getConditionGroups() as $id => $conditions) { + $group = [ + 'id' => $id, + 'quota' => $this->getQuota($id), + 'conditions' => [] + ]; + foreach ($conditions as $one) { + $fields = []; + foreach ($one->getFields() as $field) { + $fields[] = [ + 'attributes' => [ + 'type' => get_class($field), + 'id' => $field->getId(), + 'compare-operator' => $field->getCompareOperator(), + 'value' => $field->getValue() + ] + ]; + } + + $group['conditions'][] = [ + 'attributes' => [ + 'text' => $one->toString(), + 'fields' => $fields + ] + ]; + } + + $groups[] = $group; + } + + return array_merge( + parent::getPayload(), + [ + 'quota' => $this->quota, + 'conditiongroups-allowed' => $this->conditiongroups_allowed ? 'true' : 'false', + 'conditions' => $ungrouped, + 'grouped-conditions' => $groups + ] + ); + } } diff --git a/lib/admissionrules/conditionaladmission/templates/configure.php b/lib/admissionrules/conditionaladmission/templates/configure.php index daf12466b989c125e5bcb2f487dacdbd0b9530c9..d4e7a14cd6cf1406646b6328d238f4283891ff29 100644 --- a/lib/admissionrules/conditionaladmission/templates/configure.php +++ b/lib/admissionrules/conditionaladmission/templates/configure.php @@ -1,82 +1,3 @@ -<?php -use Studip\Button, Studip\LinkButton; -?> - -<h3><?= htmlReady($rule->getName()) ?></h3> -<?= $tpl ?> -<br> -<label for="conditionlist" class="caption"> - <span class="required"><?= _('Anmeldebedingungen') ?></span> -</label> - -<br> - -<a href="<?= URLHelper::getURL('dispatch.php/userfilter/filter/configure/condadmission_conditions') ?>" onclick="return STUDIP.UserFilter.configureCondition('condition', this.href)"> - <?= Icon::create('add', 'clickable', tooltip2(_('Bedingung hinzufügen')))->asImg() ?> - <?= _('Bedingung hinzufügen') ?> -</a> - -<br> - -<div id="condadmission_conditions"> - <span class="nofilter" style="<?=(!$rule->getUngroupedConditions() && !$rule->getConditiongroups()) ? '' : 'display: none'?>"> - <i><?= _('Sie haben noch keine Bedingungen festgelegt.'); ?></i> - </span> - <div class="userfilter" style="<?=(!$rule->getUngroupedConditions() || !$rule->getConditiongroups()) ? '' : 'display: none'?>"> - <? if ($rule->conditiongroupsAllowed()): ?> - <div class="grouped_conditions_template" id="new_conditiongroup" style="margin-bottom: 5px; display: none"> - <div class="condition_list"> - <?=_('Kontingent:')?> <input type="text" name="quota" size="5"> <?=_('Prozent')?> - </div> - <?= Button::create(_('Kontingent aufheben'), 'ungroup_conditions', ['class' => 'ungroup_conditions', 'onclick' => 'return STUDIP.UserFilter.ungroupConditions(this)']) ?> - </div> - <? else: ?> - <? $rule->removeConditiongroups(); ?> - <div id="no_conditiongroups"></div> - <? endif; ?> - <div class="ungrouped_conditions"> - <div class="condition_list"> - <? foreach ($rule->getUngroupedConditions() as $condition): ?> - <? $condition->show_user_count = true; ?> - <div class="condition" id="condition_<?= $condition->getId() ?>"> - <? if ($rule->conditiongroupsAllowed()): ?> - <input type="checkbox" name="conditions_checkbox[]" value="<?= htmlReady(ObjectBuilder::exportAsJson($condition)) ?>"> - <? endif; ?> - <?= $condition->toString() ?> - <a href="#" onclick="return STUDIP.UserFilter.removeConditionField($(this).parent())" - class="conditionfield_delete"> - <?= Icon::create('trash', 'clickable')->asImg(); ?></a> - <input type="hidden" name="conditions[]" value="<?= htmlReady(ObjectBuilder::exportAsJson($condition)) ?>"> - <input type="hidden" name="conditiongroup_<?=$condition->getId()?>" value=""> - </div> - <? endforeach; ?> - </div> - </div> - <? if ($rule->conditiongroupsAllowed()): ?> - <input type="hidden" name="conditiongroups_allowed" value="1"> - <?= Button::create(_('Kontingent erstellen'), 'group_conditions', ['class' => 'group_conditions', 'onclick' => 'return STUDIP.UserFilter.groupConditions()', 'style' => $rule->getUngroupedConditions() ? '' : 'display: none']) ?> - <? foreach ($rule->getConditiongroups() as $conditiongroup_id => $conditiongroup): ?> - <div class="grouped_conditions" id="conditiongroup_<?=$conditiongroup_id?>" style="margin-bottom: 5px"> - <div class="condition_list"> - <?=_('Kontingent:')?> <input type="text" name="quota_<?=$conditiongroup_id?>" value="<?=$rule->getQuota($conditiongroup_id)?>" size="5"> <?=_('Prozent')?> - <? foreach ($conditiongroup as $condition): ?> - <? $condition->show_user_count = true; ?> - <div class="condition" id="condition_<?= $condition->getId() ?>"> - <input type="checkbox" name="conditions_checkbox[]" value="<?= htmlReady(ObjectBuilder::exportAsJson($condition)) ?>" style="display: none"> - <?= $condition->toString() ?> - <a href="#" onclick="return STUDIP.UserFilter.removeConditionField($(this).parent())" - class="conditionfield_delete"> - <?= Icon::create('trash', 'clickable')->asImg(); ?></a> - <input type="hidden" name="conditions[]" value="<?= htmlReady(ObjectBuilder::exportAsJson($condition)) ?>"> - <input type="hidden" name="conditiongroup_<?=$condition->getId()?>" value="<?= $conditiongroup_id ?>"> - </div> - <? endforeach; ?> - </div> - <?= Button::create(_('Kontingent aufheben'), 'ungroup_conditions', ['class' => 'ungroup_conditions', 'onclick' => 'return STUDIP.UserFilter.ungroupConditions(this)']) ?> - </div> - <? endforeach; ?> - <? endif; ?> - </div> +<div data-admission-rule="ConditionalAdmission"> + <conditional-admission></conditional-admission> </div> - -<br> diff --git a/lib/admissionrules/conditionaladmission/templates/info.php b/lib/admissionrules/conditionaladmission/templates/info.php index 2094e3a9ff14dc9649d1cd4a6e835f82c94255ab..f380647007c391d6fdaa173dfdf186718e447c7a 100644 --- a/lib/admissionrules/conditionaladmission/templates/info.php +++ b/lib/admissionrules/conditionaladmission/templates/info.php @@ -39,22 +39,18 @@ if ($rule->getStartTime() && $rule->getEndTime()) { 'erfüllt sein:') ?> <br> <ul id="conditions"> - <? foreach ($rule->getConditiongroups() as $conditiongroup_id => $conditions): ?> - <? if ($rule->conditiongroupsAllowed()): ?> - <li> - <i><?= sprintf(_('Kontingent: %s Prozent'), $rule->getQuota($conditiongroup_id)) ?></i> - </li> - <? endif; ?> + <? foreach ($rule->getConditionGroups() as $conditiongroup_id => $conditions): ?> <li> + <i><?= sprintf(_('Kontingent: %s Prozent'), $rule->getQuota($conditiongroup_id)) ?></i> <ul id="conditiongroup_<?=$conditiongroup_id?>"> - <? foreach ($conditions as $condition): ?> - <li id="condition_<?= $condition->getId() ?>"> - <i><?= $condition->toString() ?></i> - </li> - <? endforeach; ?> + <? foreach ($conditions as $condition): ?> + <li id="condition_<?= $condition->getId() ?>"> + <i><?= $condition->toString() ?></i> + </li> + <? endforeach; ?> </ul> </li> - + <? endforeach; ?> </ul> <? endif; ?> diff --git a/lib/admissionrules/coursememberadmission/CourseMemberAdmission.php b/lib/admissionrules/coursememberadmission/CourseMemberAdmission.php index 139c36040bd816e9e206dbc4ebe123cf1f06454a..00bccee7d27755d2f38144d83f4815521a859c7f 100644 --- a/lib/admissionrules/coursememberadmission/CourseMemberAdmission.php +++ b/lib/admissionrules/coursememberadmission/CourseMemberAdmission.php @@ -88,10 +88,13 @@ class CourseMemberAdmission extends AdmissionRule $tpl = $GLOBALS['template_factory']->open('admission/rules/configure'); $tpl->set_attribute('rule', $this); + $search = new StandardSearch('Seminar_id'); + return $this->getTemplateFactory()->render('configure', [ 'rule' => $this, 'tpl' => $tpl->render(), 'courses' => $this->getDecodedCourses(), + 'search' => $search ]); } @@ -104,12 +107,15 @@ class CourseMemberAdmission extends AdmissionRule $stmt = DBManager::get()->prepare("SELECT * FROM `coursememberadmissions` WHERE `rule_id`=? LIMIT 1"); $stmt->execute([$this->id]); - if ($current = $stmt->fetchOne()) { + $current = $stmt->fetchOne(); + if ($current) { $this->message = $current['message']; $this->startTime = $current['start_time']; $this->endTime = $current['end_time']; $this->courses_to_add = $current['courses']; $this->modus = (int) $current['modus']; + } else { + $this->id = $this->generateId('coursememberadmissions'); } } @@ -154,10 +160,8 @@ class CourseMemberAdmission extends AdmissionRule */ public function setAllData($data) { - parent::setAllData($data); - $this->modus = (int) $data['modus']; - $this->courses_to_add = json_encode(array_keys($data['courses_to_add'])); + $this->courses_to_add = json_encode(array_map(fn ($course) => $course['id'], $data['courses'])); return $this; } @@ -257,4 +261,23 @@ class CourseMemberAdmission extends AdmissionRule { return new Flexi\Factory(__DIR__ . '/templates/'); } + + /** + * Get fields and settings defining this admission rule as array. + */ + public function getPayload(): array + { + return array_merge( + parent::getPayload(), + [ + 'courses' => array_map( + fn ($course) => ['id' => $course->id, 'name' => $course->getFullname()], + $this->getDecodedCourses() + ), + 'modus' => $this->modus, + 'search' => (string) new StandardSearch('Seminar_id') + ] + ); + } + } diff --git a/lib/admissionrules/coursememberadmission/templates/configure.php b/lib/admissionrules/coursememberadmission/templates/configure.php index c5d3e4efad234501acb1d39cbb0f243d648f845f..a86150b5877dc5696d320f8bb3a10fbad2477c1f 100644 --- a/lib/admissionrules/coursememberadmission/templates/configure.php +++ b/lib/admissionrules/coursememberadmission/templates/configure.php @@ -1,127 +1,3 @@ -<h3><?= htmlReady($rule->getName()) ?></h3> - -<?= $tpl ?> - -<input type="hidden" name="search_sem_qs_choose" value="title_lecturer_number"> - -<? foreach ($courses as $course) : ?> - <input type="hidden" name="mandatory_course_id_old[]" value="<?= htmlReady($course->id) ?>"> - - <label class="caption"> - <?= _('Mitgliedschaft in folgender Veranstaltung überprüfen') ?>: - </label> - <p> - <?=htmlReady($course->getFullName('number-name-semester'));?> - <a href="<?=URLHelper::getLink('dispatch.php/course/details/index/' . $course->id) ?>" data-dialog> - <?= Icon::create('info-circle')->asImg([ - 'title' =>_('Veranstaltungsdetails aufrufen') - ]) ?> - </a> - </p> -<? endforeach ?> - -<label class="caption"> - <?= _('Modus') ?>: -</label> -<div> - <label> - <input type="radio" name="modus" value="0" <? if ($rule->modus == CourseMemberAdmission::MODE_MUST_BE_IN_COURSES) echo 'checked'; ?>> - <?=_("Mitgliedschaft ist in mindestens einer dieser Veranstaltungen notwendig")?> - </label> - <label> - <input type="radio" name="modus" value="1" <? if ($rule->modus == CourseMemberAdmission::MODE_MAY_NOT_BE_IN_COURSES) echo 'checked'; ?>> - <?=_("Mitgliedschaft ist in keiner dieser Veranstaltungen erlaubt")?> - </label> +<div data-admission-rule="CourseMemberAdmission"> + <course-member-admission></course-member-admission> </div> - -<label class="caption"> - <?= _('Veranstaltung suchen') ?>: -</label> - -<div style="display:flex; align-items: flex-start; column-gap: 1em; flex-wrap: wrap"> - - <?= - QuickSearch::get('mandatory_course_id', new SeminarSearch()) - ->fireJSFunctionOnSelect('addcourse') - ->setInputStyle('flex: 0 0 40%') - ->render(); - ?> - - <div style="flex: 0 0 40%"> - <?= Semester::getSemesterSelector( - ['name' => 'search_sem_sem'], - Semester::getIndexById($_SESSION['_default_sem'], false, !$GLOBALS['perm']->have_perm('admin')), - 'key', - false - )?> - </div> - <br/><br/> - <ul> - <? foreach ($courses as $course) : ?> - <li> - <input type="hidden" id="<?= htmlReady($course->id) ?>" - name="courses_to_add[<?= htmlReady($course->id) ?>]" - value="<?= htmlReady($course->name) ?>"> - <span><?= htmlReady($course->name) ?></span> - <a href="#" onclick="return removecourse('<?= htmlReady($course->id) ?>')"> - <?= Icon::create('trash') ?> - </a> - </li> - <? endforeach ?> - </ul> -</div> - -<script> - $('#ruleform input[name="modus"]').on('change', function () { - const message = <?= json_encode([ - _('Sie sind nicht in der Veranstaltung "%s" eingetragen.'), - _('Sie dürfen nicht in der Veranstaltung "%s" eingetragen sein.'), - ]) ?>; - console.log(this, this.value); - $('#ruleform textarea').text(message[this.value]); - }).filter(':checked').change(); - - function addcourse(id, title) { - - if ($('input[name="courses_to_add[' + id + ']"]').length === 0) { - var wrapper = $('<li>'); - var input = $('<input>') - .attr('id', id) - .attr('type', 'hidden') - .attr('name', 'courses_to_add['+ id + ']') - .attr('value', title); - wrapper.append(input); - - var trash = $('<input>') - .attr('type', 'image') - .attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/trash.svg') - .attr('name', 'remove_[' + id + ']') - .attr('value', '1') - .attr('onclick', "return removecourse('" + id + "')"); - - var icon = $('<a>') - .attr('onclick', "return removecourse('" + id + "')") - .attr('href', '#'); - var img = $('<img>') - .attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/trash.svg') - .attr('width', '16px') - .attr('height', '16px'); - icon.append(img); - - var nametext = $('<span>') - .html(title) - .text(); - wrapper.append(nametext); - wrapper.append(icon); - - $('input[name=mandatory_course_id_parameter]').parent().find('ul').append(wrapper); - } - - } - - function removecourse(id) { - $('input#' + id).parent().remove(); - return false; - } - -</script> diff --git a/lib/admissionrules/limitedadmission/LimitedAdmission.php b/lib/admissionrules/limitedadmission/LimitedAdmission.php index d7f53c150177a714a736cc54e09db7b5adf4e91f..c0339cc1b921d55b413cf8d09a20f1e7bd65b293 100644 --- a/lib/admissionrules/limitedadmission/LimitedAdmission.php +++ b/lib/admissionrules/limitedadmission/LimitedAdmission.php @@ -141,11 +141,14 @@ class LimitedAdmission extends AdmissionRule $stmt = DBManager::get()->prepare("SELECT * FROM `limitedadmissions` WHERE `rule_id`=? LIMIT 1"); $stmt->execute([$this->id]); - if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { + $current = $stmt->fetchOne(); + if ($current) { $this->message = $current['message']; $this->startTime = $current['start_time']; $this->endTime = $current['end_time']; $this->maxNumber = $current['maxnumber']; + } else { + $this->id = $this->generateId('limitedadmissions'); } } @@ -279,6 +282,20 @@ class LimitedAdmission extends AdmissionRule return $message; } } + + /** + * Get fields and settings defining this admission rule as array. + */ + public function getPayload(): array + { + return array_merge( + parent::getPayload(), + [ + 'maxnumber' => $this->getMaxNumber() + ] + ); + } + } /* end of class LimitedAdmission */ ?> diff --git a/lib/admissionrules/limitedadmission/templates/configure.php b/lib/admissionrules/limitedadmission/templates/configure.php index 7a36147071aec7c129def0f6b40b93353e0231a2..5a2e0a9da471344028033a2c8b46c18bce800938 100644 --- a/lib/admissionrules/limitedadmission/templates/configure.php +++ b/lib/admissionrules/limitedadmission/templates/configure.php @@ -1,7 +1,3 @@ -<h3><?= $rule->getName() ?></h3> -<?= $tpl ?> -<br/> -<label for="maxnumber" class="caption"> - <span class="required"><?= _('Maximale Anzahl erlaubter Anmeldungen') ?></span> - <input type="number" name="maxnumber" size="4" min="1" value="<?= $rule->getMaxNumber() ?>" required/> -</label> +<div data-admission-rule="LimitedAdmission"> + <limited-admission></limited-admission> +</div> diff --git a/lib/admissionrules/lockedadmission/LockedAdmission.php b/lib/admissionrules/lockedadmission/LockedAdmission.php index 92ef5137e40ce8473f5f9e2d579862efea426523..afef13afd28b68c1f7e1dd13d462877f27fdead1 100644 --- a/lib/admissionrules/lockedadmission/LockedAdmission.php +++ b/lib/admissionrules/lockedadmission/LockedAdmission.php @@ -2,7 +2,7 @@ /** * LockedAdmission.php - * + * * Represents a rule for completely locking courses for admission. * * This program is free software; you can redistribute it and/or @@ -30,7 +30,7 @@ class LockedAdmission extends AdmissionRule { parent::__construct($ruleId, $courseSetId); $this->default_message = _('Die Anmeldung ist gesperrt.'); - + if ($ruleId) { $this->load(); } else { @@ -44,13 +44,13 @@ class LockedAdmission extends AdmissionRule public function delete() { parent::delete(); // Delete rule data. - $stmt = DBManager::get()->prepare("DELETE FROM `lockedadmissions` + $stmt = DBManager::get()->prepare("DELETE FROM `lockedadmissions` WHERE `rule_id`=?"); $stmt->execute([$this->id]); } /** - * Gets some text that describes what this AdmissionRule (or respective + * Gets some text that describes what this AdmissionRule (or respective * subclass) does. */ public static function getDescription() { @@ -68,12 +68,12 @@ class LockedAdmission extends AdmissionRule /** * Gets the template that provides a configuration GUI for this rule. - * + * * @return String */ public function getTemplate() { $factory = new Flexi\Factory(dirname(__FILE__).'/templates/'); - // Now open specific template for this rule and insert base template. + // Now open specific template for this rule and insert base template. $tpl = $factory->open('configure'); $tpl->set_attribute('rule', $this); return $tpl->render(); @@ -86,13 +86,16 @@ class LockedAdmission extends AdmissionRule $stmt = DBManager::get()->prepare("SELECT * FROM `lockedadmissions` WHERE `rule_id`=? LIMIT 1"); $stmt->execute([$this->id]); - if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { + $current = $stmt->fetchOne(); + if ($current) { $this->message = $current['message']; + } else { + $this->id = $this->generateId('lockedadmissions'); } } /** - * Does the current rule allow the given user to register as participant + * Does the current rule allow the given user to register as participant * in the given course? Never happens here as admission is completely * locked. * diff --git a/lib/admissionrules/lockedadmission/templates/configure.php b/lib/admissionrules/lockedadmission/templates/configure.php index 4a60973d09b5811e72153404f8a475ed1352075f..5be3f66002c8702edbeedfecdffe59f8ae1934bf 100644 --- a/lib/admissionrules/lockedadmission/templates/configure.php +++ b/lib/admissionrules/lockedadmission/templates/configure.php @@ -1,5 +1,3 @@ -<h3><?= $rule->getName() ?></h3> -<label for="message" class="caption"> - <?= _('Nachricht bei fehlgeschlagener Anmeldung') ?>: -</label> -<textarea name="message" rows="4" cols="50"><?= $rule->getMessage() ?></textarea> \ No newline at end of file +<div data-admission-rule="LockedAdmission"> + <locked-admission></locked-admission> +</div> diff --git a/lib/admissionrules/participantrestrictedadmission/ParticipantRestrictedAdmission.php b/lib/admissionrules/participantrestrictedadmission/ParticipantRestrictedAdmission.php index 586f82f6ef7789fe3c09e1ef36ef846ec70e1ac4..2954602258f10555055b618b4b1b845fff61f283 100644 --- a/lib/admissionrules/participantrestrictedadmission/ParticipantRestrictedAdmission.php +++ b/lib/admissionrules/participantrestrictedadmission/ParticipantRestrictedAdmission.php @@ -51,7 +51,7 @@ class ParticipantRestrictedAdmission extends AdmissionRule } } - public function isFCFSallowed() + public function isFCFSAllowed() { return $this->first_come_first_served_allowed; } @@ -123,12 +123,15 @@ class ParticipantRestrictedAdmission extends AdmissionRule $stmt = DBManager::get()->prepare("SELECT * FROM `participantrestrictedadmissions` WHERE `rule_id`=? LIMIT 1"); $stmt->execute([$this->id]); - if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { + $current = $stmt->fetchOne(); + if ($current) { $this->message = $current['message']; $this->distributionTime = $current['distribution_time']; if ($current['distribution_time'] > 0) { $this->prio_exists = DBManager::get()->fetchColumn("SELECT 1 FROM courseset_rule INNER JOIN priorities USING(set_id) WHERE rule_id = ? LIMIT 1", [$this->id]); } + } else { + $this->id = $this->generateId('participantrestrictedadmissions'); } } @@ -143,14 +146,11 @@ class ParticipantRestrictedAdmission extends AdmissionRule public function setAllData($data) { parent::setAllData($data); - if (!empty($data['distributiondate'])) { - if (!$data['distributiontime']) { - $data['distributiontime'] = '23:59'; - } - $ddate = strtotime($data['distributiondate'] . ' ' . $data['distributiontime']); - $this->setDistributionTime($ddate); + + if (!empty($data['distribution-time'])) { + $this->setDistributionTime($data['distribution-time']); } - if (!empty($data['enable_FCFS'])) { + if (!empty($data['fcfs'])) { $this->setDistributionTime(0); } if (!empty($data['startdate'])) { @@ -227,4 +227,19 @@ class ParticipantRestrictedAdmission extends AdmissionRule } return $errors; } + + /** + * Get fields and settings defining this admission rule as array. + */ + public function getPayload(): array + { + return array_merge( + parent::getPayload(), + [ + 'distribution-time' => $this->getDistributionTime(), + 'fcfs-allowed' => $this->isFCFSAllowed() + ] + ); + } + } diff --git a/lib/admissionrules/participantrestrictedadmission/templates/configure.php b/lib/admissionrules/participantrestrictedadmission/templates/configure.php index 3c02c6c65da33867a4a630db39a7083fc1b463bb..c2b50f3731e2ffb862a26b031c302dda74129af7 100644 --- a/lib/admissionrules/participantrestrictedadmission/templates/configure.php +++ b/lib/admissionrules/participantrestrictedadmission/templates/configure.php @@ -1,30 +1,5 @@ -<h3><?= $rule->getName() ?></h3> -<label for="start" class="caption"> - <?= _('Zeitpunkt der automatischen Platzverteilung') ?>: -</label> - -<label class="col-3"> - <?= _('Datum') ?> - <input type="text" name="distributiondate" id="distributiondate" - class="size-s no-hint" placeholder="tt.mm.jjjj" - value="<?= $rule->getDistributionTime() ? date('d.m.Y', $rule->getDistributionTime()) : '' ?>"/> -</label> - -<label class="col-3"> - <?= _('Uhrzeit') ?> - <input type="text" name="distributiontime" id="distributiontime" - class="size-s no-hint" placeholder="ss:mm" - value="<?= $rule->getDistributionTime() ? date('H:i', $rule->getDistributionTime()) : '23:59' ?>"/> -</label> - -<? if ($rule->isFCFSallowed()) : ?> - <label for="enable_FCFS"> - <input <?= !empty($rule->prio_exists ? 'disabled' : '') ?> type="checkbox" id="enable_FCFS" name="enable_FCFS" value="1" <?= (!is_null($rule->getDistributionTime()) && !$rule->getDistributionTime() ? "checked" : ""); ?>> - <?=_("<u>Keine</u> automatische Platzverteilung (Windhund-Verfahren)")?> - <?= !empty($rule->prio_exists) ? tooltipicon(_("Es existieren bereits Anmeldungen für die automatische Platzverteilung.")) : '' ?> - </label> -<? endif ?> -<script> - $('#distributiondate').datepicker(); - $('#distributiontime').timepicker(); -</script> +<div data-admission-rule="ParticipantRestrictedAdmission"> + <participant-restricted-admission :distribution="<?= $rule->getDistributionTime() ?>" + :fcfs="<?= $rule->isFCFSAllowed() ? 'true' : 'false'?>" + :hasPrios="false"></participant-restricted-admission> +</div> diff --git a/lib/admissionrules/participantrestrictedadmission/templates/info.php b/lib/admissionrules/participantrestrictedadmission/templates/info.php index 886ee58a14de524083286ebdb0a6ff96d6b551f6..e89a5740f61d0b87bbb79539b986f7372c60e616 100644 --- a/lib/admissionrules/participantrestrictedadmission/templates/info.php +++ b/lib/admissionrules/participantrestrictedadmission/templates/info.php @@ -3,13 +3,13 @@ <? if ($rule->getDistributionTime()) : ?> <? if ($rule->getDistributionTime() > time()) : ?> <?= sprintf(_('Die Plätze in den betreffenden Veranstaltungen werden am %s '. - 'um %s verteilt.'), date("d.m.Y", $rule->getDistributionTime()), + 'um %s verteilt.'), date("d.m.Y", $rule->getDistributionTime()), date("H:i", $rule->getDistributionTime())) ?> <? else : ?> <?= sprintf(_('Die Plätze in den betreffenden Veranstaltungen wurden am %s '. - 'um %s verteilt. Weitere Plätze werden evtl. über Wartelisten zur Verfügung gestellt.'), date("d.m.Y", $rule->getDistributionTime()), + 'um %s verteilt. Weitere Plätze werden evtl. über Wartelisten zur Verfügung gestellt.'), date("d.m.Y", $rule->getDistributionTime()), date("H:i", $rule->getDistributionTime())) ?> <? endif ?> -<? elseif ($rule->isFCFSallowed()) :?> +<? elseif ($rule->isFCFSAllowed()) :?> <?= _("Die Plätze werden in der Reihenfolge der Anmeldung vergeben.")?> -<? endif ?> \ No newline at end of file +<? endif ?> diff --git a/lib/admissionrules/passwordadmission/PasswordAdmission.php b/lib/admissionrules/passwordadmission/PasswordAdmission.php index 871502113d31c7f1e43ee136adb8588659970c77..85c6e2ddd5bd59be77fd3af491ea04421f8d1423 100644 --- a/lib/admissionrules/passwordadmission/PasswordAdmission.php +++ b/lib/admissionrules/passwordadmission/PasswordAdmission.php @@ -126,11 +126,14 @@ class PasswordAdmission extends AdmissionRule $stmt = DBManager::get()->prepare("SELECT * FROM `passwordadmissions` WHERE `rule_id`=? LIMIT 1"); $stmt->execute([$this->id]); - if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { + $current = $stmt->fetchOne(); + if ($current) { $this->message = $current['message']; $this->startTime = $current['start_time']; $this->endTime = $current['end_time']; $this->password = $current['password']; + } else { + $this->id = $this->generateId('passwordadmissions'); } } @@ -238,4 +241,18 @@ class PasswordAdmission extends AdmissionRule } return $errors; } + + /** + * Get fields and settings defining this admission rule as array. + */ + public function getPayload(): array + { + return array_merge( + parent::getPayload(), + [ + 'password' => $this->getPassword() + ] + ); + } + } diff --git a/lib/admissionrules/passwordadmission/templates/configure.php b/lib/admissionrules/passwordadmission/templates/configure.php index 4642dec280566acfc86fa70e4d7a9a68b83e75c2..5a95b8513c8e4c6b186c706febb32167a5e52c35 100644 --- a/lib/admissionrules/passwordadmission/templates/configure.php +++ b/lib/admissionrules/passwordadmission/templates/configure.php @@ -1,15 +1,3 @@ -<h3><?= htmlReady($rule->getName()) ?></h3> -<label> - <?= _('Nachricht bei fehlgeschlagener Anmeldung') ?>: - <textarea name="message" rows="4" cols="50"><?= htmlReady($rule->getMessage()) ?></textarea> -</label> -<label> - <?= _('Zugangspasswort') ?>: - <input type="password" name="password1" size="25" max="40" - value="<?= htmlReady(Request::get('password1')) ?>" <?= $rule->new ? 'required' : ''?>> -</label> -<label> - <?= _('Passwort wiederholen') ?>: - <input type="password" name="password2" size="25" max="40" - value="<?= htmlReady(Request::get('password2')) ?>" <?= $rule->new ? 'required' : ''?>> -</label> +<div data-admission-rule="PasswordAdmission"> + <password-admission></password-admission> +</div> diff --git a/lib/admissionrules/preferentialadmission/PreferentialAdmission.php b/lib/admissionrules/preferentialadmission/PreferentialAdmission.php index 2f57a446622db3397cefc202c0c7ec436a09d394..23633d6d62a7afe8f931a78b74b768c293671501 100644 --- a/lib/admissionrules/preferentialadmission/PreferentialAdmission.php +++ b/lib/admissionrules/preferentialadmission/PreferentialAdmission.php @@ -352,7 +352,8 @@ class PreferentialAdmission extends AdmissionRule $stmt = DBManager::get()->prepare("SELECT * FROM `prefadmissions` WHERE `rule_id`=? LIMIT 1"); $stmt->execute([$this->id]); - if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { + $current = $stmt->fetchOne(); + if ($current) { $this->favorSemester = $current['favor_semester']; // Retrieve conditions. $stmt = DBManager::get()->prepare("SELECT * @@ -363,6 +364,8 @@ class PreferentialAdmission extends AdmissionRule $currentCondition = new UserFilter($condition['condition_id']); $this->conditions[$condition['condition_id']] = $currentCondition; } + } else { + $this->id = $this->generateId('prefadmissions'); } } @@ -402,13 +405,20 @@ class PreferentialAdmission extends AdmissionRule */ public function setAllData($data) { - UserFilterField::getAvailableFilterFields(); parent::setAllData($data); - $this->favorSemester = (bool) $data['favor_semester']; + $this->favorSemester = (bool) $data['favor-semester']; $this->conditions = []; if ($data['conditions']) { - foreach ($data['conditions'] as $condition) { - $this->addCondition(ObjectBuilder::build($condition, 'UserFilter')); + foreach ($data['conditions'] as $con) { + $condition = new UserFilter(); + foreach ($con['attributes']['fields'] as $field) { + $classname = $field['attributes']['type']; + $obj = new $classname(); + $obj->setCompareOperator($field['attributes']['compare-operator']); + $obj->setValue($field['attributes']['value']); + $condition->addField($obj); + } + $this->addCondition($condition); } } return $this; @@ -516,7 +526,7 @@ class PreferentialAdmission extends AdmissionRule public function validate($data) { $errors = parent::validate($data); - if (!$data['conditions'] && !$data['favor_semester']) { + if (!$data['conditions'] && !$data['favor-semester']) { $errors[] = _('Es muss mindestens eine Auswahlbedingung angegeben werden.'); } return $errors; @@ -534,4 +544,41 @@ class PreferentialAdmission extends AdmissionRule $this->conditions = $cloned_conditions; } + /** + * Get fields and settings defining this admission rule as array. + */ + public function getPayload(): array + { + // Build everything as plain array. + $conditions = []; + foreach ($this->getConditions() as $one) { + $fields = []; + foreach ($one->getFields() as $field) { + $fields[] = [ + 'attributes' => [ + 'type' => get_class($field), + 'id' => $field->getId(), + 'compare-operator' => $field->getCompareOperator(), + 'value' => $field->getValue() + ] + ]; + } + + $conditions[] = [ + 'attributes' => [ + 'text' => $one->toString(), + 'fields' => $fields + ] + ]; + } + + return array_merge( + parent::getPayload(), + [ + 'conditions' => $conditions, + 'favor-semester' => $this->getFavorSemester() + ] + ); + } + } /* end of class PreferentialAdmission */ diff --git a/lib/admissionrules/preferentialadmission/templates/configure.php b/lib/admissionrules/preferentialadmission/templates/configure.php index 1568caffb9c74da97bb8f350b2e561bb414de648..240e12c4b21284c656fc971306a58d4247525823 100644 --- a/lib/admissionrules/preferentialadmission/templates/configure.php +++ b/lib/admissionrules/preferentialadmission/templates/configure.php @@ -1,38 +1,3 @@ -<h3><?= htmlReady($rule->getName()) ?></h3> -<label for="prefadmission_conditions" class="caption"> - <?= _('Folgende Personen bei der Platzverteilung bevorzugen:') ?> -</label> -<div id="prefadmission_conditions"> - <span class="nofilter" style="<?=(!$rule->getConditions() ? '' : 'display: none')?>"> - <i><?= _('Sie haben noch keine Auswahl festgelegt.'); ?></i> - </span> - <div class="userfilter" style="<?=($rule->getConditions() ? '' : 'display: none')?>"> - <div id="no_conditiongroups" class="ungrouped_conditions"> - <div class="condition_list"> - <?php foreach ($rule->getConditions() as $condition) : - $condition->show_user_count = true; ?> - - <div class="condition" id="condition_<?= $condition->getId() ?>"> - <?= $condition->toString() ?> - <a href="#" onclick="return STUDIP.UserFilter.removeConditionField($(this).parent())" - class="conditionfield_delete"> - <?= Icon::create('trash', 'clickable')->asImg(); ?></a> - <input type="hidden" name="conditions[]" value="<?= htmlReady(ObjectBuilder::exportAsJson($condition)) ?>"/> - </div> - <?php endforeach ?> - </div> - </div> - </div> - <br><br> - <a href="<?= URLHelper::getURL('dispatch.php/userfilter/filter/configure/prefadmission_conditions') ?>" - onclick="return STUDIP.UserFilter.configureCondition('condition', '<?= - URLHelper::getLink('dispatch.php/userfilter/filter/configure/prefadmission_conditions') ?>')"> - <?= Icon::create('add')->asImg(['title' => _('Bedingung hinzufügen'), 'alt' => _('Bedingung hinzufügen')]) ?> - <?= _('Bedingung hinzufügen') ?> - </a> +<div data-admission-rule="PreferentialAdmission"> + <preferential-admission></preferential-admission> </div> -<br> -<label class="caption"> - <input type="checkbox" name="favor_semester"<?= $rule->getFavorSemester() ? ' checked' : '' ?>/> - <?= _('Höhere Fachsemester bevorzugen') ?> -</label> diff --git a/lib/admissionrules/termsadmission/TermsAdmission.php b/lib/admissionrules/termsadmission/TermsAdmission.php index eb83dfcc2d07331a8e4abee4296c75991df26e38..5e98513abdab485a46c564b539b3da5d4d7fd898 100644 --- a/lib/admissionrules/termsadmission/TermsAdmission.php +++ b/lib/admissionrules/termsadmission/TermsAdmission.php @@ -137,7 +137,12 @@ class TermsAdmission extends AdmissionRule public function load() { $rule = DBManager::get()->fetchOne('SELECT * FROM termsadmissions WHERE rule_id = ?', [$this->getId()]); - $this->terms = $rule['terms']; + + if ($rule) { + $this->terms = $rule['terms']; + } else { + $this->id = $this->generateId('termsadmissions'); + } return $this; } @@ -166,4 +171,18 @@ class TermsAdmission extends AdmissionRule return $template->render(); } + + /** + * Get fields and settings defining this admission rule as array. + */ + public function getPayload(): array + { + return array_merge( + parent::getPayload(), + [ + 'terms' => $this->terms + ] + ); + } + } diff --git a/lib/admissionrules/termsadmission/templates/configure.php b/lib/admissionrules/termsadmission/templates/configure.php index 90343193548db6172e7cea750378e4d3144c5dbd..48d4dfddc56c48b98ff229cf16a5e62cd1f4c9a0 100644 --- a/lib/admissionrules/termsadmission/templates/configure.php +++ b/lib/admissionrules/termsadmission/templates/configure.php @@ -1,4 +1,3 @@ -<label> - <span class="required"><?= _('Teilnahmebedingungen') ?></span> - <textarea style="min-height: 24em; min-width: 44em;" name="terms" placeholder="<?=_('Formulieren Sie hier die Teilnahmebedingungen.')?>" required><?= htmlReady($rule->terms) ?></textarea> -</label> +<div data-admission-rule="TermsAdmission"> + <terms-admission terms="<?= htmlReady($rule->terms) ?>" id="<?= htmlReady($rule->getId()) ?>"></terms-admission> +</div> diff --git a/lib/admissionrules/timedadmission/TimedAdmission.php b/lib/admissionrules/timedadmission/TimedAdmission.php index 5f5e88570c41d3c2111e530e1516895343be7b3a..69a828e7cb43d3cdffc6e4c649bb511097a5e1a2 100644 --- a/lib/admissionrules/timedadmission/TimedAdmission.php +++ b/lib/admissionrules/timedadmission/TimedAdmission.php @@ -116,10 +116,13 @@ class TimedAdmission extends AdmissionRule $stmt = DBManager::get()->prepare("SELECT * FROM `timedadmissions` WHERE `rule_id`=? LIMIT 1"); $stmt->execute([$this->id]); - if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { + $current = $stmt->fetchOne(); + if ($current) { $this->message = $current['message']; $this->startTime = $current['start_time']; $this->endTime = $current['end_time']; + } else { + $this->id = $this->generateId('timedadmissions'); } } @@ -148,22 +151,11 @@ class TimedAdmission extends AdmissionRule */ public function setAllData($data) { parent::setAllData($data); - if ($data['startdate']) { - $sdate = $data['startdate']; - $stime = $data['starttime']; - $parsed = date_parse($sdate.' '.$stime); - $timestamp = mktime($parsed['hour'], $parsed['minute'], 0, $parsed['month'], $parsed['day'], $parsed['year']); - $this->setStartTime($timestamp); + if ($data['starttime']) { + $this->setStartTime($data['starttime']); } - if ($data['enddate']) { - $edate = $data['enddate']; - $etime = $data['endtime']; - if (!$etime) { - $etime = '23:59'; - } - $parsed = date_parse($edate.' '.$etime); - $timestamp = mktime($parsed['hour'], $parsed['minute'], 0, $parsed['month'], $parsed['day'], $parsed['year']); - $this->setEndTime($timestamp); + if ($data['endtime']) { + $this->setEndTime($data['endtime']); } return $this; } @@ -229,15 +221,29 @@ class TimedAdmission extends AdmissionRule public function validate($data) { $errors = parent::validate($data); - if (!$data['startdate'] && !$data['enddate']) { + if (!$data['starttime'] && !$data['endtime']) { $errors[] = _('Bitte geben Sie entweder ein Start- oder Enddatum an.'); } - if ($data['startdate'] && $data['enddate'] && strtotime($data['enddate'] . ' ' . $data['endtime']) < strtotime($data['startdate']. ' ' . $data['starttime'])) { + if ($data['starttime'] && $data['endtime'] && $data['endtime'] < $data['starttime']) { $errors[] = _('Das Enddatum darf nicht vor dem Startdatum liegen.'); } return $errors; } + /** + * Get fields and settings defining this admission rule as array. + */ + public function getPayload(): array + { + return array_merge( + parent::getPayload(), + [ + 'starttime' => $this->getStartTime(), + 'endtime' => $this->getEndTime() + ] + ); + } + } /* end of class TimedAdmission */ ?> diff --git a/lib/admissionrules/timedadmission/templates/configure.php b/lib/admissionrules/timedadmission/templates/configure.php index 6ea65e6bbbab2aea10ab0ec2678f1c86dae9119e..d39f6ab46548b3c70a7e650402a56c17b6ffeb4e 100644 --- a/lib/admissionrules/timedadmission/templates/configure.php +++ b/lib/admissionrules/timedadmission/templates/configure.php @@ -1,47 +1,3 @@ -<h3><?= $rule->getName() ?></h3> -<label for="message" class="caption"> - <?= _('Nachricht bei fehlgeschlagener Anmeldung') ?>: - <textarea name="message" rows="4" cols="50"><?= $rule->getMessage() ?></textarea> -</label> - -<label for="startdate" class="caption"> - <?= _('Start des Anmeldezeitraums') ?>: -</label> -<label class="col-3"> - <?= _('Datum') ?> - <input type="text" maxlength="10" name="startdate" - class="size-s no-hint" placeholder="tt.mm.jjjj" - id="startdate" value="<?= $rule->getStartTime() ? - date('d.m.Y', $rule->getStartTime()) : '' ?>" data-max-date=""/> -</label> -<label class="col-3"> - <?= _('Uhrzeit') ?> - <input type="text" name="starttime" id="starttime" - class="size-s no-hint" placeholder="ss:mm" - value="<?= $rule->getStartTime() ? date('H:i', $rule->getStartTime()) : '' ?>"/> -</label> - -<label for="enddate" class="caption"> - <?= _('Ende des Anmeldezeitraums') ?>: -</label> - -<label class="col-3"> - <?= _('Datum') ?> - <input type="text" maxlength="10" name="enddate" - class="size-s no-hint" placeholder="tt.mm.jjjj" - id="enddate" value="<?= $rule->getEndTime() ? - date('d.m.Y', $rule->getEndTime()) : '' ?>" data-min-date=""/> -</label> -<label class="col-3"> - <?= _('Uhrzeit') ?> - <input type="text" name="endtime" id="endtime" - class="size-s no-hint" placeholder="ss:mm" - value="<?= $rule->getEndTime() ? date('H:i', $rule->getEndTime()) : '' ?>"/> -</label> - -<script> - $('#startdate').datepicker(); - $('#starttime').timepicker(); - $('#enddate').datepicker(); - $('#endtime').timepicker(); -</script> +<div id="admission-rule" data-admission-rule="TimedAdmission"> + <timed-admission :start="<?= $startTime ?: time() ?>" :end="<?= $endTime ?: (time() + 3600) ?>"></timed-admission> +</div> diff --git a/lib/classes/CoursesetModel.php b/lib/classes/CoursesetModel.php index 076355d9c0717618acd2f78aca918a1e2f254b22..6a0e6919f0eb5b9d432683dc9e921f85968af1d8 100644 --- a/lib/classes/CoursesetModel.php +++ b/lib/classes/CoursesetModel.php @@ -48,7 +48,7 @@ class CoursesetModel WHERE s.status NOT IN(?) AND (semester_courses.semester_id IS NULL OR semester_courses.semester_id = ?) AND su.`user_id` = ? - GROUP BY su.`Seminar_id` "; + GROUP BY su.`Seminar_id`"; $parameters = [ $excludeTypes, $currentSemester->id, @@ -148,7 +148,7 @@ class CoursesetModel ); }; - Course::findEachMany($callable, array_unique($courses),"ORDER BY `semester_data`.`beginn` DESC, `VeranstaltungsNummer` ASC, `Name` ASC"); + Course::findEachMany($callable, array_unique($courses),"ORDER BY `VeranstaltungsNummer` ASC, `Name` ASC"); return $data; } diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 8c6037ac99035326c79ba1ddfec2667c49cc88f9..b13887f3af30a295b9ce322120dbf466a450e793 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -115,6 +115,7 @@ class RouteMap $group->get('/status-groups/{id}', Routes\StatusgroupShow::class); + $this->addAuthenticatedAdmissionRoutes($group); $this->addAuthenticatedBlubberRoutes($group); $this->addAuthenticatedClipboardRoutes($group); $this->addAuthenticatedConsultationRoutes($group); @@ -136,6 +137,7 @@ class RouteMap $this->addAuthenticatedNewsRoutes($group); $this->addAuthenticatedStockImagesRoutes($group); $this->addAuthenticatedStudyAreasRoutes($group); + $this->addAuthenticatedUserFilterRoutes($group); $this->addAuthenticatedWikiRoutes($group); } @@ -168,6 +170,20 @@ class RouteMap return $this->app->getContainer()->get('studip-authenticator'); } + private function addAuthenticatedAdmissionRoutes(RouteCollectorProxy $group): void { + $group->post('/course-sets', Routes\Admission\CourseSetsCreate::class); + $group->get('/course-sets/{id}', Routes\Admission\CourseSetsShow::class); + $group->patch('/course-sets/{id}', Routes\Admission\CourseSetsUpdate::class); + $group->delete('/course-sets/{id}', Routes\Admission\CourseSetsDelete::class); + $group->post('/admission/available-courses', Routes\Admission\AvailableCoursesIndex::class); + $group->get('/admission/rule-compatibility', Routes\Admission\RuleCompatibilityIndex::class); + $group->get('/admission-rules', Routes\Admission\AdmissionRulesIndex::class); + $group->post('/admission-rules/{type}', Routes\Admission\AdmissionRulesCreate::class); + $group->get('/admission-rules/{id}', Routes\Admission\AdmissionRulesShow::class); + $group->patch('/admission-rules/{id}', Routes\Admission\AdmissionRulesUpdate::class); + $group->delete('/admission-rules/{id}', Routes\Admission\AdmissionRulesDelete::class); + } + private function addAuthenticatedBlubberRoutes(RouteCollectorProxy $group): void { // find BlubberThreads @@ -660,6 +676,16 @@ class RouteMap $group->post('/{type:courses|institutes|users}/{id}/avatar', Routes\Avatar\AvatarUpload::class); } + private function addAuthenticatedUserFilterRoutes(RouteCollectorProxy $group): void + { + $group->get('/user-filters/{id}', Routes\UserFilters\UserFiltersShow::class); + $group->post('/user-filters', Routes\UserFilters\UserFiltersCreate::class); + $group->patch('/user-filters/{id}', Routes\UserFilters\UserFiltersUpdate::class); + $group->delete('/user-filters/{id}', Routes\UserFilters\UserFiltersDelete::class); + $group->get('/user-filter-fields', Routes\UserFilters\UserFilterFieldsIndex::class); + $group->get('/user-filter-fields/{id}', Routes\UserFilters\UserFilterFieldsShow::class); + } + private function addRelationship(RouteCollectorProxy $group, string $url, string $handler): void { $group->map(['GET', 'PATCH', 'POST', 'DELETE'], $url, $handler); diff --git a/lib/classes/JsonApi/Routes/Admission/AdmissionRulesCreate.php b/lib/classes/JsonApi/Routes/Admission/AdmissionRulesCreate.php new file mode 100644 index 0000000000000000000000000000000000000000..2c8c6f2ab3882700e01eb70b4fdcce61efea3b3e --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/AdmissionRulesCreate.php @@ -0,0 +1,50 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use JsonApi\Errors\AuthorizationFailedException; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; + +/** + * Create a new admission rule. + */ +class AdmissionRulesCreate extends JsonApiController +{ + use ValidationTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $json = $this->validate($request); + $user = $this->getUser($request); + + if (!Authority::canEditAdmissionRules($user)) { + throw new AuthorizationFailedException(); + } + + $rule = \AdmissionRule::getRule($args['type']); + $rule->setAllData(self::arrayGet($json, 'data.attributes.payload')); + $rule->id = ''; + + return $this->getCreatedResponse($rule); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (!self::arrayHas($json, 'data.attributes')) { + return 'Missing `attributes` member of data block.'; + } + if (!self::arrayHas($json, 'data.attributes.payload')) { + return 'Missing `payload` member of attributes block.'; + } + } + +} diff --git a/lib/classes/JsonApi/Routes/Admission/AdmissionRulesDelete.php b/lib/classes/JsonApi/Routes/Admission/AdmissionRulesDelete.php new file mode 100644 index 0000000000000000000000000000000000000000..16102d72e61397b8ce5d9b71beb2f90a52649407 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/AdmissionRulesDelete.php @@ -0,0 +1,38 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use JsonApi\Errors\AuthorizationFailedException; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; + +/** + * Deletes an admission rule. + */ +class AdmissionRulesDelete extends JsonApiController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $user = $this->getUser($request); + + if (!Authority::canEditAdmissionRules($user)) { + throw new AuthorizationFailedException(); + } + + [$type, $id] = explode('_', $args['id']); + + $rule = \AdmissionRule::getRule($type, $id); + if (!$rule) { + throw new RecordNotFoundException(); + } + + $rule->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Admission/AdmissionRulesIndex.php b/lib/classes/JsonApi/Routes/Admission/AdmissionRulesIndex.php new file mode 100644 index 0000000000000000000000000000000000000000..6d1fb305a1a85579009f1dd3ca32c2a14d73498e --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/AdmissionRulesIndex.php @@ -0,0 +1,24 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\JsonApiController; + +class AdmissionRulesIndex extends JsonApiController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + public function __invoke(Request $request, Response $response, $args) + { + $rules = []; + foreach (array_keys(\AdmissionRule::getAvailableAdmissionRules()) as $class) { + $rules[] = new $class(); + } + + return $this->getContentResponse($rules); + } + +} diff --git a/lib/classes/JsonApi/Routes/Admission/AdmissionRulesShow.php b/lib/classes/JsonApi/Routes/Admission/AdmissionRulesShow.php new file mode 100644 index 0000000000000000000000000000000000000000..a0eb8fe55711d0010598655341b0141f90d80cd3 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/AdmissionRulesShow.php @@ -0,0 +1,33 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; + +/** + * Shows a single admission rule. + */ +class AdmissionRulesShow extends JsonApiController +{ + protected $allowedIncludePaths = []; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $chunks = explode('_', $args['id']); + $classname = $chunks[0]; + $id = $chunks[1] ?? null; + + $rule = \AdmissionRule::getRule($classname, $id); + if (!$rule) { + throw new RecordNotFoundException(); + } + + return $this->getContentResponse($rule); + } +} diff --git a/lib/classes/JsonApi/Routes/Admission/AdmissionRulesUpdate.php b/lib/classes/JsonApi/Routes/Admission/AdmissionRulesUpdate.php new file mode 100644 index 0000000000000000000000000000000000000000..5e4fefd86d2d60c3e294d0bf190c24156c18d216 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/AdmissionRulesUpdate.php @@ -0,0 +1,60 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use JsonApi\Errors\AuthorizationFailedException; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; + +/** + * Update an admission rule. + */ +class AdmissionRulesUpdate extends JsonApiController +{ + use ValidationTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $json = $this->validate($request); + $user = $this->getUser($request); + + if (!Authority::canEditAdmissionRules($user)) { + throw new AuthorizationFailedException(); + } + + [$type, $id] = explode('_', $args['id']); + + $rule = \AdmissionRule::getRule($type, $id); + if (!$rule) { + throw new RecordNotFoundException(); + } + + $payload = self::arrayGet($json, 'data.attributes.payload'); + + $rule->setAllData($payload); + + $rule->store(); + + return $this->getContentResponse($rule); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (!self::arrayHas($json, 'data.attributes')) { + return 'Missing `attributes` member of data block.'; + } + if (!self::arrayHas($json, 'data.attributes.payload')) { + return 'Missing `payload` member of attributes block.'; + } + } + +} diff --git a/lib/classes/JsonApi/Routes/Admission/Authority.php b/lib/classes/JsonApi/Routes/Admission/Authority.php new file mode 100644 index 0000000000000000000000000000000000000000..64df1d93b9d78e152f484ddf81552eed3ca1f1ca --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/Authority.php @@ -0,0 +1,75 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use Config; +use CourseSet; +use Institute; +use User; + +class Authority +{ + + /** + * Checks if the given user may create a courseset. As this is provided as "quick action" inside of courses, + * dozent permissions are sufficient. + * @param User $user + * @return bool + */ + public static function canCreateCourseSet(User $user): bool + { + return $GLOBALS['perm']->have_perm('dozent'); + } + + public static function canCreateCourseSets(User $user): bool + { + return $GLOBALS['perm']->have_perm('admin', $user->id) + || ( + Config::get()->ALLOW_DOZENT_COURSESET_ADMIN + && $GLOBALS['perm']->have_perm('dozent', $user->id) + ); + } + + public static function canEditAdmissionRules(User $user): bool + { + return $GLOBALS['perm']->have_perm('admin', $user->id) + || ( + Config::get()->ALLOW_DOZENT_COURSESET_ADMIN + && $GLOBALS['perm']->have_perm('dozent', $user->id) + ); + } + + /** + * Checks if the given user may update the given courseset. + * + * @param User $user + * @param CourseSet $courseset + * @return bool + */ + public static function canUpdateCourseSet(User $user, CourseSet $courseset) + { + if ($GLOBALS['perm']->have_perm('root') || $courseset->getUserId() === $user->id) { + return true; + } else { + $institutes = array_map( + fn ($i) => $i['Institut_id'], + Institute::getMyInstitutes($user->id) + ); + + // Check access for admin accounts. + $access = $GLOBALS['perm']->have_perm('admin') + && array_intersect($courseset->getInstituteIds(), $institutes); + + if (!$access) { + + // Check access for lecturers if the config option is set. + $access = Config::get()->ALLOW_DOZENT_COURSESET_ADMIN + && $GLOBALS['perm']->have_perm('dozent') + && array_intersect($courseset->getInstituteIds(), $institutes); + } + + return $access; + } + } + +} diff --git a/lib/classes/JsonApi/Routes/Admission/AvailableCoursesIndex.php b/lib/classes/JsonApi/Routes/Admission/AvailableCoursesIndex.php new file mode 100644 index 0000000000000000000000000000000000000000..cb7a6456d80922c410c807e6a0aed0f7a277e428 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/AvailableCoursesIndex.php @@ -0,0 +1,40 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; + +/** + * Zeigt alle Veranstaltungen an, die keinem Anmeldeset zugeordnet sind. + */ +class AvailableCoursesIndex extends JsonApiController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $body = $request->getParsedBody(); + + $semester = \Semester::find($body['semester']); + + if (!$semester) { + throw new RecordNotFoundException(); + } + + $courses = \CoursesetModel::getInstCourses( + $body['institutes'], + $body['courseset'], + $body['exclude'], + $semester->id, + $body['filter'] + ); + + $courses = count($courses) > 0 ? \Course::findMany(array_keys($courses)) : []; + + return $this->getContentResponse($courses); + } +} diff --git a/lib/classes/JsonApi/Routes/Admission/CourseSetsCreate.php b/lib/classes/JsonApi/Routes/Admission/CourseSetsCreate.php new file mode 100644 index 0000000000000000000000000000000000000000..a6c46deaa2426bd009af2a8887379fbb9a73dac1 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/CourseSetsCreate.php @@ -0,0 +1,68 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use JsonApi\Errors\AuthorizationFailedException; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; + +/** + * Create a new courseset. + */ +class CourseSetsCreate extends JsonApiController +{ + use ValidationTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + if (!Authority::canCreateCourseSet($this->getUser($request))) { + throw new AuthorizationFailedException(); + } + + $json = $this->validate($request); + $user = $this->getUser($request); + + if (!Authority::canCreateCourseSets($user)) { + throw new AuthorizationFailedException(); + } + + $cs = new \CourseSet(); + $cs->setName(self::arrayGet($json, 'data.attributes.name')); + + foreach (self::arrayGet($json, 'data.attributes.rules') as $oneRule) { + $classname = '\\' . $oneRule['attributes']['type']; + $rule = new $classname(); + $rule->setAllData($oneRule['attributes']['payload']); + $cs->addAdmissionRule($rule); + } + + $cs->setPrivate(self::arrayGet($json, 'data.attributes.private')); + $cs->setAlgorithm('RandomAlgorithm'); + $cs->setInstitutes(self::arrayGet($json, 'data.attributes.institutes')); + $cs->setCourses(self::arrayGet($json, 'data.attributes.courses')); + $cs->setUserlists(self::arrayGet($json, 'data.attributes.userlists')); + + $cs->store(); + + return $this->getCreatedResponse($cs); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (!self::arrayHas($json, 'data.attributes')) { + return 'Missing `attributes` member of data block.'; + } + if (!self::arrayHas($json, 'data.attributes.name')) { + return 'Missing `name` member of data block.'; + } + } + +} diff --git a/lib/classes/JsonApi/Routes/Admission/CourseSetsDelete.php b/lib/classes/JsonApi/Routes/Admission/CourseSetsDelete.php new file mode 100644 index 0000000000000000000000000000000000000000..5d241d037f7befbbad913c4a2183bcd964e7e824 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/CourseSetsDelete.php @@ -0,0 +1,37 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use JsonApi\Errors\AuthorizationFailedException; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; + +/** + * Deletes a courseset + */ +class CourseSetsDelete extends JsonApiController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $user = $this->getUser($request); + + $cs = new \CourseSet($args['id']); + if (!$cs->getChdate()) { + throw new RecordNotFoundException(); + } + + if (!Authority::canUpdateCourseSet($user, $cs)) { + throw new AuthorizationFailedException(); + } + + $cs->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Admission/CourseSetsShow.php b/lib/classes/JsonApi/Routes/Admission/CourseSetsShow.php new file mode 100644 index 0000000000000000000000000000000000000000..035a406674545ba48c3a5b95b52b9d5b5c56f933 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/CourseSetsShow.php @@ -0,0 +1,35 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; + +/** + * Shows a single courseset. + */ +class CourseSetsShow extends JsonApiController +{ + protected $allowedIncludePaths = [ + 'admission-rules', + 'institutes', + 'courses', + 'semester', + 'owner' + ]; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $courseset = new \CourseSet($args['id']); + if (!$courseset) { + throw new RecordNotFoundException(); + } + + return $this->getContentResponse($courseset); + } +} diff --git a/lib/classes/JsonApi/Routes/Admission/CourseSetsUpdate.php b/lib/classes/JsonApi/Routes/Admission/CourseSetsUpdate.php new file mode 100644 index 0000000000000000000000000000000000000000..0b26f6c2b19f174f06530846dd6fbc2fdc61b8ef --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/CourseSetsUpdate.php @@ -0,0 +1,73 @@ +<?php + +namespace JsonApi\Routes\Admission; + +use JsonApi\Errors\AuthorizationFailedException; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; + +/** + * Updates an existing courseset. + */ +class CourseSetsUpdate extends JsonApiController +{ + use ValidationTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $cs = new \CourseSet($args['id']); + + if (!$cs->getChdate()) { + throw new RecordNotFoundException(); + } + + if (!Authority::canUpdateCourseSet($this->getUser($request), $cs)) { + throw new AuthorizationFailedException(); + } + + $json = $this->validate($request); + + $cs->setName(self::arrayGet($json, 'data.attributes.name')); + $cs->clearAdmissionRules(); + + foreach (self::arrayGet($json, 'data.attributes.rules') as $oneRule) { + [$classname, $id] = explode('_', $oneRule['id']); + $classname = '\\' . $classname; + + $rule = new $classname($id); + $rule->setAllData($oneRule['attributes']['payload']); + $cs->addAdmissionRule($rule); + } + + $cs->setPrivate(self::arrayGet($json, 'data.attributes.private')); + $cs->setInfoText(self::arrayGet($json, 'data.attributes.infotext')); + $cs->setAlgorithm('RandomAlgorithm'); + $cs->setInstitutes(self::arrayGet($json, 'data.attributes.institutes')); + $cs->setCourses(self::arrayGet($json, 'data.attributes.courses')); + $cs->setUserlists(self::arrayGet($json, 'data.attributes.userlists')); + + $cs->store(); + + return $this->getCreatedResponse($cs); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (!self::arrayHas($json, 'data.attributes')) { + return 'Missing `attributes` member of data block.'; + } + if (!self::arrayHas($json, 'data.attributes.name')) { + return 'Missing `name` member of data block.'; + } + } + +} diff --git a/lib/classes/JsonApi/Routes/Admission/RuleCompatibilityIndex.php b/lib/classes/JsonApi/Routes/Admission/RuleCompatibilityIndex.php new file mode 100644 index 0000000000000000000000000000000000000000..d1d9f334dd804c992bda6536d1d23673a4385040 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Admission/RuleCompatibilityIndex.php @@ -0,0 +1,20 @@ +<?php +namespace JsonApi\Routes\Admission; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\NonJsonApiController; + +class RuleCompatibilityIndex extends NonJsonApiController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + public function __invoke(Request $request, Response $response, $args) + { + $response->getBody()->write(json_encode(\AdmissionRuleCompatibility::getCompatibilityMatrix())); + + return $response->withHeader('Content-type', 'application/json'); + } + +} diff --git a/lib/classes/JsonApi/Routes/UserFilters/Authority.php b/lib/classes/JsonApi/Routes/UserFilters/Authority.php new file mode 100644 index 0000000000000000000000000000000000000000..d934894bd5e502d0c1aef8775d71e88d8b2adaf4 --- /dev/null +++ b/lib/classes/JsonApi/Routes/UserFilters/Authority.php @@ -0,0 +1,18 @@ +<?php + +namespace JsonApi\Routes\UserFilters; + +use Config; +use User; + +class Authority +{ + public static function canEditUserFilters(User $user): bool + { + return $GLOBALS['perm']->have_perm('admin', $user->id) + || ( + Config::get()->ALLOW_DOZENT_COURSESET_ADMIN + && $GLOBALS['perm']->have_perm('dozent', $user->id) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsIndex.php b/lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsIndex.php new file mode 100644 index 0000000000000000000000000000000000000000..ede43cf4397fb4a80d7f04dd575f9e039137828d --- /dev/null +++ b/lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsIndex.php @@ -0,0 +1,30 @@ +<?php + +namespace JsonApi\Routes\UserFilters; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\JsonApiController; + +class UserFilterFieldsIndex extends JsonApiController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + public function __invoke(Request $request, Response $response, $args) + { + $fields = []; + foreach (\UserFilterField::getAvailableFilterFields() as $class => $name) { + // Generic datafield conditions must be handled differently. + if (str_contains($class, '_')) { + [$classname, $typeparam] = explode('_', $class); + $fields[] = new $classname($typeparam); + } else { + $fields[] = new $class(); + } + } + + return $this->getContentResponse($fields); + } + +} diff --git a/lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsShow.php b/lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsShow.php new file mode 100644 index 0000000000000000000000000000000000000000..94afbe30d6eb87d3fdad5b70c56cab2f87083d77 --- /dev/null +++ b/lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsShow.php @@ -0,0 +1,35 @@ +<?php + +namespace JsonApi\Routes\UserFilters; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; + +/** + * Shows a single UserFilterField. + */ +class UserFilterFieldsShow extends JsonApiController +{ + protected $allowedIncludePaths = ['users']; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + [$class, $id] = explode('_', $args['id']); + + $classname = '\\' . $class; + + $field = new $classname($id); + + // The userfilter object has a new ID -> new object not yet existing in database. + if ($field->getId() !== $id) { + throw new RecordNotFoundException(); + } + + return $this->getContentResponse($field); + } +} diff --git a/lib/classes/JsonApi/Routes/UserFilters/UserFiltersCreate.php b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersCreate.php new file mode 100644 index 0000000000000000000000000000000000000000..42cd58363ba177bc11e0db99bbfa91572ed663a5 --- /dev/null +++ b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersCreate.php @@ -0,0 +1,61 @@ +<?php + +namespace JsonApi\Routes\UserFilters; + +use JsonApi\Errors\AuthorizationFailedException; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; + +/** + * Create a new UserFilter. + */ +class UserFiltersCreate extends JsonApiController +{ + use ValidationTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $json = $this->validate($request); + $user = $this->getUser($request); + + if (!Authority::canEditUserFilters($user)) { + throw new AuthorizationFailedException(); + } + + $filter = new \UserFilter(); + $filter->show_user_count = true; + + foreach (self::arrayGet($json, 'data.attributes.filters') as $one) { + $classname = '\\' . $one['attributes']['type']; + $field = !empty($one['attributes']['typeparam']) + ? new $classname($one['attributes']['typeparam']) + : new $classname(); + $field->setValue($one['attributes']['value']); + $field->setCompareOperator($one['attributes']['compare-operator']); + $filter->addField($field); + } + + $filter->id = ''; + + return $this->getCreatedResponse($filter); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (!self::arrayHas($json, 'data.attributes')) { + return 'Missing `attributes` member of data block.'; + } + if (!self::arrayHas($json, 'data.attributes.filters')) { + return 'Missing `filters` member of attributes block.'; + } + } + +} diff --git a/lib/classes/JsonApi/Routes/UserFilters/UserFiltersDelete.php b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersDelete.php new file mode 100644 index 0000000000000000000000000000000000000000..6f2b0cb3451447b04769a9918e3f0e0d20ca2efb --- /dev/null +++ b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersDelete.php @@ -0,0 +1,38 @@ +<?php + +namespace JsonApi\Routes\UserFilters; + +use JsonApi\Errors\AuthorizationFailedException; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; + +/** + * Deletes a user filter + */ +class UserFiltersDelete extends JsonApiController +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $user = $this->getUser($request); + + if (!Authority::canEditUserFilters($user)) { + throw new AuthorizationFailedException(); + } + + $filter = new \UserFilter($args['id']); + + if ($filter['id'] !== $args['id']) { + throw new RecordNotFoundException(); + } + + $filter->delete(); + + return $this->getCodeResponse(204); + } + +} diff --git a/lib/classes/JsonApi/Routes/UserFilters/UserFiltersShow.php b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersShow.php new file mode 100644 index 0000000000000000000000000000000000000000..f02a1c6308907c643bfbbae764fec97290f79120 --- /dev/null +++ b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersShow.php @@ -0,0 +1,31 @@ +<?php + +namespace JsonApi\Routes\UserFilters; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; + +/** + * Shows a single UserFilter. + */ +class UserFiltersShow extends JsonApiController +{ + protected $allowedIncludePaths = ['user-filter-fields']; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $userfilter = new \UserFilter($args['id']); + + // The userfilter object has a new ID -> new object not yet existing in database. + if ($userfilter->getId() !== $args['id']) { + throw new RecordNotFoundException(); + } + + return $this->getContentResponse($userfilter); + } +} diff --git a/lib/classes/JsonApi/Routes/UserFilters/UserFiltersUpdate.php b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersUpdate.php new file mode 100644 index 0000000000000000000000000000000000000000..309da9b9646a0624bfb069ddeb034e067092e57f --- /dev/null +++ b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersUpdate.php @@ -0,0 +1,68 @@ +<?php + +namespace JsonApi\Routes\UserFilters; + +use JsonApi\Errors\AuthorizationFailedException; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use JsonApi\Errors\RecordNotFoundException; +use JsonApi\JsonApiController; +use JsonApi\Routes\ValidationTrait; + +/** + * Updates an existing UserFilter. + */ +class UserFiltersUpdate extends JsonApiController +{ + use ValidationTrait; + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(Request $request, Response $response, $args) + { + $user = $this->getUser($request); + + if (!Authority::canEditUserFilters($user)) { + throw new AuthorizationFailedException(); + } + + $filter = new \UserFilter($args['id']); + + if ($filter['id'] !== $args['id']) { + throw new RecordNotFoundException(); + } + + $json = $this->validate($request); + + $fields = $filter->getFields(); + + foreach (self::arrayGet($json, 'data.attributes.filters') as $one) { + $classname = '\\' . $one['attributes']['type']; + $field = !empty($one['attributes']['typeparam']) + ? new $classname($one['attributes']['typeparam']) + : new $classname(); + $field->setValue($one['attributes']['value']); + $field->setCompareOperator($one['attributes']['compare-operator']); + $filter->addField($field); + } + + $filter->id = ''; + + return $this->getCreatedResponse($filter); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (!self::arrayHas($json, 'data.attributes')) { + return 'Missing `attributes` member of data block.'; + } + if (!self::arrayHas($json, 'data.attributes.filters')) { + return 'Missing `filters` member of attributes block.'; + } + } + +} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index a5c1213e689e3ba30d5f4e8c5655e14b0980118e..44bfd04dc5c511ca808ba7038b267213123f24f3 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -18,6 +18,8 @@ class SchemaMap \Avatar::class => Schemas\Avatar::class, + \AdmissionRule::class => Schemas\AdmissionRule::class, + \BlubberComment::class => Schemas\BlubberComment::class, \BlubberStatusgruppeThread::class => Schemas\BlubberStatusgruppeThread::class, \BlubberThread::class => Schemas\BlubberThread::class, @@ -29,6 +31,7 @@ class SchemaMap \ConsultationBooking::class => Schemas\ConsultationBooking::class, \ConsultationSlot::class => Schemas\ConsultationSlot::class, \ConfigValue::class => Schemas\ConfigValue::class, + \CourseSet::class => Schemas\CourseSet::class, \ContentTermsOfUse::class => Schemas\ContentTermsOfUse::class, \Course::class => Schemas\Course::class, \CourseMember::class => Schemas\CourseMember::class, @@ -59,6 +62,8 @@ class SchemaMap \File::class => Schemas\File::class, \FileRef::class => Schemas\FileRef::class, \FolderType::class => Schemas\Folder::class, + \UserFilter::class => Schemas\UserFilter::class, + \UserFilterField::class => Schemas\UserFilterField::class, \Courseware\Block::class => Schemas\Courseware\Block::class, \Courseware\BlockComment::class => Schemas\Courseware\BlockComment::class, diff --git a/lib/classes/JsonApi/Schemas/AdmissionRule.php b/lib/classes/JsonApi/Schemas/AdmissionRule.php new file mode 100644 index 0000000000000000000000000000000000000000..ccb0721378215fa2a850ab2d38e1f2f94a0d1add --- /dev/null +++ b/lib/classes/JsonApi/Schemas/AdmissionRule.php @@ -0,0 +1,34 @@ +<?php + +namespace JsonApi\Schemas; + +use Neomerx\JsonApi\Contracts\Schema\ContextInterface; + +class AdmissionRule extends SchemaProvider +{ + const TYPE = 'admission-rules'; + + public function getId($resource): ?string + { + return \get_class($resource) . '_' . $resource->getId(); + } + + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'type' => \get_class($resource), + 'name' => $resource->getName(), + 'description' => $resource->getDescription(), + 'payload' => $resource->getPayload(), + 'ruletext' => $resource->toString() + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + return []; + } +} diff --git a/lib/classes/JsonApi/Schemas/CourseSet.php b/lib/classes/JsonApi/Schemas/CourseSet.php new file mode 100644 index 0000000000000000000000000000000000000000..e37ad4604a2ecb81c4a22b99c8309881d66fcbba --- /dev/null +++ b/lib/classes/JsonApi/Schemas/CourseSet.php @@ -0,0 +1,166 @@ +<?php + +namespace JsonApi\Schemas; + +use Neomerx\JsonApi\Contracts\Schema\ContextInterface; +use Neomerx\JsonApi\Schema\Link; + +class CourseSet extends SchemaProvider +{ + const TYPE = 'course-sets'; + + const REL_RULES = 'admission-rules'; + const REL_INSTITUTES = 'institutes'; + const REL_SEMESTER = 'semester'; + const REL_COURSES = 'courses'; + const REL_OWNER = 'owner'; + + public function getId($courseset): ?string + { + return $courseset->getId(); + } + + public function getAttributes($courseset, ContextInterface $context): iterable + { + return [ + 'name' => $courseset->getName(), + 'infotext' => $courseset->getInfoText(), + 'private' => (bool) $courseset->getPrivate(), + 'algorithm' => $courseset->getAlgorithm(), + 'algorithm-run' => (bool) $courseset->hasAlgorithmRun(), + 'num-applicants' => (int) $courseset->getNumApplicants(), + 'userlists' => (array) $courseset->getUserLists(), + 'chdate' => date('c', $courseset->getChdate()), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $relationships = []; + + $relationships = $this->addOwnerRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_OWNER) + ); + + $relationships = $this->addInstitutesRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_INSTITUTES) + ); + + $relationships = $this->addCoursesRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_COURSES) + ); + + $relationships = $this->addRulesRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_RULES) + ); + + $relationships = $this->addSemesterRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_SEMESTER) + ); + + return $relationships; + } + + private function addRulesRelationship( + array $relationships, + $resource, + $includeData + ) { + $relation = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_RULES), + ] + ]; + if ($includeData) { + $related = $resource->getAdmissionRules(); + $relation[self::RELATIONSHIP_DATA] = $related; + } + + return array_merge($relationships, [self::REL_RULES => $relation]); + } + + private function addOwnerRelationship( + array $relationships, + $resource, + $includeData + ) { + $relation = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_OWNER), + ] + ]; + if ($includeData) { + $related = $resource->getUserId() ? \User::find($resource->getUserId()) : null; + $relation[self::RELATIONSHIP_DATA] = $related; + } + + return array_merge($relationships, [self::REL_OWNER => $relation]); + } + + private function addInstitutesRelationship( + array $relationships, + $resource, + $includeData + ) { + $relation = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_INSTITUTES), + ] + ]; + if ($includeData) { + $related = $resource->getInstituteIds() ? \Institute::findMany(array_keys($resource->getInstituteIds())) : []; + $relation[self::RELATIONSHIP_DATA] = $related; + } + + return array_merge($relationships, [self::REL_INSTITUTES => $relation]); + } + + private function addCoursesRelationship( + array $relationships, + $resource, + $includeData + ) { + $relation = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_COURSES), + ] + ]; + if ($includeData) { + $related = \Course::findMany($resource->getCourses()); + $relation[self::RELATIONSHIP_DATA] = $related; + } + + return array_merge($relationships, [self::REL_COURSES => $relation]); + } + + private function addSemesterRelationship( + array $relationships, + $resource, + $includeData + ) { + $relation = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_SEMESTER), + ] + ]; + if ($includeData) { + $related = $resource->getSemester() ? \Semester::find($resource->getSemester()) : null; + $relation[self::RELATIONSHIP_DATA] = $related; + } + + return array_merge($relationships, [self::REL_SEMESTER => $relation]); + } +} diff --git a/lib/classes/JsonApi/Schemas/UserFilter.php b/lib/classes/JsonApi/Schemas/UserFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..7e390cfb341db4b7e2b5b0eae08cd08041f943d5 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/UserFilter.php @@ -0,0 +1,45 @@ +<?php + +namespace JsonApi\Schemas; + +use Neomerx\JsonApi\Contracts\Schema\ContextInterface; + +class UserFilter extends SchemaProvider +{ + const TYPE = 'user-filters'; + + public function getId($userfilter): ?string + { + return $userfilter->getId(); + } + + public function getAttributes($resource, ContextInterface $context): iterable + { + $fields = array_map( + fn($field) => [ + 'attributes' => [ + 'type' => get_class($field), + 'typeparam' => property_exists($field, 'datafield_id') ? $field->datafield_id : null, + 'id' => $field->getId(), + 'compare-operator' => $field->getCompareOperator(), + 'value' => $field->getValue(), + ] + ], + $resource->getFields() + ); + + $resource->show_user_count = true; + return [ + 'text' => $resource->toString(), + 'fields' => $fields + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + return []; + } +} diff --git a/lib/classes/JsonApi/Schemas/UserFilterField.php b/lib/classes/JsonApi/Schemas/UserFilterField.php new file mode 100644 index 0000000000000000000000000000000000000000..82b440cae699d2bf64a032a2b85f1b2fbc868500 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/UserFilterField.php @@ -0,0 +1,67 @@ +<?php + +namespace JsonApi\Schemas; + +use Neomerx\JsonApi\Contracts\Schema\ContextInterface; +use Neomerx\JsonApi\Schema\Link; + +class UserFilterField extends SchemaProvider +{ + const TYPE = 'user-filter-fields'; + + const REL_USERS = 'users'; + + public function getId($resource): ?string + { + return get_class($resource) . '_' . $resource->getId(); + } + + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'type' => get_class($resource), + 'typeparam' => property_exists($resource, 'datafield_id') ? $resource->datafield_id : null, + 'id' => $resource->getId(), + 'compare-operator' => $resource->getCompareOperator(), + 'compare-operator-text' => $resource->getCompareOperatorAsText(), + 'name' => $resource->getName(), + 'valid-compare-operators' => $resource->getValidCompareOperators(), + 'valid-values' => $resource->getValidValues(), + 'value' => $resource->getValue() + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $relationships = []; + + $relationships = $this->addUsersRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_USERS) + ); + + return $relationships; + } + + private function addUsersRelationship( + array $relationships, + $resource, + $includeData + ) { + $relation = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_USERS), + ] + ]; + if ($includeData) { + $related = $resource->getUsers(); + $relation[self::RELATIONSHIP_DATA] = $related; + } + + return array_merge($relationships, [self::REL_USERS => $relation]); + } +} diff --git a/lib/classes/StudipAutoloader.php b/lib/classes/StudipAutoloader.php index 883adc725fc621953c078bb7ba9eee8f53bd6730..2579b2ce65c5f5db1a3e3da2c76119039089dfae 100644 --- a/lib/classes/StudipAutoloader.php +++ b/lib/classes/StudipAutoloader.php @@ -110,6 +110,19 @@ class StudipAutoloader self::$autoload_paths[] = compact('path', 'prefix'); } + /** + * Returns whether the autoloader has the given path and prefix already stored. + */ + public static function hasAutoloadPath(string $path, string $prefix = ''): bool + { + $path = self::sanitizePath($path); + if ($prefix) { + $prefix = rtrim($prefix, '\\') . '\\'; + } + + return collect(self::$autoload_paths)->contains(compact('path', 'prefix')); + } + /** * Removes a path from the list of paths. * diff --git a/lib/classes/admission/AdmissionRule.php b/lib/classes/admission/AdmissionRule.php index 2b826e49ac44a6baab46d1b5e46a1a7e06590329..cff3e356e476959c0b686987ca0833ddf10c7274 100644 --- a/lib/classes/admission/AdmissionRule.php +++ b/lib/classes/admission/AdmissionRule.php @@ -17,6 +17,82 @@ abstract class AdmissionRule { + private static ?array $rules = null; + + /** + * Reads all available AdmissionRule subclasses and loads their definitions. + * + * @param bool $activeOnly Show only active rules. + */ + public static function getAvailableAdmissionRules(bool $activeOnly = true): array + { + if (self::$rules === null) { + self::$rules = []; + + $query = "SELECT * + FROM `admissionrules` + ORDER BY `id`"; + DBManager::get()->fetchAll( + $query, + [], + function ($row) { + /** @var class-string<static> $className */ + $className = $row['ruletype']; + + $autoloadPath = $GLOBALS['STUDIP_BASE_PATH'] . DIRECTORY_SEPARATOR . $row['path']; + if ( + !class_exists($className) + && is_dir($autoloadPath) + && !StudipAutoloader::hasAutoloadPath($autoloadPath) + ) { + StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'] . DIRECTORY_SEPARATOR . $row['path']); + } + + try { + $rule = new $className(); + self::$rules[$className] = [ + 'id' => $row['id'], + 'name' => $className::getName(), + 'description' => $className::getDescription(), + 'active' => (bool) $row['active'], + ]; + } catch (Exception $e) { + } + } + ); + } + + if ($activeOnly) { + return array_filter( + self::$rules, + fn($rule) => $rule['active'] + ); + } + + return self::$rules; + } + + /** + * @param class-string<static> $name + */ + public static function getRule(string $name, string $id = null): ?AdmissionRule + { + $rules = self::getAvailableAdmissionRules(); + if (!array_key_exists($name, $rules)) { + throw new InvalidArgumentException("Rule '$name' does not exist."); + } + + if (func_num_args() === 1 || $id === null) { + return new $name(); + } + + $rule = new $name($id); + if ($rule->getId() !== $id) { + return null; + } + return $rule; + } + // --- ATTRIBUTES --- /** @@ -62,6 +138,17 @@ abstract class AdmissionRule */ public $siblings_override = false; + /** + * Is the admission rule template written in PHP or is it a VueJS component? + * Valid values are 'php' and 'vue'. + */ + public string $type = 'php'; + + /** + * If the template is a VueJS component, give the path here. + */ + public ?string $component = null; + // --- OPERATIONS --- public function __construct($ruleId = '', $courseSetId = '') @@ -138,37 +225,6 @@ abstract class AdmissionRule return []; } - /** - * Reads all available AdmissionRule subclasses and loads their definitions. - * - * @param bool $activeOnly Show only active rules. - * @return Array - */ - public static function getAvailableAdmissionRules($activeOnly = true) - { - $rules = []; - $where = ($activeOnly ? " WHERE `active`=1" : ""); - $data = DBManager::get()->query("SELECT * FROM `admissionrules`".$where. - " ORDER BY `id` ASC"); - while ($current = $data->fetch(PDO::FETCH_ASSOC)) { - $className = $current['ruletype']; - if (is_dir($GLOBALS['STUDIP_BASE_PATH'] . DIRECTORY_SEPARATOR . $current['path'])) { - StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'] . DIRECTORY_SEPARATOR . $current['path']); - try { - $rule = new $className(); - $rules[$className] = [ - 'id' => $current['id'], - 'name' => $className::getName(), - 'description' => $className::getDescription(), - 'active' => $current['active'] - ]; - } catch (Exception $e) { - } - } - } - return $rules; - } - /** * Get end of validity. * @@ -448,9 +504,22 @@ abstract class AdmissionRule return AdmissionRuleCompatibility::exists([get_class($this), $admission_rule]); } + /** + * Get fields and settings defining this admission rule as array. + */ + public function getPayload(): array + { + return [ + 'start-time' => $this->startTime, + 'end-time' => $this->endTime, + 'message' => $this->message ?? $this->default_message + ]; + } + public function __clone() { $this->id = md5(uniqid(get_class($this))); $this->courseSetId = null; } + } diff --git a/lib/classes/admission/AdmissionUserList.php b/lib/classes/admission/AdmissionUserList.php index 3e5a43099fb081062eabb9affc004e88d2311491..654aa5708dc5ca67c1f1105d1fcb51dcf5a5eba9 100644 --- a/lib/classes/admission/AdmissionUserList.php +++ b/lib/classes/admission/AdmissionUserList.php @@ -200,6 +200,17 @@ class AdmissionUserList return array_values($result); } + /** + * Just counts the number of users and returns the value. + */ + public function getUserCount(): int + { + return (int) DBManager::get()->fetchColumn( + "SELECT COUNT(DISTINCT `user_id`) FROM `user_factorlist` WHERE `list_id` = :id", + ['id' => $this->getId()] + ); + } + /** * Helper function for loading data from DB. */ @@ -207,7 +218,7 @@ class AdmissionUserList { // Load basic data. $stmt = DBManager::get()->prepare("SELECT `list_id`, `name`, - CAST(`factor` AS UNSIGNED) AS factor, `owner_id`, `mkdate`, `chdate` + CAST(`factor` AS UNSIGNED) AS factor, `owner_id`, `mkdate`, `chdate` FROM `admissionfactor` WHERE `list_id`=? LIMIT 1"); $stmt->execute([$this->id]); if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { diff --git a/lib/classes/admission/UserFilter.php b/lib/classes/admission/UserFilter.php index 692422f53574fd241b4814d93bae9c72de4f055e..fd160d681364b69d57226616d644ab11ba6e49c4 100644 --- a/lib/classes/admission/UserFilter.php +++ b/lib/classes/admission/UserFilter.php @@ -32,6 +32,7 @@ class UserFilter public $id = ''; public $show_user_count = false; + // --- OPERATIONS --- /** diff --git a/lib/classes/admission/UserFilterField.php b/lib/classes/admission/UserFilterField.php index 4b5132207cf4df1ba7b7ba3fd4278035557f3dda..2a3480700d4f1f4965a290c3d0887ff7a72463f4 100644 --- a/lib/classes/admission/UserFilterField.php +++ b/lib/classes/admission/UserFilterField.php @@ -195,23 +195,29 @@ class UserFilterField public static function getAvailableFilterFields() { if (self::$available_filter_fields === null) { - $fields = []; - // Load all PHP class files found in the condition field folder. - foreach (glob(realpath(dirname(__FILE__).'/userfilter').'/*.php') as $file) { - require_once($file); - // Try to auto-calculate class name from file name. - $className = mb_substr(basename($file), 0, mb_strpos(basename($file), '.php')); - // Check if class is right. - if (is_subclass_of($className, 'UserFilterField')) { - if ($className::$isParameterized) { - $fields = array_merge($fields, $className::getParameterizedTypes()); + $fields = []; + $i = new FileSystemIterator( + $GLOBALS['STUDIP_BASE_PATH'] . '/lib/classes/admission/userfilter', + FileSystemIterator::SKIP_DOTS + ); + + foreach ($i as $class) { + require_once $class; + } + + $classes = array_filter( + get_declared_classes(), + fn($c) => is_subclass_of($c, UserFilterField::class) + ); + foreach ($classes as $class) { + if ($class::$isParameterized) { + $fields = array_merge($fields, $class::getParameterizedTypes()); } else { - $filter = new $className(); - $fields[$className] = $filter->getName(); + $filter = new $class(); + $fields[$class] = $filter->getName(); } } - } - asort($fields); + asort($fields); self::$available_filter_fields = $fields; } return self::$available_filter_fields; @@ -235,7 +241,7 @@ class UserFilterField */ public function getCompareOperatorAsText() { - return $this->getValidCompareOperators()[$this->compareOperator]; + return $this->getValidCompareOperators()[$this->compareOperator] ?? ''; } /** diff --git a/resources/assets/javascripts/bootstrap/admission.js b/resources/assets/javascripts/bootstrap/admission.js index 4b13af13965c17dd294fce94dafb848b1376d5b5..68b818a74d2caf639173cdbb104a402c5593ee80 100644 --- a/resources/assets/javascripts/bootstrap/admission.js +++ b/resources/assets/javascripts/bootstrap/admission.js @@ -2,7 +2,36 @@ * Anmeldeverfahren und -sets * ------------------------------------------------------------------------ */ -STUDIP.domReady(function () { +STUDIP.ready(function () { + + /** + * Check for admission rules with Vue components + * @type {NodeListOf<Element>} + */ + const containers = document.querySelectorAll('[data-admission-rule]'); + + containers.forEach(container => { + + const ruleType = container.dataset.admissionRule; + + if (STUDIP.Admission.availableRules[ruleType] !== undefined) { + + import('@/vue/components/admission/' + STUDIP.Admission.availableRules[ruleType]) + .then(result => { + const components = {}; + components[ruleType] = result.default; + + STUDIP.Vue.load().then(({ createApp }) => { + createApp({ + el: container, + components: components + }); + }); + }); + + } + }); + $(document).on('change', 'tr.course input', function(i) { STUDIP.Admission.toggleNotSavedAlert(); }); diff --git a/resources/assets/javascripts/lib/admission.js b/resources/assets/javascripts/lib/admission.js index df62bbe830dc4c7c7b7ac80061bec5fdd64b871a..4b135118930d467452fbea9e3db376e99a999ce5 100644 --- a/resources/assets/javascripts/lib/admission.js +++ b/resources/assets/javascripts/lib/admission.js @@ -5,6 +5,22 @@ import { $gettext } from './gettext'; import Dialog from './dialog.js'; const Admission = { + + /** + * All registered rule types with their corresponding Vue components + */ + availableRules: { + ConditionalAdmission: 'ConditionalAdmission.vue', + CourseMemberAdmission: 'CourseMemberAdmission.vue', + LimitedAdmission: 'LimitedAdmission.vue', + LockedAdmission: 'LockedAdmission.vue', + ParticipantRestrictedAdmission: 'ParticipantRestrictedAdmission.vue', + PasswordAdmission: 'PasswordAdmission.vue', + PreferentialAdmission: 'PreferentialAdmission.vue', + TermsAdmission: 'TermsAdmission.vue', + TimedAdmission: 'TimedAdmission.vue' + }, + getCourses: function(targetUrl) { var courseFilter = $('input[name="course_filter"]').val(); if (courseFilter == '') { diff --git a/resources/assets/stylesheets/scss/admission.scss b/resources/assets/stylesheets/scss/admission.scss index 02badc55983144eb6943047ccfe50cf39da90add..d9d978c77d46fde6611bc04a41bd7de74cb2df37 100644 --- a/resources/assets/stylesheets/scss/admission.scss +++ b/resources/assets/stylesheets/scss/admission.scss @@ -34,6 +34,60 @@ } } +.institute-assignment, +.rule-assignment, +.course-assignment { + + span { + display: inline-block; + margin-left: 15px; + vertical-align: top; + } + + .edit-assignment, + .delete-assignment { + display: inline; + vertical-align: middle; + } +} + +form.default { + fieldset.select_terms_of_use { + + > label { + padding: 10px 7px; + } + + .admission-rule-incompatible { + border: 1px solid var(--content-color-40); + color: var(--content-color-80); + display: block; + margin: -1px 0; + padding: 10px 5px 10px 7px; + + div { + text-decoration: unset; + } + + img { + margin-right: 10px; + margin-top: -5px; + vertical-align: middle; + } + } + } + + .admission-condition { + .condition-description { + display: inline-block; + } + + img { + vertical-align: text-bottom; + } + } +} + #userlists { div { margin-bottom: 10px; @@ -50,3 +104,13 @@ } } } + +form { + fieldset { + section { + .search-button { + margin-top: 25px; + } + } + } +} diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index 681ade370f9cab8befe989afeefd9b5147e2f581..eb4895ba52978674f8eeabc9520622d5227ccc2a 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -14,7 +14,6 @@ @import "scss/activityfeed"; @import "scss/admin"; @import "scss/admin-courses"; -@import "scss/admission"; @import "scss/article"; @import "scss/ajax"; @import "scss/avatar"; @@ -107,6 +106,7 @@ @import "scss/user-administration"; @import "scss/wiki"; @import "scss/multi_person_search"; +@import "scss/admission"; // Class for DOM elements that should only be visible to Screen readers .sr-only { diff --git a/resources/vue/components/StudipUserFilter.vue b/resources/vue/components/StudipUserFilter.vue new file mode 100644 index 0000000000000000000000000000000000000000..f9e6741d1719e2bafb48e04f6ecdab9c32cf7d31 --- /dev/null +++ b/resources/vue/components/StudipUserFilter.vue @@ -0,0 +1,128 @@ +<template> + <studip-dialog :title="$gettext('Bedingung hinzufügen')" + height="600" + width="900" + :confirmText="$gettext('Übernehmen')" + confirmClass="button accept" + @confirm="submit" + :closeText="$gettext('Abbrechen')" + closeClass="button cancel" + @close="close"> + <template v-slot:dialogContent> + <section v-for="(element, index) in currentFilter" + :key="index"> + <p v-if="index >= 1"> + {{ $gettext('und') }} + </p> + <select v-if="availableFields.length > 0" + v-model="element.attributes.type" + @change="addFieldConfig(element.attributes.type, index)" + :aria-label="$gettext('Feldname')"> + <option v-for="(field, fIndex) in availableFields" + :key="fIndex" + :value="field.attributes.type"> + {{ field.attributes.name }} + </option> + </select> + <select v-if="hasMultipleCompareOps" + v-model="element.attributes['compare-operator']" + :aria-label="$gettext('Vergleichsoperator')"> + <option v-for="(name, op) in fieldConfig[element.attributes.type]?.compareOps" + :key="op" + :value="op"> + {{ name }} + </option> + </select> + <select v-if="hasMultipleValues" + v-model="element.attributes.value" + :aria-label="$gettext('Wert')"> + <option v-for="(name, value) in fieldConfig[element.attributes.type]?.values" + :key="value" + :value="value"> + {{ name }} + </option> + </select> + <studip-icon v-if="element.attributes.type && currentFilter.length > 1" + shape="trash" + role="button" + :title="$gettext('Dieses Feld löschen')" + @click="removeField(index)"></studip-icon> + </section> + <section> + <button class="button add" + @click.prevent="addField"> + {{ $gettext('Feld hinzufügen') }} + </button> + </section> + </template> + </studip-dialog> +</template> + +<script> +export default { + name: 'StudipUserFilter', + props: { + filter: { + type: Array, + default: () => [] + } + }, + data() { + return { + availableFields: [], + currentFilter: this.filter, + fieldConfig: {} + } + }, + methods: { + addFieldConfig(type, fieldIndex) { + if (type !== '') { + if (!this.fieldConfig[type]) { + for (let i = 0; i < this.availableFields.length; i++) { + if (this.availableFields[i].attributes.type === type) { + this.fieldConfig[type] = { + typeparam: this.availableFields[i].attributes['typeparam'], + compareOps: this.availableFields[i].attributes['valid-compare-operators'], + values: this.availableFields[i].attributes['valid-values'] + }; + } + } + } + this.currentFilter[fieldIndex].attributes.type = type; + this.currentFilter[fieldIndex].attributes.typeparam = this.fieldConfig[type].typeparam; + this.currentFilter[fieldIndex].attributes['compare-operator'] = Object.keys(this.fieldConfig[type].compareOps)[0]; + this.currentFilter[fieldIndex].attributes.value = Object.keys(this.fieldConfig[type].values)[0]; + } + }, + addField() { + this.currentFilter.push({attributes: { type: null, typeparam: null, 'compare-operator': '', value: '' }}); + this.addFieldConfig(this.availableFields[0].attributes.type, this.currentFilter.length - 1); + }, + removeField(index) { + this.currentFilter.splice(index, 1); + }, + submit() { + this.$emit('submit', this.currentFilter) + }, + close() { + this.$emit('close'); + }, + hasMultipleCompareOps(element) { + return element.attributes.type + && element.attributes.type !== '' + && Object.keys(this.fieldConfig[element.attributes.type]?.compareOps).length > 1; + }, + hasMultipleValues(element) { + return element.attributes.type + && element.attributes.type !== '' + && Object.keys(this.fieldConfig[element.attributes.type]?.values).length > 1; + } + }, + created() { + STUDIP.jsonapi.withPromises().get('user-filter-fields').then(response => { + this.availableFields = response.data; + this.addField(); + }); + } +} +</script> diff --git a/resources/vue/components/admission/AdmissionRuleConfig.vue b/resources/vue/components/admission/AdmissionRuleConfig.vue new file mode 100644 index 0000000000000000000000000000000000000000..b9691a832ba056196277af78556c85991eeea8a4 --- /dev/null +++ b/resources/vue/components/admission/AdmissionRuleConfig.vue @@ -0,0 +1,89 @@ +<template> + <studip-dialog v-if="component !== null" + :title="$gettext('Anmelderegel bearbeiten')" + :close-text="$gettext('Abbrechen')" + @close="cancel" + width="900" + height="600"> + <template v-slot:dialogContent> + <studip-message-box v-if="invalidData?.length" + type="error" + :details="invalidData" + :hide-close="true" + :hide-details="false" + :aria-description="errorText" + role="alert"> + {{ $gettext('Es sind ungültige Daten angegeben worden:') }} + </studip-message-box> + <component :is="component" v-bind="props" @submit="submit" @error="error"></component> + </template> + <template v-slot:dialogButtons> + <button type="button" + class="button accept" + @click="requireData"> + {{ $gettext('Übernehmen') }} + </button> + </template> + </studip-dialog> +</template> + +<script> +export default { + name: 'AdmissionRuleConfig', + props: { + type: { + type: String, + required: true + }, + rule: { + type: Object, + default: null + }, + assignedRuleTypes: { + type: Array, + default: () => [] + } + }, + data() { + return { + component: null, + theRule: this.rule, + props: null, + invalidData: null + } + }, + computed: { + errorText() { + return this.$gettext('Es sind ungültige Daten angegeben worden:') + this.invalidData?.join(','); + } + }, + methods: { + requireData() { + STUDIP.eventBus.emit('getRuleConfiguration'); + }, + cancel() { + this.component = null; + this.$emit('cancel'); + }, + submit(data) { + this.component = null; + this.$emit('submit', data); + }, + error(message) { + this.invalidData = message; + } + }, + created() { + const file = STUDIP.Admission.availableRules[this.type]; + let components = {}; + import(`@/vue/components/admission/${file}`).then((module) => { + this.component = module.default; + this.props = { + id: this.theRule?.id, + ruleData: this.theRule, + assignedRuleTypes: this.assignedRuleTypes + }; + }); + } +} +</script> diff --git a/resources/vue/components/admission/AdmissionRuleTypeSelector.vue b/resources/vue/components/admission/AdmissionRuleTypeSelector.vue new file mode 100644 index 0000000000000000000000000000000000000000..498497aa5a184bb712a015cfab7e796778bea808 --- /dev/null +++ b/resources/vue/components/admission/AdmissionRuleTypeSelector.vue @@ -0,0 +1,113 @@ +<template> + <studip-dialog @close="closeDialog" + width="900" + height="600" + :close-text="$gettext('Abbrechen')" + :title="$gettext('Anmelderegel konfigurieren')"> + <template v-slot:dialogContent> + <form v-if="!loading" + class="default"> + <fieldset class="select_terms_of_use"> + <legend> + {{ $gettext('Typ der Anmelderegel auswählen') }} + </legend> + <template v-for="type in ruleTypes"> + <input v-if="isAvailable(type.attributes.type)" + type="radio" + v-model="selectedType" + :value="type.attributes.type" + :id="'rule-type-' + type.attributes.type" + :key="type.attributes.type + '-input'"> + <label v-if="isAvailable(type.attributes.type)" + :for="'rule-type-' + type.attributes.type" + :key="type.attributes.type + '-label'"> + <studip-icon :shape="type.attributes.type === selectedType + ? 'radiobutton-checked' + : 'radiobutton-unchecked'" + :size="24" + ></studip-icon> + <div class="text"> + {{ type.attributes.name }} + </div> + </label> + <div v-if="isAvailable(type.attributes.type)" + class="terms_of_use_description" + :key="type.id + '-description'"> + {{ type.attributes.description }} + </div> + <div v-if="!isAvailable(type.attributes.type)" + :key="type.id + '-incompatible'" + class="admission-rule-incompatible"> + <studip-icon shape="remove-circle" + :size="24" + role="inactive" + ></studip-icon> + {{ type.attributes.name }} + ({{ $gettext('nicht mit bereits vorhandenen Regeln kompatibel') }}) + </div> + </template> + </fieldset> + </form> + <studip-progress-indicator v-if="loading" + :size="32" + :description="$gettext('Verfügbare Anmelderegeln werden geladen')" + ></studip-progress-indicator> + </template> + <template v-slot:dialogButtons> + <button type="button" + class="button" + @click.prevent="configureRule" + :disabled="selectedType === null"> + {{ $gettext('Ausgewählte Regel konfigurieren') }} + </button> + </template> + </studip-dialog> +</template> + +<script> +import StudipProgressIndicator from '../StudipProgressIndicator.vue'; +import StudipDialog from '../StudipDialog.vue'; + +export default { + name: 'AdmissionRuleTypeSelector', + components: { StudipProgressIndicator, StudipDialog }, + props: { + assignedRuleTypes: { + type: Array, + default: () => [] + } + }, + data() { + return { + loading: true, + ruleTypes: [], + selectedType: null, + compatibility: {} + } + }, + methods: { + closeDialog() { + this.$emit('close'); + }, + configureRule() { + this.$emit('configureRule', this.selectedType); + }, + isAvailable(ruleType) { + return this.assignedRuleTypes.every(t => this.compatibility[ruleType]?.includes(t)); + } + }, + created() { + Promise.all([ + STUDIP.jsonapi.withPromises().get('admission-rules'), + STUDIP.jsonapi.withPromises().get('admission/rule-compatibility') + ]).then(values => { + this.loading = false; + this.ruleTypes = values[0].data; + this.compatibility = values[1]; + this.ruleTypes.forEach(t => { + this.isAvailable(t.attributes.type); + }); + }); + } +} +</script> diff --git a/resources/vue/components/admission/ConditionalAdmission.vue b/resources/vue/components/admission/ConditionalAdmission.vue new file mode 100644 index 0000000000000000000000000000000000000000..426c6f24f935b3124d5718128e86c65a9fbd41ee --- /dev/null +++ b/resources/vue/components/admission/ConditionalAdmission.vue @@ -0,0 +1,194 @@ +<template> + <form class="default"> + <section> + <label> + {{ $gettext('Nachricht bei fehlgeschlagener Anmeldung') }} + <textarea name="message" rows="4" cols="50" v-model="messageText"></textarea> + </label> + </section> + <validity-time></validity-time> + <section> + <h3> + {{ $gettext('Anmeldebedingungen') }} + </h3> + <div v-if="ungrouped?.length > 0" + role="list"> + <div v-for="(filter, index) in ungrouped" + :key="index" + class="admission-condition" + role="listitem"> + <p v-if="ungrouped.length > 1 && index >= 1"> + {{ $gettext('oder') }} + </p> + <p v-if="!groupsAllowed" + class="condition-description" + v-html="filter.attributes.text"></p> + <label v-else class="undecorated"> + <input type="checkbox" + v-model="selectedFilters" + :value="filter.id"> + <span v-html="filter.attributes.text"></span> + </label> + <a @click.prevent="deleteFilter(index)" + :title="$gettext('Diese Bedingung löschen')"> + <studip-icon shape="trash"></studip-icon> + </a> + </div> + <button v-if="selectedFilters?.length > 0" + class="button" + @click.prevent="createContingent"> + {{ $gettext('Kontingent erstellen') }} + </button> + </div> + <div v-if="groups?.length > 0" + role="list"> + <div v-for="(group, index) in groups" + :key="index" + class="admission-contingent" + role="listitem"> + <div class="col-3"> + <label> + {{ $gettext('Kontingent in Prozent') }}: + <input type="number" + min="0" + max="100" + v-model="group.quota"> + </label> + <ul> + <li v-for="(filter, fIndex) in group.conditions" + :key="fIndex"> + <p v-html="filter.attributes.text"></p> + </li> + </ul> + </div> + <button type="button" + class="undecorated delete-contingent" + tabindex="0" + :title="$gettext('Kontingent auflösen')" + @click.prevent="deleteContingent(index)"> + <studip-icon shape="trash" + :size="20"></studip-icon> + </button> + </div> + </div> + <p v-if="ungrouped?.length + groups?.length === 0"> + {{ $gettext('Sie haben noch keine Bedingungen festgelegt.') }} + </p> + </section> + <section> + <button class="button add" + @click.prevent="editFilter"> + {{ $gettext('Bedingung hinzufügen') }} + </button> + </section> + <studip-user-filter v-if="showEditFilter" + @submit="confirmDialog" + @close="closeDialog"></studip-user-filter> + </form> +</template> + +<script> +import axios from 'axios'; +import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin'; +import ValidityTime from "./ValidityTime.vue"; +import StudipUserFilter from "../StudipUserFilter.vue"; + +export default { + name: 'ConditionalAdmission', + components: { StudipUserFilter, ValidityTime}, + mixins: [AdmissionRuleMixin], + data() { + return { + messageText: this.message || this.$gettext('Zur Anmeldung müssen diese Bedingungen erfüllt sein: %s'), + ungrouped: [], + groups: [], + showEditFilter: false, + selectedFilters: [] + } + }, + computed: { + groupsAllowed() { + return this.assignedRuleTypes.includes('ParticipantRestrictedAdmission') + }, + payload() { + return { + type: 'ConditionalAdmission', + payload: { + conditions: this.ungrouped, + 'grouped-conditions': this.groups, + 'conditiongroups-allowed': this.groupsAllowed, + message: this.message + } + } + } + }, + methods: { + editFilter() { + this.showEditFilter = true; + }, + deleteFilter(index) { + this.ungrouped.splice(index, 1); + }, + closeDialog() { + this.showEditFilter = false; + }, + confirmDialog(filter) { + STUDIP.jsonapi.withPromises().post( + 'user-filters', + { + data: { + data: { + attributes: { + filters: filter + } + } + } + }) + .then(response => { + this.ungrouped.push(response.data); + this.showEditFilter = false; + }); + }, + setRuleData(data) { + this.ungrouped = data.attributes.payload['conditions']; + this.groups = data.attributes.payload['grouped-conditions']; + }, + validate() { + if (this.ungrouped.length + this.groups.length === 0) { + this.invalidData.push(this.$gettext('Bitte geben Sie mindestens eine Auswahlbedingung an.')); + } + + return this.invalidData.length === 0; + }, + createContingent() { + let setQuotas = 100; + this.groups.forEach(group => { + setQuotas -= group.quota; + }); + this.groups.push({ + id: null, + quota: Math.max(setQuotas, 0), + conditions: this.ungrouped.filter(element => { + return this.selectedFilters.includes(element.id); + }) + }); + this.ungrouped = this.ungrouped.filter(element => { + return !this.selectedFilters.includes(element.id); + }); + this.selectedFilters = []; + }, + deleteContingent(index) { + this.groups[index].conditions.forEach(filter => { + this.ungrouped.push(filter); + }); + this.groups.splice(index, 1); + } + } +} +</script> + +<style lang="scss" scoped> +.delete-contingent { + margin-top: 2ex; +} +</style> diff --git a/resources/vue/components/admission/ConfigureCourseSet.vue b/resources/vue/components/admission/ConfigureCourseSet.vue new file mode 100644 index 0000000000000000000000000000000000000000..f23f17250e0f7df9bf140d8f6bb3e7c4e766c6f1 --- /dev/null +++ b/resources/vue/components/admission/ConfigureCourseSet.vue @@ -0,0 +1,712 @@ +<template> + <div> + <form class="default" + :action="storeUrl" + method="post" + ref="courseSetForm" + data-secure="true" + > + <fieldset> + <legend>{{ $gettext('Grunddaten') }}</legend> + <section> + <label class="studiprequired"> + <span class="textlabel"> + {{ $gettext('Name des Anmeldesets') }} + </span> + <span class="asterisk" :title="$gettext('Dies ist ein Pflichtfeld')" aria-hidden="true">*</span> + <input type="text" + name="name" + maxlength="255" + v-model="name"> + </label> + </section> + <section> + <label for="private" + :aria-label="$gettext('Dieses Anmeldeset soll nur für mich selbst und alle Administratoren sichtbar und benutzbar sein.')"> + {{ $gettext('Sichtbarkeit') }} + </label> + <input type="checkbox" + name="private" + id="private" + v-model="private"> + {{ $gettext('Dieses Anmeldeset soll nur für mich selbst und alle Administratoren sichtbar und benutzbar sein.') }} + </section> + </fieldset> + <fieldset> + <legend>{{ $gettext('Einrichtungszuordnung') }}</legend> + <section v-if="instituteSearch || myInstitutes?.length > 1"> + <label for="isearch" class="studiprequired"> + <span class="textlabel"> + {{ $gettext('Einrichtung wählen') }} + </span> + <span class="asterisk" :title="$gettext('Dies ist ein Pflichtfeld')" aria-hidden="true">*</span> + </label> + <quicksearch v-if="instituteSearch" + :searchtype="instituteSearch" + name="institute" + :key="NaN" + id="isearch" + @input="addInstitute" + :aria-label="$gettext('Geben Sie einen Suchbegriff mit mehr als 3 Zeichen ein, um nach Einrichtungen zu suchen')" + ref="instituteSearch"></quicksearch> + <select v-if="myInstitutes?.length > 1" + name="institute" + id="isearch" + @change.prevent="setInstitute"> + <option value="">-- {{ $gettext('bitte wählen') }} --</option> + <option v-for="institute in myInstitutes" + :key="institute.id" + :value="institute.id" + > + {{ institute.name }} + </option> + </select> + </section> + <section> + <header> + <h2>{{ $gettext('Bereits zugeordnet') }}</h2> + </header> + <table v-if="institutes?.length > 0" class="default assignments"> + <tbody> + <tr v-for="(institute, index) in institutes" + :key="institute.id" + class="institute-assignment"> + <td> + <input type="hidden" + name="institutes[]" + :value="institute.id"> + {{ institute.name }} + </td> + <td class="actions"> + <button v-if="myInstitutes?.length !== 1" + :title="$gettextInterpolate( + $gettext('Zuordnung der Einrichtung %{name} entfernen'), + { name: institute.name } + )" + :aria-label="$gettextInterpolate( + $gettext('Zuordnung der Einrichtung %{name} entfernen'), + { name: institute.name } + )" + class="as-link delete-assignment" + tabindex="0" + @click.prevent="removeInstitute(index)"> + <studip-icon shape="trash"></studip-icon> + </button> + </td> + </tr> + </tbody> + </table> + <p v-else> + {{ $gettext('Aktuell sind keine Einrichtungen zugeordnet.') }} + </p> + </section> + </fieldset> + <fieldset v-if="institutes?.length > 0 || courses?.length > 0"> + <legend> + {{ $gettext('Veranstaltungszuordnung') }} + </legend> + <section> + <template v-if="!isSearching"> + <label class="col-2"> + {{ $gettext('Semester') }} + <select ref="semesterChooser" + v-model="selectedSemester" + @change.prevent="getAvailableCourses"> + <option v-for="semester in allSemesters" + :key="semester.id" + :value="semester.id"> + {{ semester.name }} + </option> + </select> + </label> + <label class="col-3"> + {{ $gettext('Suche nach Titel, Nummer, Lehrenden (mehr als 3 Zeichen)') }} + <input type="text" + v-model="courseSearchterm" + @keydown.enter.prevent="getAvailableCourses" + ref="courseSearch"/> + </label> + <button class="button search-button" + :disabled="!canSearchCourses" + @click.prevent="getAvailableCourses"> + {{ $gettext('Suche') }} + </button> + </template> + <studip-progress-indicator v-else :size="32" + :description="$gettext('Veranstaltungen werden gesucht...')"/> + </section> + <section> + <table v-if="availableCourses?.length > 0" + class="default"> + <caption> + {{ $gettextInterpolate($gettext('Veranstaltungen im %{semester}'), + { semester: allSemesters[selectedSemester].name }) }} + </caption> + <colgroup> + <col style="width: 15px"> + <col> + </colgroup> + <thead> + <tr> + <th colspan="2"> + {{ $gettext('Veranstaltung') }} + </th> + </tr> + </thead> + <tbody> + <tr v-for="course in availableCourses" :key="course.id"> + <td> + <label> + <input type="checkbox" + :value="course.id" + v-model="checkedCourses" + :title="$gettextInterpolate($gettext('Veranstaltung %{coursename} dem Anmeldeset zuordnen'), + { coursename: course.attributes.title })"> + <template v-if="course.attributes['course-number']"> + {{ course.attributes['course-number'] }} + </template> + {{ course.attributes.title }} + </label> + </td> + </tr> + </tbody> + </table> + <studip-message-box v-if="!isSearching && noCoursesFound" + type="info" + :hide-close="true" + role="alert"> + {{ $gettext('Es wurden keine Veranstaltungen gefunden, die zugeordnet werden könnten.') }} + </studip-message-box> + </section> + <table v-if="courses?.length > 0" + class="default assignments"> + <caption>{{ $gettext('Bereits zugeordnet') }}</caption> + <thead> + </thead> + <tbody> + <tr v-for="(course, index) in courses" + :key="course.id" + class="course-assignment" + > + <td> + <template v-if="course.attributes['course-number']"> + {{ course.attributes['course-number'] }} + </template> + {{ course.attributes.title }} + </td> + <td class="actions"> + <button :title="$gettextInterpolate( + $gettext('Zuordnung der Veranstaltung %{name} entfernen'), + { name: course.attributes.title })" + :aria-label="$gettextInterpolate( + $gettext('Zuordnung der Veranstaltung %{name} entfernen'), + { name: course.attributes.title })" + class="as-link delete-assignment" + tabindex="0" + @click.prevent="removeCourse(index)"> + <studip-icon shape="trash"></studip-icon> + </button> + </td> + </tr> + </tbody> + </table> + <button v-if="hasConfigurableCourses" + class="button" + @click.prevent="configureCourses"> + {{ $gettext('Veranstaltungen konfigurieren') }} + </button> + <button v-if="numApplicants > 0" + class="button" + @click.prevent="getApplicants"> + {{ $gettextInterpolate( + $gettext('Liste der Anmeldungen (%{number} Personen)'), + { number: numApplicants }) }} + </button> + <button v-if="numApplicants > 0" + class="button" + @click.prevent="messageApplicants"> + {{ $gettext('Nachricht an alle Angemeldeten') }} + </button> + </fieldset> + <fieldset> + <legend class="studiprequired"> + <span class="textlabel"> + {{ $gettext('Anmelderegeln') }} + </span> + <span class="asterisk" :title="$gettext('Dies ist ein Pflichtfeld')" aria-hidden="true">*</span> + </legend> + <section> + <table v-if="rules.length > 0" class="default assignments"> + <tbody> + <tr v-for="(rule, index) in rules" + :key="index" + class="rule-assignment" + > + <td v-html="rule.attributes.ruletext"></td> + <td class="actions"> + <button :title="$gettextInterpolate( + $gettext('Regel %{name} bearbeiten'), + { name: rule.attributes.name } + )" + :aria-label="$gettextInterpolate( + $gettext('Regel %{name} bearbeiten'), + { name: rule.attributes.name } + )" + class="as-link edit-assignment" + tabindex="0" + @click.prevent="configureRule(rule.attributes.type, rule, index)"> + <studip-icon shape="edit" :size="16"></studip-icon> + </button> + <button :title="$gettextInterpolate( + $gettext('Regel %{name} entfernen'), + { name: rule.attributes.name } + )" + :aria-label="$gettextInterpolate( + $gettext('Regel %{name} entfernen'), + { name: rule.attributes.name } + )" + class="as-link delete-assignment" + tabindex="0" + data-confirm="$gettext('Soll die Regel wirklich entfernt werden?')" + @click.prevent="removeRule(index)"> + <studip-icon shape="trash"></studip-icon> + </button> + </td> + </tr> + </tbody> + </table> + <button class="button add add-rule-button" + type="button" + @click="addRule" + > + {{ $gettext('Anmelderegel hinzufügen') }} + </button> + </section> + </fieldset> + <fieldset> + <legend> + {{ $gettext('Weitere Daten') }} + </legend> + <section v-if="hasUserLists"> + <label> + {{ $gettext('Personen mit Bonus/Malus bei der Platzverteilung') }} + </label> + <label v-for="list in myUserLists" :key="list.id"> + <input type="checkbox" :value="list.id" v-model="userLists"> + {{ list.name }} + ({{ userListText(list.factor, list.count) }}) + </label> + <button v-if="showUserListUsers" + class="button" + @click.prevent="openUserListUsers"> + {{ $gettext('Liste der Personen') }} + </button> + </section> + <section> + <label> + {{ $gettext('Weitere Hinweise für die Teilnehmenden') }} + <textarea name="infotext" + cols="60" + rows="3" + v-model="additional"></textarea> + </label> + </section> + </fieldset> + <footer data-dialog-button> + <button class="button accept" + type="submit" + @click.prevent="storeCourseset" + :disabled="!isStorable"> + {{ $gettext('Speichern') }} + </button> + <button class="button cancel" + type="button" + data-dialog="close" + @click.prevent="cancel" + > + {{ $gettext('Abbrechen') }} + </button> + </footer> + </form> + <admission-rule-type-selector v-if="showRuleSelector" + :assigned-rule-types="ruleTypes" + @configureRule="configureRule" + @close="closeRuleSelector" + ></admission-rule-type-selector> + <admission-rule-config v-if="showRuleConfig && ruleType !== ''" + :type="ruleType" + :rule="singleRule" + :assigned-rule-types="ruleTypes" + @submit="addRuleConfiguration" + @cancel="closeRuleConfig" + ></admission-rule-config> + </div> +</template> + +<script> +import quicksearch from '../Quicksearch.vue'; +import AdmissionRuleTypeSelector from './AdmissionRuleTypeSelector.vue'; +import AdmissionRuleConfig from './AdmissionRuleConfig.vue'; +import StudipProgressIndicator from "../StudipProgressIndicator.vue"; + +export default { + name: 'ConfigureCourseSet', + components: {StudipProgressIndicator, AdmissionRuleConfig, quicksearch, AdmissionRuleTypeSelector }, + props: { + courseSetId: { + type: String, + default: '' + }, + allSemesters: { + type: Object, + required: true + }, + semester: { + type: String, + default: '' + }, + instituteSearch: { + type: String, + default: '' + }, + myInstitutes: { + type: Array, + default: () => [] + }, + myUserLists: { + type: Array, + default: () => [] + } + }, + data() { + return { + name: '', + private: true, + numApplicants: 0, + institutes: [], + selectedSemester: this.semester, + courseSearchterm: '', + availableCourses: [], + isSearching: false, + noCoursesFound: false, + checkedCourses: [], + courses: [], + rules: [], + userLists: [], + hasUserLists: false, + additional: '', + showRuleSelector: false, + ruleType: '', + ruleId: '', + singleRule: null, + ruleIndex: null, + showRuleConfig: false, + changed: false + } + }, + computed: { + isStorable() { + return this.name !== '' + && this.institutes.length > 0 + && this.rules.length > 0; + }, + hasConfigurableCourses() { + return this.courseSetId + && this.courseSetId !== '' + && this.courses?.length > 0; + }, + storeUrl() { + let url = STUDIP.URLHelper.getURL('dispatch.php/admission/courseset/save', {}, true); + + if (this.courseSetId !== null) { + url += '/' + this.courseSetId; + } + + return url; + }, + ruleTypes() { + return this.rules.map(r => r.attributes.type); + }, + canSearchCourses() { + return this.courseSearchterm?.trim().length >= 3 + }, + showUserListUsers() { + return this.courseSetId !== '' && this.hasUserLists && this.userLists.length > 0; + } + }, + methods: { + getSelectedSemester() { + return this.allSemesters[this.selectedSemester]; + }, + getAvailableCourses() { + if (this.canSearchCourses) { + this.noCoursesFound = false; + this.isSearching = true; + this.availableCourses = []; + STUDIP.jsonapi.withPromises().post( + 'admission/available-courses', + { + data: { + institutes: this.institutes.map(i => { + return i.id; + }), + courseset: this.courseSetId ? this.courseSetId : null, + exclude: this.courses.map(course => course.id), + semester: this.selectedSemester, + filter: this.courseSearchterm + } + } + ).then(response => { + setTimeout(() => this.isSearching = false, 1000); + const currentCourses = this.courses.map(c => c.id); + this.availableCourses = response.data.filter(course => !currentCourses.includes(course.id)); + this.noCoursesFound = this.availableCourses.length === 0; + }).catch(error => { + this.isSearching = false; + STUDIP.Report.error(this.$gettext('Es ist ein Fehler aufgetreten'), error); + }); + } + }, + addRule() { + this.ruleType = ''; + this.showRuleSelector = true; + }, + closeRuleSelector() { + this.ruleType = ''; + this.ruleId = ''; + this.singleRule = null; + this.showRuleSelector = false; + }, + closeRuleConfig() { + this.ruleType = ''; + this.ruleId = ''; + this.singleRule = null; + this.showRuleConfig = false; + }, + configureRule(type, rule = null, index = null) { + this.ruleType = type; + this.ruleId = rule?.id; + this.singleRule = rule; + this.ruleIndex = index; + this.showRuleSelector = false; + this.showRuleConfig = true; + }, + addRuleConfiguration(data) { + if (!this.ruleId) { + STUDIP.jsonapi.withPromises().post( + 'admission-rules/' + data.type, + { + data: { + data: { + attributes: { + payload: data.payload + } + } + } + } + ).then(response => { + this.ruleType = ''; + this.ruleId = ''; + this.singleRule = null; + this.showRuleConfig = false; + if (this.ruleIndex !== null) { + this.rules[this.ruleIndex] = response.data; + this.ruleIndex = null; + } else { + this.rules.push(response.data); + } + this.checkForUserLists(); + }); + } else { + STUDIP.jsonapi.withPromises().patch( + 'admission-rules/' + this.ruleId, + { + data: { + data: { + attributes: { + payload: data.payload + } + } + } + } + ).then(response => { + this.ruleType = ''; + this.ruleId = ''; + this.singleRule = null; + this.showRuleConfig = false; + if (this.ruleIndex !== null) { + this.rules[this.ruleIndex] = response.data; + this.ruleIndex = null; + } else { + this.rules.push(response.data.data); + } + this.checkForUserLists(); + }); + } + }, + removeRule(index) { + this.rules.splice(index, 1); + this.checkForUserLists(); + }, + removeCourse(index) { + this.courses.splice(index, 1); + }, + setInstitute(evt) { + if (evt.currentTarget.value !== '') { + this.addInstitute( + evt.currentTarget.value, + evt.currentTarget.options[evt.currentTarget.options.selectedIndex].textContent + ); + } + }, + addInstitute(returnValue, inputValue) { + if (!this.institutes.some(i => i.id === returnValue)) { + this.institutes.push({ id: returnValue, name: inputValue }); + } + }, + removeInstitute(index) { + this.institutes.splice(index, 1); + }, + storeCourseset() { + const data = { + data: { + attributes: { + name: this.name, + private: this.private, + infotext: this.additional, + institutes: this.institutes.map(i => i.id), + courses: this.courses.map(c => c.id).concat(this.checkedCourses), + rules: this.rules, + userlists: this.hasUserLists ? this.userLists : [] + } + } + }; + if (this.courseSetId === '') { + + STUDIP.jsonapi.withPromises().post( + 'course-sets', + { data: data } + ).then(response => { + this.$refs.courseSetForm.dataset.secure = 'false'; + window.location = STUDIP.URLHelper.getURL('dispatch.php/admission/courseset'); + }); + + } else { + + STUDIP.jsonapi.withPromises().patch( + 'course-sets/' + this.courseSetId, + { data: data} + ).then(response => { + this.$refs.courseSetForm.dataset.secure = 'false'; + window.location = STUDIP.URLHelper.getURL('dispatch.php/admission/courseset'); + }); + + } + }, + cancel() { + window.location = STUDIP.URLHelper.getURL('dispatch.php/admission/courseset'); + }, + configureCourses() + { + STUDIP.Dialog.fromURL( + STUDIP.URLHelper.getURL('dispatch.php/admission/courseset/configure_courses/' + this.courseSetId) + ); + }, + getApplicants() + { + STUDIP.Dialog.fromURL( + STUDIP.URLHelper.getURL('dispatch.php/admission/courseset/applications_list/' + this.courseSetId) + ); + }, + messageApplicants() + { + STUDIP.Dialog.fromURL( + STUDIP.URLHelper.getURL('dispatch.php/admission/courseset/applicants_message/' + this.courseSetId) + ); + }, + checkForUserLists() { + const rule = this.rules?.filter(rule => rule.attributes.type === 'ParticipantRestrictedAdmission'); + this.hasUserLists = this.myUserLists.length > 0 + && (rule?.length > 0 ? rule[0].attributes.payload['distribution-time'] > 0 : false); + }, + userListText(factor, count) { + return this.$gettextInterpolate( + factor < 1 + ? this.$gettext('%{number} Personen werden nachrangig eingetragen') + : this.$gettext('%{number} Personen werden bevorzugt'), + { number: count } + ); + }, + openUserListUsers() { + STUDIP.Dialog.fromURL( + STUDIP.URLHelper.getURL('dispatch.php/admission/courseset/factored_users/' + this.courseSetId) + ); + } + }, + created() { + // Load courseset if an ID is given + if (this.courseSetId !== '') { + STUDIP.jsonapi.withPromises().get( + 'course-sets/' + this.courseSetId, + {data: {include: 'admission-rules,courses,institutes'}} + ).then(courseset => { + this.name = courseset.data.attributes.name; + this.private = courseset.data.attributes.private; + this.additional = courseset.data.attributes.infotext; + this.numApplicants = courseset.data.attributes['num-applicants']; + this.userLists = courseset.data.attributes['userlists']; + + courseset.included.forEach(entry => { + switch (entry.type) { + case 'institutes': + this.addInstitute(entry.id, entry.attributes.name); + break; + case 'courses': + this.courses.push(entry); + break; + case 'admission-rules': + this.rules.push(entry); + break; + } + }); + + this.checkForUserLists(); + }); + } else if (this.myInstitutes.length === 1) { + this.addInstitute(this.myInstitutes[0].id, this.myInstitutes[0].name); + } + + if (!this.selectedSemester) { + for (const [key, value] of Object.entries(this.allSemesters)) { + if (value.current) { + this.selectedSemester = value.id; + } + } + } + + this.getAvailableCourses(); + } +} +</script> + +<style lang="scss"> +table.assignments { + margin-bottom: unset; + width: 50%; + + .actions { + text-align: right; + } +} + +button { + &.add-rule-button { + margin-bottom: 0; + } + + img { + vertical-align: text-bottom; + } + +} +</style> diff --git a/resources/vue/components/admission/CourseMemberAdmission.vue b/resources/vue/components/admission/CourseMemberAdmission.vue new file mode 100644 index 0000000000000000000000000000000000000000..9d59e4746b1914339e63cb048672ea8ba1fdc640 --- /dev/null +++ b/resources/vue/components/admission/CourseMemberAdmission.vue @@ -0,0 +1,113 @@ +<template> + <form class="default"> + <section> + <label> + {{ $gettext('Nachricht bei fehlgeschlagener Anmeldung') }} + <textarea name="message" rows="4" cols="50" v-model="messageText"></textarea> + </label> + </section> + <validity-time></validity-time> + <section> + <label> + <input type="radio" v-model="theMode" :value="0"> + {{ $gettext('Mitgliedschaft ist in mindestens einer dieser Veranstaltungen notwendig') }} + </label> + <label> + <input type="radio" v-model="theMode" :value="1"> + {{ $gettext('Mitgliedschaft ist in keiner dieser Veranstaltungen erlaubt') }} + </label> + </section> + <section> + <label for="csearch"> + {{ $gettext('Veranstaltung(en)') }} + </label> + <quicksearch v-if="courseSearch !== null" + :searchtype="courseSearch" + name="course" + :key="NaN" + @input="addCourse" + id="csearch" + ref="courseSearch"></quicksearch> + <ul v-if="courseList.length > 0"> + <li v-for="(course, index) in courseList" :key="index"> + {{ course.name }} + </li> + </ul> + </section> + </form> +</template> + +<script> +import {AdmissionRuleMixin} from '../../mixins/AdmissionRuleMixin'; +import ValidityTime from './ValidityTime.vue'; +import quicksearch from '../Quicksearch.vue'; + +export default { + name: 'CourseMemberAdmission', + components: { ValidityTime, quicksearch }, + mixins: [AdmissionRuleMixin], + data() { + return { + messageText: this.message || ( + this.theMode === 0 + ? this.$gettext('Sie sind nicht in einer der gewählten Veranstaltungen eingetragen.') + : this.$gettext('Sie sind bereits in einer der gewählten Veranstaltungen eingetragen.') + ), + theMode: 0, + courseList: [], + courseSearch: null + } + }, + computed: { + payload() { + return { + type: 'CourseMemberAdmission', + payload: { + modus: this.theMode, + courses: this.courseList, + message: this.messageText + } + } + } + }, + methods: { + addCourse(returnValue, inputValue) { + if (!this.courseList.some(i => i.id === returnValue)) { + this.courseList.push({id: returnValue, name: inputValue}); + } + }, + setRuleData(data) { + this.courseSearch = data.attributes.payload.search; + this.courseList = data.attributes.payload.courses; + this.theMode = data.attributes.payload.modus; + }, + }, + validate() { + if (this.courseList.length === 0) { + this.invalidData.push(this.$gettext('Bitte geben Sie mindestens eine Veranstaltung an.')); + } + + return this.invalidData.length === 0; + }, + mounted() { + // Get a new rule instance so we can use quicksearch. + if (!this.id || this.id === '') { + STUDIP.jsonapi.withPromises().post('admission-rules/CourseMemberAdmission', { + data: { + data: { + attributes: { + payload: { + mode: 0, + courses: [], + message: '' + } + } + } + } + }).then(response => { + this.courseSearch = response.data.attributes.payload.search; + }); + } + }, +} +</script> diff --git a/resources/vue/components/admission/LimitedAdmission.vue b/resources/vue/components/admission/LimitedAdmission.vue new file mode 100644 index 0000000000000000000000000000000000000000..bfa57965330cafe7da2e8a3d4a04176d2bd178d7 --- /dev/null +++ b/resources/vue/components/admission/LimitedAdmission.vue @@ -0,0 +1,65 @@ +<template> + <form class="default"> + <section> + <label for="terms"> + {{ $gettext('Nachricht bei fehlgeschlagener Anmeldung') }} + <textarea rows="4" cols="50" v-model="messageText"></textarea> + </label> + </section> + <validity-time></validity-time> + <section> + <label for="maxnumber"> + <span class="required"> + {{ $gettext('Maximale Anzahl erlaubter Anmeldungen') }} + </span> + <input type="number" size="4" min="1" v-model="max"> + </label> + </section> + </form> +</template> + +<script> +import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin'; +import ValidityTime from './ValidityTime.vue'; + +export default { + name: 'LimitedAdmission', + components: { ValidityTime }, + mixins: [AdmissionRuleMixin], + props: { + maxNumber: { + type: Number, + default: 1 + } + }, + data() { + return { + messageText: this.message || this.$gettext('Sie sind bereits in die maximale Anzahl von %u Veranstaltungen eingetragen.'), + max: this.maxNumber + } + }, + computed: { + payload() { + return { + type: 'LimitedAdmission', + payload: { + maxnumber: this.max, + message: this.messageText + } + } + } + }, + methods: { + setRuleData(data) { + this.max = data.attributes.payload['maxnumber']; + }, + validate() { + if (this.max < 1) { + this.invalidData.push(this.$gettext('Bitte geben Sie eine gültige Zahl für die Anzahl der maximalen Anmeldungen an.')); + } + + return this.invalidData.length === 0; + } + } +} +</script> diff --git a/resources/vue/components/admission/LockedAdmission.vue b/resources/vue/components/admission/LockedAdmission.vue new file mode 100644 index 0000000000000000000000000000000000000000..3e050bed6b7d0463eacf4870511b5da13ca24e6b --- /dev/null +++ b/resources/vue/components/admission/LockedAdmission.vue @@ -0,0 +1,42 @@ +<template> + <form class="default"> + <section> + <label> + {{ $gettext('Nachricht bei fehlgeschlagener Anmeldung') }} + <textarea name="message" rows="4" cols="50" v-model="messageText"></textarea> + </label> + </section> + </form> +</template> + +<script> +import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin'; + +export default { + name: 'LockedAdmission', + mixins: [AdmissionRuleMixin], + props: { + message: { + type: String, + default: '' + } + }, + data() { + return { + messageText: this.message || this.$gettext('Die Anmeldung ist gesperrt.'), + password1: '', + password2: '' + } + }, + computed: { + payload() { + return { + type: 'LockedAdmission', + payload: { + message: this.messageText + } + } + } + } +} +</script> diff --git a/resources/vue/components/admission/ParticipantRestrictedAdmission.vue b/resources/vue/components/admission/ParticipantRestrictedAdmission.vue new file mode 100644 index 0000000000000000000000000000000000000000..9f75b9a7cceab52d26912b89885f58d2c1bbcb1e --- /dev/null +++ b/resources/vue/components/admission/ParticipantRestrictedAdmission.vue @@ -0,0 +1,91 @@ +<template> + <form class="default"> + <section> + <label> + {{ $gettext('Nachricht bei fehlgeschlagener Anmeldung') }} + <textarea rows="4" cols="50" v-model="messageText"></textarea> + </label> + </section> + <label> + <input type="checkbox" v-model="fcfsEnabled" :disabled="hasPrios"> + {{ $gettext('Keine automatische Platzverteilung (Windhund-Verfahren)') }} + <studip-tooltip-icon v-if="hasPrios" + :text="$gettext('Es existieren bereits Anmeldungen für die automatische Platzverteilung.')"> + </studip-tooltip-icon> + </label> + <section v-if="!fcfsAllowed || !fcfsEnabled"> + <label> + {{ $gettext('Zeitpunkt der automatischen Platzverteilung') }} + <datetimepicker v-if="loaded" :value="distributionTime" v-model="distributionTime"></datetimepicker> + </label> + </section> + </form> +</template> + +<script> +import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin'; +import Datetimepicker from '../Datetimepicker.vue'; +import StudipTooltipIcon from '../StudipTooltipIcon.vue'; + +export default { + name: 'ParticipantRestrictedAdmission', + components: { StudipTooltipIcon, Datetimepicker }, + mixins: [AdmissionRuleMixin], + props: { + distribution: { + type: Number, + default: Math.floor(new Date().getTime() / 1000 + 86400) + }, + fcfs: { + type: Boolean, + default: true + }, + hasPrios: { + type: Boolean, + default: false + }, + message: { + type: String, + default: '' + } + }, + data() { + return { + messageText: this.message, + fcfsAllowed: true, + fcfsEnabled: this.distributionTime === 0, + distributionTime: this.distribution, + loaded: false + } + }, + computed: { + payload() { + return { + type: 'ParticipantRestrictedAdmission', + payload: { + 'distribution-time': this.fcfsEnabled ? 0 : this.distributionTime, + 'fcfs': this.fcfsEnabled, + 'fcfs-allowed': this.fcfsAllowed, + message: this.messageText + } + } + } + }, + methods: { + setRuleData(data) { + this.fcfsAllowed = data.attributes.payload['fcfs-allowed']; + this.distributionTime = data.attributes.payload['distribution-time'] !== 0 + ? data.attributes.payload['distribution-time'] + : Math.floor(Date.now() / 1000 + 7 * 86400); + this.fcfsEnabled = data.attributes.payload['distribution-time'] === 0; + this.loaded = true; + } + }, + created() { + if (!this.id) { + this.distributionTime = Math.floor(new Date().getTime() / 1000 + 86400); + this.loaded = true; + } + } +} +</script> diff --git a/resources/vue/components/admission/PasswordAdmission.vue b/resources/vue/components/admission/PasswordAdmission.vue new file mode 100644 index 0000000000000000000000000000000000000000..3f0ecf4099a990fcda1b38b47702b422d1f999cf --- /dev/null +++ b/resources/vue/components/admission/PasswordAdmission.vue @@ -0,0 +1,83 @@ +<template> + <form class="default"> + <section> + <label> + {{ $gettext('Nachricht bei fehlgeschlagener Anmeldung') }} + <textarea name="message" rows="4" cols="50" v-model="messageText"></textarea> + </label> + </section> + <section> + <studip-message-box v-if="passwordSet" type="warning"> + {{ $gettext('Es ist bereits ein Passwort eingerichtet. Um es zu überschreiben, geben Sie hier ein neues ein.') }} + </studip-message-box> + <label> + {{ $gettext('Zugangspasswort') }} + <input :type="passwordVisible ? 'text' : 'password'" v-model="password1" ref="password1"> + <studip-icon class="password-visibility" @click="togglePasswordVisible" + :shape="passwordVisible ? 'visibility-invisible' : 'visibility-visible'"></studip-icon> + </label> + <label> + {{ $gettext('Passwort wiederholen') }} + <input :type="passwordVisible ? 'text' : 'password'" v-model="password2" ref="password2"> + <studip-icon class="password-visibility" @click="togglePasswordVisible" + :shape="passwordVisible ? 'visibility-invisible' : 'visibility-visible'"></studip-icon> + </label> + </section> + </form> +</template> + +<script> +import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin'; + +export default { + name: 'PasswordAdmission', + mixins: [AdmissionRuleMixin], + props: { + hasPassword: { + type: Boolean, + default: false + } + }, + data() { + return { + messageText: this.message || this.$gettext('Für die Anmeldung ist ein Passwort erforderlich.'), + password1: '', + password2: '', + passwordVisible: false, + passwordSet: this.hasPassword + } + }, + methods: { + togglePasswordVisible() { + this.passwordVisible = !this.passwordVisible; + }, + setRuleData(data) { + if (data.attributes.payload.password !== '') { + this.passwordSet = true; + } + }, + validate() { + this.invalidData = []; + if (this.password1 === '') { + this.invalidData.push(this.$gettext('Das Passwort darf nicht leer sein.')); + } + if (this.password1 !== this.password2) { + this.invalidData.push(this.$gettext('Die eingegebenen Passwörter stimmen nicht überein.')); + } + + return this.invalidData.length === 0; + } + }, + computed: { + payload() { + return { + type: 'PasswordAdmission', + payload: { + password: this.password1, + message: this.messageText + } + } + } + } +} +</script> diff --git a/resources/vue/components/admission/PreferentialAdmission.vue b/resources/vue/components/admission/PreferentialAdmission.vue new file mode 100644 index 0000000000000000000000000000000000000000..76841ec57413987d09b0f0cf2bb7493d45f7e7a9 --- /dev/null +++ b/resources/vue/components/admission/PreferentialAdmission.vue @@ -0,0 +1,120 @@ +<template> + <form class="default"> + <section> + <label> + {{ $gettext('Nachricht bei fehlgeschlagener Anmeldung') }} + <textarea name="message" rows="4" cols="50" v-model="messageText"></textarea> + </label> + </section> + <section> + <h3> + {{ $gettext('Folgende Personen bei der Platzverteilung bevorzugen:') }} + </h3> + <div v-if="conditions.length > 0" + role="list"> + <div v-for="(filter, index) in conditions" + :key="index" + role="listitem"> + <p v-if="conditions.length > 1 && index >= 1"> + {{ $gettext('oder') }} + </p> + <p v-html="filter.attributes.text"></p> + </div> + </div> + <p v-if="conditions.length === 0"> + {{ $gettext('Sie haben noch keine Auswahl festgelegt.') }} + </p> + </section> + <section> + <button class="button add" + @click.prevent="editFilter"> + {{ $gettext('Bedingung hinzufügen') }} + </button> + </section> + <section> + <label> + <input type="checkbox" + v-model="favorSemester" + value="1"> + {{ $gettext('Höhere Fachsemester bevorzugen') }} + </label> + </section> + <studip-user-filter v-if="showEditFilter" + @submit="confirmDialog" + @close="closeDialog"></studip-user-filter> + </form> +</template> + +<script> +import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin'; +import StudipUserFilter from "../StudipUserFilter.vue"; + +export default { + name: 'PreferentialAdmission', + components: { StudipUserFilter }, + mixins: [AdmissionRuleMixin], + data() { + return { + messageText: this.message || this.$gettext('Folgende Gruppen werden bei der Platzverteilung bevorzugt behandelt: %s'), + conditions: [], + favorSemester: false, + showEditFilter: false, + selectedFilters: [] + } + }, + computed: { + groupsAllowed() { + return this.assignedRuleTypes.includes('ParticipantRestrictedAdmission') + }, + payload() { + return { + type: 'PreferentialAdmission', + payload: { + conditions: this.conditions, + 'favor-semester': this.favorSemester, + message: this.messageText + } + } + } + }, + methods: { + editFilter() { + this.showEditFilter = true; + }, + closeDialog() { + this.showEditFilter = false; + }, + confirmDialog(filter) { + STUDIP.jsonapi.withPromises().post( + 'user-filters', + { + data: { + data: { + attributes: { + filters: filter + } + } + } + }) + .then(response => { + this.conditions.push(response.data); + this.showEditFilter = false; + }); + }, + setRuleData(data) { + this.conditions = data.attributes.payload['conditions']; + this.favorSemester = data.attributes.payload['favor-semester']; + }, + validate() { + if (this.conditions.length === 0 && !this.favorSemester) { + this.invalidData.push( + this.$gettext('Bitte geben Sie mindestens eine Auswahlbedingung an oder ' + + 'bevorzugen Sie höhere Fachsemester.') + ); + } + + return this.invalidData.length === 0; + } + } +} +</script> diff --git a/resources/vue/components/admission/TermsAdmission.vue b/resources/vue/components/admission/TermsAdmission.vue new file mode 100644 index 0000000000000000000000000000000000000000..98abcf8fa0b6596dd3e07fd02ba2a442f64ea7a1 --- /dev/null +++ b/resources/vue/components/admission/TermsAdmission.vue @@ -0,0 +1,57 @@ +<template> + <form class="default"> + <section> + <label for="terms"> + <span class="required"> + {{ $gettext('Teilnahmebedingungen') }} + </span> + <textarea v-model="theTerms" id="terms" rows="4" + :placeholder="$gettext('Formulieren Sie hier die Teilnahmebedingungen.')"></textarea> + </label> + </section> + </form> +</template> + +<script> +import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin'; + +export default { + name: 'TermsAdmission', + mixins: [AdmissionRuleMixin], + props: { + terms: { + type: String, + default: '' + } + }, + data() { + return { + messageText: this.message || this.$gettext('Sie müssen den Teilnahmebedingungen zustimmen.'), + theTerms: this.terms + } + }, + computed: { + payload() { + return { + type: 'TermsAdmission', + payload: { + terms: this.theTerms + } + } + } + }, + methods: { + setRuleData(data) { + this.theTerms = data.attributes.payload.terms; + }, + validate() { + this.invalidData = []; + if (this.theTerms === '') { + this.invalidData.push(this.$gettext('Es sind keine Teilnahmebedingungen angegeben.')); + } + + return this.invalidData.length === 0; + } + } +} +</script> diff --git a/resources/vue/components/admission/TimedAdmission.vue b/resources/vue/components/admission/TimedAdmission.vue new file mode 100644 index 0000000000000000000000000000000000000000..ebdc3971e7fb685f099159daca9ae200bec8cf08 --- /dev/null +++ b/resources/vue/components/admission/TimedAdmission.vue @@ -0,0 +1,83 @@ +<template> + <form class="default"> + <section> + <label> + {{ $gettext('Nachricht bei fehlgeschlagener Anmeldung') }} + <textarea name="message" rows="4" cols="50" v-model="messageText"></textarea> + </label> + </section> + <section class="col-3"> + <label> + {{ $gettext('Start des Anmeldezeitraums') }} + <datetimepicker v-model="startTime"></datetimepicker> + </label> + </section> + <section class="col-3"> + <label> + {{ $gettext('Ende des Anmeldezeitraums') }} + <datetimepicker v-model="endTime"></datetimepicker> + </label> + </section> + </form> +</template> + +<script> +import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin'; + +export default { + name: 'TimedAdmission', + mixins: [ AdmissionRuleMixin ], + props: { + start: { + type: Number, + default: 0 + }, + end: { + type: Number, + default: 0 + }, + message: { + type: String, + default: '' + } + }, + data() { + return { + messageText: this.message || this.$gettext('Die Anmeldung ist nur innerhalb des angegebenen Zeitraums möglich.'), + startTime: this.start !== 0 ? this.start : Math.floor(Date.now() / 1000), + endTime: this.end !== 0 ? this.end : Math.floor(Date.now() / 1000 + 7 * 86400) + } + }, + computed: { + payload() { + return { + type: 'TimedAdmission', + payload: { + 'starttime': this.startTime, + 'endtime': this.endTime, + message: this.messageText + } + } + } + }, + methods: { + setRuleData(data) { + this.startTime = data.attributes.payload['starttime']; + this.endTime = data.attributes.payload['endtime']; + }, + validate() { + if (this.startTime < 0) { + this.invalidData.push(this.$gettext('Bitte geben Sie eine gültige Startzeit an.')); + } + if (this.endTime < 0) { + this.invalidData.push(this.$gettext('Bitte geben Sie eine gültige Endzeit an.')); + } + if (this.endTime <= this.startTime) { + this.invalidData.push(this.$gettext('Die Endzeit muss nach der Startzeit liegen.')); + } + + return this.invalidData.length === 0; + } + } +} +</script> diff --git a/resources/vue/components/admission/ValidityTime.vue b/resources/vue/components/admission/ValidityTime.vue new file mode 100644 index 0000000000000000000000000000000000000000..21c669f3be9fdf71552fc47e6c09a42ca0e587a0 --- /dev/null +++ b/resources/vue/components/admission/ValidityTime.vue @@ -0,0 +1,69 @@ +<template> + <div> + <section> + <label> + <button class="as-link" + @click.prevent="toggleTime" + :title="configureTime + ? $gettext('Klicken, um diese Regel ab sofort unbegrenzt gelten zu lassen') + : $gettext('Klicken, um einen Zeitraum für die Gültigkeit dieser Regel festzulegen')" + > + <studip-icon :shape="configureTime ? 'checkbox-unchecked' : 'checkbox-checked'"></studip-icon> + {{ $gettext('Diese Regel soll ab sofort zeitlich unbegrenzt gelten') }} + </button> + </label> + </section> + <section v-if="configureTime" class="col-3"> + <label> + {{ $gettext('Diese Regel gilt von') }} + <datetimepicker :value="startTime"></datetimepicker> + </label> + </section> + <section v-if="configureTime" class="col-3"> + <label> + {{ $gettext('bis') }} + <datetimepicker :value="endTime"></datetimepicker> + </label> + </section> + </div> +</template> + +<script> +import Datetimepicker from '../Datetimepicker.vue'; + +export default { + name: 'ValidityTime', + components: { Datetimepicker }, + props: { + start: { + type: Number, + default: 0 + }, + end: { + type: Number, + default: 0 + } + }, + data() { + return { + configureTime: this.start !== 0 || this.end !== 0, + startTime: this.start !== 0 ? this.start : Math.floor(Date.now() / 1000), + endTime: this.end !== 0 ? this.end : Math.floor(Date.now() / 1000 + 7 * 86400) + } + }, + methods: { + toggleTime() { + this.configureTime = !this.configureTime; + + if (this.configureTime) { + this.startTime = this.start !== 0 ? this.start : Math.floor(Date.now() / 1000); + this.endTime = this.end !== 0 ? this.end : nMath.floor(Date.now() / 1000 + 7 * 86400); + } else { + this.startTime = 0; + this.endTime = 0; + } + + } + } +} +</script> diff --git a/resources/vue/mixins/AdmissionRuleMixin.js b/resources/vue/mixins/AdmissionRuleMixin.js new file mode 100644 index 0000000000000000000000000000000000000000..0badb1a563f0aec922e32242fedd02ee65b017d1 --- /dev/null +++ b/resources/vue/mixins/AdmissionRuleMixin.js @@ -0,0 +1,61 @@ +export const AdmissionRuleMixin = { + props: { + id: { + type: String, + default: '' + }, + ruleData: { + type: Object, + default: null + }, + assignedRuleTypes: { + type: Array, + default: () => [] + }, + message: { + type: String, + default: '' + } + }, + data() { + return { + theRuleData: this.ruleData, + invalidData: [] + } + }, + methods: { + loadRuleData() { + STUDIP.jsonapi.withPromises().get('admission-rules/' + this.id) + .then((response) => { + this.setRuleData(response.data); + }); + }, + validate() { + return true; + }, + submit() { + this.invalidData = []; + if (this.validate()) { + this.$emit('submit', this.payload); + } else { + this.$emit('error', this.invalidData); + } + } + }, + mounted() { + if (this.id && this.id !== '' && !this.ruleData) { + this.loadRuleData(); + } + + if (this.ruleData) { + this.setRuleData(this.ruleData); + } + + STUDIP.eventBus.on('getRuleConfiguration', () => { + this.submit(); + }); + }, + beforeDestroy() { + STUDIP.eventBus.off('getRuleConfiguration'); + } +} diff --git a/templates/userfilter/display.php b/templates/userfilter/display.php index 57fdfe64c2701c6483eaa42e0ae9fd9a1c198823..189f6cdfc90327a00c42dd4dce8a241033d73a43 100644 --- a/templates/userfilter/display.php +++ b/templates/userfilter/display.php @@ -14,9 +14,10 @@ foreach ($filter->getFields() as $field) { } if ($filter->show_user_count) { $user_count = count($filter->getUsers()); - $fieldText .= ' ('.sprintf(_('%s Personen'), $user_count); - if (!$user_count) { - $fieldText .= Icon::create('exclaim-circle', 'attention', ['title' => _("Niemand erfüllt diese Bedingung.")])->asImg(); + $fieldText .= ' (' . sprintf(ngettext('Eine Person', '%s Personen', $user_count), $user_count); + if ($user_count === 0) { + $fieldText .= ' ' . Icon::create('exclaim-circle', Icon::ROLE_ATTENTION) + ->asImg(['title' => _('Niemand erfüllt diese Bedingung.')]); } $fieldText .= ')'; }