From e6a11cb96f7a0259554580369f41b2caf259c650 Mon Sep 17 00:00:00 2001 From: Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de> Date: Fri, 3 Dec 2021 10:35:40 +0100 Subject: [PATCH] Marry course manager with the caching structure loading. Closes #446. --- .../Courseware/StructuralElementsCopy.php | 34 +++-- .../courseware/CoursewareCourseManager.vue | 3 +- .../CoursewareManagerCopySelector.vue | 11 +- .../courseware/CoursewareManagerElement.vue | 6 +- .../vue/components/courseware/ManagerApp.vue | 16 ++- .../vue/store/courseware/courseware.module.js | 19 ++- .../vue/store/courseware/structure.module.js | 128 +++++++++++------- 7 files changed, 132 insertions(+), 85 deletions(-) diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php index b63628e0114..9622fb66ab1 100755 --- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php @@ -26,24 +26,36 @@ class StructuralElementsCopy extends NonJsonApiController { $data = $request->getParsedBody()['data']; - $remote_element = \Courseware\StructuralElement::find($data['element']['id']); - $parent_element = \Courseware\StructuralElement::find($data['parent_id']); - if (!Authority::canCreateContainer($user = $this->getUser($request), $parent_element)) { + $sourceElement = StructuralElement::find($args['id']); + $newParent = StructuralElement::find($data['parent_id']); + if (!Authority::canCreateContainer($user = $this->getUser($request), $newParent)) { throw new AuthorizationFailedException(); } - $new_element = $this->copyElement($user, $remote_element, $parent_element); + $newElement = $this->copyElement($user, $sourceElement, $newParent); - $response = $response->withHeader('Content-Type', 'application/json'); - $response->getBody()->write((string) json_encode($new_element)); - - return $response; + return $this->redirectToStructuralElement($response, $newElement); } - private function copyElement(\User $user, \Courseware\StructuralElement $remote_element, \Courseware\StructuralElement $parent_element) + private function copyElement(\User $user, StructuralElement $sourceElement, StructuralElement $newParent) { - $new_element = $remote_element->copy($user, $parent_element); + $new_element = $sourceElement->copy($user, $newParent); return $new_element; } -} \ No newline at end of file + + /** + * @SuppressWarnings(PHPMD.Superglobals) + */ + private function redirectToStructuralElement(Response $response, StructuralElement $resource): Response + { + $pathinfo = $this->getSchema($resource) + ->getSelfLink($resource) + ->getStringRepresentation($this->container->get('json-api-integration-urlPrefix')); + $old = \URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']); + $url = \URLHelper::getURL($pathinfo, [], true); + \URLHelper::setBaseURL($old); + + return $response->withHeader('Location', $url)->withStatus(303); + } +} diff --git a/resources/vue/components/courseware/CoursewareCourseManager.vue b/resources/vue/components/courseware/CoursewareCourseManager.vue index 4992fc79c3b..71011cfd93a 100755 --- a/resources/vue/components/courseware/CoursewareCourseManager.vue +++ b/resources/vue/components/courseware/CoursewareCourseManager.vue @@ -88,7 +88,7 @@ </courseware-tab> <courseware-tab :name="$gettext('Kopieren')"> - <courseware-manager-copy-selector @loadSelf="reloadElements"/> + <courseware-manager-copy-selector @loadSelf="reloadElements" @reloadElement="reloadElements" /> </courseware-tab> <courseware-tab :name="$gettext('Importieren')"> @@ -233,6 +233,7 @@ export default { async reloadElements() { await this.setCurrentId(this.currentId); await this.setSelfId(this.selfId); + this.$emit("reload"); }, async setCurrentId(target) { this.currentId = target; diff --git a/resources/vue/components/courseware/CoursewareManagerCopySelector.vue b/resources/vue/components/courseware/CoursewareManagerCopySelector.vue index 602a0b4d2a6..621fe862c03 100755 --- a/resources/vue/components/courseware/CoursewareManagerCopySelector.vue +++ b/resources/vue/components/courseware/CoursewareManagerCopySelector.vue @@ -32,6 +32,7 @@ :currentElement="remoteElement" @selectElement="setRemoteId" @loadSelf="loadSelf" + @reloadElement="reloadElement" /> <courseware-companion-box v-if="remoteId === '' && hasRemoteCid" @@ -106,9 +107,9 @@ export default { }, methods: { ...mapActions({ + loadAnotherCourseware: 'courseware-structure/loadAnotherCourseware', loadUsersCourses: 'loadUsersCourses', loadStructuralElement: 'loadStructuralElement', - loadRemoteCoursewareStructure: 'loadRemoteCoursewareStructure', loadSemester: 'semesters/loadById', }), selectSource(source) { @@ -116,16 +117,15 @@ export default { }, async loadRemoteCourseware(cid) { this.remoteCid = cid; - this.remoteCoursewareInstance = await this.loadRemoteCoursewareStructure({rangeId: this.remoteCid, rangeType: 'courses'}); + this.remoteCoursewareInstance = await this.loadAnotherCourseware({ id: this.remoteCid, type: 'courses'}); if (this.remoteCoursewareInstance !== null) { this.setRemoteId(this.remoteCoursewareInstance.relationships.root.data.id); } else { this.remoteId = ''; } - }, async loadOwnCourseware() { - this.ownCoursewareInstance = await this.loadRemoteCoursewareStructure({rangeId: this.userId, rangeType: 'users'}); + this.ownCoursewareInstance = await this.loadAnotherCourseware({ id: this.userId, type: 'users' }); if (this.ownCoursewareInstance !== null) { this.setOwnId(this.ownCoursewareInstance.relationships.root.data.id); } else { @@ -188,6 +188,9 @@ export default { } return 'seminar'; + }, + reloadElement() { + this.$emit("reloadElement"); } }, async mounted() { diff --git a/resources/vue/components/courseware/CoursewareManagerElement.vue b/resources/vue/components/courseware/CoursewareManagerElement.vue index 14a22822bf8..7c0d3e37569 100755 --- a/resources/vue/components/courseware/CoursewareManagerElement.vue +++ b/resources/vue/components/courseware/CoursewareManagerElement.vue @@ -309,7 +309,7 @@ export default { let element = data.element; if (source === 'self') { element.relationships.parent.data.id = this.filingData.parentItem.id; - element.attributes.position = this.childrenById(this.filingData.parentItem.id).length; + element.attributes.position = this.childrenById(this.filingData.parentItem.id).length + 1; await this.lockObject({ id: element.id, type: 'courseware-structural-elements' }); await this.updateStructuralElement({ element: element, @@ -343,7 +343,7 @@ export default { let container = data.container; if (source === 'self') { container.relationships['structural-element'].data.id = this.filingData.parentItem.id; - container.attributes.position = this.filingData.parentItem.relationships.containers.data.length; + container.attributes.position = this.filingData.parentItem.relationships.containers.data.length + 1; await this.lockObject({id: container.id, type: 'courseware-containers'}); await this.updateContainer({ container: container, @@ -399,7 +399,7 @@ export default { await this.unlockObject({id: destinationContainer.id, type: 'courseware-containers'}); block.relationships.container.data.id = this.filingData.parentItem.id; - block.attributes.position = this.filingData.parentItem.relationships.blocks.data.length; + block.attributes.position = this.filingData.parentItem.relationships.blocks.data.length + 1; await this.lockObject({id: block.id, type: 'courseware-blocks'}); await this.updateBlock({ block: block, diff --git a/resources/vue/components/courseware/ManagerApp.vue b/resources/vue/components/courseware/ManagerApp.vue index 57d71e587b9..ed493e70b2c 100755 --- a/resources/vue/components/courseware/ManagerApp.vue +++ b/resources/vue/components/courseware/ManagerApp.vue @@ -1,5 +1,5 @@ <template> - <courseware-course-manager></courseware-course-manager> + <courseware-course-manager @reload="rebuildStructure"></courseware-course-manager> </template> <script> @@ -20,17 +20,21 @@ export default { invalidateStructureCache: 'courseware-structure/invalidateCache', loadCoursewareStructure: 'courseware-structure/load', }), + async rebuildStructure() { + // compute order of structural elements once more + await this.buildStructure(); + console.debug("built structure") + + // throw away stale cache + this.invalidateStructureCache(); + }, }, async mounted() { await this.loadCoursewareStructure(); }, watch: { async structuralElements(newElements, oldElements) { - // compute order of structural elements once more - await this.buildStructure(); - - // throw away stale cache - this.invalidateStructureCache(); + this.rebuildStructure(); }, }, }; diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index 4111eb2b943..949b574a237 100755 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -317,17 +317,14 @@ export const actions = { // console.log(resp); }); }, - copyStructuralElement({ getters }, { parentId, element }) { - const copy = { - data: { - element: element, - parent_id: parentId, - }, - }; - - return state.httpClient.post(`courseware-structural-elements/${element.id}/copy`, copy).then((resp) => { - // console.log(resp); - }); + copyStructuralElement({ dispatch, getters }, { parentId, element }) { + const copy = { data: { parent_id: parentId, }, }; + + return state.httpClient.post(`courseware-structural-elements/${element.id}/copy`, copy) + .then(({ data }) => { + const id = data.data.id; + dispatch('loadStructuralElement', id); + }); }, lockObject({ dispatch, getters }, { id, type }) { diff --git a/resources/vue/store/courseware/structure.module.js b/resources/vue/store/courseware/structure.module.js index fdfe7627585..0b71b090785 100644 --- a/resources/vue/store/courseware/structure.module.js +++ b/resources/vue/store/courseware/structure.module.js @@ -31,14 +31,21 @@ export const mutations = { const actions = { build({ commit, rootGetters }) { - const structuralElements = rootGetters['courseware-structural-elements/all']; - const root = findRoot(structuralElements); + const instance = rootGetters['courseware']; + if (!instance) { + throw new Error('Could not find current courseware'); + } + const root = rootGetters['courseware-structural-elements/related']({ + parent: { id: instance.id, type: instance.type }, + relationship: 'root', + }); if (!root) { commit('reset'); return; } + const structuralElements = rootGetters['courseware-structural-elements/all']; const children = structuralElements.reduce((memo, element) => { const parent = element.relationships.parent?.data?.id ?? null; if (parent) { @@ -81,43 +88,70 @@ const actions = { } }, + // load the structure of the current courseware async load({ commit, dispatch, rootGetters }) { - const parent = rootGetters['context']; - const relationship = 'courseware'; - const options = { - include: 'bookmarks,root', - }; + const context = rootGetters['context']; + const instance = await dispatch('loadInstance', context); + commit('coursewareSet', instance, { root: true }); - // get courseware instance - await dispatch(`courseware-instances/loadRelated`, { parent, relationship, options }, { root: true }); - const courseware = rootGetters['courseware-instances/all'][0]; - commit('coursewareSet', courseware, { root: true }); + const root = rootGetters['courseware-structural-elements/related']({ + parent: { id: instance.id, type: instance.type }, + relationship: 'root', + }); + if (!root) { + throw new Error(`Could not find root of courseware { id: ${instance.id}, type: ${instance.type}`); + } + + dispatch('fetchDescendantsWithCaching', { root }); - // load descendants - dispatch('fetchDescendants'); + return instance; }, - async fetchDescendants({ dispatch, rootGetters, commit }) { - // get root of that instance - const courseware = rootGetters['courseware']; - if (!courseware) { - return; - } - const rootElement = rootGetters['courseware-structural-elements/related']({ - parent: { id: courseware.id, type: courseware.type }, + // load the structure of a specified courseware + async loadAnotherCourseware({ commit, dispatch, rootGetters }, context) { + const instance = await dispatch('loadInstance', context); + + const root = rootGetters['courseware-structural-elements/related']({ + parent: { id: instance.id, type: instance.type }, relationship: 'root', }); - if (!rootElement) { - return; + if (!root) { + throw new Error(`Could not find root of courseware { id: ${instance.id}, type: ${instance.type}`); } + await dispatch('loadDescendants', { root }); + + return instance; + }, + + loadInstance({ commit, dispatch, rootGetters }, context) { + const parent = context; + const relationship = 'courseware'; + const options = { + include: 'bookmarks,root', + }; + + return dispatch( + `courseware-instances/loadRelated`, + { + parent, + relationship, + options, + }, + { root: true } + ).then(() => { + return rootGetters['courseware-instances/related']({ parent, relationship }); + }); + }, + + async fetchDescendantsWithCaching({ dispatch, rootGetters, commit }, { root }) { const cache = window.STUDIP.Cache.getInstance('courseware'); - const cacheKey = `descendants/${rootElement.id}/${rootGetters['userId']}`; + const cacheKey = `descendants/${root.id}/${rootGetters['userId']}`; - await unpickleDescendants(); - revalidateDescendants(); + await unpickleStaleDescendants(); + return revalidateDescendants(); - function unpickleDescendants() { + function unpickleStaleDescendants() { try { const descendants = cache.get(cacheKey); const cacheHit = descendants !== undefined; @@ -130,22 +164,7 @@ const actions = { } function revalidateDescendants() { - return loadDescendants().then(removeStaleElements).then(pickleDescendants); - } - - function loadDescendants() { - const parent = { id: rootElement.id, type: rootElement.type }; - const relationship = 'descendants'; - const options = { - 'page[offset]': 0, - 'page[limit]': 10000, - }; - - return dispatch( - 'courseware-structural-elements/loadRelated', - { parent, relationship, options }, - { root: true } - ); + return dispatch('loadDescendants', { root }).then(removeStaleElements).then(pickleDescendants); } function pickleDescendants() { @@ -156,9 +175,9 @@ const actions = { function removeStaleElements() { const idsToKeep = [ - rootElement.id, + root.id, ...rootGetters['courseware-structural-elements/related']({ - parent: rootElement, + parent: root, relationship: 'descendants', }).map(({ id }) => id), ]; @@ -168,11 +187,22 @@ const actions = { .forEach((id) => commit('courseware-structural-elements/REMOVE_RECORD', { id }, { root: true })); } }, -}; -function findRoot(nodes) { - return nodes.find((node) => !node.relationships.parent?.data); -} + loadDescendants({ dispatch }, { root }) { + const parent = { id: root.id, type: root.type }; + const relationship = 'descendants'; + const options = { + 'page[offset]': 0, + 'page[limit]': 10000, + }; + + return dispatch( + 'courseware-structural-elements/loadRelated', + { parent, relationship, options }, + { root: true } + ); + }, +}; function* visitTree(tree, current) { if (current) { -- GitLab