From f3e00212fae3e46a08ae9fc447ba4426c0255682 Mon Sep 17 00:00:00 2001 From: Ron Lucke <lucke@elan-ev.de> Date: Fri, 24 Mar 2023 07:32:43 +0000 Subject: [PATCH] fix #2126 Closes #2126 Merge request studip/studip!1441 --- lib/classes/JsonApi/Schemas/Course.php | 22 ++++ lib/models/Courseware/Instance.php | 45 +++++++ .../courseware/CoursewareShelfDialogCopy.vue | 114 +++++++++++++----- .../CoursewareStructuralElementDialogCopy.vue | 112 ++++++++++++----- .../CoursewareStructuralElementDialogLink.vue | 3 +- .../CoursewareTasksDialogDistribute.vue | 1 - .../courseware/courseware-shelf.module.js | 22 ++-- .../vue/store/courseware/courseware.module.js | 22 ++-- 8 files changed, 247 insertions(+), 94 deletions(-) diff --git a/lib/classes/JsonApi/Schemas/Course.php b/lib/classes/JsonApi/Schemas/Course.php index 7c034f8f363..acc302ec3d9 100644 --- a/lib/classes/JsonApi/Schemas/Course.php +++ b/lib/classes/JsonApi/Schemas/Course.php @@ -11,6 +11,7 @@ class Course extends SchemaProvider const TYPE = 'courses'; const REL_BLUBBER = 'blubber-threads'; + const REL_COURSEWARE = 'courseware'; const REL_END_SEMESTER = 'end-semester'; const REL_EVENTS = 'events'; const REL_FEEDBACK = 'feedback-elements'; @@ -72,6 +73,7 @@ class Course extends SchemaProvider $relationships = $this->getFilesRelationship($relationships, $course); $relationships = $this->getForumCategoriesRelationship($relationships, $course, $includeList); $relationships = $this->getBlubberRelationship($relationships, $course, $includeList); + $relationships = $this->getCoursewareRelationship($relationships, $course, $includeList); $relationships = $this->getEventsRelationship($relationships, $course, $includeList); $relationships = $this->getFeedbackRelationship($relationships, $course, $includeList); $relationships = $this->getMembershipsRelationship($relationships, $course, $includeList); @@ -187,6 +189,26 @@ class Course extends SchemaProvider return $relationships; } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function getCoursewareRelationship( + array $relationships, + \Course $course, + $includeData + ) { + $relationships[self::REL_COURSEWARE] = [ + self::RELATIONSHIP_DATA => + \Courseware\Instance::existsForRange($course) ? \Courseware\Instance::findForRange($course) : null, + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($course, self::REL_COURSEWARE), + ], + ]; + + return $relationships; + } + + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/lib/models/Courseware/Instance.php b/lib/models/Courseware/Instance.php index 97990f6a58a..66c024c1906 100644 --- a/lib/models/Courseware/Instance.php +++ b/lib/models/Courseware/Instance.php @@ -54,6 +54,51 @@ class Instance $root->delete(); } + /** + * @param \Range $range + * @return ?static + */ + public static function existsForRange(\Range $range): bool + { + switch ($range->getRangeType()) { + case 'course': + case 'user': + $result = \DBManager::get()->fetchOne( + 'SELECT COUNT(*) as count FROM cw_structural_elements WHERE range_id = ? AND range_type = ? AND parent_id IS NULL', + [$range->getRangeId(), $range->getRangeType()] + ); + + return ((int) $result['count']) > 0; + + default: + throw new \InvalidArgumentException('Only ranges of type "user" and "course" are currently supported.'); + } + } + + + /** + * @param \Range $range + * @return ?static + */ + public static function findForRange(\Range $range) + { + $root = null; + switch ($range->getRangeType()) { + case 'course': + $root = StructuralElement::getCoursewareCourse($range->getRangeId()); + break; + case 'user': + $root = StructuralElement::getCoursewareUser($range->getRangeId()); + break; + } + if (!$root) { + return null; + } + + return new self($root); + } + + /** * @var StructuralElement */ diff --git a/resources/vue/components/courseware/CoursewareShelfDialogCopy.vue b/resources/vue/components/courseware/CoursewareShelfDialogCopy.vue index 373fc41ae72..9695df3368b 100644 --- a/resources/vue/components/courseware/CoursewareShelfDialogCopy.vue +++ b/resources/vue/components/courseware/CoursewareShelfDialogCopy.vue @@ -54,38 +54,50 @@ <studip-icon shape="check-circle" size="24" class="check" /> </label> </fieldset> - <label v-if="source === 'courses'"> - <span>{{ $gettext('Veranstaltung') }}</span><span aria-hidden="true" class="wizard-required">*</span> - <studip-select - v-if="courses.length !== 0 && !loadingCourses" - :options="courses" - label="title" - :clearable="false" - :reduce="option => option.id" - v-model="selectedRange" - > - <template #open-indicator="selectAttributes"> - <span v-bind="selectAttributes" - ><studip-icon shape="arr_1down" size="10" - /></span> - </template> - <template #no-options="{}"> - {{ $gettext('Es steht keine Auswahl zur Verfügung.') }} - </template> - <template #selected-option="{ attributes }"> - <span>{{ attributes.title }}</span> - </template> - <template #option="{ attributes }"> - <span>{{ attributes.title }}</span> - </template> - </studip-select> - <p v-if="loadingCourses"> - {{$gettext('Lade Veranstaltungen…')}} - </p> - <p v-if="courses.length === 0 && !loadingCourses"> - {{$gettext('Es wurden keine geeigneten Veranstaltungen gefunden.')}} - </p> - </label> + <template v-if="source === 'courses'"> + <label> + <span>{{ $gettext('Semester') }}</span><span aria-hidden="true"></span> + <select v-model="selectedSemester"> + <option value="all">{{ $gettext('Alle Semester') }}</option> + <option v-for="semester in semesterMap" :key="semester.id" :value="semester.id"> + {{ semester.attributes.title }} + </option> + </select> + </label> + <label> + <span>{{ $gettext('Veranstaltung') }}</span><span aria-hidden="true" class="wizard-required">*</span> + <studip-select + v-if="filteredCourses.length !== 0 && !loadingCourses" + :options="filteredCourses" + label="title" + :clearable="false" + :reduce="option => option.id" + v-model="selectedRange" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes" + ><studip-icon shape="arr_1down" size="10" + /></span> + </template> + <template #no-options="{}"> + {{ $gettext('Es steht keine Auswahl zur Verfügung.') }} + </template> + <template #selected-option="{ attributes }"> + <span>{{ attributes.title }}</span> + </template> + <template #option="{ attributes }"> + <span>{{ attributes.title }}</span> + </template> + </studip-select> + <p v-if="loadingCourses"> + {{$gettext('Lade Veranstaltungen…')}} + </p> + <p v-if="filteredCourses.length === 0 && !loadingCourses"> + {{$gettext('Es wurden keine geeigneten Veranstaltungen gefunden.')}} + </p> + </label> + </template> + </form> </template> <template v-slot:unit> @@ -192,6 +204,8 @@ export default { source: '', loadingCourses: false, courses: [], + semesterMap: [], + selectedSemester: 'all', selectedRange: '', loadingUnits: false, selectedUnit: null, @@ -215,6 +229,7 @@ export default { ...mapGetters({ userId: 'userId', coursewareUnits: 'courseware-units/all', + semesterById: 'semesters/byId', structuralElementById: 'courseware-structural-elements/byId', context: 'context' }), @@ -244,6 +259,16 @@ export default { }, selectedUnitDescription() { return this.selectedUnitElement.attributes.payload.description ?? ''; + }, + filteredCourses() { + const courses = this.courses.filter((course) => { return course.id !== this.context.id}); + if (this.selectedSemester === 'all') { + return courses; + } else { + return courses.filter((course) => { + return course.relationships['start-semester'].data.id === this.selectedSemester; + }); + } } }, async mounted() { @@ -254,6 +279,7 @@ export default { companionSuccess: 'companionSuccess', loadCourseUnits: 'loadCourseUnits', loadUsersCourses: 'loadUsersCourses', + loadSemester: 'semesters/loadById', loadUserUnits: 'loadUserUnits', setShowUnitCopyDialog: 'setShowUnitCopyDialog', copyUnit: 'copyUnit', @@ -286,8 +312,27 @@ export default { async updateCourses() { this.loadingCourses = true; this.courses = await this.loadUsersCourses({ userId: this.userId, withCourseware: true }); + this.loadSemesterMap(); this.loadingCourses = false; }, + loadSemesterMap() { + let view = this; + let semesters = []; + this.courses.every(course => { + let semId = course.relationships['start-semester'].data.id; + if(!semesters.includes(semId)) { + semesters.push(semId); + } + return true; + }); + semesters.every(semester => { + view.loadSemester({id: semester}).then( () => { + view.semesterMap.push(view.semesterById({id: semester})); + view.semesterMap.sort((a, b) => a.attributes.start < b.attributes.start); + }); + return true; + }); + }, async updateCourseUnits(cid) { this.loadingUnits = true; await this.loadCourseUnits(cid); @@ -356,7 +401,10 @@ export default { this.selectedRange = this.userId; break; } + }, + selectedSemester(newSemester) { + this.selectedRange = ''; } } } -</script> \ No newline at end of file +</script> diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue b/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue index 215d86663c9..8eda4d32583 100644 --- a/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElementDialogCopy.vue @@ -53,38 +53,50 @@ <studip-icon shape="check-circle" size="24" class="check" /> </label> </fieldset> - <label v-if="source === 'courses'"> - <span>{{ $gettext('Veranstaltung') }}</span><span aria-hidden="true" class="wizard-required">*</span> - <studip-select - v-if="courses.length !== 0 && !loadingCourses" - :options="courses" - label="title" - :clearable="false" - :reduce="option => option.id" - v-model="selectedRange" - > - <template #open-indicator="selectAttributes"> - <span v-bind="selectAttributes" - ><studip-icon shape="arr_1down" size="10" - /></span> - </template> - <template #no-options="{}"> - {{ $gettext('Es steht keine Auswahl zur Verfügung.') }} - </template> - <template #selected-option="{ attributes }"> - <span>{{ attributes.title }}</span> - </template> - <template #option="{ attributes }"> - <span>{{ attributes.title }}</span> - </template> - </studip-select> - <p v-if="loadingCourses"> - {{$gettext('Lade Veranstaltungen…')}} - </p> - <p v-if="courses.length === 0 && !loadingCourses"> - {{$gettext('Es wurden keine geeigneten Veranstaltungen gefunden.')}} - </p> - </label> + <template v-if="source === 'courses'"> + <label> + <span>{{ $gettext('Semester') }}</span><span aria-hidden="true"></span> + <select v-model="selectedSemester"> + <option value="all">{{ $gettext('Alle Semester') }}</option> + <option v-for="semester in semesterMap" :key="semester.id" :value="semester.id"> + {{ semester.attributes.title }} + </option> + </select> + </label> + <label> + <span>{{ $gettext('Veranstaltung') }}</span><span aria-hidden="true" class="wizard-required">*</span> + <studip-select + v-if="filteredCourses.length !== 0 && !loadingCourses" + :options="filteredCourses" + label="title" + :clearable="false" + :reduce="option => option.id" + v-model="selectedRange" + > + <template #open-indicator="selectAttributes"> + <span v-bind="selectAttributes" + ><studip-icon shape="arr_1down" size="10" + /></span> + </template> + <template #no-options="{}"> + {{ $gettext('Es steht keine Auswahl zur Verfügung.') }} + </template> + <template #selected-option="{ attributes }"> + <span>{{ attributes.title }}</span> + </template> + <template #option="{ attributes }"> + <span>{{ attributes.title }}</span> + </template> + </studip-select> + <p v-if="loadingCourses"> + {{$gettext('Lade Veranstaltungen…')}} + </p> + <p v-if="filteredCourses.length === 0 && !loadingCourses"> + {{$gettext('Es wurden keine geeigneten Veranstaltungen gefunden.')}} + </p> + </label> + </template> + </form> </template> <template v-slot:unit> @@ -239,6 +251,8 @@ export default { source: '', loadingCourses: false, courses: [], + semesterMap: [], + selectedSemester: 'all', selectedRange: '', loadingUnits: false, selectedUnit: null, @@ -261,6 +275,7 @@ export default { ...mapGetters({ userId: 'userId', coursewareUnits: 'courseware-units/all', + semesterById: 'semesters/byId', structuralElementById: 'courseware-structural-elements/byId', context: 'context', childrenById: 'courseware-structure/children', @@ -313,6 +328,16 @@ export default { .map((id) => this.structuralElementById({ id })) .filter(Boolean); }, + filteredCourses() { + const courses = this.courses.filter((course) => { return course.id !== this.context.id}); + if (this.selectedSemester === 'all') { + return courses; + } else { + return courses.filter((course) => { + return course.relationships['start-semester'].data.id === this.selectedSemester; + }); + } + } }, mounted() { this.initWizardData(); @@ -323,6 +348,7 @@ export default { loadCourseUnits: 'loadCourseUnits', loadUserUnits: 'loadUserUnits', loadUsersCourses: 'loadUsersCourses', + loadSemester: 'semesters/loadById', loadStructuralElement: 'courseware-structural-elements/loadById', copyStructuralElement: 'copyStructuralElement', companionError: 'companionError', @@ -339,8 +365,27 @@ export default { async updateCourses() { this.loadingCourses = true; this.courses = await this.loadUsersCourses({ userId: this.userId, withCourseware: true }); + this.loadSemesterMap(); this.loadingCourses = false; }, + loadSemesterMap() { + let view = this; + let semesters = []; + this.courses.every(course => { + let semId = course.relationships['start-semester'].data.id; + if(!semesters.includes(semId)) { + semesters.push(semId); + } + return true; + }); + semesters.every(semester => { + view.loadSemester({id: semester}).then( () => { + view.semesterMap.push(view.semesterById({id: semester})); + view.semesterMap.sort((a, b) => a.attributes.start < b.attributes.start); + }); + return true; + }); + }, async updateCourseUnits(cid) { this.loadingUnits = true; await this.loadCourseUnits(cid); @@ -461,6 +506,9 @@ export default { this.selectedRange = this.userId; break; } + }, + selectedSemester(newSemester) { + this.selectedRange = ''; } } } diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue b/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue index 4703858c4bf..38d3953527b 100644 --- a/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElementDialogLink.vue @@ -184,7 +184,6 @@ export default { ...mapActions({ showElementLinkDialog: 'showElementLinkDialog', loadUserUnits: 'loadUserUnits', - loadUsersCourses: 'loadUsersCourses', loadStructuralElement: 'courseware-structural-elements/loadById', linkStructuralElement: 'linkStructuralElement', companionError: 'companionError', @@ -261,4 +260,4 @@ export default { }, } } -</script> \ No newline at end of file +</script> diff --git a/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue b/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue index 36ad58b01a5..b850558670b 100644 --- a/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue +++ b/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue @@ -542,7 +542,6 @@ export default { setShowTasksDistributeDialog: 'setShowTasksDistributeDialog', loadCourseUnits: 'loadCourseUnits', loadUserUnits: 'loadUserUnits', - loadUsersCourses: 'loadUsersCourses', loadStructuralElement: 'courseware-structural-elements/loadById', copyStructuralElement: 'copyStructuralElement', companionError: 'companionError', diff --git a/resources/vue/store/courseware/courseware-shelf.module.js b/resources/vue/store/courseware/courseware-shelf.module.js index 1b91ff48f78..e1b07747a9b 100644 --- a/resources/vue/store/courseware/courseware-shelf.module.js +++ b/resources/vue/store/courseware/courseware-shelf.module.js @@ -296,7 +296,7 @@ export const actions = { }); const otherMemberships = memberships.filter(({ attributes, relationships }) => { - return ['dozent', 'tutor'].includes(attributes.permission) && state.context.id !== relationships.course.data.id; + return ['dozent', 'tutor'].includes(attributes.permission); }); if (!withCourseware) { @@ -305,20 +305,16 @@ export const actions = { }); } - const items = await Promise.all( - otherMemberships.map((membership) => { - const course = getCourse(membership); + const items = otherMemberships.map((membership) => { + let course = getCourse(membership); + course['userPermission'] = membership.attributes.permission; - return dispatch('loadRemoteCoursewareStructure', { - rangeId: course.id, - rangeType: course.type - }).then((instance) => ({ instance, membership, course })); - }) - ) + return { membership, course }; + }); - return items - .filter(({ instance, membership }) => { - return instance?.relationships?.root && (membership.attributes.permission === 'dozent' || instance.attributes['editing-permission-level'] === 'tutor'); + return items + .filter(({ membership, course }) => { + return course.relationships.courseware; }) .map(({ course }) => course); diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index 996cb37c6d4..7c1f2fec6dd 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -1093,20 +1093,16 @@ export const actions = { }); } - const items = await Promise.all( - otherMemberships.map((membership) => { - const course = getCourse(membership); - - return dispatch('loadRemoteCoursewareStructure', { - rangeId: course.id, - rangeType: course.type - }).then((instance) => ({ instance, membership, course })); - }) - ) + const items = otherMemberships.map((membership) => { + let course = getCourse(membership); + course['userPermission'] = membership.attributes.permission; + + return { membership, course }; + }); - return items - .filter(({ instance, membership }) => { - return instance?.relationships?.root && (membership.attributes.permission === 'dozent' || instance.attributes['editing-permission-level'] === 'tutor'); + return items + .filter(({ membership, course }) => { + return course.relationships.courseware; }) .map(({ course }) => course); -- GitLab