aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/admission/courseset.php59
-rw-r--r--app/controllers/admission/restricted_courses.php2
-rw-r--r--app/views/admission/courseset/_institute_choose.php15
-rw-r--r--app/views/admission/courseset/configure.php268
-rw-r--r--app/views/admission/rule/configure.php15
-rw-r--r--lib/admissionrules/conditionaladmission/ConditionalAdmission.php120
-rw-r--r--lib/admissionrules/conditionaladmission/templates/configure.php83
-rw-r--r--lib/admissionrules/conditionaladmission/templates/info.php20
-rw-r--r--lib/admissionrules/coursememberadmission/CourseMemberAdmission.php31
-rw-r--r--lib/admissionrules/coursememberadmission/templates/configure.php128
-rw-r--r--lib/admissionrules/limitedadmission/LimitedAdmission.php19
-rw-r--r--lib/admissionrules/limitedadmission/templates/configure.php10
-rw-r--r--lib/admissionrules/lockedadmission/LockedAdmission.php19
-rw-r--r--lib/admissionrules/lockedadmission/templates/configure.php8
-rw-r--r--lib/admissionrules/participantrestrictedadmission/ParticipantRestrictedAdmission.php33
-rw-r--r--lib/admissionrules/participantrestrictedadmission/templates/configure.php35
-rw-r--r--lib/admissionrules/participantrestrictedadmission/templates/info.php8
-rw-r--r--lib/admissionrules/passwordadmission/PasswordAdmission.php19
-rw-r--r--lib/admissionrules/passwordadmission/templates/configure.php18
-rw-r--r--lib/admissionrules/preferentialadmission/PreferentialAdmission.php59
-rw-r--r--lib/admissionrules/preferentialadmission/templates/configure.php39
-rw-r--r--lib/admissionrules/termsadmission/TermsAdmission.php21
-rw-r--r--lib/admissionrules/termsadmission/templates/configure.php7
-rw-r--r--lib/admissionrules/timedadmission/TimedAdmission.php42
-rw-r--r--lib/admissionrules/timedadmission/templates/configure.php50
-rw-r--r--lib/classes/CoursesetModel.php4
-rw-r--r--lib/classes/JsonApi/RouteMap.php26
-rw-r--r--lib/classes/JsonApi/Routes/Admission/AdmissionRulesCreate.php50
-rw-r--r--lib/classes/JsonApi/Routes/Admission/AdmissionRulesDelete.php38
-rw-r--r--lib/classes/JsonApi/Routes/Admission/AdmissionRulesIndex.php24
-rw-r--r--lib/classes/JsonApi/Routes/Admission/AdmissionRulesShow.php33
-rw-r--r--lib/classes/JsonApi/Routes/Admission/AdmissionRulesUpdate.php60
-rw-r--r--lib/classes/JsonApi/Routes/Admission/Authority.php75
-rw-r--r--lib/classes/JsonApi/Routes/Admission/AvailableCoursesIndex.php40
-rw-r--r--lib/classes/JsonApi/Routes/Admission/CourseSetsCreate.php68
-rw-r--r--lib/classes/JsonApi/Routes/Admission/CourseSetsDelete.php37
-rw-r--r--lib/classes/JsonApi/Routes/Admission/CourseSetsShow.php35
-rw-r--r--lib/classes/JsonApi/Routes/Admission/CourseSetsUpdate.php73
-rw-r--r--lib/classes/JsonApi/Routes/Admission/RuleCompatibilityIndex.php20
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/Authority.php18
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsIndex.php30
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsShow.php35
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/UserFiltersCreate.php61
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/UserFiltersDelete.php38
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/UserFiltersShow.php31
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/UserFiltersUpdate.php68
-rw-r--r--lib/classes/JsonApi/SchemaMap.php5
-rw-r--r--lib/classes/JsonApi/Schemas/AdmissionRule.php34
-rw-r--r--lib/classes/JsonApi/Schemas/CourseSet.php166
-rw-r--r--lib/classes/JsonApi/Schemas/UserFilter.php45
-rw-r--r--lib/classes/JsonApi/Schemas/UserFilterField.php67
-rw-r--r--lib/classes/StudipAutoloader.php13
-rw-r--r--lib/classes/admission/AdmissionRule.php131
-rw-r--r--lib/classes/admission/AdmissionUserList.php13
-rw-r--r--lib/classes/admission/UserFilter.php1
-rw-r--r--lib/classes/admission/UserFilterField.php36
-rw-r--r--resources/assets/javascripts/bootstrap/admission.js31
-rw-r--r--resources/assets/javascripts/lib/admission.js16
-rw-r--r--resources/assets/stylesheets/scss/admission.scss64
-rw-r--r--resources/assets/stylesheets/studip.scss2
-rw-r--r--resources/vue/components/StudipUserFilter.vue128
-rw-r--r--resources/vue/components/admission/AdmissionRuleConfig.vue89
-rw-r--r--resources/vue/components/admission/AdmissionRuleTypeSelector.vue113
-rw-r--r--resources/vue/components/admission/ConditionalAdmission.vue194
-rw-r--r--resources/vue/components/admission/ConfigureCourseSet.vue712
-rw-r--r--resources/vue/components/admission/CourseMemberAdmission.vue113
-rw-r--r--resources/vue/components/admission/LimitedAdmission.vue65
-rw-r--r--resources/vue/components/admission/LockedAdmission.vue42
-rw-r--r--resources/vue/components/admission/ParticipantRestrictedAdmission.vue91
-rw-r--r--resources/vue/components/admission/PasswordAdmission.vue83
-rw-r--r--resources/vue/components/admission/PreferentialAdmission.vue120
-rw-r--r--resources/vue/components/admission/TermsAdmission.vue57
-rw-r--r--resources/vue/components/admission/TimedAdmission.vue83
-rw-r--r--resources/vue/components/admission/ValidityTime.vue69
-rw-r--r--resources/vue/mixins/AdmissionRuleMixin.js61
-rw-r--r--templates/userfilter/display.php7
76 files changed, 3872 insertions, 771 deletions
diff --git a/app/controllers/admission/courseset.php b/app/controllers/admission/courseset.php
index e694a24..90ae5f5 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 1a4a191..eb4cc16 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 48af5f8..9a68b1a 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 cc2209c..0000000
--- 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 b00c27b..64908ee 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 640e800..ab26cc2 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 daf1246..d4e7a14 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 2094e3a..f380647 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 139c360..00bccee 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 c5d3e4e..a86150b 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 d7f53c1..c0339cc 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 7a36147..5a2e0a9 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 92ef513..afef13a 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 4a60973..5be3f66 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 586f82f..2954602 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 3c02c6c..c2b50f3 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 886ee58..e89a574 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 8715021..85c6e2d 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 4642dec..5a95b85 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 2f57a44..23633d6 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 1568caf..240e12c 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 eb83dfc..5e98513 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 9034319..48d4dfd 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 5f5e885..69a828e 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 6ea65e6..d39f6ab 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 076355d..6a0e691 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 8c6037a..b13887f 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 0000000..2c8c6f2
--- /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 0000000..16102d7
--- /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 0000000..6d1fb30
--- /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 0000000..a0eb8fe
--- /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 0000000..5e4fefd
--- /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 0000000..64df1d9
--- /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 0000000..cb7a645
--- /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 0000000..a6c46de
--- /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 0000000..5d241d0
--- /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 0000000..035a406
--- /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 0000000..0b26f6c
--- /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 0000000..d1d9f33
--- /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 0000000..d934894
--- /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 0000000..ede43cf
--- /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 0000000..94afbe3
--- /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 0000000..42cd583
--- /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 0000000..6f2b0cb
--- /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 0000000..f02a1c6
--- /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 0000000..309da9b
--- /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 a5c1213..44bfd04 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 0000000..ccb0721
--- /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 0000000..e37ad46
--- /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 0000000..7e390cf
--- /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 0000000..82b440c
--- /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 883adc7..2579b2c 100644
--- a/lib/classes/StudipAutoloader.php
+++ b/lib/classes/StudipAutoloader.php
@@ -111,6 +111,19 @@ class StudipAutoloader
}
/**
+ * 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.
*
* @param string $path the path to remove
diff --git a/lib/classes/admission/AdmissionRule.php b/lib/classes/admission/AdmissionRule.php
index 2b826e4..cff3e35 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 = '')
@@ -139,37 +226,6 @@ abstract class AdmissionRule
}
/**
- * 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.
*
* @return Integer
@@ -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 3e5a430..654aa57 100644
--- a/lib/classes/admission/AdmissionUserList.php
+++ b/lib/classes/admission/AdmissionUserList.php
@@ -201,13 +201,24 @@ class AdmissionUserList
}
/**
+ * 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.
*/
public function load()
{
// 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 692422f..fd160d6 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 4b51322..2a34807 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 4b13af1..68b818a 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 df62bbe..4b13511 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 02badc5..d9d978c 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 681ade3..eb4895b 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 0000000..f9e6741
--- /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 0000000..b9691a8
--- /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 0000000..498497a
--- /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 0000000..426c6f2
--- /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 0000000..f23f172
--- /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 0000000..9d59e47
--- /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 0000000..bfa5796
--- /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 0000000..3e050be
--- /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 0000000..9f75b9a
--- /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 0000000..3f0ecf4
--- /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 0000000..76841ec
--- /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 0000000..98abcf8
--- /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 0000000..ebdc397
--- /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 0000000..21c669f
--- /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 0000000..0badb1a
--- /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 57fdfe6..189f6cd 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 .= '&nbsp;' . Icon::create('exclaim-circle', Icon::ROLE_ATTENTION)
+ ->asImg(['title' => _('Niemand erfüllt diese Bedingung.')]);
}
$fieldText .= ')';
}