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