From 333b1181662f26afe13256b26c6c87605d627d10 Mon Sep 17 00:00:00 2001 From: Ron Lucke Date: Mon, 7 Feb 2022 14:27:25 +0000 Subject: StEP 00357 --- app/controllers/admin/courseware.php | 34 ++ app/controllers/contents/courseware.php | 86 +-- app/controllers/course/courseware.php | 24 +- app/routes/Activity.php | 11 + app/views/admin/courseware/admin_action_widget.php | 1 + app/views/admin/courseware/admin_view_widget.php | 1 + app/views/admin/courseware/index.php | 1 + .../contents/courseware/bookmark_filter_widget.php | 1 + app/views/contents/courseware/bookmarks.php | 38 +- app/views/contents/courseware/courses_overview.php | 2 +- app/views/contents/courseware/index.php | 51 +- .../contents/courseware/overview_action_widget.php | 1 + .../contents/courseware/overview_filter_widget.php | 1 + app/views/course/courseware/dashboard.php | 11 +- .../course/courseware/dashboard_view_widget.php | 1 + db/migrations/5.1.17_add_courseware_templates.php | 34 ++ db/migrations/5.1.18_add_courseware_tasks.php | 78 +++ ...dd_courseware_structural_element_discussion.php | 49 ++ lib/activities/Activity.php | 1 + lib/activities/ActivityObserver.php | 34 ++ lib/activities/Context.php | 42 +- lib/activities/CourseContext.php | 3 + lib/activities/CoursewareProvider.php | 244 +++++++- lib/activities/Filter.php | 78 ++- lib/classes/JsonApi/RouteMap.php | 40 ++ .../JsonApi/Routes/Courseware/Authority.php | 178 +++++- .../Routes/Courseware/BlockCommentsCreate.php | 3 + .../Routes/Courseware/BlockFeedbacksCreate.php | 3 + .../Routes/Courseware/BlockFeedbacksDelete.php | 33 ++ .../Routes/Courseware/BlockFeedbacksUpdate.php | 113 ++++ .../JsonApi/Routes/Courseware/BlocksCreate.php | 2 + .../JsonApi/Routes/Courseware/BlocksUpdate.php | 3 + .../BookmarkedStructuralElementsIndex.php | 2 +- .../JsonApi/Routes/Courseware/ContainersUpdate.php | 2 + .../Courseware/CoursewareInstancesHelper.php | 5 +- .../Rel/BookmarkedStructuralElements.php | 4 + .../Rel/UsersBookmarkedStructuralElements.php | 180 ++++++ .../Courseware/StructuralElementCommentsCreate.php | 86 +++ .../Courseware/StructuralElementCommentsDelete.php | 33 ++ ...ralElementCommentsOfStructuralElementsIndex.php | 35 ++ .../Courseware/StructuralElementCommentsShow.php | 34 ++ .../Courseware/StructuralElementCommentsUpdate.php | 113 ++++ .../Courseware/StructuralElementFeedbackCreate.php | 86 +++ .../Courseware/StructuralElementFeedbackDelete.php | 33 ++ ...ralElementFeedbackOfStructuralElementsIndex.php | 38 ++ .../Courseware/StructuralElementFeedbackShow.php | 36 ++ .../Courseware/StructuralElementFeedbackUpdate.php | 113 ++++ .../Routes/Courseware/StructuralElementsCopy.php | 3 + .../Routes/Courseware/StructuralElementsCreate.php | 41 +- .../Routes/Courseware/TaskFeedbackCreate.php | 89 +++ .../Routes/Courseware/TaskFeedbackDelete.php | 36 ++ .../JsonApi/Routes/Courseware/TaskFeedbackShow.php | 38 ++ .../Routes/Courseware/TaskFeedbackUpdate.php | 84 +++ .../JsonApi/Routes/Courseware/TaskGroupsCreate.php | 212 +++++++ .../JsonApi/Routes/Courseware/TaskGroupsShow.php | 46 ++ .../JsonApi/Routes/Courseware/TasksDelete.php | 39 ++ .../JsonApi/Routes/Courseware/TasksIndex.php | 106 ++++ .../JsonApi/Routes/Courseware/TasksShow.php | 46 ++ .../JsonApi/Routes/Courseware/TasksUpdate.php | 118 ++++ .../JsonApi/Routes/Courseware/TemplatesCreate.php | 88 +++ .../JsonApi/Routes/Courseware/TemplatesDelete.php | 33 ++ .../JsonApi/Routes/Courseware/TemplatesIndex.php | 32 + .../JsonApi/Routes/Courseware/TemplatesShow.php | 34 ++ .../JsonApi/Routes/Courseware/TemplatesUpdate.php | 80 +++ .../UsersBookmarkedStructuralElementsIndex.php | 55 ++ lib/classes/JsonApi/SchemaMap.php | 6 + .../Schemas/Courseware/StructuralElement.php | 64 +- .../Courseware/StructuralElementComment.php | 62 ++ .../Courseware/StructuralElementFeedback.php | 62 ++ lib/classes/JsonApi/Schemas/Courseware/Task.php | 85 +++ .../JsonApi/Schemas/Courseware/TaskFeedback.php | 61 ++ .../JsonApi/Schemas/Courseware/TaskGroup.php | 104 ++++ .../JsonApi/Schemas/Courseware/Template.php | 44 ++ lib/classes/JsonApi/Schemas/User.php | 18 + lib/models/Courseware/Block.php | 15 + lib/models/Courseware/BlockComment.php | 16 + lib/models/Courseware/BlockFeedback.php | 16 + lib/models/Courseware/BlockTypes/BlockType.php | 15 +- lib/models/Courseware/BlockTypes/Code.php | 8 + lib/models/Courseware/BlockTypes/Confirm.php | 8 + lib/models/Courseware/BlockTypes/Date.php | 8 + lib/models/Courseware/BlockTypes/Headline.php | 9 + lib/models/Courseware/BlockTypes/KeyPoint.php | 8 + lib/models/Courseware/BlockTypes/Link.php | 9 + lib/models/Courseware/BlockTypes/Text.php | 8 + lib/models/Courseware/BlockTypes/Typewriter.php | 8 + lib/models/Courseware/Bookmark.php | 6 +- .../ContainerTypes/AccordionContainer.php | 24 + .../Courseware/ContainerTypes/ContainerType.php | 11 + .../Courseware/ContainerTypes/ListContainer.php | 20 + .../Courseware/ContainerTypes/TabsContainer.php | 24 + lib/models/Courseware/StructuralElement.php | 160 ++++- lib/models/Courseware/StructuralElementComment.php | 42 ++ .../Courseware/StructuralElementFeedback.php | 42 ++ lib/models/Courseware/Task.php | 217 +++++++ lib/models/Courseware/TaskFeedback.php | 58 ++ lib/models/Courseware/TaskGroup.php | 61 ++ lib/models/Courseware/Template.php | 28 + lib/navigation/AdminNavigation.php | 7 + lib/navigation/ContentsNavigation.php | 2 +- package.json | 1 + public/assets/images/icons/black/bullet-arrow.svg | 1 + public/assets/images/icons/black/bullet-dot.svg | 1 + .../images/icons/black/bullet-double-arrow.svg | 1 + public/assets/images/icons/black/bullet-line.svg | 1 + .../assets/images/icons/black/category-draft.svg | 1 + .../assets/images/icons/black/category-others.svg | 1 + .../images/icons/black/category-portfolio.svg | 1 + public/assets/images/icons/black/category-task.svg | 1 + .../images/icons/black/category-template.svg | 1 + public/assets/images/icons/black/content2.svg | 1 + public/assets/images/icons/blue/bullet-arrow.svg | 1 + public/assets/images/icons/blue/bullet-dot.svg | 1 + .../images/icons/blue/bullet-double-arrow.svg | 1 + public/assets/images/icons/blue/bullet-line.svg | 1 + public/assets/images/icons/blue/category-draft.svg | 1 + .../assets/images/icons/blue/category-others.svg | 1 + .../images/icons/blue/category-portfolio.svg | 1 + public/assets/images/icons/blue/category-task.svg | 1 + .../assets/images/icons/blue/category-template.svg | 1 + public/assets/images/icons/blue/content2.svg | 2 +- public/assets/images/icons/green/bullet-arrow.svg | 1 + public/assets/images/icons/green/bullet-dot.svg | 1 + .../images/icons/green/bullet-double-arrow.svg | 1 + public/assets/images/icons/green/bullet-line.svg | 1 + .../assets/images/icons/green/category-draft.svg | 1 + .../assets/images/icons/green/category-others.svg | 1 + .../images/icons/green/category-portfolio.svg | 1 + public/assets/images/icons/green/category-task.svg | 1 + .../images/icons/green/category-template.svg | 1 + public/assets/images/icons/green/content2.svg | 1 + public/assets/images/icons/grey/bullet-arrow.svg | 1 + public/assets/images/icons/grey/bullet-dot.svg | 1 + .../images/icons/grey/bullet-double-arrow.svg | 1 + public/assets/images/icons/grey/bullet-line.svg | 1 + public/assets/images/icons/grey/category-draft.svg | 1 + .../assets/images/icons/grey/category-others.svg | 1 + .../images/icons/grey/category-portfolio.svg | 1 + public/assets/images/icons/grey/category-task.svg | 1 + .../assets/images/icons/grey/category-template.svg | 1 + public/assets/images/icons/grey/content2.svg | 1 + public/assets/images/icons/red/bullet-arrow.svg | 1 + public/assets/images/icons/red/bullet-dot.svg | 1 + .../images/icons/red/bullet-double-arrow.svg | 1 + public/assets/images/icons/red/bullet-line.svg | 1 + public/assets/images/icons/red/category-draft.svg | 1 + public/assets/images/icons/red/category-others.svg | 1 + .../assets/images/icons/red/category-portfolio.svg | 1 + public/assets/images/icons/red/category-task.svg | 1 + .../assets/images/icons/red/category-template.svg | 1 + public/assets/images/icons/red/content2.svg | 1 + public/assets/images/icons/white/bullet-arrow.svg | 1 + public/assets/images/icons/white/bullet-dot.svg | 1 + .../images/icons/white/bullet-double-arrow.svg | 1 + public/assets/images/icons/white/bullet-line.svg | 1 + .../assets/images/icons/white/category-draft.svg | 1 + .../assets/images/icons/white/category-others.svg | 1 + .../images/icons/white/category-portfolio.svg | 1 + public/assets/images/icons/white/category-task.svg | 1 + .../images/icons/white/category-template.svg | 1 + public/assets/images/icons/white/content2.svg | 1 + public/assets/images/icons/yellow/bullet-arrow.svg | 1 + public/assets/images/icons/yellow/bullet-dot.svg | 1 + .../images/icons/yellow/bullet-double-arrow.svg | 1 + public/assets/images/icons/yellow/bullet-line.svg | 1 + .../assets/images/icons/yellow/category-draft.svg | 1 + .../assets/images/icons/yellow/category-others.svg | 1 + .../images/icons/yellow/category-portfolio.svg | 1 + .../assets/images/icons/yellow/category-task.svg | 1 + .../images/icons/yellow/category-template.svg | 1 + public/assets/images/icons/yellow/content2.svg | 1 + .../core/ActivityFeed/ActivityFeed.php | 3 +- .../assets/javascripts/bootstrap/courseware.js | 33 ++ resources/assets/stylesheets/scss/courseware.scss | 658 ++++++++++++++++++--- resources/vue/components/courseware/AdminApp.vue | 33 ++ .../components/courseware/ContentBookmarkApp.vue | 21 + .../components/courseware/ContentOverviewApp.vue | 25 + .../courseware/CoursewareAccordionContainer.vue | 51 +- .../courseware/CoursewareActionWidget.vue | 74 ++- .../courseware/CoursewareActivityItem.vue | 40 +- .../courseware/CoursewareAdminActionWidget.vue | 30 + .../courseware/CoursewareAdminTemplates.vue | 237 ++++++++ .../courseware/CoursewareAdminViewWidget.vue | 31 + .../courseware/CoursewareBlockActions.vue | 21 +- .../courseware/CoursewareBlockComments.vue | 72 ++- .../courseware/CoursewareBlockDiscussion.vue | 60 ++ .../courseware/CoursewareBlockFeedback.vue | 72 ++- .../components/courseware/CoursewareChartBlock.vue | 2 +- .../courseware/CoursewareCollapsibleBox.vue | 5 + .../courseware/CoursewareContainerActions.vue | 12 +- .../CoursewareContentBookmarkFilterWidget.vue | 42 ++ .../courseware/CoursewareContentBookmarks.vue | 107 ++++ .../CoursewareContentOverviewActionWidget.vue | 23 + .../CoursewareContentOverviewElements.vue | 535 +++++++++++++++++ .../CoursewareContentOverviewFilterWidget.vue | 51 ++ .../courseware/CoursewareCourseDashboard.vue | 86 ++- .../courseware/CoursewareCourseManager.vue | 265 +++++---- .../courseware/CoursewareDashboardActivities.vue | 99 +++- .../courseware/CoursewareDashboardProgress.vue | 9 +- .../courseware/CoursewareDashboardStudents.vue | 377 ++++++++++++ .../courseware/CoursewareDashboardTasks.vue | 264 +++++++++ .../courseware/CoursewareDashboardViewWidget.vue | 59 ++ .../components/courseware/CoursewareDateInput.vue | 34 ++ .../courseware/CoursewareDefaultBlock.vue | 66 +-- .../courseware/CoursewareDefaultBlockElements.vue | 4 - .../courseware/CoursewareDefaultContainer.vue | 21 + .../courseware/CoursewareHeadlineBlock.vue | 8 +- .../courseware/CoursewareImageMapBlock.vue | 2 +- .../courseware/CoursewareKeyPointBlock.vue | 4 +- .../courseware/CoursewareListContainer.vue | 80 ++- .../CoursewareManagerTaskDistributor.vue | 316 ++++++++++ .../vue/components/courseware/CoursewareOblong.vue | 12 +- .../courseware/CoursewareStructuralElement.vue | 245 +++++++- .../CoursewareStructuralElementComments.vue | 128 ++++ .../CoursewareStructuralElementDiscussion.vue | 60 ++ .../CoursewareStructuralElementFeedback.vue | 130 ++++ .../courseware/CoursewareTableOfContentsBlock.vue | 23 +- .../courseware/CoursewareTabsContainer.vue | 62 +- .../components/courseware/CoursewareTreeItem.vue | 122 +++- .../components/courseware/CoursewareViewWidget.vue | 45 +- .../vue/components/courseware/DashboardApp.vue | 29 +- resources/vue/components/courseware/IndexApp.vue | 4 +- resources/vue/courseware-admin-app.js | 42 ++ resources/vue/courseware-content-bookmark-app.js | 81 +++ resources/vue/courseware-content-overview-app.js | 97 +++ resources/vue/courseware-dashboard-app.js | 81 +++ resources/vue/courseware-index-app.js | 5 + resources/vue/courseware-manager-app.js | 2 + resources/vue/mixins/courseware/task-helper.js | 66 +++ .../store/courseware/courseware-admin.module.js | 47 ++ .../vue/store/courseware/courseware.module.js | 259 +++++++- 231 files changed, 9410 insertions(+), 652 deletions(-) create mode 100755 app/controllers/admin/courseware.php create mode 100755 app/views/admin/courseware/admin_action_widget.php create mode 100755 app/views/admin/courseware/admin_view_widget.php create mode 100755 app/views/admin/courseware/index.php create mode 100755 app/views/contents/courseware/bookmark_filter_widget.php create mode 100755 app/views/contents/courseware/overview_action_widget.php create mode 100755 app/views/contents/courseware/overview_filter_widget.php create mode 100755 app/views/course/courseware/dashboard_view_widget.php create mode 100755 db/migrations/5.1.17_add_courseware_templates.php create mode 100755 db/migrations/5.1.18_add_courseware_tasks.php create mode 100755 db/migrations/5.1.19_add_courseware_structural_element_discussion.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksDelete.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksUpdate.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsCreate.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsDelete.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsOfStructuralElementsIndex.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsShow.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsUpdate.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackCreate.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackDelete.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackOfStructuralElementsIndex.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackShow.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackUpdate.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TaskFeedbackCreate.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TaskFeedbackDelete.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TaskFeedbackShow.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TaskFeedbackUpdate.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php create mode 100644 lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TasksDelete.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TasksIndex.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TasksShow.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TemplatesCreate.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TemplatesDelete.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TemplatesIndex.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TemplatesShow.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/TemplatesUpdate.php create mode 100755 lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php create mode 100755 lib/classes/JsonApi/Schemas/Courseware/StructuralElementComment.php create mode 100755 lib/classes/JsonApi/Schemas/Courseware/StructuralElementFeedback.php create mode 100755 lib/classes/JsonApi/Schemas/Courseware/Task.php create mode 100755 lib/classes/JsonApi/Schemas/Courseware/TaskFeedback.php create mode 100755 lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php create mode 100755 lib/classes/JsonApi/Schemas/Courseware/Template.php create mode 100755 lib/models/Courseware/StructuralElementComment.php create mode 100755 lib/models/Courseware/StructuralElementFeedback.php create mode 100755 lib/models/Courseware/Task.php create mode 100755 lib/models/Courseware/TaskFeedback.php create mode 100644 lib/models/Courseware/TaskGroup.php create mode 100755 lib/models/Courseware/Template.php create mode 100644 public/assets/images/icons/black/bullet-arrow.svg create mode 100644 public/assets/images/icons/black/bullet-dot.svg create mode 100644 public/assets/images/icons/black/bullet-double-arrow.svg create mode 100644 public/assets/images/icons/black/bullet-line.svg create mode 100644 public/assets/images/icons/black/category-draft.svg create mode 100644 public/assets/images/icons/black/category-others.svg create mode 100644 public/assets/images/icons/black/category-portfolio.svg create mode 100644 public/assets/images/icons/black/category-task.svg create mode 100644 public/assets/images/icons/black/category-template.svg create mode 100644 public/assets/images/icons/black/content2.svg create mode 100644 public/assets/images/icons/blue/bullet-arrow.svg create mode 100644 public/assets/images/icons/blue/bullet-dot.svg create mode 100644 public/assets/images/icons/blue/bullet-double-arrow.svg create mode 100644 public/assets/images/icons/blue/bullet-line.svg create mode 100644 public/assets/images/icons/blue/category-draft.svg create mode 100644 public/assets/images/icons/blue/category-others.svg create mode 100644 public/assets/images/icons/blue/category-portfolio.svg create mode 100644 public/assets/images/icons/blue/category-task.svg create mode 100644 public/assets/images/icons/blue/category-template.svg create mode 100644 public/assets/images/icons/green/bullet-arrow.svg create mode 100644 public/assets/images/icons/green/bullet-dot.svg create mode 100644 public/assets/images/icons/green/bullet-double-arrow.svg create mode 100644 public/assets/images/icons/green/bullet-line.svg create mode 100644 public/assets/images/icons/green/category-draft.svg create mode 100644 public/assets/images/icons/green/category-others.svg create mode 100644 public/assets/images/icons/green/category-portfolio.svg create mode 100644 public/assets/images/icons/green/category-task.svg create mode 100644 public/assets/images/icons/green/category-template.svg create mode 100644 public/assets/images/icons/green/content2.svg create mode 100644 public/assets/images/icons/grey/bullet-arrow.svg create mode 100644 public/assets/images/icons/grey/bullet-dot.svg create mode 100644 public/assets/images/icons/grey/bullet-double-arrow.svg create mode 100644 public/assets/images/icons/grey/bullet-line.svg create mode 100644 public/assets/images/icons/grey/category-draft.svg create mode 100644 public/assets/images/icons/grey/category-others.svg create mode 100644 public/assets/images/icons/grey/category-portfolio.svg create mode 100644 public/assets/images/icons/grey/category-task.svg create mode 100644 public/assets/images/icons/grey/category-template.svg create mode 100644 public/assets/images/icons/grey/content2.svg create mode 100644 public/assets/images/icons/red/bullet-arrow.svg create mode 100644 public/assets/images/icons/red/bullet-dot.svg create mode 100644 public/assets/images/icons/red/bullet-double-arrow.svg create mode 100644 public/assets/images/icons/red/bullet-line.svg create mode 100644 public/assets/images/icons/red/category-draft.svg create mode 100644 public/assets/images/icons/red/category-others.svg create mode 100644 public/assets/images/icons/red/category-portfolio.svg create mode 100644 public/assets/images/icons/red/category-task.svg create mode 100644 public/assets/images/icons/red/category-template.svg create mode 100644 public/assets/images/icons/red/content2.svg create mode 100644 public/assets/images/icons/white/bullet-arrow.svg create mode 100644 public/assets/images/icons/white/bullet-dot.svg create mode 100644 public/assets/images/icons/white/bullet-double-arrow.svg create mode 100644 public/assets/images/icons/white/bullet-line.svg create mode 100644 public/assets/images/icons/white/category-draft.svg create mode 100644 public/assets/images/icons/white/category-others.svg create mode 100644 public/assets/images/icons/white/category-portfolio.svg create mode 100644 public/assets/images/icons/white/category-task.svg create mode 100644 public/assets/images/icons/white/category-template.svg create mode 100644 public/assets/images/icons/white/content2.svg create mode 100644 public/assets/images/icons/yellow/bullet-arrow.svg create mode 100644 public/assets/images/icons/yellow/bullet-dot.svg create mode 100644 public/assets/images/icons/yellow/bullet-double-arrow.svg create mode 100644 public/assets/images/icons/yellow/bullet-line.svg create mode 100644 public/assets/images/icons/yellow/category-draft.svg create mode 100644 public/assets/images/icons/yellow/category-others.svg create mode 100644 public/assets/images/icons/yellow/category-portfolio.svg create mode 100644 public/assets/images/icons/yellow/category-task.svg create mode 100644 public/assets/images/icons/yellow/category-template.svg create mode 100644 public/assets/images/icons/yellow/content2.svg create mode 100755 resources/vue/components/courseware/AdminApp.vue create mode 100755 resources/vue/components/courseware/ContentBookmarkApp.vue create mode 100755 resources/vue/components/courseware/ContentOverviewApp.vue create mode 100755 resources/vue/components/courseware/CoursewareAdminActionWidget.vue create mode 100755 resources/vue/components/courseware/CoursewareAdminTemplates.vue create mode 100755 resources/vue/components/courseware/CoursewareAdminViewWidget.vue create mode 100755 resources/vue/components/courseware/CoursewareBlockDiscussion.vue create mode 100755 resources/vue/components/courseware/CoursewareContentBookmarkFilterWidget.vue create mode 100755 resources/vue/components/courseware/CoursewareContentBookmarks.vue create mode 100755 resources/vue/components/courseware/CoursewareContentOverviewActionWidget.vue create mode 100755 resources/vue/components/courseware/CoursewareContentOverviewElements.vue create mode 100755 resources/vue/components/courseware/CoursewareContentOverviewFilterWidget.vue create mode 100755 resources/vue/components/courseware/CoursewareDashboardStudents.vue create mode 100755 resources/vue/components/courseware/CoursewareDashboardTasks.vue create mode 100755 resources/vue/components/courseware/CoursewareDashboardViewWidget.vue create mode 100644 resources/vue/components/courseware/CoursewareDateInput.vue create mode 100755 resources/vue/components/courseware/CoursewareManagerTaskDistributor.vue create mode 100755 resources/vue/components/courseware/CoursewareStructuralElementComments.vue create mode 100755 resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue create mode 100755 resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue create mode 100755 resources/vue/courseware-admin-app.js create mode 100755 resources/vue/courseware-content-bookmark-app.js create mode 100755 resources/vue/courseware-content-overview-app.js create mode 100755 resources/vue/mixins/courseware/task-helper.js create mode 100755 resources/vue/store/courseware/courseware-admin.module.js diff --git a/app/controllers/admin/courseware.php b/app/controllers/admin/courseware.php new file mode 100755 index 0000000..964ff4d --- /dev/null +++ b/app/controllers/admin/courseware.php @@ -0,0 +1,34 @@ +check('root'); + PageLayout::setTitle(_('Coursewareverwaltung')); + Navigation::activateItem('/admin/locations/courseware'); + } + + public function index_action() + { + $this->setSidebar(); + } + + private function setSidebar() + { + $sidebar = Sidebar::Get(); + $views = new TemplateWidget( + _('Ansichten'), + $this->get_template_factory()->open('admin/courseware/admin_view_widget') + ); + $sidebar->addWidget($views)->addLayoutCSSClass('courseware-admin-view-widget'); + + $views = new TemplateWidget( + _('Aktionen'), + $this->get_template_factory()->open('admin/courseware/admin_action_widget') + ); + $sidebar->addWidget($views)->addLayoutCSSClass('courseware-admin-action-widget'); + } + +} \ No newline at end of file diff --git a/app/controllers/contents/courseware.php b/app/controllers/contents/courseware.php index c00f3e0..ffec9de 100755 --- a/app/controllers/contents/courseware.php +++ b/app/controllers/contents/courseware.php @@ -30,18 +30,34 @@ class Contents_CoursewareController extends AuthenticatedController * @SuppressWarnings(PHPMD.Superglobals) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function index_action($action = false, $widgetId = null) + public function index_action() { - Navigation::activateItem('/contents/courseware/projects'); - $this->setProjectsSidebar($action); - $this->courseware_root = StructuralElement::getCoursewareUser($this->user->id); + Navigation::activateItem('/contents/courseware/overview'); + $this->user_id = $GLOBALS['user']->id; + $this->setOverviewSidebar(); + $this->courseware_root = \Courseware\StructuralElement::getCoursewareUser($this->user_id); if (!$this->courseware_root) { // create initial courseware dataset - $new = StructuralElement::createEmptyCourseware($this->user->id, 'user'); + $new = \Courseware\StructuralElement::createEmptyCourseware($this->user_id, 'user'); $this->courseware_root = $new->getRoot(); } + $this->licenses = $this->getLicences(); + } + + private function setOverviewSidebar() + { + $sidebar = Sidebar::Get(); + $views = new TemplateWidget( + _('Aktionen'), + $this->get_template_factory()->open('contents/courseware/overview_action_widget') + ); + $sidebar->addWidget($views)->addLayoutCSSClass('courseware-overview-filter-widget'); - $this->elements = $this->getProjects('all'); + $views = new TemplateWidget( + _('Filter'), + $this->get_template_factory()->open('contents/courseware/overview_filter_widget') + ); + $sidebar->addWidget($views)->addLayoutCSSClass('courseware-overview-filter-widget'); } /** @@ -87,12 +103,7 @@ class Contents_CoursewareController extends AuthenticatedController $last[$this->user_id] = $this->entry_element_id; UserConfig::get($this->user_id)->store('COURSEWARE_LAST_ELEMENT', $last); - $this->licenses = array(); - $sorm_licenses = License::findBySQL("1 ORDER BY name ASC"); - foreach($sorm_licenses as $license) { - array_push($this->licenses, $license->toArray()); - } - $this->licenses = json_encode($this->licenses); + $this->licenses = $this->getLicences(); $this->oer_enabled = Config::get()->OERCAMPUS_ENABLED && $perm->have_perm(Config::get()->OER_PUBLIC_STATUS); } @@ -111,8 +122,16 @@ class Contents_CoursewareController extends AuthenticatedController $this->get_template_factory()->open('course/courseware/view_widget') ); $sidebar->addWidget($views)->addLayoutCSSClass('courseware-view-widget'); + } - + private function getLicences() + { + $licenses = array(); + $sorm_licenses = License::findBySQL("1 ORDER BY name ASC"); + foreach($sorm_licenses as $license) { + array_push($licenses, $license->toArray()); + } + return json_encode($licenses); } /** @@ -141,31 +160,21 @@ class Contents_CoursewareController extends AuthenticatedController * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function bookmarks_action($action = false, $widgetId = null) + public function bookmarks_action() { Navigation::activateItem('/contents/courseware/bookmarks'); - $this->bookmarks = array(); - $cw_bookmarks = Courseware\Bookmark::findUsersBookmarks($this->user->id); - foreach($cw_bookmarks as $bookmark) { - $bm = array(); - $bm['bookmark'] = $bookmark; - $element = Courseware\StructuralElement::find($bookmark->element_id); - if(empty($element)) { - continue; - } - $element['payload'] = json_decode($element['payload'], true); - $bm['element'] = $element; - if ($element->range_type === 'course') { - $bm['url'] = URLHelper::getURL('dispatch.php/course/courseware/?cid='.$element['range_id'].'#/structural_element/'.$element['id']); - $bm['course'] = Course::find($element['range_id']); - } - if ($element->range_type === 'user' && $element->range_id === $this->user->id) { - $bm['url'] = URLHelper::getURL('dispatch.php/contents/courseware/courseware#/structural_element/'.$element['id']); - $bm['user'] = $this->user; - } + $this->user_id = $GLOBALS['user']->id; + $this->setBookmarkSidebar(); + } - array_push($this->bookmarks, $bm); - } + private function setBookmarkSidebar() + { + $sidebar = Sidebar::Get(); + $views = new TemplateWidget( + _('Filter'), + $this->get_template_factory()->open('contents/courseware/bookmark_filter_widget') + ); + $sidebar->addWidget($views)->addLayoutCSSClass('courseware-bookmark-filter-widget'); } /** @@ -419,4 +428,11 @@ class Contents_CoursewareController extends AuthenticatedController $actions->addLink(_('Neues Lernmaterial anlegen'), $this->url_for('contents/courseware/create_project'), Icon::create('add', 'clickable'))->asDialog('size=700'); $sidebar->addWidget($actions); } + + public function pdf_export_action($element_id) + { + $element = \Courseware\StructuralElement::findOneById($element_id); + + $this->render_pdf($element->pdfExport($this->user), trim($element->title).'.pdf'); + } } diff --git a/app/controllers/course/courseware.php b/app/controllers/course/courseware.php index 1d93a0a..7c8cd78 100755 --- a/app/controllers/course/courseware.php +++ b/app/controllers/course/courseware.php @@ -7,6 +7,7 @@ use Courseware\UserProgress; /** * @property ?string $entry_element_id * @property int $last_visitdate + * @property mixed $course_id * @property mixed $courseware_progress_data * @property mixed $courseware_chapter_counter */ @@ -71,10 +72,11 @@ class Course_CoursewareController extends AuthenticatedController public function dashboard_action(): void { global $perm, $user; - $course_progress = $perm->have_studip_perm('dozent', Context::getId(), $user->id); - $this->courseware_progress_data = $this->getProgressData($course_progress); + $this->is_teacher = $perm->have_studip_perm('tutor', Context::getId(), $user->id); + $this->courseware_progress_data = $this->getProgressData($this->is_teacher); $this->courseware_chapter_counter = $this->getChapterCounter($this->courseware_progress_data); Navigation::activateItem('course/courseware/dashboard'); + $this->setDashboardSidebar(); } public function manager_action(): void @@ -89,6 +91,13 @@ class Course_CoursewareController extends AuthenticatedController } } + public function pdf_export_action($element_id) + { + $element = \Courseware\StructuralElement::findOneById($element_id); + + $this->render_pdf($element->pdfExport($this->user), trim($element->title).'.pdf'); + } + private function setIndexSidebar(): void { $sidebar = Sidebar::Get(); @@ -105,6 +114,17 @@ class Course_CoursewareController extends AuthenticatedController $sidebar->addWidget($views)->addLayoutCSSClass('courseware-view-widget'); } + + private function setDashboardSidebar(): void + { + $sidebar = Sidebar::Get(); + $views = new TemplateWidget( + _('Ansichten'), + $this->get_template_factory()->open('course/courseware/dashboard_view_widget') + ); + $sidebar->addWidget($views)->addLayoutCSSClass('courseware-dashboard-view-widget'); + } + private function getProgressData(bool $showProgressForAllParticipants = false): iterable { /** @var ?\Course $course */ diff --git a/app/routes/Activity.php b/app/routes/Activity.php index 8f6127f..a37c01e 100644 --- a/app/routes/Activity.php +++ b/app/routes/Activity.php @@ -70,6 +70,17 @@ class Activity extends \RESTAPI\RouteMap $scrollfrom = \Request::int('scrollfrom', false); $filtertype = \Request::get('filtertype', ''); + $objectType = \Request::get('object_type', ''); + $filter->setObjectType($objectType); + + $objectId = \Request::get('object_id', ''); + $filter->setObjectId($objectId); + + $context = \Request::get('context_type', ''); + $filter->setContext($context); + + $contextId = \Request::get('context_id', ''); + $filter->setContextId($contextId); if (!empty($filtertype)) { $filter->setType(json_decode($filtertype)); diff --git a/app/views/admin/courseware/admin_action_widget.php b/app/views/admin/courseware/admin_action_widget.php new file mode 100755 index 0000000..85f366c --- /dev/null +++ b/app/views/admin/courseware/admin_action_widget.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/views/admin/courseware/admin_view_widget.php b/app/views/admin/courseware/admin_view_widget.php new file mode 100755 index 0000000..a46667c --- /dev/null +++ b/app/views/admin/courseware/admin_view_widget.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/views/admin/courseware/index.php b/app/views/admin/courseware/index.php new file mode 100755 index 0000000..b40dc54 --- /dev/null +++ b/app/views/admin/courseware/index.php @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/app/views/contents/courseware/bookmark_filter_widget.php b/app/views/contents/courseware/bookmark_filter_widget.php new file mode 100755 index 0000000..8bfb00e --- /dev/null +++ b/app/views/contents/courseware/bookmark_filter_widget.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/views/contents/courseware/bookmarks.php b/app/views/contents/courseware/bookmarks.php index 14975e5..a080320 100755 --- a/app/views/contents/courseware/bookmarks.php +++ b/app/views/contents/courseware/bookmarks.php @@ -1,34 +1,6 @@ -
- - - - - +
diff --git a/app/views/contents/courseware/courses_overview.php b/app/views/contents/courseware/courses_overview.php index 4786af7..f6a3d98 100644 --- a/app/views/contents/courseware/courses_overview.php +++ b/app/views/contents/courseware/courses_overview.php @@ -1,4 +1,4 @@ -
+

diff --git a/app/views/contents/courseware/index.php b/app/views/contents/courseware/index.php index 5682e67..c0d761d 100755 --- a/app/views/contents/courseware/index.php +++ b/app/views/contents/courseware/index.php @@ -1,43 +1,10 @@ -
- - - -
-
-
-

- - - -
-
- + +
diff --git a/app/views/contents/courseware/overview_action_widget.php b/app/views/contents/courseware/overview_action_widget.php new file mode 100755 index 0000000..7f45e5e --- /dev/null +++ b/app/views/contents/courseware/overview_action_widget.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/views/contents/courseware/overview_filter_widget.php b/app/views/contents/courseware/overview_filter_widget.php new file mode 100755 index 0000000..b0f7677 --- /dev/null +++ b/app/views/contents/courseware/overview_filter_widget.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/views/course/courseware/dashboard.php b/app/views/course/courseware/dashboard.php index d666b0d..830cc90 100755 --- a/app/views/course/courseware/dashboard.php +++ b/app/views/course/courseware/dashboard.php @@ -1,9 +1,12 @@ - - - -
+
+
diff --git a/app/views/course/courseware/dashboard_view_widget.php b/app/views/course/courseware/dashboard_view_widget.php new file mode 100755 index 0000000..4e9075a --- /dev/null +++ b/app/views/course/courseware/dashboard_view_widget.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/db/migrations/5.1.17_add_courseware_templates.php b/db/migrations/5.1.17_add_courseware_templates.php new file mode 100755 index 0000000..bf75897 --- /dev/null +++ b/db/migrations/5.1.17_add_courseware_templates.php @@ -0,0 +1,34 @@ +exec("CREATE TABLE `cw_templates` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `purpose` ENUM('content', 'template', 'oer', 'portfolio', 'draft', 'other') COLLATE latin1_bin, + `structure` MEDIUMTEXT NOT NULL, + `mkdate` int(11) NOT NULL, + `chdate` int(11) NOT NULL, + + PRIMARY KEY (`id`) + ) + "); + + } + + public function down() + { + $db = \DBManager::get(); + + $db->exec("DROP TABLE IF EXISTS `cw_templates`"); + } +} diff --git a/db/migrations/5.1.18_add_courseware_tasks.php b/db/migrations/5.1.18_add_courseware_tasks.php new file mode 100755 index 0000000..2cd4594 --- /dev/null +++ b/db/migrations/5.1.18_add_courseware_tasks.php @@ -0,0 +1,78 @@ +exec("CREATE TABLE `cw_task_groups` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `seminar_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `lecturer_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `target_id` int(11) NOT NULL, + `task_template_id` int(11) NOT NULL, + `solver_may_add_blocks` tinyint(1) NOT NULL, + `title` varchar(255) NOT NULL, + `mkdate` int(11) NOT NULL, + `chdate` int(11) NOT NULL, + + PRIMARY KEY (`id`), + INDEX index_seminar_id (`seminar_id`), + INDEX index_lecturer_id (`lecturer_id`) + ) + "); + + $db->exec("CREATE TABLE `cw_tasks` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `task_group_id` int(11) NOT NULL, + `structural_element_id` int(11) NOT NULL, + `solver_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `solver_type` ENUM('autor', 'group') COLLATE latin1_bin, + `submission_date` int(11) NOT NULL, + `submitted` tinyint(1) NOT NULL, + `renewal` ENUM('pending', 'granted', 'declined') COLLATE latin1_bin, + `renewal_date` int(11) NOT NULL, + `feedback_id` int(11) NULL DEFAULT NULL, + `mkdate` int(11) NOT NULL, + `chdate` int(11) NOT NULL, + + PRIMARY KEY (`id`), + INDEX index_task_group_id (`task_group_id`), + INDEX index_structural_element_id (`structural_element_id`), + INDEX index_solver_id (`solver_id`) + ) + "); + + $db->exec("CREATE TABLE `cw_task_feedbacks` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `task_id` int(11) NOT NULL, + `lecturer_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `content` MEDIUMTEXT NOT NULL, + `mkdate` int(11) NOT NULL, + `chdate` int(11) NOT NULL, + + PRIMARY KEY (`id`), + INDEX index_task_id (`task_id`), + INDEX index_lecturer_id (`lecturer_id`) + ) + "); + + $db->exec("ALTER TABLE `cw_structural_elements` + CHANGE `purpose` `purpose` ENUM('content','draft','task','template','oer','other','portfolio') + CHARACTER SET latin1 COLLATE latin1_bin NULL DEFAULT NULL;" + ); + } + + public function down() + { + $db = \DBManager::get(); + + $db->exec("DROP TABLE IF EXISTS `cw_tasks`, `cw_task_feedbacks`"); + } +} diff --git a/db/migrations/5.1.19_add_courseware_structural_element_discussion.php b/db/migrations/5.1.19_add_courseware_structural_element_discussion.php new file mode 100755 index 0000000..e10f52d --- /dev/null +++ b/db/migrations/5.1.19_add_courseware_structural_element_discussion.php @@ -0,0 +1,49 @@ +exec("CREATE TABLE `cw_structural_element_comments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `structural_element_id` int(11) NOT NULL, + `user_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `comment` MEDIUMTEXT NOT NULL, + `mkdate` int(11) NOT NULL, + `chdate` int(11) NOT NULL, + + PRIMARY KEY (`id`), + INDEX index_structural_element_id (`structural_element_id`), + INDEX index_user_id (`user_id`) + ) + "); + + $db->exec("CREATE TABLE `cw_structural_element_feedbacks` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `structural_element_id` int(11) NOT NULL, + `user_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `feedback` MEDIUMTEXT NOT NULL, + `mkdate` int(11) NOT NULL, + `chdate` int(11) NOT NULL, + + PRIMARY KEY (`id`), + INDEX index_structural_element_id (`structural_element_id`), + INDEX index_user_id (`user_id`) + ) + "); + } + + public function down() + { + $db = \DBManager::get(); + + $db->exec("DROP TABLE IF EXISTS `cw_structural_element_feedbacks`, `cw_structural_element_comments`"); + } +} \ No newline at end of file diff --git a/lib/activities/Activity.php b/lib/activities/Activity.php index b8ed662..b46c9b2 100644 --- a/lib/activities/Activity.php +++ b/lib/activities/Activity.php @@ -143,6 +143,7 @@ class Activity extends \SimpleORMap 'passed' => _('bestand %s'), 'shared' => _('teilte %s'), 'sent' => _('sendete %s'), + 'set' => _('stellte %s'), 'voided' => _('löschte %s') ]; diff --git a/lib/activities/ActivityObserver.php b/lib/activities/ActivityObserver.php index 80652cf..5bfd4cc 100644 --- a/lib/activities/ActivityObserver.php +++ b/lib/activities/ActivityObserver.php @@ -39,5 +39,39 @@ class ActivityObserver //Notifications for ScheduleProvider (Course) \NotificationCenter::addObserver('\Studip\Activity\ScheduleProvider', 'postActivity','CourseDidChangeSchedule'); + + + // Notifications for CoursewareProvider + foreach ( + [ + \Courseware\Block::class, + \Courseware\BlockComment::class, + \Courseware\BlockFeedback::class, + \Courseware\StructuralElementComment::class, + \Courseware\StructuralElement::class, + \Courseware\StructuralElementFeedback::class, + \Courseware\Task::class, + \Courseware\TaskFeedback::class, + ] as $class + ) { + \NotificationCenter::addObserver( + \Studip\Activity\CoursewareProvider::class, + 'postActivity', + $class . 'DidCreate' + ); + } + + foreach ( + [ + \Courseware\Block::class, + \Courseware\TaskFeedback::class + ] as $class + ) { + \NotificationCenter::addObserver( + \Studip\Activity\CoursewareProvider::class, + 'postActivity', + $class . 'DidUpdate' + ); + } } } diff --git a/lib/activities/Context.php b/lib/activities/Context.php index 3f84452..9f7978f 100644 --- a/lib/activities/Context.php +++ b/lib/activities/Context.php @@ -10,6 +10,24 @@ namespace Studip\Activity; abstract class Context { + public static $objectTypes = [ + 'documents', + 'message', + 'news', + 'participants', + 'schedule', + 'wiki', + 'courseware', + 'forum' + ]; + + public static $contexTypes = [ + 'system', + 'course', + 'institute', + 'user' + ]; + protected $provider, $observer; @@ -63,6 +81,24 @@ abstract class Context public function getActivities(Filter $filter) { $providers = $this->filterProvider($this->getProvider(), $filter); + + $query = 'context = ? AND context_id = ? AND mkdate >= ? AND mkdate <= ? ORDER BY mkdate DESC'; + $params = [$this->getContextType(), $this->getRangeId(), $filter->getStartDate(), $filter->getEndDate()]; + + if ($filter->getContext() !== null && $filter->getContextId() !== null) { + $params = [$filter->getContext(), $filter->getContextId(), $filter->getStartDate(), $filter->getEndDate()]; + } + + if(\in_array($filter->getObjectType(), $this::$objectTypes)) { + $query = 'object_type = ? AND ' . $query; + \array_unshift($params, $filter->getObjectType()); + + //Object ID Filter only available when object type is set + if($filter->getObjectId() !== null && \strlen($filter->getObjectId()) > 0) { + $query = 'object_id = ? AND ' . $query; + \array_unshift($params, $filter->getObjectId()); + } + } $activities = Activity::findAndMapBySQL( function ($activity) use ($providers) { if (isset($providers[$activity->provider])) { // provider is available @@ -72,9 +108,9 @@ abstract class Context } } }, - 'context = ? AND context_id = ? AND mkdate >= ? AND mkdate <= ? ORDER BY mkdate DESC' - , - [$this->getContextType(), $this->getRangeId(), $filter->getStartDate(), $filter->getEndDate()]); + $query, + $params + ); return array_filter($activities); } diff --git a/lib/activities/CourseContext.php b/lib/activities/CourseContext.php index 3822847..d09a508 100644 --- a/lib/activities/CourseContext.php +++ b/lib/activities/CourseContext.php @@ -52,6 +52,9 @@ class CourseContext extends Context } //news $this->addProvider('Studip\Activity\NewsProvider'); + + //courseware + $this->addProvider('Studip\Activity\CoursewareProvider'); } return $this->provider; diff --git a/lib/activities/CoursewareProvider.php b/lib/activities/CoursewareProvider.php index 5e5faae..335f7bb 100755 --- a/lib/activities/CoursewareProvider.php +++ b/lib/activities/CoursewareProvider.php @@ -2,29 +2,43 @@ namespace Studip\Activity; +use Courseware\Block; +use Courseware\BlockComment; +use Courseware\BlockFeedback; +use Courseware\Container; +use Courseware\StructuralElement; +use Courseware\StructuralElementComment; +use Courseware\StructuralElementFeedback; +use Courseware\Task; +use Courseware\TaskFeedback; class CoursewareProvider implements ActivityProvider { - public function getActivityDetails($activity) { - $structural_element = \Courseware\StructuralElement::find($activity->object_id); + $structural_element = StructuralElement::find($activity->object_id); if (!$structural_element) { return false; } - $payload = json_decode($structural_element['payload']); - $activity->content = formatReady($payload['description']); + $activity->content = formatReady($activity->getValue('content')); - if ($activity->context == "course") { - $url = \URLHelper::getURL('dispatch.php/course/courseware/?cid='). $activity->context_id . '#/structural_element/' . $structural_element->id; + if ($activity->context == 'course') { + $url = + \URLHelper::getURL('dispatch.php/course/courseware/?cid=') . + $activity->context_id . + '#/structural_element/' . + $structural_element->id; $activity->object_url = [ - $url => _('Zur Courseware in der Veranstaltung') + $url => _('Zur Courseware in der Veranstaltung'), ]; - } elseif ($activity->context == "user") { - $url = \URLHelper::getURL('dispatch.php/contents/my_contents'). '#/structural_element/' . $structural_element->id; + } elseif ($activity->context == 'user') { + $url = + \URLHelper::getURL('dispatch.php/contents/my_contents') . + '#/structural_element/' . + $structural_element->id; $activity->object_url = [ - $url => _('Zur eigenen Courseware') + $url => _('Zur eigenen Courseware'), ]; } @@ -33,6 +47,212 @@ class CoursewareProvider implements ActivityProvider public static function getLexicalField() { - return _('eine Courseware-Aktivität'); + return _('einen Courseware-Inhalt'); + } + + /** + * TODO + * + * @param String $event a notification for an activity + * @param \SimpleORMap $resource + */ + public static function postActivity($event, $resource) + { + $data = null; + switch ($event) { + case Block::class . 'DidCreate': + /** + * @var \Courseware\Block $resource + * @var \Courseware\StructuralElement $structuralElement + */ + $structuralElement = $resource->getStructuralElement(); + $data = [ + 'provider' => self::class, + 'context' => $structuralElement->range_type, + 'context_id' => $structuralElement->range_id, + 'content' => null, + 'actor_type' => 'user', + 'actor_id' => $resource->owner_id, + 'verb' => 'created', + 'object_id' => $structuralElement->id, + 'object_type' => 'courseware', + 'mkdate' => time(), + ]; + break; + + case Block::class . 'DidUpdate': + /** + * @var \Courseware\Block $resource + * @var \Courseware\StructuralElement $structuralElement + */ + $structuralElement = $resource->getStructuralElement(); + $payload = $resource->type->getPayload(); + if ( + (isset($payload['text']) && $payload['text'] != '') || + (isset($payload['content']) && $payload['content'] != '') + ) { + $data = [ + 'provider' => self::class, + 'context' => $structuralElement->range_type, + 'context_id' => $structuralElement->range_id, + 'content' => null, + 'actor_type' => 'user', + 'actor_id' => $resource->editor_id, + 'verb' => 'edited', + 'object_id' => $structuralElement->id, + 'object_type' => 'courseware', + 'mkdate' => time(), + ]; + } + break; + + case BlockComment::class . 'DidCreate': + /** + * @var \Courseware\BlockComment $resource + * @var \Courseware\StructuralElement $structuralElement + */ + $structuralElement = $resource->getStructuralElement(); + $data = [ + 'provider' => self::class, + 'context' => $structuralElement->range_type, + 'context_id' => $structuralElement->range_id, + 'content' => $resource->comment, + 'actor_type' => 'user', + 'actor_id' => $resource->user_id, + 'verb' => 'interacted', + 'object_id' => $structuralElement->id, + 'object_type' => 'courseware', + 'mkdate' => time(), + ]; + break; + + case BlockFeedback::class . 'DidCreate': + /** + * @var \Courseware\BlockFeedback $resource + * @var \Courseware\StructuralElement $structuralElement + */ + $structuralElement = $resource->getStructuralElement(); + $data = [ + 'provider' => self::class, + 'context' => $structuralElement->range_type, + 'context_id' => $structuralElement->range_id, + 'content' => $resource->feedback, + 'actor_type' => 'user', + 'actor_id' => $resource->user_id, + 'verb' => 'answered', + 'object_id' => $structuralElement->id, + 'object_type' => 'courseware', + 'mkdate' => time(), + ]; + break; + + case StructuralElement::class . 'DidCreate': + /** + * @var \Courseware\StructuralElement $resource + */ + if ($resource->range_type === 'courses') { + $data = [ + 'provider' => self::class, + 'context' => $resource->range_type, + 'context_id' => $resource->range_id, + 'content' => null, + 'actor_type' => 'user', + 'actor_id' => $resource->owner_id, + 'verb' => 'created', + 'object_id' => $resource->id, + 'object_type' => 'courseware', + 'mkdate' => time(), + ]; + } + break; + + case StructuralElementComment::class . 'DidCreate': + /** + * @var \Courseware\StructuralElementComment $resource + * @var \Courseware\StructuralElement $structuralElement + */ + $structuralElement = $resource['structural_element']; + $data = [ + 'provider' => self::class, + 'context' => $structuralElement->range_type, + 'context_id' => $structuralElement->range_id, + 'content' => $resource->comment, + 'actor_type' => 'user', + 'actor_id' => $resource->user_id, + 'verb' => 'interacted', + 'object_id' => $structuralElement->id, + 'object_type' => 'courseware', + 'mkdate' => time(), + ]; + break; + + case StructuralElementFeedback::class . 'DidCreate': + /** + * @var \Courseware\StructuralElementFeedback $resource + * @var \Courseware\StructuralElement $structuralElement + */ + $structuralElement = $resource['structural_element']; + $data = [ + 'provider' => self::class, + 'context' => $structuralElement->range_type, + 'context_id' => $structuralElement->range_id, + 'content' => $resource->feedback, + 'actor_type' => 'user', + 'actor_id' => $resource->user_id, + 'verb' => 'answered', + 'object_id' => $structuralElement->id, + 'object_type' => 'courseware', + 'mkdate' => time(), + ]; + break; + + case Task::class . 'DidCreate': + /** + * @var \Courseware\Task $resource + * @var \Courseware\StructuralElement $structuralElement + */ + $structuralElement = $resource['structural_element']; + if ($structuralElement->range_type === 'courses') { + $data = [ + 'provider' => self::class, + 'context' => $structuralElement->range_type, + 'context_id' => $structuralElement->range_id, + 'content' => null, + 'actor_type' => 'user', + 'actor_id' => $resource->task_group->lecturer_id, + 'verb' => 'set', + 'object_id' => $structuralElement->id, + 'object_type' => 'courseware', + 'mkdate' => time(), + ]; + } + break; + + case TaskFeedback::class . 'DidCreate': + /** + * @var \Courseware\TaskFeedback $resource + * @var \Courseware\StructuralElement $structuralElement + */ + $structuralElement = $resource->getStructuralElement(); + if ($structuralElement->range_type === 'courses') { + $data = [ + 'provider' => self::class, + 'context' => $structuralElement->range_type, + 'context_id' => $structuralElement->range_id, + 'content' => $resource->content, + 'actor_type' => 'user', + 'actor_id' => $resource->lecturer_id, + 'verb' => 'answered', + 'object_id' => $structuralElement->id, + 'object_type' => 'courseware', + 'mkdate' => time(), + ]; + } + break; + } + + if ($data) { + Activity::create($data); + } } -} \ No newline at end of file +} diff --git a/lib/activities/Filter.php b/lib/activities/Filter.php index 8e1947e..e904f58 100644 --- a/lib/activities/Filter.php +++ b/lib/activities/Filter.php @@ -13,7 +13,11 @@ class Filter $start_date, $end_date, $type, - $verb; + $verb, + $objectType, + $objectId, + $context, + $contextId; /** * @@ -86,4 +90,76 @@ class Filter { $this->verb = $verb; } + + /** + * + * @return string + */ + public function getObjectType() + { + return $this->objectType; + } + + /** + * + * @param string $objectType + */ + public function setObjectType($objectType) + { + $this->objectType = $objectType; + } + + /** + * + * @return string + */ + public function getObjectId() + { + return $this->objectId; + } + + /** + * + * @param string $objectId + */ + public function setObjectId($objectId) + { + $this->objectId = $objectId; + } + + /** + * + * @return string + */ + public function getContext() + { + return $this->context; + } + + /** + * + * @param string $context + */ + public function setContext($context) + { + $this->context = $context; + } + + /** + * + * @return string + */ + public function getContextId() + { + return $this->contextId; + } + + /** + * + * @param string $contextId + */ + public function setContextId($contextId) + { + $this->contextId = $contextId; + } } diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index a577f8b..408e572 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -307,6 +307,13 @@ class RouteMap ); $group->get('/courseware-instances/{id}/bookmarks', Routes\Courseware\BookmarkedStructuralElementsIndex::class); + $group->get('/users/{id}/courseware-bookmarks', Routes\Courseware\UsersBookmarkedStructuralElementsIndex::class); + $this->addRelationship( + $group, + '/users/{id}/relationships/courseware-bookmarks', + Routes\Courseware\Rel\UsersBookmarkedStructuralElements::class + ); + $group->get('/courseware-blocks/{id}', Routes\Courseware\BlocksShow::class); $group->post('/courseware-blocks', Routes\Courseware\BlocksCreate::class); $group->patch('/courseware-blocks/{id}', Routes\Courseware\BlocksUpdate::class); @@ -390,6 +397,18 @@ class RouteMap // not a JSON route $group->post('/courseware-structural-elements/{id}/copy', Routes\Courseware\StructuralElementsCopy::class); + $group->get('/courseware-structural-elements/{id}/comments', Routes\Courseware\StructuralElementCommentsOfStructuralElementsIndex::class); + $group->post('/courseware-structural-element-comments', Routes\Courseware\StructuralElementCommentsCreate::class); + $group->get('/courseware-structural-element-comments/{id}', Routes\Courseware\StructuralElementCommentsShow::class); + $group->patch('/courseware-structural-element-comments/{id}', Routes\Courseware\StructuralElementCommentsUpdate::class); + $group->delete('/courseware-structural-element-comments/{id}', Routes\Courseware\StructuralElementCommentsDelete::class); + + $group->get('/courseware-structural-elements/{id}/feedback', Routes\Courseware\StructuralElementFeedbackOfStructuralElementsIndex::class); + $group->post('/courseware-structural-element-feedback', Routes\Courseware\StructuralElementFeedbackCreate::class); + $group->get('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackShow::class); + $group->patch('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackUpdate::class); + $group->delete('/courseware-structural-element-feedback/{id}', Routes\Courseware\StructuralElementFeedbackDelete::class); + $group->get('/courseware-blocks/{id}/user-data-field', Routes\Courseware\UserDataFieldOfBlocksShow::class); $group->get('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsShow::class); $group->patch('/courseware-user-data-fields/{id}', Routes\Courseware\UserDataFieldsUpdate::class); @@ -407,6 +426,27 @@ class RouteMap $group->get('/courseware-blocks/{id}/feedback', Routes\Courseware\BlockFeedbacksOfBlocksIndex::class); $group->post('/courseware-block-feedback', Routes\Courseware\BlockFeedbacksCreate::class); $group->get('/courseware-block-feedback/{id}', Routes\Courseware\BlockFeedbacksShow::class); + $group->patch('/courseware-block-feedback/{id}', Routes\Courseware\BlockFeedbacksUpdate::class); + $group->delete('/courseware-block-feedback/{id}', Routes\Courseware\BlockFeedbacksDelete::class); + + $group->get('/courseware-tasks/{id}', Routes\Courseware\TasksShow::class); + $group->get('/courseware-tasks', Routes\Courseware\TasksIndex::class); + $group->patch('/courseware-tasks/{id}', Routes\Courseware\TasksUpdate::class); + $group->delete('/courseware-tasks/{id}', Routes\Courseware\TasksDelete::class); + + $group->get('/courseware-task-groups/{id}', Routes\Courseware\TaskGroupsShow::class); + $group->post('/courseware-task-groups', Routes\Courseware\TaskGroupsCreate::class); + + $group->get('/courseware-task-feedback/{id}', Routes\Courseware\TaskFeedbackShow::class); + $group->post('/courseware-task-feedback', Routes\Courseware\TaskFeedbackCreate::class); + $group->patch('/courseware-task-feedback/{id}', Routes\Courseware\TaskFeedbackUpdate::class); + $group->delete('/courseware-task-feedback/{id}', Routes\Courseware\TaskFeedbackDelete::class); + + $group->get('/courseware-templates/{id}', Routes\Courseware\TemplatesShow::class); + $group->get('/courseware-templates', Routes\Courseware\TemplatesIndex::class); + $group->post('/courseware-templates', Routes\Courseware\TemplatesCreate::class); + $group->patch('/courseware-templates/{id}', Routes\Courseware\TemplatesUpdate::class); + $group->delete('/courseware-templates/{id}', Routes\Courseware\TemplatesDelete::class); } private function addAuthenticatedFilesRoutes(RouteCollectorProxy $group): void diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php index cfc15ce..04f955e 100755 --- a/lib/classes/JsonApi/Routes/Courseware/Authority.php +++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php @@ -8,6 +8,10 @@ use Courseware\BlockFeedback; use Courseware\Container; use Courseware\Instance; use Courseware\StructuralElement; +use Courseware\Task; +use Courseware\TaskFeedback; +use Courseware\TaskGroup; +use Courseware\Template; use Courseware\UserDataField; use Courseware\UserProgress; use User; @@ -133,6 +137,21 @@ class Authority return self::canShowCoursewareInstance($user, $resource); } + public static function canAddBookmarkToAUser(User $actor, User $user) + { + return $actor->id === $user->id; + } + + public static function canModifyBookmarksOfAUser(User $actor, User $user) + { + return $actor->id === $user->id; + } + + public static function canIndexBookmarksOfAUser(User $actor, User $user) + { + return $actor->id === $user->id; + } + /** * @SuppressWarnings(PHPMD.Superglobals) */ @@ -183,8 +202,12 @@ class Authority public static function canUpdateBlockComment(User $user, BlockComment $resource) { - return $user->id == $resource->user_id; - // should dozent be able to update? + $perm = $GLOBALS['perm']->have_studip_perm( + $resource->block->container->structural_element->course->config->COURSEWARE_EDITING_PERMISSION, + $resource->block->container->structural_element->course->id, + $user->id + ); + return $user->id === $resource->user_id || $perm; } public static function canDeleteBlockComment(User $user, BlockComment $resource) @@ -216,4 +239,155 @@ class Authority { return self::canUploadStructuralElementsImage($user, $resource); } + + public static function canShowTaskGroup(User $user, TaskGroup $resource): bool + { + return $resource['lecturer_id'] === $user->id; + } + + public static function canShowTask(User $user, Task $resource): bool + { + return self::canUpdateTask($user, $resource); + } + + public static function canIndexTasks(User $user): bool + { + // TODO: filtered index permissions are handled in the route + return $GLOBALS['perm']->have_perm('root', $user->id); + } + + public static function canCreateTasks(User $user, StructuralElement $resource): bool + { + return $resource->hasEditingPermission($user); + } + + public static function canUpdateTask(User $user, Task $resource): bool + { + return $resource->canUpdate($user); + } + + public static function canDeleteTask(User $user, Task $resource): bool + { + return self::canCreateTasks($user, $resource['structural_element']); + } + + public static function canCreateTaskFeedback(User $user, Task $resource): bool + { + return self::canCreateTasks($user, $resource['structural_element']); + } + + public static function canShowTaskFeedback(User $user, Task $resource): bool + { + return self::canShowTask($user, $resource); + } + + public static function canUpdateTaskFeedback(User $user, Task $resource): bool + { + return self::canCreateTaskFeedback($user, $resource); + } + + public static function canDeleteTaskFeedback(User $user, Task $resource): bool + { + return self::canCreateTaskFeedback($user, $resource); + } + + + public static function canIndexStructuralElementComments(User $user, StructuralElement $resource) + { + return self::canShowStructuralElement($user, $resource); + } + + public static function canShowStructuralElementComment(User $user, StructuralElementComment $resource) + { + return self::canShowStructuralElement($user, $resource); + } + + public static function canCreateStructuralElementComment(User $user, StructuralElement $resource) + { + return self::canShowStructuralElement($user, $resource); + } + + public static function canUpdateStructuralElementComment(User $user, StructuralElementComment $resource) + { + if ($GLOBALS['perm']->have_perm('root')) { + return true; + } + + $perm = $GLOBALS['perm']->have_studip_perm( + $resource->structural_element->course->config->COURSEWARE_EDITING_PERMISSION, + $resource->structural_element->course->id, + $user->id + ); + + return $user->id == $resource->user_id || $perm; + } + + public static function canDeleteStructuralElementComment(User $user, StructuralElementComment $resource) + { + return self::canUpdateStructuralElementComment($user, $resource); + } + + public static function canIndexStructuralElementFeedback(User $user, StructuralElement $resource) + { + return self::canUpdateStructuralElement($user, $resource); + } + + public static function canCreateStructuralElementFeedback(User $user, StructuralElement $resource) + { + if ($GLOBALS['perm']->have_perm('root')) { + return true; + } + + $perm = $GLOBALS['perm']->have_studip_perm( + $resource->course->config->COURSEWARE_EDITING_PERMISSION, + $resource->course->id, + $user->id + ); + + return $perm; + } + + public static function canUpdateStructuralElementFeedback(User $user, StructuralElementComment $resource) + { + return self::canCreateStructuralElementFeedback($user, $resource->structural_element); + } + + public static function canShowStructuralElementFeedback(User $user, StructuralElementFeedback $resource) + { + return $resource->user_id === $user->id || self::canUpdateStructuralElement($resource->structural_element); + } + + public static function canDeleteStructuralElementFeedback(User $user, StructuralElementComment $resource) + { + return self::canUpdateStructuralElementFeedback($user, $resource); + } + + + public static function canShowTemplate(User $user, Template $resource) + { + // templates are for everybody, aren't they? + return true; + } + + public static function canIndexTemplates(User $user) + { + // templates are for everybody, aren't they? + return true; + } + + public static function canCreateTemplate(User $user) + { + return $GLOBALS['perm']->have_perm('admin'); + } + + public static function canUpdateTemplate(User $user, Template $resource) + { + return self::canCreateTemplate($user); + } + + public static function canDeleteTemplate(User $user, Template $resource) + { + return self::canCreateTemplate($user); + } + } diff --git a/lib/classes/JsonApi/Routes/Courseware/BlockCommentsCreate.php b/lib/classes/JsonApi/Routes/Courseware/BlockCommentsCreate.php index eec8818..07034cf 100755 --- a/lib/classes/JsonApi/Routes/Courseware/BlockCommentsCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/BlockCommentsCreate.php @@ -9,6 +9,9 @@ use JsonApi\Schemas\Courseware\Block as BlockSchema; use JsonApi\Schemas\Courseware\BlockComment as BlockCommentSchema; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; +use Studip\Activity\Activity; +use Studip\Activity\CoursewareProvider; +use Courseware\Container; /** * Create a comment on a block. diff --git a/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksCreate.php b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksCreate.php index 10c4759..05e1974 100755 --- a/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksCreate.php @@ -2,6 +2,7 @@ namespace JsonApi\Routes\Courseware; +use Courseware\Container; use JsonApi\Errors\AuthorizationFailedException; use JsonApi\JsonApiController; use JsonApi\Routes\ValidationTrait; @@ -9,6 +10,8 @@ use JsonApi\Schemas\Courseware\Block as BlockSchema; use JsonApi\Schemas\Courseware\BlockFeedback as BlockFeedbackSchema; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; +use Studip\Activity\Activity; +use Studip\Activity\CoursewareProvider; /** * Create feedback on a block. diff --git a/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksDelete.php b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksDelete.php new file mode 100755 index 0000000..fa416e5 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksDelete.php @@ -0,0 +1,33 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + $resource->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksUpdate.php b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksUpdate.php new file mode 100755 index 0000000..51cc616 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/BlockFeedbacksUpdate.php @@ -0,0 +1,113 @@ +validate($request, $resource); + if (!Authority::canUpdateBlockFeedback($this->getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + $blockFeedback = $this->updateBlockFeedback($json, $resource); + + return $this->getContentResponse($blockFeedback); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + * @SuppressWarnings(CyclomaticComplexity) + * @SuppressWarnings(NPathComplexity) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (BlockFeedbackSchema::TYPE !== self::arrayGet($json, 'data.type')) { + return 'Wrong `type` member of document´s `data`.'; + } + if (self::arrayGet($json, 'data.id') !== $data->id) { + return 'Mismatch in document `id`.'; + } + + if (!($feedback = self::arrayGet($json, 'data.attributes.feedback'))) { + return 'Missing `feedback` attribute.'; + } + if (!is_string($feedback)) { + return 'Attribute `feedback` must be a string.'; + } + if ($feedback == '') { + return 'Attribute `feedback` must not be empty.'; + } + + if (self::arrayHas($json, 'data.relationships.user')) { + if (!($user = $this->getUserFromJson($json))) { + return 'Invalid `user` relationship.'; + } + if ($user->id !== $data['user_id']) { + return 'Cannot update `user` relationship.'; + } + } + + if (self::arrayHas($json, 'data.relationships.block')) { + if (!($block = $this->getBlockFromJson($json))) { + return 'Invalid `block` relationship.'; + } + if ($block->id !== $data['block_id']) { + return 'Cannot update `block` relationship.'; + } + } + } + + private function getBlockFromJson($json) + { + if (!$this->validateResourceObject($json, 'data.relationships.block', BlockSchema::TYPE)) { + return null; + } + $blockId = self::arrayGet($json, 'data.relationships.block.data.id'); + + return \Courseware\Block::find($blockId); + } + + private function getUserFromJson($json) + { + if (!$this->validateResourceObject($json, 'data.relationships.user', UserSchema::TYPE)) { + return null; + } + $userId = self::arrayGet($json, 'data.relationships.user.data.id'); + + return \User::find($userId); + } + + private function updateBlockFeedback(array $json, \Courseware\BlockFeedback $resource) + { + $resource->feedback = self::arrayGet($json, 'data.attributes.feedback', ''); + $resource->store(); + + return $resource; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php b/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php index c29020c..04c7d92 100755 --- a/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php @@ -11,6 +11,8 @@ use JsonApi\Schemas\Courseware\Block as BlockSchema; use JsonApi\Schemas\Courseware\Container as ContainerSchema; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; +use Studip\Activity\Activity; +use Studip\Activity\CoursewareProvider; /** * Create a block in a container. diff --git a/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php b/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php index 09eb9b2..fcec2bb 100755 --- a/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php @@ -3,6 +3,7 @@ namespace JsonApi\Routes\Courseware; use Courseware\Block; +use Courseware\Container; use JsonApi\Errors\AuthorizationFailedException; use JsonApi\Errors\RecordNotFoundException; use JsonApi\Errors\UnprocessableEntityException; @@ -11,6 +12,8 @@ use JsonApi\Routes\ValidationTrait; use JsonApi\Schemas\Courseware\Block as BlockSchema; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; +use Studip\Activity\Activity; +use Studip\Activity\CoursewareProvider; /** * Update one Block. diff --git a/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php index a9b3c09..51a2a01 100755 --- a/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php +++ b/lib/classes/JsonApi/Routes/Courseware/BookmarkedStructuralElementsIndex.php @@ -46,6 +46,6 @@ class BookmarkedStructuralElementsIndex extends JsonApiController $total = count($resources); list($offset, $limit) = $this->getOffsetAndLimit(); - return $this->getPaginatedResponse(array_slice($resources, $offset, $limit), $total); + return $this->getPaginatedContentResponse(array_slice($resources, $offset, $limit), $total); } } diff --git a/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php b/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php index ae6a4e6..77d3d2c 100755 --- a/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php @@ -73,6 +73,8 @@ class ContainersUpdate extends JsonApiController ); } + $resource->position = $json['data']['attributes']['position']; + $resource->editor_id = $user->id; $resource->store(); diff --git a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php index d22d9e3..843f7c2 100755 --- a/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php +++ b/lib/classes/JsonApi/Routes/Courseware/CoursewareInstancesHelper.php @@ -11,7 +11,10 @@ trait CoursewareInstancesHelper { private function findInstance(string $instanceId): Instance { - list($rangeType, $rangeId) = explode('_', $instanceId); + [$rangeType, $rangeId] = explode('_', $instanceId); + if (!is_string($rangeType) || !is_string($rangeId)) { + throw new BadRequestException('Invalid instance id: "' . $instanceId . '".'); + } return $this->findInstanceWithRange($rangeType, $rangeId); } diff --git a/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php b/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php index 7d191f5..1e68624 100755 --- a/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php +++ b/lib/classes/JsonApi/Routes/Courseware/Rel/BookmarkedStructuralElements.php @@ -169,6 +169,10 @@ class BookmarkedStructuralElements extends RelationshipsController private function addBookmarks(\User $user, array $newIds): void { foreach ($newIds as $structuralElementId) { + if (Bookmark::countBySQL('user_id = ? AND element_id = ?', [$user->id, $structuralElementId])) { + continue; + } + Bookmark::create(['user_id' => $user->id, 'element_id' => $structuralElementId]); } } diff --git a/lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php b/lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php new file mode 100755 index 0000000..7f05c66 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/Rel/UsersBookmarkedStructuralElements.php @@ -0,0 +1,180 @@ +getOffsetAndLimit(); + $page = array_slice($bookmarks, $offset, $limit); + + return $this->getPaginatedIdentifiersResponse($page, $total); + } + + protected function replaceRelationship(Request $request, $related) + { + $json = $this->validate($request); + $structuralElements = $this->validateStructuralElements($user = $this->getUser($request), $json, $related); + $this->replaceBookmarks($related, $structuralElements); + + return $this->getCodeResponse(204); + } + + protected function addToRelationship(Request $request, $related) + { + $json = $this->validate($request); + $structuralElements = $this->validateStructuralElements($user = $this->getUser($request), $json, $related); + $this->addBookmarks($related, $structuralElements); + + return $this->getCodeResponse(204); + } + + protected function removeFromRelationship(Request $request, $related) + { + $json = $this->validate($request); + $structuralElements = $this->validateStructuralElements($user = $this->getUser($request), $json, $related); + $this->removeBookmarks($user, $structuralElements); + + return $this->getCodeResponse(204); + } + + protected function findRelated(array $args) + { + if (!($related = \User::find($args['id']))) { + throw new RecordNotFoundException(); + } + + return $related; + } + + protected function authorize(Request $request, $resource) + { + $observer = $this->getUser($request); + $observed = $resource; + switch ($request->getMethod()) { + case 'GET': + return Authority::canIndexBookmarksOfAUser($observer, $observed); + + case 'DELETE': + case 'PATCH': + case 'POST': + return Authority::canModifyBookmarksOfAUser($observer, $observed); + + default: + return false; + } + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function getRelationshipSelfLink($resource, $schema, $userData) + { + return $schema->getRelationshipSelfLink($resource, \JsonApi\Schemas\User::REL_COURSEWARE_BOOKMARKS); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function getRelationshipRelatedLink($resource, $schema, $userData) + { + return $schema->getRelationshipRelatedLink($resource, \JsonApi\Schemas\User::REL_COURSEWARE_BOOKMARKS); + } + + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + + $data = self::arrayGet($json, 'data'); + + if (!is_array($data)) { + return 'Document´s ´data´ must be an array.'; + } + + foreach ($data as $item) { + if (\JsonApi\Schemas\Courseware\StructuralElement::TYPE !== self::arrayGet($item, 'type')) { + return 'Wrong `type` in document´s `data`.'; + } + + if (!self::arrayGet($item, 'id')) { + return 'Missing `id` of document´s `data`.'; + } + } + + if (self::arrayHas($json, 'data.attributes')) { + return 'Document must not have `attributes`.'; + } + } + + private function validateStructuralElements(\User $actor, $json, \User $user) + { + $structuralElements = []; + + foreach (self::arrayGet($json, 'data') as $structuralElementResource) { + if (!($structuralElement = StructuralElement::find($structuralElementResource['id']))) { + throw new RecordNotFoundException(); + } + + if (!Authority::canModifyBookmarksOfAUser($actor, $user)) { + throw new AuthorizationFailedException(); + } + + if (!Authority::canShowStructuralElement($user, $structuralElement)) { + throw new RecordNotFoundException(); + } + + $structuralElements[] = $structuralElement->id; + } + + return $structuralElements; + } + + private function replaceBookmarks(\User $user, array $newIds) + { + $oldIds = array_column(Bookmark::findUsersBookmarks($user), 'element_id'); + $onlyInOld = array_diff($oldIds, $newIds); + $onlyInNew = array_diff($newIds, $oldIds); + + $this->removeBookmarks($user, $onlyInOld); + $this->addBookmarks($user, $onlyInNew); + } + + private function addBookmarks(\User $user, array $newIds): void + { + foreach ($newIds as $structuralElementId) { + if (Bookmark::countBySQL('user_id = ? AND element_id = ?', [$user->id, $structuralElementId])) { + continue; + } + Bookmark::create(['user_id' => $user->id, 'element_id' => $structuralElementId]); + } + } + + private function removeBookmarks(\User $user, array $oldIds): void + { + Bookmark::deleteBySQL('user_id = ? AND element_id IN (?)', [$user->id, $oldIds]); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsCreate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsCreate.php new file mode 100755 index 0000000..9dfa77b --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsCreate.php @@ -0,0 +1,86 @@ +validate($request); + $structuralElement = $this->getStructuralElementFromJson($json); + if (!Authority::canCreateStructuralElementComment($user = $this->getUser($request), $structuralElement)) { + throw new AuthorizationFailedException(); + } + $structuralElementComment = $this->createStructuralElementComment($user, $json, $structuralElement); + + return $this->getCreatedResponse($structuralElementComment); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (StructuralElementCommentSchema::TYPE !== self::arrayGet($json, 'data.type')) { + return 'Wrong `type` member of document´s `data`.'; + } + if (self::arrayHas($json, 'data.id')) { + return 'New document must not have an `id`.'; + } + + if (!self::arrayHas($json, 'data.attributes.comment')) { + return 'Missing `comment` attribute.'; + } + + if (!self::arrayHas($json, 'data.relationships.structural-element')) { + return 'Missing `structural-element` relationship.'; + } + if (!$this->getStructuralElementFromJson($json)) { + return 'Invalid `structural-element` relationship.'; + } + } + + private function getStructuralElementFromJson($json) + { + if (!$this->validateResourceObject($json, 'data.relationships.structural-element', StructuralElementSchema::TYPE)) { + return null; + } + $structuralElementId = self::arrayGet($json, 'data.relationships.structural-element.data.id'); + + return \Courseware\StructuralElement::find($structuralElementId); + } + + private function createStructuralElementComment(\User $user, array $json, \Courseware\StructuralElement $structuralElement) + { + $structuralElementComment = \Courseware\StructuralElementComment::build([ + 'structural_element_id' => $structuralElement->id, + 'user_id' => $user->id, + 'comment' => self::arrayGet($json, 'data.attributes.comment', ''), + ]); + $structuralElementComment->store(); + + return $structuralElementComment; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsDelete.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsDelete.php new file mode 100755 index 0000000..6ea097c --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsDelete.php @@ -0,0 +1,33 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + $resource->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsOfStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsOfStructuralElementsIndex.php new file mode 100755 index 0000000..9515966 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsOfStructuralElementsIndex.php @@ -0,0 +1,35 @@ +getUser($request), $structural_element)) { + throw new AuthorizationFailedException(); + } + $resources = StructuralElementComment::findBySql('structural_element_id = ?', [$structural_element->id]); + + return $this->getContentResponse($resources); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsShow.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsShow.php new file mode 100755 index 0000000..6f334a9 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsShow.php @@ -0,0 +1,34 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($resource); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsUpdate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsUpdate.php new file mode 100755 index 0000000..5728847 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementCommentsUpdate.php @@ -0,0 +1,113 @@ +validate($request, $resource); + if (!Authority::canUpdateStructuralElementComment($this->getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + $structuralElementComment = $this->updateStructuralElementComment($json, $resource); + + return $this->getContentResponse($structuralElementComment); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + * @SuppressWarnings(CyclomaticComplexity) + * @SuppressWarnings(NPathComplexity) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (StructuralElementCommentSchema::TYPE !== self::arrayGet($json, 'data.type')) { + return 'Wrong `type` member of document´s `data`.'; + } + if (self::arrayGet($json, 'data.id') !== $data->id) { + return 'Mismatch in document `id`.'; + } + + if (!($comment = self::arrayGet($json, 'data.attributes.comment'))) { + return 'Missing `comment` attribute.'; + } + if (!is_string($comment)) { + return 'Attribute `comment` must be a string.'; + } + if ($comment == '') { + return 'Attribute `comment` must not be empty.'; + } + + if (self::arrayHas($json, 'data.relationships.user')) { + if (!($user = $this->getUserFromJson($json))) { + return 'Invalid `user` relationship.'; + } + if ($user->id !== $data['user_id']) { + return 'Cannot update `user` relationship.'; + } + } + + if (self::arrayHas($json, 'data.relationships.structural-element')) { + if (!($structural_element = $this->getStructuralElementFromJson($json))) { + return 'Invalid `structural-element` relationship.'; + } + if ($structural_element->id !== $data['structural_element_id']) { + return 'Cannot update `structural-element` relationship.'; + } + } + } + + private function getStructuralElementFromJson($json) + { + if (!$this->validateResourceObject($json, 'data.relationships.structural-element', StructuralElementSchema::TYPE)) { + return null; + } + $structuralElementId = self::arrayGet($json, 'data.relationships.structural-element.data.id'); + + return \Courseware\StructuralElement::find($structuralElementId); + } + + private function getUserFromJson($json) + { + if (!$this->validateResourceObject($json, 'data.relationships.user', UserSchema::TYPE)) { + return null; + } + $userId = self::arrayGet($json, 'data.relationships.user.data.id'); + + return \User::find($userId); + } + + private function updateStructuralElementComment(array $json, \Courseware\StructuralElementComment $resource) + { + $resource->comment = self::arrayGet($json, 'data.attributes.comment', ''); + $resource->store(); + + return $resource; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackCreate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackCreate.php new file mode 100755 index 0000000..bcf5425 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackCreate.php @@ -0,0 +1,86 @@ +validate($request); + $structuralElement = $this->getStructuralElementFromJson($json); + if (!Authority::canCreateStructuralElementFeedback($user = $this->getUser($request), $structuralElement)) { + throw new AuthorizationFailedException(); + } + $structuralElementFeedback = $this->createStructuralElementFeedback($user, $json, $structuralElement); + + return $this->getCreatedResponse($structuralElementFeedback); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (StructuralElementFeedbackSchema::TYPE !== self::arrayGet($json, 'data.type')) { + return 'Wrong `type` member of document´s `data`.'; + } + if (self::arrayHas($json, 'data.id')) { + return 'New document must not have an `id`.'; + } + + if (!self::arrayHas($json, 'data.attributes.feedback')) { + return 'Missing `feedback` attribute.'; + } + + if (!self::arrayHas($json, 'data.relationships.structural-element')) { + return 'Missing `structural-element` relationship.'; + } + if (!$this->getStructuralElementFromJson($json)) { + return 'Invalid `structural-element` relationship.'; + } + } + + private function getStructuralElementFromJson($json) + { + if (!$this->validateResourceObject($json, 'data.relationships.structural-element', StructuralElementSchema::TYPE)) { + return null; + } + $structuralElementId = self::arrayGet($json, 'data.relationships.structural-element.data.id'); + + return \Courseware\StructuralElement::find($structuralElementId); + } + + private function createStructuralElementFeedback(\User $user, array $json, \Courseware\StructuralElement $structuralElement) + { + $structuralElementFeedback = \Courseware\StructuralElementFeedback::build([ + 'structural_element_id' => $structuralElement->id, + 'user_id' => $user->id, + 'feedback' => self::arrayGet($json, 'data.attributes.feedback'), + ]); + $structuralElementFeedback->store(); + + return $structuralElementFeedback; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackDelete.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackDelete.php new file mode 100755 index 0000000..5c61bad --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackDelete.php @@ -0,0 +1,33 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + $resource->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackOfStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackOfStructuralElementsIndex.php new file mode 100755 index 0000000..855ce60 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackOfStructuralElementsIndex.php @@ -0,0 +1,38 @@ +getUser($request), $structuralElement)) { + throw new AuthorizationFailedException(); + } + /** @var StructuralElementFeedback[] $resources */ + $resources = $structuralElement->feedback; + + return $this->getContentResponse($resources); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackShow.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackShow.php new file mode 100755 index 0000000..752e178 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackShow.php @@ -0,0 +1,36 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($resource); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackUpdate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackUpdate.php new file mode 100755 index 0000000..5d4b0af --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementFeedbackUpdate.php @@ -0,0 +1,113 @@ +validate($request, $resource); + if (!Authority::canUpdateStructuralElementFeedback($this->getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + $structuralElementFeedback = $this->updateStructuralElementFeedback($json, $resource); + + return $this->getContentResponse($structuralElementFeedback); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + * @SuppressWarnings(CyclomaticComplexity) + * @SuppressWarnings(NPathComplexity) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (StructuralElementFeedbackSchema::TYPE !== self::arrayGet($json, 'data.type')) { + return 'Wrong `type` member of document´s `data`.'; + } + if (self::arrayGet($json, 'data.id') !== $data->id) { + return 'Mismatch in document `id`.'; + } + + if (!($feedback = self::arrayGet($json, 'data.attributes.feedback'))) { + return 'Missing `feedback` attribute.'; + } + if (!is_string($feedback)) { + return 'Attribute `feedback` must be a string.'; + } + if ($feedback == '') { + return 'Attribute `feedback` must not be empty.'; + } + + if (self::arrayHas($json, 'data.relationships.user')) { + if (!($user = $this->getUserFromJson($json))) { + return 'Invalid `user` relationship.'; + } + if ($user->id !== $data['user_id']) { + return 'Cannot update `user` relationship.'; + } + } + + if (self::arrayHas($json, 'data.relationships.structural-element')) { + if (!($structuralElement = $this->getStructuralElementFromJson($json))) { + return 'Invalid `structural-element` relationship.'; + } + if ($structuralElement->id !== $data['structural_element_id']) { + return 'Cannot update `structural-element` relationship.'; + } + } + } + + private function getStructuralElementFromJson($json) + { + if (!$this->validateResourceObject($json, 'data.relationships.structural-element', StructuralElementSchema::TYPE)) { + return null; + } + $structuralElementId = self::arrayGet($json, 'data.relationships.structural-element.data.id'); + + return \Courseware\StructuralElement::find($structuralElementId); + } + + private function getUserFromJson($json) + { + if (!$this->validateResourceObject($json, 'data.relationships.user', UserSchema::TYPE)) { + return null; + } + $userId = self::arrayGet($json, 'data.relationships.user.data.id'); + + return \User::find($userId); + } + + private function updateStructuralElementFeedback(array $json, \Courseware\StructuralElementFeedback $resource) + { + $resource->feedback = self::arrayGet($json, 'data.attributes.feedback', ''); + $resource->store(); + + return $resource; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php index 9622fb6..5dcb6d4 100755 --- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php @@ -33,6 +33,9 @@ class StructuralElementsCopy extends NonJsonApiController } $newElement = $this->copyElement($user, $sourceElement, $newParent); + if ($data['remove_purpose']) { + $newElement->purpose = ''; + } return $this->redirectToStructuralElement($response, $newElement); } diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php index ed1374a..8f1c8bd 100755 --- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php @@ -8,6 +8,7 @@ use JsonApi\Routes\ValidationTrait; use JsonApi\Schemas\Courseware\StructuralElement as StructuralElementSchema; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; +use Studip\Activity\Activity; /** * Create a block in a container. @@ -78,11 +79,49 @@ class StructuralElementsCreate extends JsonApiController 'editor_id' => $user->id, 'edit_blocker_id' => '', 'title' => self::arrayGet($json, 'data.attributes.title', ''), - 'purpose' => 'CONTENT', + 'purpose' => self::arrayGet($json, 'data.attributes.purpose', 'content'), + 'payload' => self::arrayGet($json, 'data.attributes.payload', ''), 'position' => $parent->countChildren() ]); $struct->store(); + $template = \Courseware\Template::find(self::arrayGet($json, 'data.templateId')); + + if ($template) { + $structure = json_decode($template->structure, true); + + foreach($structure['containers'] as $container) { + + $new_container = \Courseware\Container::build([ + 'structural_element_id' => $struct->id, + 'owner_id' => $user->id, + 'editor_id' => $user->id, + 'edit_blocker_id' => '', + 'position' => $struct->countContainers(), + 'container_type' => $container['attributes']['container-type'], + 'payload' => json_encode($container['attributes']['payload']), + ]); + + $new_container->store(); + $blockMap = []; + foreach($container['blocks'] as $block) { + $new_block = \Courseware\Block::build([ + 'container_id' => $new_container->id, + 'owner_id' => $user->id, + 'editor_id' => $user->id, + 'position' => $new_container->countBlocks(), + 'block_type' => $block['attributes']['block-type'], + 'payload' => json_encode($block['attributes']['payload']), + 'visible' => 1, + ]); + + $new_block->store(); + $blockMap[$block['id']] = $new_block->id; + } + $new_container['payload'] = $new_container->type->copyPayload($blockMap); + $new_container->store(); + } + } return $struct; } diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackCreate.php b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackCreate.php new file mode 100755 index 0000000..e7d7552 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackCreate.php @@ -0,0 +1,89 @@ +validate($request); + $task = $this->getTaskFromJson($json); + if (!Authority::canCreateTaskFeedback($lecturer = $this->getUser($request), $task)) { + throw new AuthorizationFailedException(); + } + + $feedback = $this->createTaskFeedback($lecturer, $json, $task); + + return $this->getCreatedResponse($feedback); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (TaskFeedbackSchema::TYPE !== self::arrayGet($json, 'data.type')) { + return 'Wrong `type` member of document´s `data`.'; + } + if (self::arrayHas($json, 'data.id')) { + return 'New document must not have an `id`.'; + } + if (!$this->getTaskFromJson($json)) { + return 'Invalid `task` relationship.'; + } + } + + private function getTaskFromJson($json) + { + if (!$this->validateResourceObject($json, 'data.relationships.task', TaskSchema::TYPE)) { + return null; + } + + $taskId = self::arrayGet($json, 'data.relationships.task.data.id'); + + return \Courseware\Task::find($taskId); + } + + private function createTaskFeedback(\User $lecturer, array $json, \Courseware\Task $task): TaskFeedback + { + $get = function ($key, $default = '') use ($json) { + return self::arrayGet($json, $key, $default); + }; + + $feedback = TaskFeedback::build([ + 'lecturer_id' => $lecturer->id, + 'task_id' => $task->id, + 'content' => self::arrayGet($json, 'data.attributes.content', '') + ]); + + $feedback->store(); + $task->feedback_id = $feedback->id; + $task->store(); + + return $feedback; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackDelete.php b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackDelete.php new file mode 100755 index 0000000..b7459f8 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackDelete.php @@ -0,0 +1,36 @@ +task_id); + if (!Authority::canDeleteTaskFeedback($user = $this->getUser($request), $task)) { + throw new AuthorizationFailedException(); + } + $task->feedback_id = null; + $task->store(); + $resource->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackShow.php b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackShow.php new file mode 100755 index 0000000..b540118 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackShow.php @@ -0,0 +1,38 @@ +task_id); + if (!Authority::canShowTaskFeedback($this->getUser($request), $task)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($resource); + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackUpdate.php new file mode 100755 index 0000000..4097818 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TaskFeedbackUpdate.php @@ -0,0 +1,84 @@ +validate($request, $resource); + $task = Task::find($resource->task_id); + if (!Authority::canUpdateTaskFeedback($user = $this->getUser($request), $task)) { + throw new AuthorizationFailedException(); + } + $resource = $this->updateTaskFeedback($user, $resource, $json); + + return $this->getContentResponse($resource); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + + if (!self::arrayHas($json, 'data.id')) { + return 'Document must have an `id`.'; + } + } + + private function updateTaskFeedback(\User $user, TaskFeedback $resource, array $json): TaskFeedback + { + if (self::arrayHas($json, 'data.attributes.content')) { + $resource->content = self::arrayGet( + $json, + 'data.attributes.content' + ); + } + $resource->store(); + + if ($struct->range_type === 'courses') { + $data = [ + 'provider' => 'Studip\Activity\CoursewareProvider', + 'context' => 'course', + 'context_id' => $task->seminar_id, + 'content' => self::arrayGet($json, 'data.attributes.content', ''), + 'actor_type' => 'user', + 'actor_id' => $user->id, + 'verb' => 'answered', + 'object_id' => $task->structural_element_id, + 'object_type' => 'courseware', + 'mkdate' => time() + ]; + + $activity = Activity::create($data); + } + + return $resource; + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php new file mode 100644 index 0000000..67cc27c --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php @@ -0,0 +1,212 @@ +validate($request); + $structuralElement = $this->getTargetFromJson($json); + if (!Authority::canCreateTasks($user = $this->getUser($request), $structuralElement)) { + throw new AuthorizationFailedException(); + } + $taskGroup = $this->createTaskGroup($user, $json); + + return $this->getCreatedResponse($taskGroup); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + * + * @param array $json + * @param mixed $data + * + * @return string|void + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + if (TaskGroupSchema::TYPE !== self::arrayGet($json, 'data.type')) { + return 'Wrong `type` member of document´s `data`.'; + } + if (self::arrayHas($json, 'data.id')) { + return 'New document must not have an `id`.'; + } + if (!self::arrayHas($json, 'data.attributes.title')) { + return 'Missing `title` attribute.'; + } + if (!self::arrayHas($json, 'data.attributes.submission-date')) { + return 'Missing `submission-date` attribute.'; + } + $submissionDate = self::arrayGet($json, 'data.attributes.submission-date'); + if (!self::isValidTimestamp($submissionDate)) { + return '`submission-date` is not an ISO 8601 timestamp.'; + } + + if (!self::arrayHas($json, 'data.relationships.target')) { + return 'Missing `target` relationship.'; + } + if (!self::arrayHas($json, 'data.relationships.task-template')) { + return 'Missing `task-template` relationship.'; + } + + if (!self::arrayHas($json, 'data.relationships.solvers')) { + return 'Missing `solvers` relationship.'; + } + + if (!$this->validateSolvers($json)) { + return 'Invalid `solvers` relationship.'; + } + if (!$this->getTargetFromJson($json)) { + return 'Invalid `target` relationship.'; + } + if (!$this->getTaskTemplateFromJson($json)) { + return 'Invalid `task-template` relationship.'; + } + } + + private function validateSolvers(array $json): bool + { + if (!self::arrayHas($json, 'data.relationships.solvers.data')) { + return false; + } + + $data = self::arrayGet($json, 'data.relationships.solvers.data'); + + if (!is_array($data) || !count($data)) { + return false; + } + + foreach ($data as $resourceIdentifier) { + if ( + !( + $this->validateResourceObject($resourceIdentifier, '', UserSchema::TYPE) || + $this->validateResourceObject($resourceIdentifier, '', StatusGroupSchema::TYPE) + ) + ) { + return false; + } + } + + return true; + } + + private function getSolversFromJson(array $json): iterable + { + if (!self::arrayHas($json, 'data.relationships.solvers.data')) { + return []; + } + + $solvers = []; + $mapping = [UserSchema::TYPE => \User::class, StatusGroupSchema::TYPE => \Statusgruppen::class]; + foreach (self::arrayGet($json, 'data.relationships.solvers.data') as $resourceIdentifier) { + $solvers[] = $mapping[$resourceIdentifier['type']]::find($resourceIdentifier['id']); + } + + return $solvers; + } + + private function getTargetFromJson(array $json): ?StructuralElement + { + if (!$this->validateResourceObject($json, 'data.relationships.target', StructuralElementSchema::TYPE)) { + return null; + } + $resourceId = self::arrayGet($json, 'data.relationships.target.data.id'); + + return StructuralElement::find($resourceId); + } + + private function getTaskTemplateFromJson(array $json): ?StructuralElement + { + if (!$this->validateResourceObject($json, 'data.relationships.task-template', StructuralElementSchema::TYPE)) { + return null; + } + $resourceId = self::arrayGet($json, 'data.relationships.task-template.data.id'); + + return StructuralElement::find($resourceId); + } + + private function createTaskGroup(\User $lecturer, array $json): TaskGroup + { + $tasks = []; + + $solvers = $this->getSolversFromJson($json); + $taskTemplate = $this->getTaskTemplateFromJson($json); + $target = $this->getTargetFromJson($json); + + $solverMayAddBlocks = self::arrayGet($json, 'data.attributes.solver-may-add-blocks', ''); + $submissionDate = self::arrayGet($json, 'data.attributes.submission-date', ''); + $submissionDate = self::fromISO8601($submissionDate); + $title = self::arrayGet($json, 'data.attributes.title', ''); + + /** @var TaskGroup $taskGroup */ + $taskGroup = TaskGroup::create([ + 'seminar_id' => $target['range_id'], + 'lecturer_id' => $lecturer->getId(), + 'target_id' => $target->getId(), + 'task_template_id' => $taskTemplate->getId(), + 'solver_may_add_blocks' => $solverMayAddBlocks, + 'title' => $title, + ]); + + foreach ($solvers as $solver) { + $task = Task::build([ + 'task_group_id' => $taskGroup->getId(), + 'solver_id' => $solver->getId(), + 'solver_type' => $this->getSolverType($solver), + 'submission_date' => $submissionDate->getTimestamp(), + ]); + + // copy task template + $taskElement = $taskTemplate->copy($lecturer, $target); + $taskElement->purpose = 'task'; + $taskElement->store(); + + //update task with element id + $task['structural_element_id'] = $taskElement->id; + $task->store(); + } + + return $taskGroup; + } + + /** + * @param \User|\Statusgruppen $solver + */ + private function getSolverType($solver): string + { + $solverTypes = [\User::class => 'autor', \Statusgruppen::class => 'group']; + + return $solverTypes[get_class($solver)]; + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php new file mode 100644 index 0000000..c8ebb86 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsShow.php @@ -0,0 +1,46 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($resource); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksDelete.php b/lib/classes/JsonApi/Routes/Courseware/TasksDelete.php new file mode 100755 index 0000000..45c78db --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TasksDelete.php @@ -0,0 +1,39 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + if ($feedback = $resource->getFeedback()) { + $feedback->delete(); + } + $resource->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php b/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php new file mode 100755 index 0000000..f0b2ce9 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php @@ -0,0 +1,106 @@ +validateFilters()) { + throw new BadRequestException($error); + } + + $filtering = $this->getQueryParameters()->getFilteringParameters() ?: []; + $resources = []; + + if (empty($filtering)) { + if (!Authority::canIndexTasks($this->getUser($request))) { + throw new AuthorizationFailedException('Only root users may index all tasks without a `filter[cid]`.'); + } + + $resources = Task::findBySQL('1 ORDER BY mkdate', []); + } else { + $user = $this->getUser($request); + /** @var ?\Course $course */ + $course = \Course::find($filtering['cid']); + + if ($GLOBALS['perm']->have_studip_perm('tutor', $course->getId(), $user->getId())) { + $resources = $this->findTasksByCourse($course); + } else { + $resources = $this->findTasksByCourseMember($user, $course); + } + } + + return $this->getContentResponse($resources); + } + + private function validateFilters() + { + $filtering = $this->getQueryParameters()->getFilteringParameters() ?: []; + + // course + if (isset($filtering['cid'])) { + $course = \Course::find($filtering['cid']); + if (!$course) { + return 'Could not find a course matching this `filter[cid]`.'; + } + } + } + + private function findTasksByCourse(\Course $course): \SimpleCollection + { + $taskGroups = TaskGroup::findBySQL('seminar_id = ?', [$course->getId()]); + + $tasks = []; + foreach ($taskGroups as $taskGroup) { + $tasks[] = $taskGroup->tasks->getArrayCopy(); + } + $tasks = \SimpleORMapCollection::createFromArray(array_flatten($tasks), false)->orderBy('id asc'); + + return $tasks; + } + + private function findTasksByCourseMember(\User $user, \Course $course): \SimpleCollection + { + $groupIds = $course['statusgruppen'] + ->filter(function (\Statusgruppen $group) use ($user) { + return $group->isMember($user->getId()); + }) + ->pluck('id'); + + return $this->findTasksByCourse($course)->filter(function ($task) use ($user, $groupIds) { + return ('autor' === $task['solver_type'] && $task['solver_id'] === $user->getId()) || + ('group' === $task['solver_type'] && in_array($task['solver_id'], $groupIds)); + }); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksShow.php b/lib/classes/JsonApi/Routes/Courseware/TasksShow.php new file mode 100755 index 0000000..619e7ea --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TasksShow.php @@ -0,0 +1,46 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($resource); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php new file mode 100755 index 0000000..3728dba --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php @@ -0,0 +1,118 @@ +validate($request, $resource); + if (!Authority::canUpdateTask($user = $this->getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + $resource = $this->updateTask($user, $resource, $json); + + return $this->getContentResponse($resource); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + * @param array $json + * @param mixed $data + * @return string|void + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + + if (!self::arrayHas($json, 'data.id')) { + return 'Document must have an `id`.'; + } + + if (self::arrayHas($json, 'data.attributes.renewal-date')) { + $renewalDate = self::arrayGet($json, 'data.attributes.renewal-date'); + if (!self::isValidTimestamp($renewalDate)) { + return '`renewal-date` is not an ISO 8601 timestamp.'; + } + } + } + + private function updateTask(\User $user, Task $resource, array $json): Task + { + if (Authority::canDeleteTask($user, $resource)) { + if (self::arrayHas($json, 'data.attributes.renewal')) { + $newRenewalState = self::arrayGet($json, 'data.attributes.renewal'); + if ('declined' === $newRenewalState) { + $resource->renewal = $newRenewalState; + } + if ('granted' === $newRenewalState && self::arrayHas($json, 'data.attributes.renewal-date')) { + $renewalDate = self::arrayGet($json, 'data.attributes.renewal-date', ''); + $renewalDate = self::fromISO8601($renewalDate); + + $resource->renewal = $newRenewalState; + $resource->renewal_date = $renewalDate->getTimestamp(); + } + } + } else { + if (self::arrayHas($json, 'data.attributes.submitted')) { + $newSubmittedState = self::arrayGet($json, 'data.attributes.submitted'); + if ($this->canSubmit($resource, $newSubmittedState)) { + $resource->submitted = $newSubmittedState; + if ('pending' === $resource->renewal) { + $resource->renewal = ''; + } + } + } + if (self::arrayHas($json, 'data.attributes.renewal')) { + $newRenewalState = self::arrayGet($json, 'data.attributes.renewal'); + if ('pending' === $newRenewalState) { + $resource->renewal = $newRenewalState; + } + } + } + + $resource->store(); + + return $resource; + } + + private function canSubmit(Task $resource, string $newSubmittedState): bool + { + $now = time(); + if (1 === (int) $resource->submitted || !$newSubmittedState) { + return false; + } + if ('granted' === $resource->renewal) { + return $now <= $resource->renewal_date; + } else { + return $now <= $resource->submission_date; + } + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TemplatesCreate.php b/lib/classes/JsonApi/Routes/Courseware/TemplatesCreate.php new file mode 100755 index 0000000..f23468c --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TemplatesCreate.php @@ -0,0 +1,88 @@ +validate($request); + if (!Authority::canCreateTemplate($this->getUser($request))) { + throw new AuthorizationFailedException(); + } + + $template = $this->createTemplate($json); + + return $this->getCreatedResponse($template); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + + if (self::arrayHas($json, 'data.id')) { + return 'New document must not have an `id`.'; + } + if (!self::arrayHas($json, 'data.name')) { + return 'Missing `name` value.'; + } + if (!self::arrayHas($json, 'data.purpose')) { + return 'Missing `purpose` attribute.'; + } + if (!self::arrayHas($json, 'data.structure')) { + return 'Missing `structure` attribute.'; + } + } + + private function createTemplate(array $json): Template + { + $get = function ($key, $default = '') use ($json) { + return self::arrayGet($json, $key, $default); + }; + + $template = Template::build([ + 'name' => $get('data.name'), + 'purpose' => $get('data.purpose'), + 'structure' => $this->cleanStructure($get('data.structure'), $get('data.name')), + ]); + $template->store(); + + return $template; + } + + private function cleanStructure($json, $name): string + { + $structural_element_uploaded = json_decode($json, true); + + $structural_element = [ + 'type' => $structural_element_uploaded['type'], + 'attributes' => ['title' => $name], + 'containers' => $structural_element_uploaded['containers'], + ]; + + return json_encode($structural_element); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TemplatesDelete.php b/lib/classes/JsonApi/Routes/Courseware/TemplatesDelete.php new file mode 100755 index 0000000..406a372 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TemplatesDelete.php @@ -0,0 +1,33 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + $resource->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/TemplatesIndex.php b/lib/classes/JsonApi/Routes/Courseware/TemplatesIndex.php new file mode 100755 index 0000000..1831bb7 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TemplatesIndex.php @@ -0,0 +1,32 @@ +getUser($request))) { + throw new AuthorizationFailedException(); + } + + $resources = Template::findBySQL('1 ORDER BY mkdate', []); + + return $this->getContentResponse($resources); + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Courseware/TemplatesShow.php b/lib/classes/JsonApi/Routes/Courseware/TemplatesShow.php new file mode 100755 index 0000000..ef28e0b --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TemplatesShow.php @@ -0,0 +1,34 @@ +getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($resource); + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Courseware/TemplatesUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TemplatesUpdate.php new file mode 100755 index 0000000..2ccb8a7 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/TemplatesUpdate.php @@ -0,0 +1,80 @@ +validate($request, $resource); + if (!Authority::canUpdateTemplate($user = $this->getUser($request), $resource)) { + throw new AuthorizationFailedException(); + } + $resource = $this->updateTemplate($resource, $json); + + return $this->getContentResponse($resource); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + + if (!self::arrayHas($json, 'data.id')) { + return 'Document must have an `id`.'; + } + + if (!self::arrayHas($json, 'data.name')) { + return 'Document must have an `name`.'; + } + + if (!self::arrayHas($json, 'data.purpose')) { + return 'Document must have an `purpose`.'; + } + } + + private function updateTemplate(Template $resource, array $json): Template + { + if (self::arrayHas($json, 'data.name')) { + $resource->name = self::arrayGet( + $json, + 'data.name' + ); + } + + if (self::arrayHas($json, 'data.purpose')) { + $resource->purpose = self::arrayGet( + $json, + 'data.purpose' + ); + } + + $resource->store(); + + return $resource; + } + +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php b/lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php new file mode 100755 index 0000000..618dc2e --- /dev/null +++ b/lib/classes/JsonApi/Routes/Courseware/UsersBookmarkedStructuralElementsIndex.php @@ -0,0 +1,55 @@ +getUser($request); + if (!Authority::canIndexBookmarksOfAUser($actor, $user)) { + throw new AuthorizationFailedException(); + } + + $resources = array_column(Bookmark::findUsersBookmarks($user), 'element'); + $total = count($resources); + [$offset, $limit] = $this->getOffsetAndLimit(); + + return $this->getPaginatedContentResponse(array_slice($resources, $offset, $limit), $total); + } +} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index 042ee58..de6e247 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -54,8 +54,14 @@ class SchemaMap \Courseware\Container::class => Schemas\Courseware\Container::class, \Courseware\Instance::class => Schemas\Courseware\Instance::class, \Courseware\StructuralElement::class => Schemas\Courseware\StructuralElement::class, + \Courseware\StructuralElementComment::class => Schemas\Courseware\StructuralElementComment::class, + \Courseware\StructuralElementFeedback::class => Schemas\Courseware\StructuralElementFeedback::class, \Courseware\UserDataField::class => Schemas\Courseware\UserDataField::class, \Courseware\UserProgress::class => Schemas\Courseware\UserProgress::class, + \Courseware\Task::class => Schemas\Courseware\Task::class, + \Courseware\TaskGroup::class => Schemas\Courseware\TaskGroup::class, + \Courseware\TaskFeedback::class => Schemas\Courseware\TaskFeedback::class, + \Courseware\Template::class => Schemas\Courseware\Template::class, ]; } } diff --git a/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php b/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php index 15c5ee2..49bd4cd 100755 --- a/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php +++ b/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php @@ -22,6 +22,7 @@ class StructuralElement extends SchemaProvider const REL_OWNER = 'owner'; const REL_PARENT = 'parent'; const REL_USER = 'user'; + const REL_TASK = 'task'; /** * {@inheritdoc} @@ -79,11 +80,7 @@ class StructuralElement extends SchemaProvider $this->shouldInclude($context, self::REL_CONTAINERS) ); - $relationships = $this->addRangeRelationship( - $relationships, - $resource, - $context - ); + $relationships = $this->addRangeRelationship($relationships, $resource, $context); $relationships = $this->addOwnerRelationship( $relationships, @@ -127,6 +124,12 @@ class StructuralElement extends SchemaProvider $this->shouldInclude($context, self::REL_IMAGE) ); + $relationships = $this->addTaskRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_TASK) + ); + return $relationships; } @@ -158,11 +161,9 @@ class StructuralElement extends SchemaProvider if ($includeData) { $user = $this->currentUser; - $relation[self::RELATIONSHIP_DATA] = $resource->children->filter( - function ($child) use ($user) { - return $child->canRead($user); - } - ); + $relation[self::RELATIONSHIP_DATA] = $resource->children->filter(function ($child) use ($user) { + return $child->canRead($user); + }); } $relationships[self::REL_CHILDREN] = $relation; @@ -241,7 +242,9 @@ class StructuralElement extends SchemaProvider $relation[self::RELATIONSHIP_LINKS] = [ Link::RELATED => $this->createLinkToUser($resource['edit_blocker_id']), ]; - $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->edit_blocker : new Identifier($resource['edit_blocker_id'], \JsonApi\Schemas\User::TYPE); + $relation[self::RELATIONSHIP_DATA] = $includeData + ? $resource->edit_blocker + : new Identifier($resource['edit_blocker_id'], \JsonApi\Schemas\User::TYPE); } else { $relation[self::RELATIONSHIP_DATA] = null; } @@ -257,7 +260,9 @@ class StructuralElement extends SchemaProvider $relation[self::RELATIONSHIP_LINKS] = [ Link::RELATED => $this->createLinkToUser($resource['editor_id']), ]; - $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->editor : new Identifier($resource['editor_id'], \JsonApi\Schemas\User::TYPE); + $relation[self::RELATIONSHIP_DATA] = $includeData + ? $resource->editor + : new Identifier($resource['editor_id'], \JsonApi\Schemas\User::TYPE); } else { $relation[self::RELATIONSHIP_DATA] = null; } @@ -273,7 +278,9 @@ class StructuralElement extends SchemaProvider $relation[self::RELATIONSHIP_LINKS] = [ Link::RELATED => $this->createLinkToUser($resource['owner_id']), ]; - $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->owner : new Identifier($resource['owner_id'], \JsonApi\Schemas\User::TYPE); + $relation[self::RELATIONSHIP_DATA] = $includeData + ? $resource->owner + : new Identifier($resource['owner_id'], \JsonApi\Schemas\User::TYPE); } else { $relation[self::RELATIONSHIP_DATA] = null; } @@ -290,7 +297,9 @@ class StructuralElement extends SchemaProvider $relation[self::RELATIONSHIP_LINKS] = [ Link::RELATED => $this->createLinkToStructuralElement($resource['parent_id']), ]; - $relation[self::RELATIONSHIP_DATA] = $includeData ? $resource->parent : new Identifier($resource['parent_id'], self::TYPE); + $relation[self::RELATIONSHIP_DATA] = $includeData + ? $resource->parent + : new Identifier($resource['parent_id'], self::TYPE); } else { $relation[self::RELATIONSHIP_DATA] = null; } @@ -307,7 +316,9 @@ class StructuralElement extends SchemaProvider self::RELATIONSHIP_LINKS => [ Link::RELATED => $this->createLinkToCourse($resource['range_id']), ], - self::RELATIONSHIP_DATA => $includeData ? $resource->course : new Identifier($resource['range_id'], \JsonApi\Schemas\Course::TYPE), + self::RELATIONSHIP_DATA => $includeData + ? $resource->course + : new Identifier($resource['range_id'], \JsonApi\Schemas\Course::TYPE), ]; } elseif ($resource['range_type'] === 'user') { $includeData = $this->shouldInclude($context, self::REL_USER); @@ -315,13 +326,34 @@ class StructuralElement extends SchemaProvider self::RELATIONSHIP_LINKS => [ Link::RELATED => $this->createLinkToUser($resource['range_id']), ], - self::RELATIONSHIP_DATA => $includeData ? $resource->user : new Identifier($resource['range_id'], \JsonApi\Schemas\User::TYPE), + self::RELATIONSHIP_DATA => $includeData + ? $resource->user + : new Identifier($resource['range_id'], \JsonApi\Schemas\User::TYPE), ]; } return $relationships; } + private function addTaskRelationship( + array $relationships, + $resource, + bool $shouldInclude + ): array { + $relationships[self::REL_TASK] = $resource->task + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->task), + ], + self::RELATIONSHIP_DATA => $resource->task, + ] + : [ + self::RELATIONSHIP_DATA => null, + ]; + + return $relationships; + } + private static $memo = []; private function createLinkToCourse($rangeId) diff --git a/lib/classes/JsonApi/Schemas/Courseware/StructuralElementComment.php b/lib/classes/JsonApi/Schemas/Courseware/StructuralElementComment.php new file mode 100755 index 0000000..7fe75ff --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Courseware/StructuralElementComment.php @@ -0,0 +1,62 @@ +id; + } + + /** + * {@inheritdoc} + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'comment' => (string) $resource['comment'], + 'mkdate' => date('c', $resource['mkdate']), + 'chdate' => date('c', $resource['chdate']), + ]; + } + + /** + * {@inheritdoc} + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $isPrimary = $context->getPosition()->getLevel() === 0; + $includeList = $context->getIncludePaths(); + + $relationships = []; + + $relationships[self::REL_STRUCTURAL_ELEMENT] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->structural_element), + ], + self::RELATIONSHIP_DATA => $resource->structural_element, + ]; + + $relationships[self::REL_USER] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->user), + ], + self::RELATIONSHIP_DATA => $resource->user, + ]; + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/Courseware/StructuralElementFeedback.php b/lib/classes/JsonApi/Schemas/Courseware/StructuralElementFeedback.php new file mode 100755 index 0000000..c7a9cc8 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Courseware/StructuralElementFeedback.php @@ -0,0 +1,62 @@ +id; + } + + /** + * {@inheritdoc} + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'feedback' => (string) $resource['feedback'], + 'mkdate' => date('c', $resource['mkdate']), + 'chdate' => date('c', $resource['chdate']), + ]; + } + + /** + * {@inheritdoc} + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $isPrimary = $context->getPosition()->getLevel() === 0; + $includeList = $context->getIncludePaths(); + + $relationships = []; + + $relationships[self::REL_STRUCTURAL_ELEMENT] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->structural_element), + ], + self::RELATIONSHIP_DATA => $resource->structural_element, + ]; + + $relationships[self::REL_USER] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->user), + ], + self::RELATIONSHIP_DATA => $resource->user, + ]; + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/Courseware/Task.php b/lib/classes/JsonApi/Schemas/Courseware/Task.php new file mode 100755 index 0000000..32d4a18 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Courseware/Task.php @@ -0,0 +1,85 @@ +id; + } + + /** + * {@inheritdoc} + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'progress' => (float) $resource->getTaskProgress(), + 'submission-date' => date('c', $resource['submission_date']), + 'submitted' => (bool) $resource['submitted'], + 'renewal' => empty($resource['renewal']) ? null : (string) $resource['renewal'], + 'renewal-date' => $resource['renewal_date'] ? date('c', $resource['renewal_date']) : null, + 'mkdate' => date('c', $resource['mkdate']), + 'chdate' => date('c', $resource['chdate']), + ]; + } + + /** + * {@inheritdoc} + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $relationships = []; + + $relationships[self::REL_FEEDBACK] = $resource->getFeedback() + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->getFeedback()), + ], + self::RELATIONSHIP_DATA => $resource->getFeedback(), + ] + : [self::RELATIONSHIP_DATA => null]; + + $relationships[self::REL_SOLVER] = $resource['solver_id'] + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->getSolver()), + ], + self::RELATIONSHIP_DATA => $resource->getSolver(), + ] + : [self::RELATIONSHIP_DATA => null]; + + $relationships[self::REL_STRUCTURAL_ELEMENT] = $resource['structural_element_id'] + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource['structural_element']), + ], + self::RELATIONSHIP_DATA => $resource['structural_element'], + ] + : [self::RELATIONSHIP_DATA => null]; + + $relationships[self::REL_TASK_GROUP] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource['task_group']), + ], + self::RELATIONSHIP_DATA => $resource['task_group'], + ]; + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/Courseware/TaskFeedback.php b/lib/classes/JsonApi/Schemas/Courseware/TaskFeedback.php new file mode 100755 index 0000000..8d800c8 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Courseware/TaskFeedback.php @@ -0,0 +1,61 @@ +id; + } + + /** + * {@inheritdoc} + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'content' => (string) $resource['content'], + 'mkdate' => date('c', $resource['mkdate']), + 'chdate' => date('c', $resource['chdate']), + ]; + } + + /** + * {@inheritdoc} + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $relationships = []; + + $relationships[self::REL_LECTURER] = $resource['lecturer_id'] + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->lecturer), + ], + self::RELATIONSHIP_DATA => $resource->lecturer, + ] + : [self::RELATIONSHIP_DATA => $resource->lecturer]; + + $relationships[self::REL_TASK] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_TASK), + ], + ]; + + return $relationships; + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php b/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php new file mode 100755 index 0000000..12dbc6c --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php @@ -0,0 +1,104 @@ +id; + } + + /** + * {@inheritdoc} + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'solver-may-add-blocks' => (bool) $resource['solver_may_add_blocks'], + 'title' => (string) $resource->title, + 'mkdate' => date('c', $resource['mkdate']), + 'chdate' => date('c', $resource['chdate']), + ]; + } + + /** + * {@inheritdoc} + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $relationships = []; + + $relationships[self::REL_COURSE] = $resource['seminar_id'] + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->course), + ], + self::RELATIONSHIP_DATA => $resource->course, + ] + : [self::RELATIONSHIP_DATA => null]; + + $relationships[self::REL_LECTURER] = $resource['lecturer_id'] + ? [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($resource->lecturer), + ], + self::RELATIONSHIP_DATA => $resource->lecturer, + ] + : [self::RELATIONSHIP_DATA => null]; + + $relationships[self::REL_SOLVERS] = [ + self::RELATIONSHIP_DATA => $resource->getSolvers(), + ]; + + $target = StructuralElement::build(['id' => $resource['target_id']]); + $relationships[self::REL_TARGET] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($target), + ], + self::RELATIONSHIP_DATA => $this->shouldInclude($context, self::REL_TARGET) ? $resource['target'] : $target, + ]; + + $taskTemplate = StructuralElement::build(['id' => $resource['task_template_id']]); + $relationships[self::REL_TASK_TEMPLATE] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($taskTemplate), + ], + self::RELATIONSHIP_DATA => $this->shouldInclude($context, self::REL_TASK_TEMPLATE) + ? $resource['task_template'] + : $taskTemplate, + ]; + + $relationships[self::REL_TASKS] = [ + self::RELATIONSHIP_DATA => $this->shouldInclude($context, self::REL_TASKS) + ? $resource['tasks'] + : \DBManager::get()->fetchFirst( + 'SELECT id FROM cw_tasks WHERE task_group_id = ?', + [$resource->getId()], + function ($id) { + return new Identifier($id, Task::TYPE); + } + ), + ]; + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/Courseware/Template.php b/lib/classes/JsonApi/Schemas/Courseware/Template.php new file mode 100755 index 0000000..4c0d0ce --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Courseware/Template.php @@ -0,0 +1,44 @@ +id; + } + + /** + * {@inheritdoc} + */ + public function getAttributes($resource, ContextInterface $context): iterable + { + return [ + 'name' => (string) $resource['name'], + 'purpose' => (string) $resource['purpose'], + 'structure' => (string) $resource['structure'], + 'mkdate' => date('c', $resource['mkdate']), + 'chdate' => date('c', $resource['chdate']), + ]; + } + + /** + * {@inheritdoc} + */ + public function getRelationships($resource, ContextInterface $context): iterable + { + $relationships = []; + + return $relationships; + } +} \ No newline at end of file diff --git a/lib/classes/JsonApi/Schemas/User.php b/lib/classes/JsonApi/Schemas/User.php index 657f8f9..14787ce 100644 --- a/lib/classes/JsonApi/Schemas/User.php +++ b/lib/classes/JsonApi/Schemas/User.php @@ -16,6 +16,7 @@ class User extends SchemaProvider const REL_CONTACTS = 'contacts'; const REL_COURSES = 'courses'; const REL_COURSE_MEMBERSHIPS = 'course-memberships'; + const REL_COURSEWARE_BOOKMARKS = 'courseware-bookmarks'; const REL_EVENTS = 'events'; const REL_FILES = 'file-refs'; const REL_FOLDERS = 'folders'; @@ -165,6 +166,7 @@ class User extends SchemaProvider $relationships = $this->getNewsRelationship($relationships, $user, $this->shouldInclude($context, self::REL_NEWS)); $relationships = $this->getOutboxRelationship($relationships, $user, $this->shouldInclude($context, self::REL_OUTBOX)); $relationships = $this->getScheduleRelationship($relationships, $user, $this->shouldInclude($context, self::REL_SCHEDULE)); + $relationships = $this->getCoursewareBookmarksRelationship($relationships, $user, $this->shouldInclude($context, self::REL_COURSEWARE_BOOKMARKS)); } return $relationships; @@ -251,6 +253,22 @@ class User extends SchemaProvider self::RELATIONSHIP_LINKS => [ Link::RELATED => $this->getRelationshipRelatedLink($user, self::REL_COURSE_MEMBERSHIPS), ], + self::RELATIONSHIP_DATA => $resource->course_memberships, + ]; + + return $relationships; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function getCoursewareBookmarksRelationship(array $relationships, \User $user, $includeData) + { + $relationships[self::REL_COURSEWARE_BOOKMARKS] = [ + self::RELATIONSHIP_LINKS_SELF => true, + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($user, self::REL_COURSEWARE_BOOKMARKS), + ], ]; return $relationships; diff --git a/lib/models/Courseware/Block.php b/lib/models/Courseware/Block.php index 48009fe..05d5d45 100755 --- a/lib/models/Courseware/Block.php +++ b/lib/models/Courseware/Block.php @@ -207,4 +207,19 @@ class Block extends \SimpleORMap return 'error'; } } + + public function getStructuralElement(): ?StructuralElement + { + $sql = 'SELECT se.* + FROM cw_blocks b + JOIN cw_containers c ON c.id = b.container_id + JOIN cw_structural_elements se ON se.id = c.structural_element_id + WHERE b.id = ?'; + $structuralElement = \DBManager::get()->fetchOne($sql, [$this->getId()]); + if (!count($structuralElement)) { + return null; + } + + return StructuralElement::build($structuralElement, false); + } } diff --git a/lib/models/Courseware/BlockComment.php b/lib/models/Courseware/BlockComment.php index c985e89..16f3a73 100755 --- a/lib/models/Courseware/BlockComment.php +++ b/lib/models/Courseware/BlockComment.php @@ -41,4 +41,20 @@ class BlockComment extends \SimpleORMap parent::configure($config); } + + public function getStructuralElement(): ?StructuralElement + { + $sql = 'SELECT se.* + FROM cw_block_comments bc + JOIN cw_blocks b ON b.id = bc.block_id + JOIN cw_containers c ON c.id = b.container_id + JOIN cw_structural_elements se ON se.id = c.structural_element_id + WHERE bc.id = ?'; + $structuralElement = \DBManager::get()->fetchOne($sql, [$this->getId()]); + if (!count($structuralElement)) { + return null; + } + + return StructuralElement::build($structuralElement, false); + } } diff --git a/lib/models/Courseware/BlockFeedback.php b/lib/models/Courseware/BlockFeedback.php index 08e5c6f..4225770 100755 --- a/lib/models/Courseware/BlockFeedback.php +++ b/lib/models/Courseware/BlockFeedback.php @@ -39,4 +39,20 @@ class BlockFeedback extends \SimpleORMap parent::configure($config); } + + public function getStructuralElement(): ?StructuralElement + { + $sql = 'SELECT se.* + FROM cw_block_feedbacks bf + JOIN cw_blocks b ON b.id = bf.block_id + JOIN cw_containers c ON c.id = b.container_id + JOIN cw_structural_elements se ON se.id = c.structural_element_id + WHERE bf.id = ?'; + $structuralElement = \DBManager::get()->fetchOne($sql, [$this->getId()]); + if (!count($structuralElement)) { + return null; + } + + return StructuralElement::build($structuralElement, false); + } } diff --git a/lib/models/Courseware/BlockTypes/BlockType.php b/lib/models/Courseware/BlockTypes/BlockType.php index 0d99434..98e24ae 100755 --- a/lib/models/Courseware/BlockTypes/BlockType.php +++ b/lib/models/Courseware/BlockTypes/BlockType.php @@ -310,7 +310,7 @@ abstract class BlockType $user ); - return isset($copiedFile) ? $copiedFile->id : ''; + return isset($copiedFile->id) ? $copiedFile->id : ''; } return ''; @@ -373,4 +373,17 @@ abstract class BlockType return $destinationFolder; } + + public function pdfExport() + { + $html = '
' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '
'; + $html .= '
' . _('Block-Daten') . ': ' . '
'; + foreach($this->getPayload() as $key => $value) { + if ($value !== '') { + $html .= '
' . $key . ' => ' . $value . '
'; + } + } + + return $html; + } } diff --git a/lib/models/Courseware/BlockTypes/Code.php b/lib/models/Courseware/BlockTypes/Code.php index 2ee6342..99888d4 100755 --- a/lib/models/Courseware/BlockTypes/Code.php +++ b/lib/models/Courseware/BlockTypes/Code.php @@ -58,4 +58,12 @@ class Code extends BlockType { return []; } + + public function pdfExport() + { + $html = '
' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '
'; + $html .= '
' . htmlspecialchars($this->getPayload()['content']) . '
'; + + return $html; + } } diff --git a/lib/models/Courseware/BlockTypes/Confirm.php b/lib/models/Courseware/BlockTypes/Confirm.php index 7204e6b..92c61d4 100755 --- a/lib/models/Courseware/BlockTypes/Confirm.php +++ b/lib/models/Courseware/BlockTypes/Confirm.php @@ -57,4 +57,12 @@ class Confirm extends BlockType { return []; } + + public function pdfExport() + { + $html = '
' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '
'; + $html .= '

' . htmlspecialchars($this->getPayload()['text']) . '

'; + + return $html; + } } diff --git a/lib/models/Courseware/BlockTypes/Date.php b/lib/models/Courseware/BlockTypes/Date.php index df37590..66075f3 100755 --- a/lib/models/Courseware/BlockTypes/Date.php +++ b/lib/models/Courseware/BlockTypes/Date.php @@ -58,4 +58,12 @@ class Date extends BlockType { return []; } + + public function pdfExport() + { + $html = '
' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '
'; + $html .= '

' . date('d.m.Y h:i', (int) $this->getPayload()['timestamp'] / 1000) . '

'; + + return $html; + } } diff --git a/lib/models/Courseware/BlockTypes/Headline.php b/lib/models/Courseware/BlockTypes/Headline.php index 855e2a9..a3add74 100755 --- a/lib/models/Courseware/BlockTypes/Headline.php +++ b/lib/models/Courseware/BlockTypes/Headline.php @@ -106,4 +106,13 @@ class Headline extends BlockType { return []; } + + public function pdfExport() + { + $html = '
' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '
'; + $html .= '
' . htmlspecialchars($this->getPayload()['title']) . '
'; + $html .= '
' . htmlspecialchars($this->getPayload()['subtitle']) . '
'; + + return $html; + } } diff --git a/lib/models/Courseware/BlockTypes/KeyPoint.php b/lib/models/Courseware/BlockTypes/KeyPoint.php index fae16d3..90f4852 100755 --- a/lib/models/Courseware/BlockTypes/KeyPoint.php +++ b/lib/models/Courseware/BlockTypes/KeyPoint.php @@ -59,4 +59,12 @@ class KeyPoint extends BlockType { return []; } + + public function pdfExport() + { + $html = '
' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '
'; + $html .= '

' . htmlspecialchars($this->getPayload()['text']) . '

'; + + return $html; + } } diff --git a/lib/models/Courseware/BlockTypes/Link.php b/lib/models/Courseware/BlockTypes/Link.php index 7b93aeb..1e804b7 100755 --- a/lib/models/Courseware/BlockTypes/Link.php +++ b/lib/models/Courseware/BlockTypes/Link.php @@ -60,4 +60,13 @@ class Link extends BlockType { return []; } + + public function pdfExport() + { + $html = '
' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '
'; + $html .= '

' . htmlspecialchars($this->getPayload()['title']) . '

'; + $html .= '

' . htmlspecialchars($this->getPayload()['url']) . '

'; + + return $html; + } } diff --git a/lib/models/Courseware/BlockTypes/Text.php b/lib/models/Courseware/BlockTypes/Text.php index c4fe67c..3857c02 100755 --- a/lib/models/Courseware/BlockTypes/Text.php +++ b/lib/models/Courseware/BlockTypes/Text.php @@ -165,4 +165,12 @@ class Text extends BlockType return array(); }); } + + public function pdfExport() + { + $html = '
' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '
'; + $html .= $this->getPayload()['text']; + + return $html; + } } diff --git a/lib/models/Courseware/BlockTypes/Typewriter.php b/lib/models/Courseware/BlockTypes/Typewriter.php index 583e64e..064065b 100755 --- a/lib/models/Courseware/BlockTypes/Typewriter.php +++ b/lib/models/Courseware/BlockTypes/Typewriter.php @@ -60,4 +60,12 @@ class Typewriter extends BlockType { return []; } + + public function pdfExport() + { + $html = '
' . sprintf(_('Block-Typ: %s'), $this->getTitle()) . '
'; + $html .= '

' . htmlspecialchars($this->getPayload()['text']) . '

'; + + return $html; + } } diff --git a/lib/models/Courseware/Bookmark.php b/lib/models/Courseware/Bookmark.php index d6e5910..96f919d 100755 --- a/lib/models/Courseware/Bookmark.php +++ b/lib/models/Courseware/Bookmark.php @@ -54,12 +54,12 @@ class Bookmark extends \SimpleORMap /** * Returns all bookmarks of a user. * - * @param string $userId the user's ID for whom to search for bookmarks + * @param \User $user the user for whom to search for bookmarks * * @return Bookmark[] the list of bookmarks */ - public function findUsersBookmarks(string $userId): array + public function findUsersBookmarks($user): array { - return self::findBySQL('user_id = ?', [$userId]); + return self::findBySQL('user_id = ? ORDER BY chdate', [$user->id]); } } diff --git a/lib/models/Courseware/ContainerTypes/AccordionContainer.php b/lib/models/Courseware/ContainerTypes/AccordionContainer.php index 8325204..510a388 100755 --- a/lib/models/Courseware/ContainerTypes/AccordionContainer.php +++ b/lib/models/Courseware/ContainerTypes/AccordionContainer.php @@ -56,4 +56,28 @@ class AccordionContainer extends ContainerType return Schema::fromJsonString(file_get_contents($schemaFile)); } + + public function pdfExport() + { + $html = '

' . sprintf(_('Container-Typ: %s'), $this->getTitle()) . '

'; + + $payload = $this->getPayload(); + + $sections = $payload['sections']; + foreach ($sections as $section) { + $block_ids = $section['blocks']; + $html .= '

' . $section['name'] . '

'; + foreach ($block_ids as $block_id) { + $block = $this->container->blocks->find($block_id); + if ($block) { + $html .= $block->type->PdfExport(); + } + else { + $html .= '

' . _('Block konnte nicht gefunden werden') . '

'; + } + } + } + + return $html; + } } diff --git a/lib/models/Courseware/ContainerTypes/ContainerType.php b/lib/models/Courseware/ContainerTypes/ContainerType.php index 3403077..a2552d4 100755 --- a/lib/models/Courseware/ContainerTypes/ContainerType.php +++ b/lib/models/Courseware/ContainerTypes/ContainerType.php @@ -244,4 +244,15 @@ abstract class ContainerType return _('unbekannter Courseware-Container'); } } + + public function pdfExport() + { + $html = '

' . sprintf(_('Container-Typ: %s'), $this->getTitle()) . '

'; + + foreach ($this->container->blocks as $block) { + $html .= $block->type->PdfExport(); + } + + return $html; + } } diff --git a/lib/models/Courseware/ContainerTypes/ListContainer.php b/lib/models/Courseware/ContainerTypes/ListContainer.php index 4918271..d8e283c 100755 --- a/lib/models/Courseware/ContainerTypes/ListContainer.php +++ b/lib/models/Courseware/ContainerTypes/ListContainer.php @@ -56,4 +56,24 @@ class ListContainer extends ContainerType return Schema::fromJsonString(file_get_contents($schemaFile)); } + + public function pdfExport() + { + $html = '

' . sprintf(_('Container-Typ: %s'), $this->getTitle()) . '

'; + + $payload = $this->getPayload(); + $block_ids = $payload['sections'][0]['blocks']; + + foreach ($block_ids as $block_id) { + $block = $this->container->blocks->find($block_id); + if ($block) { + $html .= $block->type->PdfExport(); + } + else { + $html .= '

' . _('Block konnte nicht gefunden werden') . '

'; + } + } + + return $html; + } } diff --git a/lib/models/Courseware/ContainerTypes/TabsContainer.php b/lib/models/Courseware/ContainerTypes/TabsContainer.php index b884bbb..72a15f4 100755 --- a/lib/models/Courseware/ContainerTypes/TabsContainer.php +++ b/lib/models/Courseware/ContainerTypes/TabsContainer.php @@ -57,4 +57,28 @@ class TabsContainer extends ContainerType return Schema::fromJsonString(file_get_contents($schemaFile)); } + + public function pdfExport() + { + $html = '

' . sprintf(_('Container-Typ: %s'), $this->getTitle()) . '

'; + + $payload = $this->getPayload(); + + $sections = $payload['sections']; + foreach ($sections as $section) { + $block_ids = $section['blocks']; + $html .= '

' . $section['name'] . '

'; + foreach ($block_ids as $block_id) { + $block = $this->container->blocks->find($block_id); + if ($block) { + $html .= $block->type->PdfExport(); + } + else { + $html .= '

' . _('Block konnte nicht gefunden werden') . '

'; + } + } + } + + return $html; + } } diff --git a/lib/models/Courseware/StructuralElement.php b/lib/models/Courseware/StructuralElement.php index 4445150..54e2880 100755 --- a/lib/models/Courseware/StructuralElement.php +++ b/lib/models/Courseware/StructuralElement.php @@ -44,6 +44,9 @@ use User; * @property \User $editor belongs_to User * @property ?\User $edit_blocker belongs_to User * @property ?\FileRef $image has_one FileRef + * @property ?\Courseware\Task $task has_one Courseware\Task + * @property \SimpleORMapCollection $comments has_many Courseware\StructuralElementComment + * @property \SimpleORMapCollection $feedback has_many Courseware\StructuralElementFeedback * * @SuppressWarnings(PHPMD.TooManyPublicMethods) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -76,6 +79,12 @@ class StructuralElement extends \SimpleORMap 'order_by' => 'ORDER BY position', ]; + $config['has_one']['task'] = [ + 'class_name' => Task::class, + 'assoc_foreign_key' => 'structural_element_id', + 'on_delete' => 'delete', + ]; + $config['belongs_to']['parent'] = [ 'class_name' => StructuralElement::class, 'foreign_key' => 'parent_id', @@ -97,14 +106,17 @@ class StructuralElement extends \SimpleORMap 'class_name' => User::class, 'foreign_key' => 'owner_id', ]; + $config['belongs_to']['editor'] = [ 'class_name' => User::class, 'foreign_key' => 'editor_id', ]; + $config['belongs_to']['edit_blocker'] = [ 'class_name' => User::class, 'foreign_key' => 'edit_blocker_id', ]; + $config['has_one']['image'] = [ 'class_name' => \FileRef::class, 'foreign_key' => 'image_id', @@ -112,6 +124,22 @@ class StructuralElement extends \SimpleORMap 'on_store' => 'store', ]; + $config['has_many']['comments'] = [ + 'class_name' => StructuralElementComment::class, + 'assoc_foreign_key' => 'structural_element_id', + 'on_delete' => 'delete', + 'on_store' => 'store', + 'order_by' => 'ORDER BY chdate', + ]; + + $config['has_many']['feedback'] = [ + 'class_name' => StructuralElementFeedback::class, + 'assoc_foreign_key' => 'structural_element_id', + 'on_delete' => 'delete', + 'on_store' => 'store', + 'order_by' => 'ORDER BY chdate', + ]; + parent::configure($config); } @@ -170,6 +198,14 @@ class StructuralElement extends \SimpleORMap } /** + * @return bool true, if this object purpose is task, false otherwise + */ + public function isTask(): bool + { + return $this->purpose === 'task'; + } + + /** * @param User $user the user to validate * * @return bool true if the user may edit this instance @@ -187,12 +223,25 @@ class StructuralElement extends \SimpleORMap return $this->range_id === $user->id; case 'course': - $haveStudipPerm = $GLOBALS['perm']->have_studip_perm( - \CourseConfig::get($this->range_id)->COURSEWARE_EDITING_PERMISSION, - $this->range_id, - $user->id - ); - if ($haveStudipPerm) { + $hasEditingPermission = $this->hasEditingPermission($user); + if ($this->isTask()) { + // TODO: Was tun wir, wenn dieses Strukturelement purpose=task aber keinen Task hat? + if (!$this->task) { + return false; + } + + if ($hasEditingPermission) { + return false; + } + + if ($this->task->isSubmitted()) { + return false; + } + + return $this->task->userIsASolver($user); + } + + if ($hasEditingPermission) { return true; } @@ -259,6 +308,19 @@ class StructuralElement extends \SimpleORMap return false; } + if ($this->isTask()) { + // TODO: Was tun wir, wenn dieses Strukturelement purpose=task aber keinen Task hat? + if (!$this->task) { + return false; + } + + if ($this->task->isSubmitted() && $this->hasEditingPermission($user)) { + return true; + } + + return $this->task->userIsASolver($user); + } + if ($this->canEdit($user)) { return true; } @@ -274,6 +336,19 @@ class StructuralElement extends \SimpleORMap } } + /** + * @param \User|\Seminar_User $user + */ + public function hasEditingPermission($user): bool + { + return $GLOBALS['perm']->have_perm('root', $user->id) || + $GLOBALS['perm']->have_studip_perm( + \CourseConfig::get($this->range_id)->COURSEWARE_EDITING_PERMISSION, + $this->range_id, + $user->id + ); + } + private function hasReadApproval($user): bool { if (!count($this->read_approval)) { @@ -410,10 +485,7 @@ class StructuralElement extends \SimpleORMap foreach ($this->containers as $container) { foreach ($container->blocks as $block) { /** @var ?UserProgress $progress */ - $progress = UserProgress::findOneBySQL('user_id = ? and block_id = ?', [ - $user->id, - $block->id, - ]); + $progress = UserProgress::findOneBySQL('user_id = ? and block_id = ?', [$user->id, $block->id]); if (!$progress || $progress->grade != 1) { return false; @@ -438,7 +510,11 @@ class StructuralElement extends \SimpleORMap if ('all' == $purpose) { return self::findBySQL('range_id = ? AND parent_id = ? ORDER BY position ASC', [$userId, $root->id]); } else { - return self::findBySQL('range_id = ? AND parent_id = ? AND purpose = ? ORDER BY position ASC', [$userId, $root->id, $purpose]); + return self::findBySQL('range_id = ? AND parent_id = ? AND purpose = ? ORDER BY position ASC', [ + $userId, + $root->id, + $purpose, + ]); } } @@ -563,7 +639,7 @@ class StructuralElement extends \SimpleORMap SQL; $params = [$range->getRangeId(), $range->getRangeType(), $user->id]; - return \DBManager::get()->fetchAll($sql, $params, StructuralElement::class.'::buildExisting'); + return \DBManager::get()->fetchAll($sql, $params, StructuralElement::class . '::buildExisting'); } /** @@ -638,4 +714,64 @@ SQL; $child->copy($user, $newElement); } } + + public function pdfExport($user) + { + $doc = new \ExportPDF('P', 'mm', 'A4', true, 'UTF-8', false); + $doc->setHeaderTitle(_('Courseware')); + if ($this->course) { + $doc->setHeaderTitle(sprintf(_('Courseware aus %s'), $this->course->name)); + } + if ($this->user) { + $doc->setHeaderTitle(sprintf(_('Courseware von %s'), $this->user->getFullname())); + } + + $doc->addPage(); + + if (!self::canRead($user)) { + $doc->addContent(_('Diese Seite steht Ihnen nicht zur Verfügung!')); + + return $doc; + } + + $doc->writeHTML($this->getElementPdfExport()); + + return $doc; + } + + private function getElementPdfExport(string $parent_name = '', bool $with_children = false) + { + if ($parent_name !== '') { + $parent_name .= ' / '; + } + $html = '

' . $parent_name . $this->title . '

'; + $html .= $this->getContainerPdfExport(); + if ($with_children) { + $html .= $this->getChildrenPdfExport($parent_name); + } + + return $html; + } + + private function getChildrenPdfExport(string $parent_name) + { + $children = self::findBySQL('parent_id = ?', [$this->id]); + $html = ''; + foreach ($children as $child) { + $html .= $child->getElementPdfExport($parent_name . $this->title); + } + + return $html; + } + + private function getContainerPdfExport() + { + $containers = \Courseware\Container::findBySQL('structural_element_id = ?', [$this->id]); + + foreach ($containers as $container) { + $html .= $container->type->pdfExport(); + } + + return $html; + } } diff --git a/lib/models/Courseware/StructuralElementComment.php b/lib/models/Courseware/StructuralElementComment.php new file mode 100755 index 0000000..e5fbc81 --- /dev/null +++ b/lib/models/Courseware/StructuralElementComment.php @@ -0,0 +1,42 @@ + + * @license GPL2 or any later version + * + * @since Stud.IP 5.1 + * + * @property int $id database column + * @property int $structural_element_id database column + * @property string $user_id database column + * @property string $comment database column + * @property int $mkdate database column + * @property int $chdate database column + * @property \User $user belongs_to User + * @property \Courseware\StructuralElement $structural_element belongs_to Courseware\StructuralElement + */ +class StructuralElementComment extends \SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'cw_structural_element_comments'; + + $config['belongs_to']['user'] = [ + 'class_name' => User::class, + 'foreign_key' => 'user_id', + ]; + + $config['belongs_to']['structural_element'] = [ + 'class_name' => StructuralElement::class, + 'foreign_key' => 'structural_element_id', + ]; + + parent::configure($config); + } +} diff --git a/lib/models/Courseware/StructuralElementFeedback.php b/lib/models/Courseware/StructuralElementFeedback.php new file mode 100755 index 0000000..a2e3b8b --- /dev/null +++ b/lib/models/Courseware/StructuralElementFeedback.php @@ -0,0 +1,42 @@ + + * @license GPL2 or any later version + * + * @since Stud.IP 5.1 + * + * @property int $id database column + * @property int $structural_element_id database column + * @property string $user_id database column + * @property string $feedback database column + * @property int $mkdate database column + * @property int $chdate database column + * @property \User $user belongs_to User + * @property \Courseware\StructuralElement $structural_element belongs_to Courseware\StructuralElement + */ +class StructuralElementFeedback extends \SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'cw_structural_element_feedbacks'; + + $config['belongs_to']['user'] = [ + 'class_name' => User::class, + 'foreign_key' => 'user_id', + ]; + + $config['belongs_to']['structural_element'] = [ + 'class_name' => StructuralElement::class, + 'foreign_key' => 'structural_element_id', + ]; + + parent::configure($config); + } +} diff --git a/lib/models/Courseware/Task.php b/lib/models/Courseware/Task.php new file mode 100755 index 0000000..fe61a2e --- /dev/null +++ b/lib/models/Courseware/Task.php @@ -0,0 +1,217 @@ + + * @license GPL2 or any later version + * + * @since Stud.IP 5.1 + * + * @property int $id database column + * @property int $task_group_id database column + * @property int $structural_element_id database column + * @property string $solver_id database column + * @property string $solver_type database column + * @property int $submission_date database column + * @property int $submitted database column + * @property string $renewal database column + * @property int $renewal_date database column + * @property int $feedback_id database column + * @property int $mkdate database column + * @property int $chdate database column + * @property \Courseware\TaskGroup $task_group belongs_to Courseware\TaskGroup + * @property \Courseware\StructuralElement $structural_element belongs_to Courseware\TaskGroup + * @property \User $user belongs_to User + * @property \Statusgruppen $group belongs_to Statusgruppen + * @property \Courseware\TaskFeedback $task_feedback belongs_to Courseware\TaskFeedback + * @property-read \User|\Statusgruppen|null $solver belongs_to User or Statusgruppen + */ +class Task extends \SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'cw_tasks'; + + $config['belongs_to']['task_group'] = [ + 'class_name' => TaskGroup::class, + 'foreign_key' => 'task_group_id', + ]; + + $config['belongs_to']['structural_element'] = [ + 'class_name' => StructuralElement::class, + 'foreign_key' => 'structural_element_id', + ]; + + $config['belongs_to']['lecturer'] = [ + 'class_name' => User::class, + 'foreign_key' => 'lecturer_id', + ]; + + $config['belongs_to']['user'] = [ + 'class_name' => User::class, + 'foreign_key' => 'solver_id', + 'assoc_foreign_key' => 'user_id', + ]; + + $config['belongs_to']['group'] = [ + 'class_name' => \Statusgruppen::class, + 'foreign_key' => 'solver_id', + 'assoc_foreign_key' => 'statusgruppe_id', + ]; + + $config['belongs_to']['course'] = [ + 'class_name' => \Course::class, + 'foreign_key' => 'seminar_id', + ]; + + $config['belongs_to']['structural_element'] = [ + 'class_name' => StructuralElement::class, + 'foreign_key' => 'structural_element_id', + ]; + + $config['belongs_to']['task_feedback'] = [ + 'class_name' => TaskFeedback::class, + 'foreign_key' => 'feedback_id', + ]; + + $config['additional_fields']['solver'] = [ + 'get' => 'getSolver', + 'set' => false, + ]; + + parent::configure($config); + } + + /** + * Returns the structural element of this task. + * This structural element and all its children are part of the task. + * + * @return StructuralElement the structural element + */ + public function getStructuralElement(): StructuralElement + { + return $this->structural_element; + } + + /** + * Returns the feedback for this task. + * + * @return TaskFeedback the task feedback + */ + public function getFeedback(): ?TaskFeedback + { + return $this->task_feedback; + } + + /** + * @return bool true if task is submitted + */ + public function isSubmitted(): bool + { + return 1 === (int) $this->submitted; + } + + /** + * @param \User|\Seminar_User $user + */ + public function canUpdate($user): bool + { + $perm = false; + switch ($this->solver_type) { + case 'autor': + if ($this->solver_id === $user->id) { + return true; + } + break; + + case 'group': + $group = \Statusgruppen::find($this->solver_id); + if (isset($group) && $group->isMember($user->id)) { + return true; + } + break; + } + + return $this->getStructuralElement()->hasEditingPermission($user); + } + + /** + * @param \User|\Seminar_User $user + */ + public function userIsASolver($user): bool + { + switch ($this->solver_type) { + case 'autor': + return $this->solver_id === $user->id; + + case 'group': + $group = $this->getSolver(); + + return $group->isMember($user->id); + } + + return false; + } + + /** + * @return \User|\Statusgruppen|null the solver + */ + public function getSolver() + { + switch ($this->solver_type) { + case 'autor': + return \User::find($this->solver_id); + + case 'group': + return \Statusgruppen::find($this->solver_id); + } + + return null; + } + + public function getTaskProgress(): float + { + $children = $this->structural_element->findDescendants(); + + $element_counter = 1; + $progress = $this->getStructuralElementProgress($this->structural_element); + foreach ($children as $child) { + ++$element_counter; + $progress = ($this->getStructuralElementProgress($child) + $progress) / $element_counter; + } + + return $progress * 100; + } + + private function getStructuralElementProgress(StructuralElement $structural_element): float + { + $containers = Container::findBySQL('structural_element_id = ?', [intval($structural_element->id)]); + $counter = 0; + $progress = 0; + $b = []; + foreach ($containers as $container) { + $blockCount = $container->countBlocks(); + + $counter += $blockCount; + if ($blockCount > 0) { + $blocks = Block::findBySQL('container_id = ?', [$container->id]); + foreach ($blocks as $block) { + $b[] = $block->id; + if ($this->task_group->lecturer_id === $block->owner_id && $block->owner_id !== $block->editor_id) { + ++$progress; + } + } + } + } + if ($counter > 0) { + return $progress / $counter; + } + + return 0; + } +} diff --git a/lib/models/Courseware/TaskFeedback.php b/lib/models/Courseware/TaskFeedback.php new file mode 100755 index 0000000..57e2ce0 --- /dev/null +++ b/lib/models/Courseware/TaskFeedback.php @@ -0,0 +1,58 @@ + +* @license GPL2 or any later version +* +* @since Stud.IP 5.1 +* +* @property int $id database column +* @property int $task_id database column +* @property string $lecturer_id database column +* @property string $content database column +* @property int $mkdate database column +* @property int $chdate database column + +* @property \User $lecturer belongs_to User +* @property \Courseware\Task $task belongs_to Courseware\Task +*/ +class TaskFeedback extends \SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'cw_task_feedbacks'; + + $config['belongs_to']['lecturer'] = [ + 'class_name' => User::class, + 'foreign_key' => 'lecturer_id', + ]; + + $config['belongs_to']['task'] = [ + 'class_name' => Task::class, + 'foreign_key' => 'task_id', + ]; + + parent::configure($config); + } + + public function getStructuralElement(): ?StructuralElement + { + $sql = 'SELECT se.* + FROM cw_task_feedbacks tf + JOIN cw_tasks t ON t.id = tf.task_id + JOIN cw_structural_elements se ON se.id = t.structural_element_id + WHERE tf.id = ?'; + $structuralElement = \DBManager::get()->fetchOne($sql, [$this->getId()]); + if (!count($structuralElement)) { + return null; + } + + return StructuralElement::build($structuralElement, false); + } +} diff --git a/lib/models/Courseware/TaskGroup.php b/lib/models/Courseware/TaskGroup.php new file mode 100644 index 0000000..7ca6eb1 --- /dev/null +++ b/lib/models/Courseware/TaskGroup.php @@ -0,0 +1,61 @@ + + * @license GPL2 or any later version + * + * @since Stud.IP 5.1 + * + * @property int $id database column + * @property string $seminar_id database column + * @property string $lecturer_id database column + * @property int $structural_element_id database column + * @property int $solver_may_add_blocks database column + * @property string $title database column + * @property int $mkdate database column + * @property int $chdate database column + * @property \User $lecturer belongs_to User + * @property \Course $course belongs_to Course + * @property \Courseware\StructuralElement $structural_element belongs_to Courseware\StructuralElement + * @property \SimpleORMapCollection $tasks has_many Courseware\Task + */ +class TaskGroup extends \SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'cw_task_groups'; + + $config['belongs_to']['lecturer'] = [ + 'class_name' => User::class, + 'foreign_key' => 'lecturer_id', + ]; + + $config['belongs_to']['course'] = [ + 'class_name' => \Course::class, + 'foreign_key' => 'seminar_id', + ]; + + $config['has_many']['tasks'] = [ + 'class_name' => Task::class, + 'assoc_foreign_key' => 'task_group_id', + 'on_delete' => 'delete', + 'on_store' => 'store', + 'order_by' => 'ORDER BY mkdate', + ]; + + parent::configure($config); + } + + public function getSolvers(): iterable + { + $solvers = $this->tasks->pluck('solver'); + + return $solvers; + } +} diff --git a/lib/models/Courseware/Template.php b/lib/models/Courseware/Template.php new file mode 100755 index 0000000..8161d98 --- /dev/null +++ b/lib/models/Courseware/Template.php @@ -0,0 +1,28 @@ + +* @license GPL2 or any later version +* +* @since Stud.IP 5.1 +* +* @property int $id database column +* @property string $name database column +* @property string $purpose database column +* @property string $structure database column +* @property int $mkdate database column +* @property int $chdate database column +*/ +class Template extends \SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'cw_templates'; + + parent::configure($config); + } +} \ No newline at end of file diff --git a/lib/navigation/AdminNavigation.php b/lib/navigation/AdminNavigation.php index 715f95f..915ceb3 100644 --- a/lib/navigation/AdminNavigation.php +++ b/lib/navigation/AdminNavigation.php @@ -132,6 +132,13 @@ class AdminNavigation extends Navigation if (Config::get()->BANNER_ADS_ENABLE) { $navigation->addSubNavigation('banner', new Navigation(_('Werbebanner'), 'dispatch.php/admin/banner')); } + $navigation->addSubNavigation( + 'courseware', + new Navigation( + _('Courseware'), + 'dispatch.php/admin/courseware/index' + ) + ); if (Config::get()->OERCAMPUS_ENABLED) { $navigation->addSubNavigation( 'oer', diff --git a/lib/navigation/ContentsNavigation.php b/lib/navigation/ContentsNavigation.php index 0487ede..767b86e 100755 --- a/lib/navigation/ContentsNavigation.php +++ b/lib/navigation/ContentsNavigation.php @@ -48,7 +48,7 @@ class ContentsNavigation extends Navigation $courseware->setImage(Icon::create('courseware')); $courseware->addSubNavigation( - 'projects', + 'overview', new Navigation(_('Übersicht'), 'dispatch.php/contents/courseware/index') ); $courseware->addSubNavigation( diff --git a/package.json b/package.json index 683e2c1..ea98320 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "vue-template-compiler": "^2.6.12", "vue-twentytwenty": "^0.10.1", "vue-typer": "^1.2.0", + "vuedraggable": "^2.24.3", "vuex": "^3.6.2", "webpack": "5.6.0", "webpack-cli": "4.2.0", diff --git a/public/assets/images/icons/black/bullet-arrow.svg b/public/assets/images/icons/black/bullet-arrow.svg new file mode 100644 index 0000000..749a45e --- /dev/null +++ b/public/assets/images/icons/black/bullet-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/bullet-dot.svg b/public/assets/images/icons/black/bullet-dot.svg new file mode 100644 index 0000000..846e6e4 --- /dev/null +++ b/public/assets/images/icons/black/bullet-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/bullet-double-arrow.svg b/public/assets/images/icons/black/bullet-double-arrow.svg new file mode 100644 index 0000000..37f2642 --- /dev/null +++ b/public/assets/images/icons/black/bullet-double-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/bullet-line.svg b/public/assets/images/icons/black/bullet-line.svg new file mode 100644 index 0000000..e8c0e83 --- /dev/null +++ b/public/assets/images/icons/black/bullet-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/category-draft.svg b/public/assets/images/icons/black/category-draft.svg new file mode 100644 index 0000000..9841a61 --- /dev/null +++ b/public/assets/images/icons/black/category-draft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/category-others.svg b/public/assets/images/icons/black/category-others.svg new file mode 100644 index 0000000..cba7cd9 --- /dev/null +++ b/public/assets/images/icons/black/category-others.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/category-portfolio.svg b/public/assets/images/icons/black/category-portfolio.svg new file mode 100644 index 0000000..3c60df2 --- /dev/null +++ b/public/assets/images/icons/black/category-portfolio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/category-task.svg b/public/assets/images/icons/black/category-task.svg new file mode 100644 index 0000000..e8dd236 --- /dev/null +++ b/public/assets/images/icons/black/category-task.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/category-template.svg b/public/assets/images/icons/black/category-template.svg new file mode 100644 index 0000000..fdba7ab --- /dev/null +++ b/public/assets/images/icons/black/category-template.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/content2.svg b/public/assets/images/icons/black/content2.svg new file mode 100644 index 0000000..abb38b2 --- /dev/null +++ b/public/assets/images/icons/black/content2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/bullet-arrow.svg b/public/assets/images/icons/blue/bullet-arrow.svg new file mode 100644 index 0000000..474d6cf --- /dev/null +++ b/public/assets/images/icons/blue/bullet-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/bullet-dot.svg b/public/assets/images/icons/blue/bullet-dot.svg new file mode 100644 index 0000000..a987401 --- /dev/null +++ b/public/assets/images/icons/blue/bullet-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/bullet-double-arrow.svg b/public/assets/images/icons/blue/bullet-double-arrow.svg new file mode 100644 index 0000000..c686f81 --- /dev/null +++ b/public/assets/images/icons/blue/bullet-double-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/bullet-line.svg b/public/assets/images/icons/blue/bullet-line.svg new file mode 100644 index 0000000..fc53d91 --- /dev/null +++ b/public/assets/images/icons/blue/bullet-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/category-draft.svg b/public/assets/images/icons/blue/category-draft.svg new file mode 100644 index 0000000..4efaccd --- /dev/null +++ b/public/assets/images/icons/blue/category-draft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/category-others.svg b/public/assets/images/icons/blue/category-others.svg new file mode 100644 index 0000000..04ec1b4 --- /dev/null +++ b/public/assets/images/icons/blue/category-others.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/category-portfolio.svg b/public/assets/images/icons/blue/category-portfolio.svg new file mode 100644 index 0000000..9e40698 --- /dev/null +++ b/public/assets/images/icons/blue/category-portfolio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/category-task.svg b/public/assets/images/icons/blue/category-task.svg new file mode 100644 index 0000000..639dd91 --- /dev/null +++ b/public/assets/images/icons/blue/category-task.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/category-template.svg b/public/assets/images/icons/blue/category-template.svg new file mode 100644 index 0000000..852b4c3 --- /dev/null +++ b/public/assets/images/icons/blue/category-template.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/content2.svg b/public/assets/images/icons/blue/content2.svg index 9d8d712..ce93472 100644 --- a/public/assets/images/icons/blue/content2.svg +++ b/public/assets/images/icons/blue/content2.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/images/icons/green/bullet-arrow.svg b/public/assets/images/icons/green/bullet-arrow.svg new file mode 100644 index 0000000..e6845b6 --- /dev/null +++ b/public/assets/images/icons/green/bullet-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/bullet-dot.svg b/public/assets/images/icons/green/bullet-dot.svg new file mode 100644 index 0000000..e045370 --- /dev/null +++ b/public/assets/images/icons/green/bullet-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/bullet-double-arrow.svg b/public/assets/images/icons/green/bullet-double-arrow.svg new file mode 100644 index 0000000..ea9fdb1 --- /dev/null +++ b/public/assets/images/icons/green/bullet-double-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/bullet-line.svg b/public/assets/images/icons/green/bullet-line.svg new file mode 100644 index 0000000..809690d --- /dev/null +++ b/public/assets/images/icons/green/bullet-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/category-draft.svg b/public/assets/images/icons/green/category-draft.svg new file mode 100644 index 0000000..df4399e --- /dev/null +++ b/public/assets/images/icons/green/category-draft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/category-others.svg b/public/assets/images/icons/green/category-others.svg new file mode 100644 index 0000000..6712133 --- /dev/null +++ b/public/assets/images/icons/green/category-others.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/category-portfolio.svg b/public/assets/images/icons/green/category-portfolio.svg new file mode 100644 index 0000000..a47558a --- /dev/null +++ b/public/assets/images/icons/green/category-portfolio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/category-task.svg b/public/assets/images/icons/green/category-task.svg new file mode 100644 index 0000000..f6912c5 --- /dev/null +++ b/public/assets/images/icons/green/category-task.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/category-template.svg b/public/assets/images/icons/green/category-template.svg new file mode 100644 index 0000000..74bb961 --- /dev/null +++ b/public/assets/images/icons/green/category-template.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/content2.svg b/public/assets/images/icons/green/content2.svg new file mode 100644 index 0000000..3952142 --- /dev/null +++ b/public/assets/images/icons/green/content2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/bullet-arrow.svg b/public/assets/images/icons/grey/bullet-arrow.svg new file mode 100644 index 0000000..8fff0f1 --- /dev/null +++ b/public/assets/images/icons/grey/bullet-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/bullet-dot.svg b/public/assets/images/icons/grey/bullet-dot.svg new file mode 100644 index 0000000..01510e4 --- /dev/null +++ b/public/assets/images/icons/grey/bullet-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/bullet-double-arrow.svg b/public/assets/images/icons/grey/bullet-double-arrow.svg new file mode 100644 index 0000000..ceb9d9a --- /dev/null +++ b/public/assets/images/icons/grey/bullet-double-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/bullet-line.svg b/public/assets/images/icons/grey/bullet-line.svg new file mode 100644 index 0000000..1f5af73 --- /dev/null +++ b/public/assets/images/icons/grey/bullet-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/category-draft.svg b/public/assets/images/icons/grey/category-draft.svg new file mode 100644 index 0000000..45b0474 --- /dev/null +++ b/public/assets/images/icons/grey/category-draft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/category-others.svg b/public/assets/images/icons/grey/category-others.svg new file mode 100644 index 0000000..06f8871 --- /dev/null +++ b/public/assets/images/icons/grey/category-others.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/category-portfolio.svg b/public/assets/images/icons/grey/category-portfolio.svg new file mode 100644 index 0000000..ee7d3b8 --- /dev/null +++ b/public/assets/images/icons/grey/category-portfolio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/category-task.svg b/public/assets/images/icons/grey/category-task.svg new file mode 100644 index 0000000..7065982 --- /dev/null +++ b/public/assets/images/icons/grey/category-task.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/category-template.svg b/public/assets/images/icons/grey/category-template.svg new file mode 100644 index 0000000..9294d4a --- /dev/null +++ b/public/assets/images/icons/grey/category-template.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/content2.svg b/public/assets/images/icons/grey/content2.svg new file mode 100644 index 0000000..3521bf9 --- /dev/null +++ b/public/assets/images/icons/grey/content2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/bullet-arrow.svg b/public/assets/images/icons/red/bullet-arrow.svg new file mode 100644 index 0000000..63b4f3c --- /dev/null +++ b/public/assets/images/icons/red/bullet-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/bullet-dot.svg b/public/assets/images/icons/red/bullet-dot.svg new file mode 100644 index 0000000..4be587b --- /dev/null +++ b/public/assets/images/icons/red/bullet-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/bullet-double-arrow.svg b/public/assets/images/icons/red/bullet-double-arrow.svg new file mode 100644 index 0000000..5502413 --- /dev/null +++ b/public/assets/images/icons/red/bullet-double-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/bullet-line.svg b/public/assets/images/icons/red/bullet-line.svg new file mode 100644 index 0000000..9f2cc6b --- /dev/null +++ b/public/assets/images/icons/red/bullet-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/category-draft.svg b/public/assets/images/icons/red/category-draft.svg new file mode 100644 index 0000000..9e3e0f4 --- /dev/null +++ b/public/assets/images/icons/red/category-draft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/category-others.svg b/public/assets/images/icons/red/category-others.svg new file mode 100644 index 0000000..918d2ea --- /dev/null +++ b/public/assets/images/icons/red/category-others.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/category-portfolio.svg b/public/assets/images/icons/red/category-portfolio.svg new file mode 100644 index 0000000..c8e9adc --- /dev/null +++ b/public/assets/images/icons/red/category-portfolio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/category-task.svg b/public/assets/images/icons/red/category-task.svg new file mode 100644 index 0000000..3a9bee9 --- /dev/null +++ b/public/assets/images/icons/red/category-task.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/category-template.svg b/public/assets/images/icons/red/category-template.svg new file mode 100644 index 0000000..e25390b --- /dev/null +++ b/public/assets/images/icons/red/category-template.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/content2.svg b/public/assets/images/icons/red/content2.svg new file mode 100644 index 0000000..0065efd --- /dev/null +++ b/public/assets/images/icons/red/content2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/bullet-arrow.svg b/public/assets/images/icons/white/bullet-arrow.svg new file mode 100644 index 0000000..cc3da9f --- /dev/null +++ b/public/assets/images/icons/white/bullet-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/bullet-dot.svg b/public/assets/images/icons/white/bullet-dot.svg new file mode 100644 index 0000000..0620a65 --- /dev/null +++ b/public/assets/images/icons/white/bullet-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/bullet-double-arrow.svg b/public/assets/images/icons/white/bullet-double-arrow.svg new file mode 100644 index 0000000..67a3ae6 --- /dev/null +++ b/public/assets/images/icons/white/bullet-double-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/bullet-line.svg b/public/assets/images/icons/white/bullet-line.svg new file mode 100644 index 0000000..1bafde9 --- /dev/null +++ b/public/assets/images/icons/white/bullet-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/category-draft.svg b/public/assets/images/icons/white/category-draft.svg new file mode 100644 index 0000000..d1cd91e --- /dev/null +++ b/public/assets/images/icons/white/category-draft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/category-others.svg b/public/assets/images/icons/white/category-others.svg new file mode 100644 index 0000000..0b4cbb4 --- /dev/null +++ b/public/assets/images/icons/white/category-others.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/category-portfolio.svg b/public/assets/images/icons/white/category-portfolio.svg new file mode 100644 index 0000000..d7d8faf --- /dev/null +++ b/public/assets/images/icons/white/category-portfolio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/category-task.svg b/public/assets/images/icons/white/category-task.svg new file mode 100644 index 0000000..534657e --- /dev/null +++ b/public/assets/images/icons/white/category-task.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/category-template.svg b/public/assets/images/icons/white/category-template.svg new file mode 100644 index 0000000..b7b1d04 --- /dev/null +++ b/public/assets/images/icons/white/category-template.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/content2.svg b/public/assets/images/icons/white/content2.svg new file mode 100644 index 0000000..bca1195 --- /dev/null +++ b/public/assets/images/icons/white/content2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/bullet-arrow.svg b/public/assets/images/icons/yellow/bullet-arrow.svg new file mode 100644 index 0000000..7af9423 --- /dev/null +++ b/public/assets/images/icons/yellow/bullet-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/bullet-dot.svg b/public/assets/images/icons/yellow/bullet-dot.svg new file mode 100644 index 0000000..644e1f8 --- /dev/null +++ b/public/assets/images/icons/yellow/bullet-dot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/bullet-double-arrow.svg b/public/assets/images/icons/yellow/bullet-double-arrow.svg new file mode 100644 index 0000000..44855a8 --- /dev/null +++ b/public/assets/images/icons/yellow/bullet-double-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/bullet-line.svg b/public/assets/images/icons/yellow/bullet-line.svg new file mode 100644 index 0000000..40abc52 --- /dev/null +++ b/public/assets/images/icons/yellow/bullet-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/category-draft.svg b/public/assets/images/icons/yellow/category-draft.svg new file mode 100644 index 0000000..e9ec7ee --- /dev/null +++ b/public/assets/images/icons/yellow/category-draft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/category-others.svg b/public/assets/images/icons/yellow/category-others.svg new file mode 100644 index 0000000..58fa357 --- /dev/null +++ b/public/assets/images/icons/yellow/category-others.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/category-portfolio.svg b/public/assets/images/icons/yellow/category-portfolio.svg new file mode 100644 index 0000000..bac2607 --- /dev/null +++ b/public/assets/images/icons/yellow/category-portfolio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/category-task.svg b/public/assets/images/icons/yellow/category-task.svg new file mode 100644 index 0000000..ddde9f9 --- /dev/null +++ b/public/assets/images/icons/yellow/category-task.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/category-template.svg b/public/assets/images/icons/yellow/category-template.svg new file mode 100644 index 0000000..9d5774e --- /dev/null +++ b/public/assets/images/icons/yellow/category-template.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/content2.svg b/public/assets/images/icons/yellow/content2.svg new file mode 100644 index 0000000..3ec9dc6 --- /dev/null +++ b/public/assets/images/icons/yellow/content2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/plugins_packages/core/ActivityFeed/ActivityFeed.php b/public/plugins_packages/core/ActivityFeed/ActivityFeed.php index a966e95..f42b34b 100644 --- a/public/plugins_packages/core/ActivityFeed/ActivityFeed.php +++ b/public/plugins_packages/core/ActivityFeed/ActivityFeed.php @@ -115,7 +115,8 @@ class ActivityFeed extends StudIPPlugin implements PortalPlugin 'wiki' => _('Wiki'), 'schedule' => _('Ablaufplan'), 'news' => _('Ankündigungen'), - 'blubber' => _('Blubber') + 'blubber' => _('Blubber'), + 'courseware' => _('Courseware') ]; $modules[\Context::INSTITUTE] = $modules[\Context::COURSE]; diff --git a/resources/assets/javascripts/bootstrap/courseware.js b/resources/assets/javascripts/bootstrap/courseware.js index f9a85fe..2c2938d 100755 --- a/resources/assets/javascripts/bootstrap/courseware.js +++ b/resources/assets/javascripts/bootstrap/courseware.js @@ -31,4 +31,37 @@ STUDIP.domReady(() => { }); }); } + + if (document.getElementById('courseware-content-overview-app')) { + STUDIP.Vue.load().then(({ createApp }) => { + import( + /* webpackChunkName: "courseware-content-overview-app" */ + '@/vue/courseware-content-overview-app.js' + ).then(({ default: mountApp }) => { + return mountApp(STUDIP, createApp, '#courseware-content-overview-app'); + }); + }); + } + + if (document.getElementById('courseware-content-bookmark-app')) { + STUDIP.Vue.load().then(({ createApp }) => { + import( + /* webpackChunkName: "courseware-content-bookmark-app" */ + '@/vue/courseware-content-bookmark-app.js' + ).then(({ default: mountApp }) => { + return mountApp(STUDIP, createApp, '#courseware-content-bookmark-app'); + }); + }); + } + + if (document.getElementById('courseware-admin-app')) { + STUDIP.Vue.load().then(({ createApp }) => { + import( + /* webpackChunkName: "courseware-content-bookmark-app" */ + '@/vue/courseware-admin-app.js' + ).then(({ default: mountApp }) => { + return mountApp(STUDIP, createApp, '#courseware-admin-app'); + }); + }); + } }); diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index cd4c5c8..54f4400 100755 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -10,6 +10,22 @@ $companion-types: ( pointing: pointing-right ); +$element-icons: ( + content: content2, + draft: category-draft, + task: category-task, + template: category-template, + oer: oer-campus, + other: category-others, + portfolio: category-portfolio +); + +$tree-item-flag-icons: ( + date: date, + write: edit, + cant-read: lock-locked2 +); + $tile-colors: ( black: #000, charcoal: #3c454e, @@ -76,6 +92,10 @@ $media-buttons: ( /* * * * * * * * c o n t e n t s * * * * * * * * */ +.cw-content-overview { + max-width: 1100px; +} + .cw-contents-overview-teaser { max-width: 782px; background-color: $content-color-20; @@ -122,6 +142,56 @@ c o n t e n t s .cw-loading-indicator-content { margin-top: 76px; } +.cw-content-loading { + /* Loading animation from activity feed */ + .loading-indicator { + text-align: center; + padding: 1em 0; + } + + .loading-indicator span { + background-color: #CCCCDD; + border-radius: 50%; + height: 10px; + position: relative; + width: 10px; + display: inline-block; + } + + .loading-indicator span.load-1 { + animation: loading-animation-1 1s linear 20; + } + + .loading-indicator span.load-2 { + animation: loading-animation-2 1s linear 20; + } + + .loading-indicator span.load-3 { + animation: loading-animation-3 1s linear 20; + } + + @keyframes loading-animation-1 { + 0% { transform: scale(1); } + 16% { transform: scale(1.3); } + 33% { transform: scale(1); } + 100% { transform: scale(1); } + } + + @keyframes loading-animation-2 { + 0% { transform: scale(1); } + 33% { transform: scale(1); } + 49% { transform: scale(1.3); } + 65% { transform: scale(1); } + 100% { transform: scale(1); } + } + + @keyframes loading-animation-3 { + 0% { transform: scale(1); } + 66% { transform: scale(1); } + 81% { transform: scale(1.3); } + 100% { transform: scale(1); } + } +} /* * * * * * * * * * * c o n t e n t s e n d @@ -502,18 +572,29 @@ ribbon end } + .cw-structural-element-discussion { + max-width: 1606px; + width: 100%; + margin-bottom: 1em; + } + .cw-container-wrapper { - max-width: 1115px; + max-width: 1095px; margin: 0; padding: 0; display: flex; flex-wrap: wrap; align-items: stretch; + justify-content: space-between; &.cw-container-wrapper-consume { margin: 0 auto; padding: 6em 1em 1em 1em; } + + &.cw-container-wrapper-discuss { + max-width: 1606px; + } } .cw-structural-element-description { @@ -647,7 +728,6 @@ ribbon end &.cw-container-colspan-half { max-width: 540px; width: 100%; - margin-right: 15px; } &.cw-container-colspan-half-center { width: 1095px; @@ -734,6 +814,24 @@ form.cw-container-dialog-edit-form { } } +.cw-container-wrapper-discuss { + flex-direction: column; + + .cw-container-colspan-full { + max-width: unset; + } + .cw-container-colspan-half-center, + .cw-container-colspan-half { + max-width: 1050px; + } + .cw-container-colspan-half-center { + width: 100%; + .cw-container-content { + width: 1050px; + } + } +} + /* * * * * * * container end * * * * * * */ @@ -766,6 +864,19 @@ form.cw-container-dialog-edit-form { padding: 0.5em; } } +.cw-container-wrapper-discuss { + .cw-container-colspan-full { + .cw-content-wrapper { + max-width: 1095px; + } + } + .cw-container-colspan-half, + .cw-container-colspan-half-center { + .cw-content-wrapper { + max-width: 540px; + } + } +} .cw-block-header { background-color: $content-color-20; max-height: 30px; @@ -788,6 +899,7 @@ form.cw-container-dialog-edit-form { } } +.cw-discuss-wrapper, .cw-block-features { header{ @@ -802,6 +914,44 @@ form.cw-container-dialog-edit-form { } } +.cw-discuss-wrapper { + flex-shrink: 3; + flex-grow: 2; + margin-left: 10px; +} + +@media only screen and (max-width: 1820px) { + .cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss { + max-width: 1095px; + .cw-container.cw-container-list.cw-container-item.cw-container-colspan-full { + .cw-default-block { + flex-flow: column; + .cw-discuss-wrapper { + margin-left: 0; + margin-top: 8px; + } + } + } + } +} + +@media only screen and (max-width: 1200px) { + .cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss { + max-width: 1095px; + .cw-container.cw-container-list.cw-container-item.cw-container-colspan-half, + .cw-container.cw-container-list.cw-container-item.cw-container-colspan-half-center { + .cw-default-block { + flex-flow: column; + .cw-discuss-wrapper { + margin-left: 0; + margin-top: 8px; + max-width: 540px; + } + } + } + } +} + .cw-button-feature-close { float: right; border: none; @@ -962,6 +1112,66 @@ label[for="cw-keypoint-color"] { block end * * * * * */ +/* * * * * * * * + sortable handle + * * * * * * * */ +.cw-container-list-sort-mode { + .block-ghost { + opacity: 0.6; + } + &.cw-container-list-sort-mode-empty { + min-height: 4em; + border: dashed thin $content-color-40; + } +} +.cw-structural-element-list-sort-mode { + list-style: none; + padding-left: 0; + + .cw-container-item-sortable { + border: solid thin $content-color-40; + background-color: $content-color-20; + color: $base-color; + font-weight: 700; + margin-bottom: 0.5em; + padding: 0.5em; + } + .container-ghost { + opacity: 0.6; + } +} +.cw-structural-element-list-sort-mode, +.cw-container-list-sort-mode { + .cw-sortable-handle { + display: inline-block; + cursor: grab; + background-image: url("#{$image-path}/anfasser_24.png"); + background-repeat: no-repeat; + width: 7px; + height: 24px; + padding-right: 4px; + vertical-align: middle; + } + .cw-content-wrapper-active:hover { + border: solid thin $base-color; + } +} + +.cw-container-item-sortable.sortable-chosen { + .cw-sortable-handle { + cursor: grabbing; + } +} + +.cw-container-sort-buttons { + display: block; +} + + +/* * * * * * * * * * * + sortable handle end + * * * * * * * * * * */ + /* * * * * t r e e * * * * */ @@ -972,6 +1182,7 @@ label[for="cw-keypoint-color"] { padding-left: 1.25em; margin-bottom: 20px; + &.cw-tree-subchapter-list, &.cw-tree-chapter-list, &.cw-tree-root-list { padding-left: 0; @@ -985,11 +1196,11 @@ label[for="cw-keypoint-color"] { padding-left: 3px; } } - .cw-tree-item-is-root{ + .cw-tree-item-is-root { display: block; font-size: 18px; .cw-tree-item-link { - padding-left: 24px; + padding-left: 26px; @include background-icon(courseware, clickable, 18); background-repeat: no-repeat; @@ -1007,26 +1218,48 @@ label[for="cw-keypoint-color"] { &:hover { background-color: $content-color-20; } + .cw-tree-item-link, + .cw-tree-item-link:hover, + .cw-tree-item-link.cw-tree-item-link-current { + background-image: none; + } + + @each $type, $icon in $element-icons { + &.cw-tree-item-#{$type} .cw-tree-item-link { + background-repeat: no-repeat; + background-position: 3px 3px; + padding-left: 26px; + @include background-icon(#{$icon}, clickable, 18); + &:hover { + @include background-icon(#{$icon}, attention, 18); + } + &.cw-tree-item-link-current { + @include background-icon(#{$icon}, info, 18); + } + } + } } .cw-tree-item-link { display: inline-block; width: calc(100% - 14px); text-align: justify; + background-repeat: no-repeat; + padding-left: 20px; + background-position: 4px 1px; + + @include background-icon(bullet-dot, clickable, 18); + &:hover { + @include background-icon(bullet-dot, attention, 18); + } + &.cw-tree-item-link-current { + @include background-icon(bullet-dot, info, 18); + } &:hover { background-color: $light-gray-color-20; color: $active-color; } - &::before { - content: '\2022'; - color: $base-color; - font-weight: 700; - width: 1em; - margin-left: -1em; - margin-right: 4px; - vertical-align: top; - } &.cw-tree-item-link-current { color: $black; @@ -1035,17 +1268,35 @@ label[for="cw-keypoint-color"] { color: $black; } } + @each $type, $icon in $tree-item-flag-icons { + .cw-tree-item-flag-#{$type} { + display: inline-block; + width: 16px; + height: 16px; + vertical-align: top; + @include background-icon(#{$icon}, clickable, 16); + } + &:hover .cw-tree-item-flag-#{$type} { + @include background-icon(#{$icon}, attention, 16); + } + &.cw-tree-item-link-current .cw-tree-item-flag-#{$type} { + @include background-icon(#{$icon}, info, 16); + } + } } - - } - - .cw-tree-item-first-level, - .cw-tree-item-is-root { - .cw-tree-item-link::before{ - content: ''; - width: 0; - margin: 0; + @each $type, $icon in $element-icons { + .cw-tree-item-#{$type} .cw-tree-item-link { + background-position: 0px 2px; + @include background-icon(#{$icon}, clickable, 16); + &:hover { + @include background-icon(#{$icon}, attention, 16); + } + &.cw-tree-item-link-current { + @include background-icon(#{$icon}, info, 16); + } + } } + } .cw-tree-item { @@ -1109,6 +1360,13 @@ c o l l a p s i b l e b o x } } +form .cw-collapsible .cw-collapsible-content.cw-collapsible-content-open { + padding: unset; + label { + margin: 1.5ex; + } +} + /* * * * * * * * * * * * * * * * * * c o l l a p s i b l e b o x e n d * * * * * * * * * * * * * * * * * */ @@ -1612,6 +1870,14 @@ c o m p a n i o n o v e r l a y background-image: url("#{$image-path}/companion/Tin_#{$image}.svg"); } } + + &.cw-companion-box-in-form { + margin-top: 8px; + } + + p { + margin: 0 1em 10px 0; + } } .cw-container-wrapper { @@ -1650,6 +1916,9 @@ v i e w w i d g e t .cw-action-widget-edit{ @include background-icon(edit, clickable); } + .cw-action-widget-sort{ + @include background-icon(arr_1sort, clickable); + } .cw-action-widget-add{ @include background-icon(add, clickable); } @@ -1665,6 +1934,9 @@ v i e w w i d g e t .cw-action-widget-export{ @include background-icon(export, clickable); } + .cw-action-widget-export-pdf{ + @include background-icon(file-pdf, clickable); + } .cw-action-widget-oer{ @include background-icon(oer-campus, clickable); } @@ -1678,20 +1950,28 @@ v i e w w i d g e t e n d c o m m e n t s & f e e d b a c k * * * * * * * * * * * * * * * * * * */ +.cw-structural-element-feedback, +.cw-structural-element-comments { + padding: 0 1em; +} + +.cw-structural-element-feedback-items, +.cw-structural-element-comments-items, .cw-block-feedback-items, .cw-block-comments-items { min-height: 1em; max-height: 225px; overflow-y: auto; overflow-x: hidden; - margin: -1em -1em 0em -0.5em; + margin: -1em -1em 0em 0em; + padding: 0em 1em 0em 0em; scroll-behavior: smooth; } .cw-talk-bubble { margin: 10px 20px; position: relative; - width: 80%; + width: 85%; height: auto; background-color: $dark-gray-color-10; border-radius: 5px; @@ -1753,6 +2033,8 @@ c o m m e n t s & f e e d b a c k } } +.cw-structural-element-feedback-create, +.cw-structural-element-comment-create, .cw-block-feedback-create, .cw-block-comment-create { border-top: solid thin $content-color-40; @@ -1760,6 +2042,21 @@ c o m m e n t s & f e e d b a c k textarea { width: calc(100% - 6px); resize: none; + border: solid thin $content-color-40; + &:active { + border: solid thin $content-color-80; + } + } +} +.cw-structural-element-comments-empty, +.cw-structural-element-feedback-empty, +.cw-block-comments-empty, +.cw-block-feedback-empty { + .cw-structural-element-feedback-create, + .cw-structural-element-comment-create, + .cw-block-feedback-create, + .cw-block-comment-create { + border-top: none; } } @@ -1893,8 +2190,7 @@ d a s h b o a r d .cw-dashboard { display: flex; - // TODO: Fixed width? - width: 1112px; + max-width: 1112px; flex-wrap: wrap; .cw-dashboard-box { @@ -1902,24 +2198,28 @@ d a s h b o a r d margin-right: 1em; &.cw-dashboard-box-full { - // TODO: Fixed width? - width: 1095px; + max-width: 1095px; + width: calc(100% - 16px); } &.cw-dashboard-box-half { - // TODO: Fixed width? - width: 540px; + width: calc(50% - 16px); + } + + &.cw-collapsible .cw-collapsible-content.cw-collapsible-content-open { + padding: 0; } } .cw-dashboard-overview { display: flex; - justify-content: space-evenly; - .cw-oblong { - margin-right: 1em; - } + padding: 10px; + flex-wrap: wrap; + justify-content: center; + } .cw-dashboard-progress { .cw-dashboard-progress-breadcrumb { + padding: 10px; span { color: $base-color; cursor: pointer; @@ -1932,6 +2232,7 @@ d a s h b o a r d .cw-dashboard-progress-chapter { text-align: center; + margin-bottom: -3.5em; h1 { border: none; @@ -1945,28 +2246,64 @@ d a s h b o a r d &.cw-dashboard-progress-current { font-size: 12px; - margin: -4.5em 0 2em 260px; + top: -4.5em; + left: -2.5em; } } } .cw-dashboard-progress-subchapter-list { border-top: solid thin $content-color-40; - margin: -0.5em; - height: 300px; + height: 349px; overflow-y: scroll; overflow-x: hidden; - padding: 1em; + padding: 0 1em 0 1em; scrollbar-width: thin; scrollbar-color: $base-color $dark-gray-color-5; + + .cw-dashboard-empty-info { + margin-top: 10px; + } + } + } + &.cw-dashboard-task-view { + display: unset; + max-width: unset; + flex-wrap: unset; + } + &.cw-dashboard-activity-view { + .cw-dashboard-activities { + max-height: 760px; + } + + } +} + +#course-courseware-dashboard { + .action-menu-item a { + cursor: pointer; + } +} + +.responsive-display { + .cw-dashboard { + .cw-dashboard-box { + + &.cw-dashboard-box-full { + width: 100% + } + &.cw-dashboard-box-half { + width: 100% + } } } } .cw-dashboard-progress-item { border-bottom: solid thin $content-color-40; - width: 492px; + width: 100%; cursor: pointer; + padding: 8px 0 8px 0; &:hover{ background-color: hsla(217,6%,45%,.2); @@ -1979,9 +2316,7 @@ d a s h b o a r d .cw-dashboard-progress-item-value, .cw-dashboard-progress-item-description { display: inline-block; - height: 70px; vertical-align: top; - line-height: 70px; } .cw-dashboard-progress-item-value { @@ -1995,46 +2330,95 @@ d a s h b o a r d } } .cw-dashboard-progress-item-description { - width: 404px; + width: calc(100% - 90px); color: $base-color; padding-left: 14px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; + padding: 0.5em 0 0 1em; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } } +.cw-dashboard-activities-wrapper { + .cw-companion-box { + margin: 10px; + } -.cw-dashboard-activities { - max-height: 476px; - list-style: none; - padding: 0; - margin: -0.5em; - scrollbar-width: thin; - scrollbar-color:$base-color #f5f5f5; - overflow-y: auto; - overflow-x: hidden; - - .cw-activity-item { - border-bottom: solid thin $content-color-40; - padding: 0.5em; + .cw-dashboard-activities { + max-height: 525px; + list-style: none; + padding: 0; + scrollbar-width: thin; + scrollbar-color:$base-color #f5f5f5; + overflow-y: auto; + overflow-x: hidden; - &:last-child { - border: none; - } + .cw-activity-item { + border-bottom: solid thin $content-color-40; + padding: 0.5em; - p { - margin: 0 0 4px 0; - img { - padding-right: 0.5em; - vertical-align: text-bottom; + &:last-child { + border: none; } - &.cw-activity-item-text { - padding-left: 23px; + + p { + margin: 0 0 4px 0; + img { + padding-right: 0.5em; + vertical-align: text-bottom; + } + &.cw-activity-item-text { + padding-left: 23px; + } } } - a{ + } +} + +.cw-dashboard-box { + .cw-dashboard-tasks-wrapper, + .cw-dashboard-students-wrapper { + padding: 10px; + } +} + +.cw-dashboard-tasks-wrapper, +.cw-dashboard-students-wrapper { + overflow-x: auto; + scrollbar-width: thin; + scrollbar-color:$base-color #f5f5f5; + max-height: 280px; + table.default { + margin: 0; + thead { + tr { + th { + &.feedback { + min-width: 11em; + } + &.renewal { + min-width: 14em; + } + } + } + } + tbody { + tr { + td { + img { + vertical-align: text-bottom; + &.display-feedback, + &.edit { + cursor: pointer; + } + } + } + } } } } @@ -2048,7 +2432,7 @@ o b l o n g * * * * * */ .cw-oblong-large { - border: solid thin $base-color; + border: solid thin $content-color-40; width: 520px; .cw-oblong-value, @@ -2061,25 +2445,29 @@ o b l o n g } .cw-oblong-value { - width: 90px; - color: $base-color; + width: 89px; + color: $black; + background-color: $content-color-20; + border-right: solid thin $content-color-40; font-size: xx-large; } .cw-oblong-description { width: 426px; - background-color: $base-color; - color: $white; + color: $black; + font-size: large; img { vertical-align: middle; - margin-right: 4px; + margin-right: 10px; } } } .cw-oblong-small { - border: solid thin $base-color; - width: 271px; + border: solid thin $content-color-40; + width: 340px; + margin-right: 1em; + margin-bottom: 5px; .cw-oblong-value, .cw-oblong-description { @@ -2091,15 +2479,17 @@ o b l o n g } .cw-oblong-value { - width: 60px; - background-color: $base-color; - color: $white; + width: 59px; + background-color: $content-color-20; + border-right: solid thin $content-color-40; + color: $black; font-size: x-large; } .cw-oblong-description { width: calc(100% - 64px); background-color: $white; - color: $base-color; + color: $black; + overflow: hidden; img { vertical-align: middle; margin-right: 8px; @@ -2321,9 +2711,7 @@ m a n a g e r } } - .cw-manager-element-subchapters { - } .cw-manager-element-item { border: solid thin $content-color-40; padding: 1em; @@ -2367,8 +2755,8 @@ m a n a g e r &.cw-manager-filing-active { @include background-icon(arr_eol-down, info-alt, 24); - background-color: $activity-color; - border: solid thin $activity-color; + background-color: $base-color; + border: solid thin $base-color; color: $white; } &.cw-manager-filing-disabled { @@ -3694,6 +4082,7 @@ headline block overflow: hidden; background-position: center; background-size: 1095px; + background-repeat: no-repeat; &.half { min-height: 300px; @@ -3861,8 +4250,8 @@ headline block } } } - -.cw-container-colspan-half { +.cw-container-colspan-half, +.cw-container-colspan-half-center { .cw-block-headline { .cw-block-headline-content { min-height: 300px; @@ -3997,6 +4386,23 @@ headline block } } +.responsive-display { + .cw-block-headline { + .cw-block-headline-content { + background-size: 100%; + &.bigicon_before { + .icon-layer { + background-size: 144px; + } + .cw-block-headline-textbox .cw-block-headline-title h1 { + font-size: 4em; + margin-left: 225px; + } + } + } + } +} + /* headline block end */ @@ -4004,6 +4410,12 @@ headline block end /* toc block */ +.cw-block-table-of-contents { + .cw-block-content { + overflow: unset; + } +} + .cw-block-table-of-contents-list { padding: 0; list-style: none; @@ -4124,6 +4536,7 @@ cw tiles } }; } + .preview-image { height: 180px; width: 100%; @@ -4135,14 +4548,16 @@ cw tiles @include background-icon(courseware, clickable, 128); } } + .description { height: 220px; - padding: 10px 14px; + padding: 14px; color: $white; position: relative; header { - font-size: 1.25em; + font-size: 20px; + line-height: 22px; color: $white; border: none; margin-bottom: 0.75em; @@ -4150,6 +4565,16 @@ cw tiles overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + background-repeat: no-repeat; + background-position: 0 0; + + @each $type, $icon in $element-icons { + &.description-icon-#{$type} { + width: 212px; + padding-left: 28px; + @include background-icon(#{$icon}, info_alt, 22); + } + } } .description-text-wrapper { @@ -4160,11 +4585,11 @@ cw tiles -webkit-line-clamp: 7; -webkit-box-orient: vertical; p { - text-align: justify; + text-align: left; } } - footer{ + footer { width: 242px; text-align: right; color: $white; @@ -4232,3 +4657,60 @@ vSelect end } /* cw manager copy end*/ + +/* courseware template preview */ +.cw-template-preview { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + width: calc(100% - 20px);; + padding: 10px; + .cw-template-preview-container-wrapper { + margin-bottom: 10px; + + &.cw-template-preview-container-full { + width: 100% + } + &.cw-template-preview-container-half { + width: calc(50% - 4px); + } + &.cw-template-preview-container-half-center { + width: 100%; + .cw-template-preview-container-content { + width: 50%; + margin: auto; + } + } + + .cw-template-preview-container-content { + border: solid thin $content-color-40; + } + + .cw-template-preview-container-title { + font-weight: 700; + padding: 4px 4px 4px 8px; + color: $base-color; + background-color: $content-color-20; + } + + .cw-template-preview-blocks { + border: solid thin $content-color-40; + padding: 1em; + margin: 5px; + background-color: $white; + + } + } +} +/* courseware template preview end*/ + +/* contents courseware courses */ +.cw-content-courses { + h2 { + margin-top: 0; + } + ul.cw-tiles { + margin-bottom: 20px; + } +} +/* contents courseware courses end*/ diff --git a/resources/vue/components/courseware/AdminApp.vue b/resources/vue/components/courseware/AdminApp.vue new file mode 100755 index 0000000..01bde38 --- /dev/null +++ b/resources/vue/components/courseware/AdminApp.vue @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/resources/vue/components/courseware/ContentBookmarkApp.vue b/resources/vue/components/courseware/ContentBookmarkApp.vue new file mode 100755 index 0000000..b0c9fbc --- /dev/null +++ b/resources/vue/components/courseware/ContentBookmarkApp.vue @@ -0,0 +1,21 @@ + + + diff --git a/resources/vue/components/courseware/ContentOverviewApp.vue b/resources/vue/components/courseware/ContentOverviewApp.vue new file mode 100755 index 0000000..cad3fc0 --- /dev/null +++ b/resources/vue/components/courseware/ContentOverviewApp.vue @@ -0,0 +1,25 @@ + + + diff --git a/resources/vue/components/courseware/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/CoursewareAccordionContainer.vue index 4a318c3..28f69aa 100755 --- a/resources/vue/components/courseware/CoursewareAccordionContainer.vue +++ b/resources/vue/components/courseware/CoursewareAccordionContainer.vue @@ -6,6 +6,7 @@ :isTeacher="isTeacher" @storeContainer="storeContainer" @closeEdit="initCurrentData" + @sortBlocks="enableSort" > @@ -20,19 +43,33 @@ diff --git a/resources/vue/components/courseware/CoursewareManagerTaskDistributor.vue b/resources/vue/components/courseware/CoursewareManagerTaskDistributor.vue new file mode 100755 index 0000000..839bd7b --- /dev/null +++ b/resources/vue/components/courseware/CoursewareManagerTaskDistributor.vue @@ -0,0 +1,316 @@ + + + diff --git a/resources/vue/components/courseware/CoursewareOblong.vue b/resources/vue/components/courseware/CoursewareOblong.vue index e3a9051..b1e601d 100755 --- a/resources/vue/components/courseware/CoursewareOblong.vue +++ b/resources/vue/components/courseware/CoursewareOblong.vue @@ -4,7 +4,7 @@
- {{ name }} + {{ name }}

@@ -29,6 +29,16 @@ export default { return 'small'; } }, + iconSize() { + switch (this.size) { + case 'large': + return 48; + case 'small': + return 24; + default: + return 24; + } + }, }, }; diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue index b670013..090f37a 100755 --- a/resources/vue/components/courseware/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue @@ -6,7 +6,7 @@ v-if="validContext" >
- +
+
-
+
+ + +
  • + + {{ container.attributes.title }} ({{ container.attributes.width }}) +
  • +
    +
    +
    + + +
    +
    +
    @@ -415,7 +459,7 @@

    {{ currentLicenseName }}

    @@ -452,6 +496,7 @@ import ContainerComponents from './container-components.js'; import CoursewarePluginComponents from './plugin-components.js'; import CoursewareStructuralElementPermissions from './CoursewareStructuralElementPermissions.vue'; +import CoursewareStructuralElementDiscussion from './CoursewareStructuralElementDiscussion.vue'; import CoursewareAccordionContainer from './CoursewareAccordionContainer.vue'; import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; import CoursewareWellcomeScreen from './CoursewareWellcomeScreen.vue'; @@ -465,11 +510,13 @@ import CoursewareTab from './CoursewareTab.vue'; import CoursewareExport from '@/vue/mixins/courseware/export.js'; import IsoDate from './IsoDate.vue'; import StudipDialog from '../StudipDialog.vue'; +import draggable from 'vuedraggable'; import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-structural-element', components: { + CoursewareStructuralElementDiscussion, CoursewareStructuralElementPermissions, CoursewareRibbon, CoursewareListContainer, @@ -483,6 +530,7 @@ export default { CoursewareTab, IsoDate, StudipDialog, + draggable, }, props: ['canVisit', 'orderedStructuralElements', 'structuralElement'], @@ -526,16 +574,27 @@ export default { exportRunning: false, exportChildren: false, oerChildren: true, + containerList: [], + isDragging: false, + dragOptions: { + animation: 0, + group: 'description', + disabled: false, + ghostClass: 'container-ghost', + }, + errorEmptyChapterName: false, }; }, computed: { ...mapGetters({ courseware: 'courseware', + context: 'context', consumeMode: 'consumeMode', containerById: 'courseware-containers/byId', relatedContainers: 'courseware-containers/related', relatedStructuralElements: 'courseware-structural-elements/related', + relatedTaskGroups: 'courseware-task-groups/related', relatedUsers: 'users/related', structuralElementById: 'courseware-structural-elements/byId', userIsTeacher: 'userIsTeacher', @@ -552,6 +611,9 @@ export default { exportState: 'exportState', exportProgress: 'exportProgress', userId: 'userId', + sortMode: 'structuralElementSortMode', + viewMode: 'viewMode', + taskById: 'courseware-tasks/byId', }), currentId() { @@ -624,7 +686,7 @@ export default { if (!parentId) { return null; } - const element = this.structuralElementById({id: parentId}); + const element = this.structuralElementById({ id: parentId }); if (!element) { console.error(`CoursewareStructuralElement#ancestors: Could not find parent by ID: "${parentId}".`); } @@ -636,11 +698,11 @@ export default { const parent = finder(node); if (parent) { yield parent; - yield *visitAncestors(parent); + yield* visitAncestors(parent); } }; - return [...visitAncestors(this.structuralElement)].reverse() + return [...visitAncestors(this.structuralElement)].reverse(); }, prevElement() { const currentIndex = this.orderedStructuralElements.indexOf(this.structuralElement.id); @@ -697,10 +759,6 @@ export default { return this.structuralElement.attributes['can-edit']; }, - isTeacher() { - return this.userIsTeacher; - }, - isRoot() { return this.structuralElement.relationships.parent.data === null; }, @@ -724,8 +782,8 @@ export default { }, menuItems() { let menu = [ - { id: 3, label: this.$gettext('Informationen anzeigen'), icon: 'info', emit: 'showInfo' }, - { id: 4, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' }, + { id: 4, label: this.$gettext('Informationen anzeigen'), icon: 'info', emit: 'showInfo' }, + { id: 5, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' }, ]; if (this.canEdit) { menu.push({ @@ -734,20 +792,38 @@ export default { icon: 'edit', emit: 'editCurrentElement', }); - menu.push({ id: 2, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' }); menu.push({ - id: 5, + id: 2, + label: this.$gettext('Abschnitte sortieren'), + icon: 'arr_1sort', + emit: 'sortContainers', + }); + + menu.push({ id: 3, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' }); + } + if ((this.userIsTeacher && this.canEdit) || this.context.type === 'users') { + menu.push({ + id: 6, label: this.$gettext('Seite exportieren'), icon: 'export', emit: 'showExportOptions', }); } - if (this.canEdit && this.oerEnabled) { - menu.push({ id: 6, label: this.textOer.title, icon: 'oer-campus', emit: 'oerCurrentElement' }); - } - if (!this.isRoot && this.canEdit) { + if ((this.userIsTeacher || this.canEdit) && this.canVisit) { menu.push({ id: 7, + type: 'link', + label: this.$gettext('Seite als pdf-Dokument exportieren'), + icon: 'file-pdf', + url: this.pdfExportURL, + }); + } + if (this.canEdit && this.oerEnabled && this.userIsTeacher) { + menu.push({ id: 8, label: this.textOer.title, icon: 'oer-campus', emit: 'oerCurrentElement' }); + } + if (!this.isRoot && this.canEdit && !this.isTask) { + menu.push({ + id: 9, label: this.$gettext('Seite löschen'), icon: 'trash', emit: 'deleteCurrentElement', @@ -928,6 +1004,56 @@ export default { blockedByAnotherUser() { return this.blocked && this.userId !== this.blockerId; }, + discussView() { + return this.viewMode === 'discuss'; + }, + pdfExportURL() { + if (this.context.type === 'users') { + return STUDIP.URLHelper.getURL( + 'dispatch.php/contents/courseware/pdf_export/' + this.structuralElement.id + ); + } + if (this.context.type === 'courses') { + return STUDIP.URLHelper.getURL( + 'dispatch.php/course/courseware/pdf_export/' + this.structuralElement.id + ); + } + + return ''; + }, + isTask() { + return this.structuralElement?.relationships.task.data !== null; + }, + task() { + if (!this.isTask) { + return null; + } + + return this.taskById({ id: this.structuralElement.relationships.task.data.id }); + }, + canAddElements() { + if (!this.isTask) { + return true; + } + + // still loading + if (!this.task) { + return false; + } + + const taskGroup = this.relatedTaskGroups({ parent: this.task, relationship: 'task-group' }); + + return taskGroup?.attributes['solver-may-add-blocks']; + }, + showEmptyElementBox() { + if (!this.empty) { + return false; + } + + return ( + (!this.isRoot && this.canEdit) || !this.canEdit || (!this.noContainers && this.isRoot && this.canEdit) + ); + }, }, methods: { @@ -948,6 +1074,10 @@ export default { showElementInfoDialog: 'showElementInfoDialog', showElementDeleteDialog: 'showElementDeleteDialog', showElementOerDialog: 'showElementOerDialog', + updateContainer: 'updateContainer', + setStructuralElementSortMode: 'setStructuralElementSortMode', + sortContainersInStructualElements: 'sortContainersInStructualElements', + loadTask: 'loadTask', }), initCurrent() { @@ -964,7 +1094,7 @@ export default { } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); - } catch(error) { + } catch (error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); } else { @@ -978,6 +1108,7 @@ export default { case 'addElement': this.newChapterName = ''; this.newChapterParent = 'descendant'; + this.errorEmptyChapterName = false; this.showElementAddDialog(true); break; case 'deleteCurrentElement': @@ -996,6 +1127,24 @@ export default { case 'setBookmark': this.setBookmark(); break; + case 'sortContainers': + if (this.blockedByAnotherUser) { + this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); + + return false; + } + try { + await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + } catch (error) { + if (error.status === 409) { + this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); + } else { + console.log(error); + } + + return false; + } + this.enableSortContainers(); } }, async closeEditDialog() { @@ -1057,6 +1206,25 @@ export default { this.initCurrent(); }, + enableSortContainers() { + this.setStructuralElementSortMode(true); + }, + + storeSort() { + this.setStructuralElementSortMode(false); + + this.sortContainersInStructualElements({ + structuralElement: this.structuralElement, + containers: this.containerList, + }); + this.$emit('select', this.currentId); + }, + + resetSort() { + this.setStructuralElementSortMode(false); + this.containerList = this.containers; + }, + async exportCurrentElement(data) { if (this.exportRunning) { return; @@ -1088,10 +1256,14 @@ export default { parentId: this.structuralElement.relationships.parent.data.id, }); this.$router.push(parent_id); + this.companionInfo({ info: this.$gettext('Die Seite wurde gelöscht.') }); }, async createElement() { let title = this.newChapterName; // this is the title of the new element let parent_id = this.currentId; // new page is descandant as default + if (this.errorEmptyChapterName = title.trim() === '') { + return; + } if (this.newChapterParent === 'sibling') { parent_id = this.structuralElement.relationships.parent.data.id; } @@ -1108,6 +1280,7 @@ export default { info: this.$gettextInterpolate('Die Seite %{ pageTitle } wurde erfolgreich angelegt.', {pageTitle: newElement.attributes.title}) }); + this.newChapterName = ''; }, containerComponent(container) { return 'courseware-' + container.attributes['container-type'] + '-container'; @@ -1130,6 +1303,14 @@ export default { watch: { structuralElement() { this.initCurrent(); + if (this.isTask) { + this.loadTask({ + taskId: this.structuralElement.relationships.task.data.id, + }); + } + }, + containers() { + this.containerList = this.containers; }, }, diff --git a/resources/vue/components/courseware/CoursewareStructuralElementComments.vue b/resources/vue/components/courseware/CoursewareStructuralElementComments.vue new file mode 100755 index 0000000..da6de34 --- /dev/null +++ b/resources/vue/components/courseware/CoursewareStructuralElementComments.vue @@ -0,0 +1,128 @@ + + + \ No newline at end of file diff --git a/resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue b/resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue new file mode 100755 index 0000000..fcd275c --- /dev/null +++ b/resources/vue/components/courseware/CoursewareStructuralElementDiscussion.vue @@ -0,0 +1,60 @@ + + + diff --git a/resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue b/resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue new file mode 100755 index 0000000..cc079ea --- /dev/null +++ b/resources/vue/components/courseware/CoursewareStructuralElementFeedback.vue @@ -0,0 +1,130 @@ + + + \ No newline at end of file diff --git a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue index e35e041..852f213 100755 --- a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue +++ b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue @@ -45,9 +45,9 @@

    {{ child.attributes.payload.description }}