From 4b946d95b8a1a84614e52168ea73f20dc0f57623 Mon Sep 17 00:00:00 2001 From: Thomas Hackl Date: Mon, 25 Nov 2024 07:04:02 +0000 Subject: Resolve "Umbau der Verwaltung von Anmeldesets auf Vue.js" Closes #3270 Merge request studip/studip!2413 --- app/controllers/admission/courseset.php | 59 ++ app/controllers/admission/restricted_courses.php | 2 +- .../admission/courseset/_institute_choose.php | 15 +- app/views/admission/courseset/configure.php | 268 -------- app/views/admission/rule/configure.php | 15 - .../conditionaladmission/ConditionalAdmission.php | 120 +++- .../conditionaladmission/templates/configure.php | 83 +-- .../conditionaladmission/templates/info.php | 20 +- .../CourseMemberAdmission.php | 31 +- .../coursememberadmission/templates/configure.php | 128 +--- .../limitedadmission/LimitedAdmission.php | 19 +- .../limitedadmission/templates/configure.php | 10 +- .../lockedadmission/LockedAdmission.php | 19 +- .../lockedadmission/templates/configure.php | 8 +- .../ParticipantRestrictedAdmission.php | 33 +- .../templates/configure.php | 35 +- .../templates/info.php | 8 +- .../passwordadmission/PasswordAdmission.php | 19 +- .../passwordadmission/templates/configure.php | 18 +- .../PreferentialAdmission.php | 59 +- .../preferentialadmission/templates/configure.php | 39 +- .../termsadmission/TermsAdmission.php | 21 +- .../termsadmission/templates/configure.php | 7 +- .../timedadmission/TimedAdmission.php | 42 +- .../timedadmission/templates/configure.php | 50 +- lib/classes/CoursesetModel.php | 4 +- lib/classes/JsonApi/RouteMap.php | 26 + .../Routes/Admission/AdmissionRulesCreate.php | 50 ++ .../Routes/Admission/AdmissionRulesDelete.php | 38 ++ .../Routes/Admission/AdmissionRulesIndex.php | 24 + .../Routes/Admission/AdmissionRulesShow.php | 33 + .../Routes/Admission/AdmissionRulesUpdate.php | 60 ++ lib/classes/JsonApi/Routes/Admission/Authority.php | 75 +++ .../Routes/Admission/AvailableCoursesIndex.php | 40 ++ .../JsonApi/Routes/Admission/CourseSetsCreate.php | 68 ++ .../JsonApi/Routes/Admission/CourseSetsDelete.php | 37 ++ .../JsonApi/Routes/Admission/CourseSetsShow.php | 35 + .../JsonApi/Routes/Admission/CourseSetsUpdate.php | 73 +++ .../Routes/Admission/RuleCompatibilityIndex.php | 20 + .../JsonApi/Routes/UserFilters/Authority.php | 18 + .../Routes/UserFilters/UserFilterFieldsIndex.php | 30 + .../Routes/UserFilters/UserFilterFieldsShow.php | 35 + .../Routes/UserFilters/UserFiltersCreate.php | 61 ++ .../Routes/UserFilters/UserFiltersDelete.php | 38 ++ .../JsonApi/Routes/UserFilters/UserFiltersShow.php | 31 + .../Routes/UserFilters/UserFiltersUpdate.php | 68 ++ lib/classes/JsonApi/SchemaMap.php | 5 + lib/classes/JsonApi/Schemas/AdmissionRule.php | 34 + lib/classes/JsonApi/Schemas/CourseSet.php | 166 +++++ lib/classes/JsonApi/Schemas/UserFilter.php | 45 ++ lib/classes/JsonApi/Schemas/UserFilterField.php | 67 ++ lib/classes/StudipAutoloader.php | 13 + lib/classes/admission/AdmissionRule.php | 131 +++- lib/classes/admission/AdmissionUserList.php | 13 +- lib/classes/admission/UserFilter.php | 1 + lib/classes/admission/UserFilterField.php | 36 +- .../assets/javascripts/bootstrap/admission.js | 31 +- resources/assets/javascripts/lib/admission.js | 16 + resources/assets/stylesheets/scss/admission.scss | 64 ++ resources/assets/stylesheets/studip.scss | 2 +- resources/vue/components/StudipUserFilter.vue | 128 ++++ .../components/admission/AdmissionRuleConfig.vue | 89 +++ .../admission/AdmissionRuleTypeSelector.vue | 113 ++++ .../components/admission/ConditionalAdmission.vue | 194 ++++++ .../components/admission/ConfigureCourseSet.vue | 712 +++++++++++++++++++++ .../components/admission/CourseMemberAdmission.vue | 113 ++++ .../vue/components/admission/LimitedAdmission.vue | 65 ++ .../vue/components/admission/LockedAdmission.vue | 42 ++ .../admission/ParticipantRestrictedAdmission.vue | 91 +++ .../vue/components/admission/PasswordAdmission.vue | 83 +++ .../components/admission/PreferentialAdmission.vue | 120 ++++ .../vue/components/admission/TermsAdmission.vue | 57 ++ .../vue/components/admission/TimedAdmission.vue | 83 +++ .../vue/components/admission/ValidityTime.vue | 69 ++ resources/vue/mixins/AdmissionRuleMixin.js | 61 ++ templates/userfilter/display.php | 7 +- 76 files changed, 3872 insertions(+), 771 deletions(-) delete mode 100644 app/views/admission/courseset/configure.php create mode 100644 lib/classes/JsonApi/Routes/Admission/AdmissionRulesCreate.php create mode 100644 lib/classes/JsonApi/Routes/Admission/AdmissionRulesDelete.php create mode 100644 lib/classes/JsonApi/Routes/Admission/AdmissionRulesIndex.php create mode 100644 lib/classes/JsonApi/Routes/Admission/AdmissionRulesShow.php create mode 100644 lib/classes/JsonApi/Routes/Admission/AdmissionRulesUpdate.php create mode 100644 lib/classes/JsonApi/Routes/Admission/Authority.php create mode 100644 lib/classes/JsonApi/Routes/Admission/AvailableCoursesIndex.php create mode 100644 lib/classes/JsonApi/Routes/Admission/CourseSetsCreate.php create mode 100644 lib/classes/JsonApi/Routes/Admission/CourseSetsDelete.php create mode 100644 lib/classes/JsonApi/Routes/Admission/CourseSetsShow.php create mode 100644 lib/classes/JsonApi/Routes/Admission/CourseSetsUpdate.php create mode 100644 lib/classes/JsonApi/Routes/Admission/RuleCompatibilityIndex.php create mode 100644 lib/classes/JsonApi/Routes/UserFilters/Authority.php create mode 100644 lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsIndex.php create mode 100644 lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsShow.php create mode 100644 lib/classes/JsonApi/Routes/UserFilters/UserFiltersCreate.php create mode 100644 lib/classes/JsonApi/Routes/UserFilters/UserFiltersDelete.php create mode 100644 lib/classes/JsonApi/Routes/UserFilters/UserFiltersShow.php create mode 100644 lib/classes/JsonApi/Routes/UserFilters/UserFiltersUpdate.php create mode 100644 lib/classes/JsonApi/Schemas/AdmissionRule.php create mode 100644 lib/classes/JsonApi/Schemas/CourseSet.php create mode 100644 lib/classes/JsonApi/Schemas/UserFilter.php create mode 100644 lib/classes/JsonApi/Schemas/UserFilterField.php create mode 100644 resources/vue/components/StudipUserFilter.vue create mode 100644 resources/vue/components/admission/AdmissionRuleConfig.vue create mode 100644 resources/vue/components/admission/AdmissionRuleTypeSelector.vue create mode 100644 resources/vue/components/admission/ConditionalAdmission.vue create mode 100644 resources/vue/components/admission/ConfigureCourseSet.vue create mode 100644 resources/vue/components/admission/CourseMemberAdmission.vue create mode 100644 resources/vue/components/admission/LimitedAdmission.vue create mode 100644 resources/vue/components/admission/LockedAdmission.vue create mode 100644 resources/vue/components/admission/ParticipantRestrictedAdmission.vue create mode 100644 resources/vue/components/admission/PasswordAdmission.vue create mode 100644 resources/vue/components/admission/PreferentialAdmission.vue create mode 100644 resources/vue/components/admission/TermsAdmission.vue create mode 100644 resources/vue/components/admission/TimedAdmission.vue create mode 100644 resources/vue/components/admission/ValidityTime.vue create mode 100644 resources/vue/mixins/AdmissionRuleMixin.js 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 @@
(: - + | - + | - + )
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 @@ -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']); -} -?> - -

-
-
- - - isUserAllowedToEdit($GLOBALS['user']->id) && !$instant_course_set_view)) : ?> - - getPrivate() ? ' checked="checked"' : '') : 'checked' ?>/> - - - - -
- getUserId()) ?> - - - getFullName()) ?> (username) ?>) - - - - -
- - - have_perm('admin') || $GLOBALS['perm']->have_perm('dozent') && Config::get()->ALLOW_DOZENT_COURSESET_ADMIN) : ?> -
- - - - - - - - class="institute" onclick="STUDIP.Admission.getCourses( - 'url_for('admission/courseset/instcourses', $courseset ? $courseset->getId() : '') ?>')"/> - - - - -
- - - - -
- 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')" - ]) ?> - - asImg(['title' => _("Suche starten")])?> -
- - - - - -
- - orderBy('Name') as $institute) : ?> - -
- - -
-
- - - - -
- -
- -
- url_for('admission/courseset/configure_courses/' . $courseset->getId()), - ['data-dialog' => 'size=big', 'class' => 'autosave'] - ); ?> - getNumApplicants()) :?> - url_for('admission/courseset/applications_list/' . $courseset->getId()), - ['data-dialog' => '', 'class' => 'autosave'] - ); ?> - url_for('admission/courseset/applicants_message/' . $courseset->getId()), - ['data-dialog' => '', 'class' => 'autosave'] - ); ?> - -
- - - 100) :?> - - - getFullName('number-name-semester')); - echo '
'; - }, - "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], - ) - ?> - - -
-
- -
- -
- getAdmissionRules() as $rule) { ?> - render_partial('admission/rule/save', ['rule' => $rule]) ?> - -
- - - - -
- -
- url_for('admission/rule/select_type' . ($courseset ? '/'.$courseset->getId() : '')), - [ - 'onclick' => "return STUDIP.Admission.selectRuleType(this)" - ] - ); ?> -
-
-
- -
- - - - getSeatDistributionTime()) :?> - - - getId(), $userlistIds)) { - $checked = ' checked="checked"'; - } - ?> - /> getName() ?>
- - - - - -
- url_for('admission/courseset/factored_users/' . $courseset->getId()), - ['data-dialog' => ''] - ); ?> -
- - - - - - - -
- -
- - ''] : []) ?> - - getId(), - ['really' => 1])) ?> - - url_for('admission/courseset')) ?> - -
- -
- - - 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; */ ?>
-
-
- - - -
-
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 @@ - - -

getName()) ?>

- -
- - -
- - - asImg() ?> - - - -
- -
- - - -
- conditiongroupsAllowed()): ?> - - - removeConditiongroups(); ?> -
- -
-
- getUngroupedConditions() as $condition): ?> - show_user_count = true; ?> -
- conditiongroupsAllowed()): ?> - - - toString() ?> - - asImg(); ?> - - -
- -
-
- conditiongroupsAllowed()): ?> - - 'group_conditions', 'onclick' => 'return STUDIP.UserFilter.groupConditions()', 'style' => $rule->getUngroupedConditions() ? '' : 'display: none']) ?> - getConditiongroups() as $conditiongroup_id => $conditiongroup): ?> -
-
- - - show_user_count = true; ?> -
- - toString() ?> - - asImg(); ?> - - -
- -
- 'ungroup_conditions', 'onclick' => 'return STUDIP.UserFilter.ungroupConditions(this)']) ?> -
- - -
+
+
- -
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:') ?>
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 @@ -

getName()) ?>

- - - - - - - - - -

- getFullName('number-name-semester'));?> - - asImg([ - 'title' =>_('Veranstaltungsdetails aufrufen') - ]) ?> - -

- - - -
- - +
+
- - - -
- - fireJSFunctionOnSelect('addcourse') - ->setInputStyle('flex: 0 0 40%') - ->render(); - ?> - -
- 'search_sem_sem'], - Semester::getIndexById($_SESSION['_default_sem'], false, !$GLOBALS['perm']->have_perm('admin')), - 'key', - false - )?> -
-

-
    - -
  • - - name) ?> - - - -
  • - -
-
- - 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 @@ -

getName() ?>

- -
- +
+ +
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 @@ -

getName() ?>

- - \ No newline at end of file +
+ +
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 @@ -

getName() ?>

- - - - - - -isFCFSallowed()) : ?> - - - +
+ +
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 @@ getDistributionTime()) : ?> getDistributionTime() > time()) : ?> getDistributionTime()), + 'um %s verteilt.'), date("d.m.Y", $rule->getDistributionTime()), date("H:i", $rule->getDistributionTime())) ?> 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())) ?> -isFCFSallowed()) :?> +isFCFSAllowed()) :?> - \ No newline at end of file + 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 @@ -

getName()) ?>

- - - +
+ +
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 @@ -

getName()) ?>

- -
- - - -
-
-
- getConditions() as $condition) : - $condition->show_user_count = true; ?> - -
- toString() ?> - - asImg(); ?> - -
- -
-
-
-

- - asImg(['title' => _('Bedingung hinzufügen'), 'alt' => _('Bedingung hinzufügen')]) ?> - - +
+
-
- 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 @@ - +
+ +
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 @@ -

getName() ?>

- - - - - - - - - - - - +
+ +
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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + $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 @@ + 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 @@ +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 @@ +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 @@ + 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 $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 $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} + */ + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 .= ' ' . Icon::create('exclaim-circle', Icon::ROLE_ATTENTION) + ->asImg(['title' => _('Niemand erfüllt diese Bedingung.')]); } $fieldText .= ')'; } -- cgit v1.0