From d25a23a626b43baab9714c8a4a68a20144cb3f00 Mon Sep 17 00:00:00 2001 From: Rasmus Fuhse Date: Fri, 4 Jul 2025 06:13:27 +0000 Subject: Resolve "Forum 3" Closes #5146 Merge request studip/studip!3845 --- ChangeLog.md | 2 +- app/controllers/admin/user.php | 14 +- .../course/forum/ForumBaseController.php | 66 + app/controllers/course/forum/admin.php | 133 -- app/controllers/course/forum/area.php | 79 - app/controllers/course/forum/categories.php | 122 ++ app/controllers/course/forum/configs.php | 34 + app/controllers/course/forum/discussion_types.php | 83 + app/controllers/course/forum/discussions.php | 220 +++ app/controllers/course/forum/forum_controller.php | 51 - app/controllers/course/forum/index.php | 829 --------- app/controllers/course/forum/recent.php | 24 + app/controllers/course/forum/search.php | 217 +++ app/controllers/course/forum/subscriptions.php | 19 + app/controllers/course/forum/topics.php | 157 ++ app/controllers/course/topics.php | 3 - app/controllers/institute/basicdata.php | 8 - app/controllers/privacy.php | 2 +- app/views/course/forum/admin/childs.php | 31 - app/views/course/forum/admin/index.php | 31 - app/views/course/forum/area/_add_area_form.php | 15 - app/views/course/forum/area/_edit_area_form.php | 7 - .../course/forum/area/_edit_category_form.php | 8 - app/views/course/forum/area/_js_templates.php | 45 - app/views/course/forum/area/add.php | 85 - app/views/course/forum/configs/edit.php | 52 + app/views/course/forum/discussion_types/index.php | 80 + app/views/course/forum/index/_abo_link.php | 23 - app/views/course/forum/index/_areas.php | 103 -- app/views/course/forum/index/_breadcrumb.php | 16 - app/views/course/forum/index/_favorite.php | 12 - app/views/course/forum/index/_js_templates.php | 16 - app/views/course/forum/index/_last_post.php | 21 - app/views/course/forum/index/_like.php | 48 - app/views/course/forum/index/_new_category.php | 19 - app/views/course/forum/index/_new_entry.php | 62 - app/views/course/forum/index/_post.php | 270 --- app/views/course/forum/index/_postings.php | 15 - app/views/course/forum/index/_threads.php | 197 --- app/views/course/forum/index/index.php | 264 --- app/views/course/forum/messages.php | 7 - cli/Commands/Make/Plugin.php | 1 - db/migrations/6.1.6_forum3.php | 400 +++++ lib/activities/CourseContext.php | 1 - lib/activities/ForumProvider.php | 48 - lib/activities/InstituteContext.php | 1 - lib/archiv.inc.php | 6 +- lib/classes/Forum/DTO/ForumMember.php | 53 + lib/classes/Forum/DTO/ForumTag.php | 45 + .../Forum/Enum/SubscriptionNotificationType.php | 25 + .../Forum/Service/DiscussionNotification.php | 62 + lib/classes/Forum/Service/PostingNotification.php | 140 ++ lib/classes/ForumAbo.php | 187 --- lib/classes/ForumActivity.php | 149 -- lib/classes/ForumEntry.php | 1418 ---------------- lib/classes/ForumFavorite.php | 41 - lib/classes/ForumHelpers.php | 278 --- lib/classes/ForumIssue.php | 96 -- lib/classes/ForumLike.php | 105 -- lib/classes/ForumPerm.php | 217 --- lib/classes/ForumVisit.php | 171 -- lib/classes/JsonApi/Models/ForumCat.php | 46 - lib/classes/JsonApi/Models/ForumEntry.php | 146 -- lib/classes/JsonApi/RouteMap.php | 68 +- .../JsonApi/Routes/Forum/AbstractEntriesCreate.php | 65 - .../JsonApi/Routes/Forum/ForumAuthority.php | 25 - .../JsonApi/Routes/Forum/ForumCategoriesCreate.php | 61 - .../JsonApi/Routes/Forum/ForumCategoriesDelete.php | 42 - .../JsonApi/Routes/Forum/ForumCategoriesIndex.php | 44 - .../JsonApi/Routes/Forum/ForumCategoriesShow.php | 33 - .../JsonApi/Routes/Forum/ForumCategoriesUpdate.php | 63 - .../Routes/Forum/ForumCategoryEntriesCreate.php | 43 - .../Routes/Forum/ForumCategoryEntriesIndex.php | 44 - .../JsonApi/Routes/Forum/ForumCategoryIndex.php | 37 + .../JsonApi/Routes/Forum/ForumCategoryShow.php | 36 + .../JsonApi/Routes/Forum/ForumCategoryTopics.php | 46 + .../Routes/Forum/ForumCategoryUpdateSort.php | 62 + .../JsonApi/Routes/Forum/ForumConfigIndex.php | 32 + .../JsonApi/Routes/Forum/ForumDiscussionIndex.php | 60 + .../Routes/Forum/ForumDiscussionPostings.php | 51 + .../JsonApi/Routes/Forum/ForumDiscussionShow.php | 39 + .../Forum/ForumDiscussionTypeDiscussions.php | 31 + .../Routes/Forum/ForumDiscussionTypeIndex.php | 25 + .../Routes/Forum/ForumDiscussionTypeShow.php | 25 + .../JsonApi/Routes/Forum/ForumEntriesDelete.php | 37 - .../JsonApi/Routes/Forum/ForumEntriesShow.php | 33 - .../JsonApi/Routes/Forum/ForumEntriesUpdate.php | 72 - .../Routes/Forum/ForumEntryEntriesCreate.php | 38 - .../Routes/Forum/ForumEntryEntriesIndex.php | 45 - .../JsonApi/Routes/Forum/ForumPostingDelete.php | 38 + .../Routes/Forum/ForumPostingReactionDelete.php | 32 + .../Routes/Forum/ForumPostingReactionShow.php | 26 + .../Routes/Forum/ForumPostingReactionStore.php | 77 + .../JsonApi/Routes/Forum/ForumPostingReactions.php | 43 + .../JsonApi/Routes/Forum/ForumPostingShow.php | 40 + .../JsonApi/Routes/Forum/ForumPostingStore.php | 100 ++ .../JsonApi/Routes/Forum/ForumPostingUpdate.php | 69 + .../Routes/Forum/ForumSubscriptionDelete.php | 32 + .../Routes/Forum/ForumSubscriptionIndex.php | 45 + .../JsonApi/Routes/Forum/ForumSubscriptionShow.php | 36 + .../Routes/Forum/ForumSubscriptionStore.php | 86 + .../JsonApi/Routes/Forum/ForumTopicDiscussions.php | 50 + .../JsonApi/Routes/Forum/ForumTopicIndex.php | 38 + .../JsonApi/Routes/Forum/ForumTopicShow.php | 33 + .../JsonApi/Routes/Forum/ForumTopicUpdateSort.php | 61 + lib/classes/JsonApi/SchemaMap.php | 11 +- lib/classes/JsonApi/Schemas/Activity.php | 2 +- .../JsonApi/Schemas/Forum/ForumCategory.php | 74 + .../JsonApi/Schemas/Forum/ForumDiscussion.php | 154 ++ .../JsonApi/Schemas/Forum/ForumDiscussionType.php | 54 + lib/classes/JsonApi/Schemas/Forum/ForumMember.php | 30 + lib/classes/JsonApi/Schemas/Forum/ForumPosting.php | 123 ++ .../JsonApi/Schemas/Forum/ForumPostingReaction.php | 71 + .../JsonApi/Schemas/Forum/ForumSubscription.php | 84 + lib/classes/JsonApi/Schemas/Forum/ForumTag.php | 27 + lib/classes/JsonApi/Schemas/Forum/ForumTopic.php | 107 ++ lib/classes/JsonApi/Schemas/ForumCategory.php | 76 - lib/classes/JsonApi/Schemas/ForumEntry.php | 78 - lib/classes/Privacy.php | 2 +- lib/classes/Score.php | 2 +- lib/classes/Siteinfo.php | 40 +- lib/classes/StudipKing.php | 18 +- lib/classes/UserManagement.php | 7 +- lib/classes/forms/ColorInput.php | 18 + lib/classes/globalsearch/GlobalSearchForum.php | 9 +- lib/cronjobs/garbage_collector.php | 7 - lib/models/Course.php | 6 +- lib/models/CourseTopic.php | 66 +- lib/models/Forum/ForumCategory.php | 107 ++ lib/models/Forum/ForumDiscussion.php | 201 +++ lib/models/Forum/ForumDiscussionType.php | 36 + lib/models/Forum/ForumPosting.php | 118 ++ lib/models/Forum/ForumPostingReaction.php | 49 + lib/models/Forum/ForumPostingRead.php | 64 + lib/models/Forum/ForumSubscription.php | 53 + lib/models/Forum/ForumTopic.php | 153 ++ lib/models/ForumCat.php | 258 --- lib/models/PersonalNotifications.php | 2 +- lib/models/User.php | 5 - lib/modules/CoreForum.php | 211 +-- lib/navigation/AdminNavigation.php | 8 + lib/object.inc.php | 2 + lib/plugins/core/ForumModule.php | 140 -- package-lock.json | 74 +- package.json | 3 +- .../images/forum/forum-keyvisual-positive.svg | 1 + public/assets/images/icons/black/add-reaction.svg | 1 + public/assets/images/icons/black/pin.svg | 1 + public/assets/images/icons/black/quote.svg | 1 + public/assets/images/icons/black/quote2.svg | 1 + .../assets/images/icons/black/subscription-all.svg | 1 + .../assets/images/icons/black/subscription-end.svg | 1 + .../images/icons/black/subscription-none.svg | 1 + .../images/icons/black/subscription-quotes.svg | 1 + public/assets/images/icons/blue/add-reaction.svg | 1 + public/assets/images/icons/blue/pin.svg | 1 + public/assets/images/icons/blue/quote.svg | 1 + public/assets/images/icons/blue/quote2.svg | 1 + .../assets/images/icons/blue/subscription-all.svg | 1 + .../assets/images/icons/blue/subscription-end.svg | 1 + .../assets/images/icons/blue/subscription-none.svg | 1 + .../images/icons/blue/subscription-quotes.svg | 1 + public/assets/images/icons/green/add-reaction.svg | 1 + public/assets/images/icons/green/pin.svg | 1 + public/assets/images/icons/green/quote.svg | 1 + public/assets/images/icons/green/quote2.svg | 1 + .../assets/images/icons/green/subscription-all.svg | 1 + .../assets/images/icons/green/subscription-end.svg | 1 + .../images/icons/green/subscription-none.svg | 1 + .../images/icons/green/subscription-quotes.svg | 1 + public/assets/images/icons/grey/add-reaction.svg | 1 + public/assets/images/icons/grey/pin.svg | 1 + public/assets/images/icons/grey/quote.svg | 1 + public/assets/images/icons/grey/quote2.svg | 1 + .../assets/images/icons/grey/subscription-all.svg | 1 + .../assets/images/icons/grey/subscription-end.svg | 1 + .../assets/images/icons/grey/subscription-none.svg | 1 + .../images/icons/grey/subscription-quotes.svg | 1 + public/assets/images/icons/red/add-reaction.svg | 1 + public/assets/images/icons/red/pin.svg | 1 + public/assets/images/icons/red/quote.svg | 1 + public/assets/images/icons/red/quote2.svg | 1 + .../assets/images/icons/red/subscription-all.svg | 1 + .../assets/images/icons/red/subscription-end.svg | 1 + .../assets/images/icons/red/subscription-none.svg | 1 + .../images/icons/red/subscription-quotes.svg | 1 + public/assets/images/icons/white/add-reaction.svg | 1 + public/assets/images/icons/white/pin.svg | 1 + public/assets/images/icons/white/quote.svg | 1 + public/assets/images/icons/white/quote2.svg | 1 + .../assets/images/icons/white/subscription-all.svg | 1 + .../assets/images/icons/white/subscription-end.svg | 1 + .../images/icons/white/subscription-none.svg | 1 + .../images/icons/white/subscription-quotes.svg | 1 + public/assets/images/icons/yellow/add-reaction.svg | 1 + public/assets/images/icons/yellow/pin.svg | 1 + public/assets/images/icons/yellow/quote.svg | 1 + public/assets/images/icons/yellow/quote2.svg | 1 + .../images/icons/yellow/subscription-all.svg | 1 + .../images/icons/yellow/subscription-end.svg | 1 + .../images/icons/yellow/subscription-none.svg | 1 + .../images/icons/yellow/subscription-quotes.svg | 1 + resources/assets/javascripts/init.js | 2 - resources/assets/javascripts/lib/forum.js | 853 ---------- resources/assets/javascripts/lib/jsonapiUtils.js | 15 + .../assets/javascripts/lib/number_formatter.js | 21 + resources/assets/stylesheets/scss/forms.scss | 4 +- resources/assets/stylesheets/scss/forum.scss | 1768 +++++++++++++++++--- resources/assets/stylesheets/scss/links.scss | 11 + .../stylesheets/scss/personal-notifications.scss | 12 +- resources/assets/stylesheets/scss/select.scss | 37 + resources/assets/stylesheets/studip.scss | 250 +++ resources/vue/apps/forum/categories/Edit.vue | 92 + resources/vue/apps/forum/categories/Index.vue | 270 +++ resources/vue/apps/forum/categories/Show.vue | 125 ++ resources/vue/apps/forum/discussions/Edit.vue | 164 ++ resources/vue/apps/forum/discussions/Show.vue | 325 ++++ .../vue/apps/forum/discussions_types/Edit.vue | 91 + resources/vue/apps/forum/recent/Index.vue | 71 + resources/vue/apps/forum/search/Index.vue | 282 ++++ resources/vue/apps/forum/subscriptions/Index.vue | 233 +++ resources/vue/apps/forum/topics/Edit.vue | 125 ++ resources/vue/apps/forum/topics/Index.vue | 100 ++ resources/vue/apps/forum/topics/Show.vue | 129 ++ resources/vue/components/Dropdown.vue | 55 + resources/vue/components/LinksPreview.vue | 51 + resources/vue/components/StudipDateTime.vue | 113 +- resources/vue/components/StudipSwitch.vue | 155 ++ resources/vue/components/UserAvatar.vue | 102 ++ resources/vue/components/forum/EmptyForum.vue | 40 + resources/vue/components/forum/ForumApp.vue | 34 + resources/vue/components/forum/ForumMembers.vue | 134 ++ resources/vue/components/forum/Loader.vue | 19 + resources/vue/components/forum/SelectTagsInput.vue | 35 + resources/vue/components/forum/SelectUserInput.vue | 30 + .../vue/components/forum/SubscriptionDropdown.vue | 207 +++ .../vue/components/forum/UserAvatarDropdown.vue | 44 + .../components/forum/categories/CategoryItem.vue | 212 +++ .../vue/components/forum/categories/Create.vue | 27 + .../vue/components/forum/discussions/Create.vue | 30 + .../forum/discussions/DiscussionIndex.vue | 290 ++++ .../forum/discussions/DiscussionTimeline.vue | 88 + .../forum/discussions/SelectDiscussionType.vue | 35 + .../forum/enums/SubscriptionNotificationType.ts | 5 + resources/vue/components/forum/helpers/index.js | 16 + .../vue/components/forum/helpers/transformers.js | 18 + resources/vue/components/forum/helpers/urls.js | 17 + resources/vue/components/forum/posts/Post.vue | 235 +++ .../vue/components/forum/posts/PostContent.vue | 66 + .../vue/components/forum/posts/PostCreateForm.vue | 161 ++ .../vue/components/forum/posts/PostEditForm.vue | 107 ++ .../vue/components/forum/posts/PostReactions.vue | 141 ++ resources/vue/components/forum/posts/reactions.js | 35 + .../vue/components/forum/topics/CreateTopic.vue | 37 + .../components/forum/topics/SelectTopicInput.vue | 53 + .../vue/components/forum/topics/TopicItem.vue | 203 +++ .../vue/components/forum/topics/TopicsIndex.vue | 228 +++ resources/vue/composables/useDetectOutsideClick.js | 18 + resources/vue/composables/useSortable.js | 72 + resources/vue/store/pinia/forum/ForumConfig.js | 34 + resources/vue/store/pinia/forum/ForumPost.js | 53 + templates/forms/color_input.php | 17 + templates/personal_notifications/notification.php | 9 +- tests/jsonapi/ForumCategoriesCreateTest.php | 67 - tests/jsonapi/ForumCategoriesIndexTest.php | 60 - tests/jsonapi/ForumCategoriesShowTest.php | 62 - tests/jsonapi/ForumCategoriesUpdateTest.php | 64 - tests/jsonapi/ForumCategoryDeleteTest.php | 60 - tests/jsonapi/ForumEntriesCreateTest.php | 115 -- tests/jsonapi/ForumEntriesDeleteTest.php | 61 - tests/jsonapi/ForumEntriesShowTest.php | 154 -- tests/jsonapi/ForumEntriesUpdateTest.php | 66 - tests/jsonapi/ForumTestHelper.php | 116 -- 273 files changed, 11886 insertions(+), 8841 deletions(-) create mode 100644 app/controllers/course/forum/ForumBaseController.php delete mode 100644 app/controllers/course/forum/admin.php delete mode 100644 app/controllers/course/forum/area.php create mode 100644 app/controllers/course/forum/categories.php create mode 100644 app/controllers/course/forum/configs.php create mode 100644 app/controllers/course/forum/discussion_types.php create mode 100644 app/controllers/course/forum/discussions.php delete mode 100644 app/controllers/course/forum/forum_controller.php delete mode 100644 app/controllers/course/forum/index.php create mode 100644 app/controllers/course/forum/recent.php create mode 100644 app/controllers/course/forum/search.php create mode 100644 app/controllers/course/forum/subscriptions.php create mode 100644 app/controllers/course/forum/topics.php delete mode 100644 app/views/course/forum/admin/childs.php delete mode 100644 app/views/course/forum/admin/index.php delete mode 100644 app/views/course/forum/area/_add_area_form.php delete mode 100644 app/views/course/forum/area/_edit_area_form.php delete mode 100644 app/views/course/forum/area/_edit_category_form.php delete mode 100644 app/views/course/forum/area/_js_templates.php delete mode 100644 app/views/course/forum/area/add.php create mode 100644 app/views/course/forum/configs/edit.php create mode 100644 app/views/course/forum/discussion_types/index.php delete mode 100644 app/views/course/forum/index/_abo_link.php delete mode 100644 app/views/course/forum/index/_areas.php delete mode 100644 app/views/course/forum/index/_breadcrumb.php delete mode 100644 app/views/course/forum/index/_favorite.php delete mode 100644 app/views/course/forum/index/_js_templates.php delete mode 100644 app/views/course/forum/index/_last_post.php delete mode 100644 app/views/course/forum/index/_like.php delete mode 100644 app/views/course/forum/index/_new_category.php delete mode 100644 app/views/course/forum/index/_new_entry.php delete mode 100644 app/views/course/forum/index/_post.php delete mode 100644 app/views/course/forum/index/_postings.php delete mode 100644 app/views/course/forum/index/_threads.php delete mode 100644 app/views/course/forum/index/index.php delete mode 100644 app/views/course/forum/messages.php create mode 100644 db/migrations/6.1.6_forum3.php delete mode 100644 lib/activities/ForumProvider.php create mode 100644 lib/classes/Forum/DTO/ForumMember.php create mode 100644 lib/classes/Forum/DTO/ForumTag.php create mode 100644 lib/classes/Forum/Enum/SubscriptionNotificationType.php create mode 100644 lib/classes/Forum/Service/DiscussionNotification.php create mode 100644 lib/classes/Forum/Service/PostingNotification.php delete mode 100644 lib/classes/ForumAbo.php delete mode 100644 lib/classes/ForumActivity.php delete mode 100644 lib/classes/ForumEntry.php delete mode 100644 lib/classes/ForumFavorite.php delete mode 100644 lib/classes/ForumHelpers.php delete mode 100644 lib/classes/ForumIssue.php delete mode 100644 lib/classes/ForumLike.php delete mode 100644 lib/classes/ForumPerm.php delete mode 100644 lib/classes/ForumVisit.php delete mode 100644 lib/classes/JsonApi/Models/ForumCat.php delete mode 100644 lib/classes/JsonApi/Models/ForumEntry.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/AbstractEntriesCreate.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumAuthority.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoriesCreate.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoriesDelete.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoriesIndex.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoriesShow.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoriesUpdate.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoryEntriesCreate.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoryEntriesIndex.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoryIndex.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoryShow.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoryTopics.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumCategoryUpdateSort.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumConfigIndex.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumDiscussionIndex.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumDiscussionPostings.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumDiscussionShow.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeDiscussions.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeIndex.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeShow.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumEntriesDelete.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumEntriesShow.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumEntriesUpdate.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumEntryEntriesCreate.php delete mode 100644 lib/classes/JsonApi/Routes/Forum/ForumEntryEntriesIndex.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumPostingDelete.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumPostingReactionDelete.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumPostingReactionShow.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumPostingReactionStore.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumPostingReactions.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumPostingShow.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumPostingStore.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumPostingUpdate.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumSubscriptionDelete.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumSubscriptionIndex.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumSubscriptionShow.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumSubscriptionStore.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumTopicDiscussions.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumTopicIndex.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumTopicShow.php create mode 100644 lib/classes/JsonApi/Routes/Forum/ForumTopicUpdateSort.php create mode 100644 lib/classes/JsonApi/Schemas/Forum/ForumCategory.php create mode 100644 lib/classes/JsonApi/Schemas/Forum/ForumDiscussion.php create mode 100644 lib/classes/JsonApi/Schemas/Forum/ForumDiscussionType.php create mode 100644 lib/classes/JsonApi/Schemas/Forum/ForumMember.php create mode 100644 lib/classes/JsonApi/Schemas/Forum/ForumPosting.php create mode 100644 lib/classes/JsonApi/Schemas/Forum/ForumPostingReaction.php create mode 100644 lib/classes/JsonApi/Schemas/Forum/ForumSubscription.php create mode 100644 lib/classes/JsonApi/Schemas/Forum/ForumTag.php create mode 100644 lib/classes/JsonApi/Schemas/Forum/ForumTopic.php delete mode 100644 lib/classes/JsonApi/Schemas/ForumCategory.php delete mode 100644 lib/classes/JsonApi/Schemas/ForumEntry.php create mode 100644 lib/classes/forms/ColorInput.php create mode 100644 lib/models/Forum/ForumCategory.php create mode 100644 lib/models/Forum/ForumDiscussion.php create mode 100644 lib/models/Forum/ForumDiscussionType.php create mode 100644 lib/models/Forum/ForumPosting.php create mode 100644 lib/models/Forum/ForumPostingReaction.php create mode 100644 lib/models/Forum/ForumPostingRead.php create mode 100644 lib/models/Forum/ForumSubscription.php create mode 100644 lib/models/Forum/ForumTopic.php delete mode 100644 lib/models/ForumCat.php delete mode 100644 lib/plugins/core/ForumModule.php create mode 100644 public/assets/images/forum/forum-keyvisual-positive.svg create mode 100644 public/assets/images/icons/black/add-reaction.svg create mode 100644 public/assets/images/icons/black/pin.svg create mode 100644 public/assets/images/icons/black/quote.svg create mode 100644 public/assets/images/icons/black/quote2.svg create mode 100644 public/assets/images/icons/black/subscription-all.svg create mode 100644 public/assets/images/icons/black/subscription-end.svg create mode 100644 public/assets/images/icons/black/subscription-none.svg create mode 100644 public/assets/images/icons/black/subscription-quotes.svg create mode 100644 public/assets/images/icons/blue/add-reaction.svg create mode 100644 public/assets/images/icons/blue/pin.svg create mode 100644 public/assets/images/icons/blue/quote.svg create mode 100644 public/assets/images/icons/blue/quote2.svg create mode 100644 public/assets/images/icons/blue/subscription-all.svg create mode 100644 public/assets/images/icons/blue/subscription-end.svg create mode 100644 public/assets/images/icons/blue/subscription-none.svg create mode 100644 public/assets/images/icons/blue/subscription-quotes.svg create mode 100644 public/assets/images/icons/green/add-reaction.svg create mode 100644 public/assets/images/icons/green/pin.svg create mode 100644 public/assets/images/icons/green/quote.svg create mode 100644 public/assets/images/icons/green/quote2.svg create mode 100644 public/assets/images/icons/green/subscription-all.svg create mode 100644 public/assets/images/icons/green/subscription-end.svg create mode 100644 public/assets/images/icons/green/subscription-none.svg create mode 100644 public/assets/images/icons/green/subscription-quotes.svg create mode 100644 public/assets/images/icons/grey/add-reaction.svg create mode 100644 public/assets/images/icons/grey/pin.svg create mode 100644 public/assets/images/icons/grey/quote.svg create mode 100644 public/assets/images/icons/grey/quote2.svg create mode 100644 public/assets/images/icons/grey/subscription-all.svg create mode 100644 public/assets/images/icons/grey/subscription-end.svg create mode 100644 public/assets/images/icons/grey/subscription-none.svg create mode 100644 public/assets/images/icons/grey/subscription-quotes.svg create mode 100644 public/assets/images/icons/red/add-reaction.svg create mode 100644 public/assets/images/icons/red/pin.svg create mode 100644 public/assets/images/icons/red/quote.svg create mode 100644 public/assets/images/icons/red/quote2.svg create mode 100644 public/assets/images/icons/red/subscription-all.svg create mode 100644 public/assets/images/icons/red/subscription-end.svg create mode 100644 public/assets/images/icons/red/subscription-none.svg create mode 100644 public/assets/images/icons/red/subscription-quotes.svg create mode 100644 public/assets/images/icons/white/add-reaction.svg create mode 100644 public/assets/images/icons/white/pin.svg create mode 100644 public/assets/images/icons/white/quote.svg create mode 100644 public/assets/images/icons/white/quote2.svg create mode 100644 public/assets/images/icons/white/subscription-all.svg create mode 100644 public/assets/images/icons/white/subscription-end.svg create mode 100644 public/assets/images/icons/white/subscription-none.svg create mode 100644 public/assets/images/icons/white/subscription-quotes.svg create mode 100644 public/assets/images/icons/yellow/add-reaction.svg create mode 100644 public/assets/images/icons/yellow/pin.svg create mode 100644 public/assets/images/icons/yellow/quote.svg create mode 100644 public/assets/images/icons/yellow/quote2.svg create mode 100644 public/assets/images/icons/yellow/subscription-all.svg create mode 100644 public/assets/images/icons/yellow/subscription-end.svg create mode 100644 public/assets/images/icons/yellow/subscription-none.svg create mode 100644 public/assets/images/icons/yellow/subscription-quotes.svg delete mode 100644 resources/assets/javascripts/lib/forum.js create mode 100644 resources/assets/javascripts/lib/jsonapiUtils.js create mode 100644 resources/assets/javascripts/lib/number_formatter.js create mode 100644 resources/vue/apps/forum/categories/Edit.vue create mode 100644 resources/vue/apps/forum/categories/Index.vue create mode 100644 resources/vue/apps/forum/categories/Show.vue create mode 100644 resources/vue/apps/forum/discussions/Edit.vue create mode 100644 resources/vue/apps/forum/discussions/Show.vue create mode 100644 resources/vue/apps/forum/discussions_types/Edit.vue create mode 100644 resources/vue/apps/forum/recent/Index.vue create mode 100644 resources/vue/apps/forum/search/Index.vue create mode 100644 resources/vue/apps/forum/subscriptions/Index.vue create mode 100644 resources/vue/apps/forum/topics/Edit.vue create mode 100644 resources/vue/apps/forum/topics/Index.vue create mode 100644 resources/vue/apps/forum/topics/Show.vue create mode 100644 resources/vue/components/Dropdown.vue create mode 100644 resources/vue/components/LinksPreview.vue create mode 100644 resources/vue/components/StudipSwitch.vue create mode 100644 resources/vue/components/UserAvatar.vue create mode 100644 resources/vue/components/forum/EmptyForum.vue create mode 100644 resources/vue/components/forum/ForumApp.vue create mode 100644 resources/vue/components/forum/ForumMembers.vue create mode 100644 resources/vue/components/forum/Loader.vue create mode 100644 resources/vue/components/forum/SelectTagsInput.vue create mode 100644 resources/vue/components/forum/SelectUserInput.vue create mode 100644 resources/vue/components/forum/SubscriptionDropdown.vue create mode 100644 resources/vue/components/forum/UserAvatarDropdown.vue create mode 100644 resources/vue/components/forum/categories/CategoryItem.vue create mode 100644 resources/vue/components/forum/categories/Create.vue create mode 100644 resources/vue/components/forum/discussions/Create.vue create mode 100644 resources/vue/components/forum/discussions/DiscussionIndex.vue create mode 100644 resources/vue/components/forum/discussions/DiscussionTimeline.vue create mode 100644 resources/vue/components/forum/discussions/SelectDiscussionType.vue create mode 100644 resources/vue/components/forum/enums/SubscriptionNotificationType.ts create mode 100644 resources/vue/components/forum/helpers/index.js create mode 100644 resources/vue/components/forum/helpers/transformers.js create mode 100644 resources/vue/components/forum/helpers/urls.js create mode 100644 resources/vue/components/forum/posts/Post.vue create mode 100644 resources/vue/components/forum/posts/PostContent.vue create mode 100644 resources/vue/components/forum/posts/PostCreateForm.vue create mode 100644 resources/vue/components/forum/posts/PostEditForm.vue create mode 100644 resources/vue/components/forum/posts/PostReactions.vue create mode 100644 resources/vue/components/forum/posts/reactions.js create mode 100644 resources/vue/components/forum/topics/CreateTopic.vue create mode 100644 resources/vue/components/forum/topics/SelectTopicInput.vue create mode 100644 resources/vue/components/forum/topics/TopicItem.vue create mode 100644 resources/vue/components/forum/topics/TopicsIndex.vue create mode 100644 resources/vue/composables/useDetectOutsideClick.js create mode 100644 resources/vue/composables/useSortable.js create mode 100644 resources/vue/store/pinia/forum/ForumConfig.js create mode 100644 resources/vue/store/pinia/forum/ForumPost.js create mode 100644 templates/forms/color_input.php delete mode 100644 tests/jsonapi/ForumCategoriesCreateTest.php delete mode 100644 tests/jsonapi/ForumCategoriesIndexTest.php delete mode 100644 tests/jsonapi/ForumCategoriesShowTest.php delete mode 100644 tests/jsonapi/ForumCategoriesUpdateTest.php delete mode 100644 tests/jsonapi/ForumCategoryDeleteTest.php delete mode 100644 tests/jsonapi/ForumEntriesCreateTest.php delete mode 100644 tests/jsonapi/ForumEntriesDeleteTest.php delete mode 100644 tests/jsonapi/ForumEntriesShowTest.php delete mode 100644 tests/jsonapi/ForumEntriesUpdateTest.php delete mode 100644 tests/jsonapi/ForumTestHelper.php diff --git a/ChangeLog.md b/ChangeLog.md index 5d26e7c..3ae0293 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -2954,7 +2954,7 @@ https://gitlab.studip.de/studip/studip/-/issues?milestone_title=Stud.IP+4.5.8&st - Löschen von Einzelterminen führt zu kaputten Raumanfragen [#707] - "Anfrage auf ausgewählte Termine stellen" funktioniert nicht mehr [#711] - Personenliste anlegen ohne Platzverteilung wirft Fehler [#927] -- PHP message: InvalidArgumentException: navigation item 'course/forum2/newest' not found [#944] +- PHP message: InvalidArgumentException: navigation item 'course/forum/newest' not found [#944] - Unklares Verhalten bei `url_for` mit URL-Fragmenten [#985] - Anzeigen/Drucken des QR-Codes verhält sich komisch bzw. ist kaputt [#995] - Dialog/Seite zu Fragebögen verlinkt nicht auf die vorhandene Hilfe [#1011] diff --git a/app/controllers/admin/user.php b/app/controllers/admin/user.php index 88b5272..5a0cd9d 100644 --- a/app/controllers/admin/user.php +++ b/app/controllers/admin/user.php @@ -1517,15 +1517,11 @@ class Admin_UserController extends AuthenticatedController 'details' => "files", ]; - foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) { - $table = $plugin->getEntryTableInfo(); - $queries[] = [ - 'desc' => $plugin->getPluginName() . ' - ' . _("Anzahl der Postings"), - 'query' => 'SELECT COUNT(*) FROM `' . $table['table'] . '` - WHERE `' . $table['user_id'] . '` = ? - GROUP BY `' . $table['user_id'] . '`', - ]; - } + // Forum + $queries[] = [ + 'desc' => _('Forum - Anzahl der Postings'), + 'query' => "SELECT COUNT(*) FROM `forum_postings` WHERE `user_id` = ? GROUP BY `user_id`" + ]; // Evaluate queries foreach ($queries as $index => $query) { diff --git a/app/controllers/course/forum/ForumBaseController.php b/app/controllers/course/forum/ForumBaseController.php new file mode 100644 index 0000000..cefa5a4 --- /dev/null +++ b/app/controllers/course/forum/ForumBaseController.php @@ -0,0 +1,66 @@ +course_id = Context::getId(); + $this->is_moderator = CoreForum::isModerator($this->course_id); + $this->is_admin = CoreForum::isAdmin($this->course_id); + + $this->buildSidebar(); + + parent::before_filter($action, $args); + } + + protected function buildSidebar(): void + { + $actions = new ActionsWidget(); + + if ($this->is_moderator) { + $actions->addLink( + _('Neue Diskussion starten'), + $this->url_for('course/forum/discussions/edit'), + Icon::create('add', Icon::ROLE_CLICKABLE, ['title' => _('Neue Diskussion starten')]) + )->asDialog('width=900;height=750'); + } + + if ($this->is_admin) { + $actions->addLink( + _('Forum verwalten'), + $this->url_for('course/forum/configs/edit'), + Icon::create('admin', Icon::ROLE_CLICKABLE, ['title' => _('Forum verwalten')]), + ['data-dialog' => 'width=500;height=300'] + ); + } + + Sidebar::Get()->addWidget($actions); + + $search = new SearchWidget($this->url_for('course/forum/search', [ + 'begin' => Request::int('begin'), + 'end' => Request::int('end') + ])); + + $search->addNeedle( + _('Suche nach Diskussionen oder Beiträge'), + 'keyword', + true + ); + + Sidebar::Get()->addWidget($search, 'forum_search'); + } +} diff --git a/app/controllers/course/forum/admin.php b/app/controllers/course/forum/admin.php deleted file mode 100644 index e4d21d8..0000000 --- a/app/controllers/course/forum/admin.php +++ /dev/null @@ -1,133 +0,0 @@ - - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - */ - -require_once 'forum_controller.php'; - -class Course_Forum_AdminController extends ForumController -{ - /* * * * * * * * * * * * * * * * * * * * */ - /* * * A D M I N M E T H O D S * * */ - /* * * * * * * * * * * * * * * * * * * * */ - - /** - * show the administration page for mass-editing forum-entries - */ - function index_action() - { - ForumPerm::check('admin', $this->getId()); - $nav = Navigation::getItem('course/forum2'); - $nav->setImage(Icon::create('forum', 'info')); - Navigation::activateItem('course/forum2/admin'); - - $list = ForumEntry::getList('flat', $this->getId()); - - // sort by cat - $new_list = []; - $this->categories = []; - // iterate over all categories and add the belonging areas to them - $categories = ForumCat::getListWithAreas($this->getId(), false) ; - foreach ($categories as $category) { - if (!empty($category['topic_id'])) { - $new_list[$category['category_id']][$category['topic_id']] = $list['list'][$category['topic_id']]; - unset($list['list'][$category['topic_id']]); - } else if (ForumPerm::has('add_area', $this->seminar_id)) { - $new_list[$category['category_id']] = []; - } - $this->categories[$category['category_id']] = $category['entry_name']; - } - - if (!empty($list['list'])) { - // append the remaining entries to the standard category - $new_list[$this->getId()] = array_merge((array)$new_list[$this->getId()], $list['list']); - } - - $this->list = $new_list; - - } - - /** - * show child entries for the passed entry - * - * @param string $parent_id id of entry to get the childs for - */ - function childs_action($parent_id) - { - $this->set_layout(null); - - // if the parent-id is a category-id, get all areas for this category - if ($cat = ForumCat::get($parent_id)) { - ForumPerm::check('admin', $cat['seminar_id']); // check the perms in the categories seminar - $this->entries = ForumEntry::parseEntries(ForumCat::getAreas($parent_id)); - } else { - ForumPerm::check('admin', $this->getId(), $parent_id); - $entries = ForumEntry::getList('flat', $parent_id); - $this->entries = $entries['list']; - } - } - - /** - * move the submitted topics[] to the passed destination - * - * @param string $destination id of seminar to move topics to - */ - function move_action($destination) - { - // check if destination is a category_id. if yes, use seminar_id instead - if (ForumCat::get($destination)) { - $category_id = $destination; - $destination = $this->getId(); - } else { - $category_id = null; - } - - ForumPerm::check('admin', $this->getId(), $destination); - - foreach (Request::getArray('topics') as $topic_id) { - // make sure every passed topic_id is checked against the current seminar - ForumPerm::check('admin', $this->getId(), $topic_id); - - // if the source is an area and the target a category, just move this area to the category - $entry = ForumEntry::getEntry($topic_id); - if ($entry['depth'] == 1 && $category_id) { - ForumCat::removeArea($topic_id); - ForumCat::addArea($category_id, $topic_id); - } else { - // first step: move the whole topic with all childs - ForumEntry::move($topic_id, $destination); - - // if the current topic id is an area, remove it from any categories - ForumCat::removeArea($topic_id); - - // second step: move all to deep childs a level up (depth > 3) - $data = ForumEntry::getList('depth_to_large', $topic_id); - - if (!empty($data['list'])) { - foreach ($data['list'] as $entry) { - $path = ForumEntry::getPathToPosting($entry['topic_id']); - array_shift($path); // Category - array_shift($path); // Area - - $thread = array_shift($path); // Thread - - ForumEntry::move($entry['topic_id'], $thread['id']); - } - } - - // add entry to passed category when moving to the top - if ($category_id) { - ForumCat::addArea($category_id, $topic_id); - } - } - } - - $this->render_nothing(); - } -} diff --git a/app/controllers/course/forum/area.php b/app/controllers/course/forum/area.php deleted file mode 100644 index 821129f..0000000 --- a/app/controllers/course/forum/area.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - */ - -require_once 'forum_controller.php'; - -class Course_Forum_AreaController extends ForumController -{ - function add_action($category_id) - { - ForumPerm::check('add_area', $this->getId()); - - $new_id = md5(uniqid(rand())); - - $name = Request::get('name', _('Kein Titel')); - $content = Request::get('content'); - - ForumEntry::insert([ - 'topic_id' => $new_id, - 'seminar_id' => $this->getId(), - 'user_id' => $GLOBALS['user']->id, - 'name' => $name, - 'content' => $content, - 'author' => get_fullname($GLOBALS['user']->id), - 'author_host' => ($GLOBALS['user']->id == 'nobody') ? getenv('REMOTE_ADDR') : '' - ], $this->getId()); - - ForumCat::addArea($category_id, $new_id); - - if (Request::isXhr()) { - $this->set_layout(null); - $entries = ForumEntry::parseEntries([ForumEntry::getEntry($new_id)]); - $this->entry = array_pop($entries); - $this->visitdate = ForumVisit::getLastVisit($this->getId()); - } else { - $this->redirect('course/forum/index/index/'); - } - } - - function edit_action($area_id) - { - ForumPerm::check('edit_area', $this->getId(), $area_id); - - ForumEntry::update($area_id, Request::get('name'), Request::get('content')); - if (Request::isAjax()) { - $this->render_json(['content' => ForumEntry::killFormat(ForumEntry::killEdit(Request::get('content')))]); - } else { - $this->flash['messages'] = ['success' => _('Die Änderungen am Bereich wurden gespeichert.')]; - $this->redirect('course/forum/index/index'); - } - - } - - function save_order_action() - { - ForumPerm::check('sort_area', $this->getId()); - - foreach (Request::getArray('areas') as $category_id => $areas) { - $pos = 0; - foreach ($areas as $area_id) { - ForumPerm::checkCategoryId($this->getId(), $category_id); - ForumPerm::check('sort_area', $this->getId(), $area_id); - - ForumCat::addArea($category_id, $area_id); - ForumCat::setAreaPosition($area_id, $pos); - $pos++; - } - } - - $this->render_nothing(); - } -} diff --git a/app/controllers/course/forum/categories.php b/app/controllers/course/forum/categories.php new file mode 100644 index 0000000..2ff5dc6 --- /dev/null +++ b/app/controllers/course/forum/categories.php @@ -0,0 +1,122 @@ +course_id)->FORUM_HIDE_CATEGORIES_NAVIGATION) { + Navigation::activateItem('course/forum/categories'); + } else { + Navigation::activateItem('course/forum/topics'); + } + } + + public function index_action() + { + $this->render_vue_app( + Studip\VueApp::create('forum/categories/Index') + ); + } + + public function show_action($category_id) + { + $category = ForumCategory::find($category_id); + + if (!$category) { + throw new AccessDeniedException(); + } + + PageLayout::setTitle($category->name); + + $this->render_vue_app( + Studip\VueApp::create('forum/categories/Show') + ->withProps([ + 'category' => $category->transformData(), + 'metadata' => [ + 'postings_count' => (int) $category->metadata['postings_count'], + 'users_count' => (int) $category->metadata['users_count'], + 'recent_activity' => date('c', $category->metadata['recent_activity']) + ] + ]) + ); + } + + public function edit_action($category_id = null) + { + if (!$this->is_moderator) { + throw new AccessDeniedException(); + } + + if ($category_id) { + PageLayout::setTitle(_('Kategorie bearbeiten')); + $category = ForumCategory::findOneBySQL("range_id = ? AND category_id = ?", [$this->course_id, $category_id]); + + if (!$category) { + throw new AccessDeniedException(); + } + } else { + PageLayout::setTitle(_('Neue Kategorie anlegen')); + $category = new ForumCategory(); + } + + $this->render_vue_app( + Studip\VueApp::create('forum/categories/Edit') + ->withProps([ + 'category' => $category->transformData() + ]) + ); + } + + public function save_action($category_id = null) + { + if (!$this->is_moderator) { + throw new AccessDeniedException(); + } + + CSRFProtection::verifyUnsafeRequest(); + + if ($category_id) { + $category = ForumCategory::findOneBySQL("range_id = ? AND category_id = ?", [$this->course_id, $category_id]); + if (!$category) { + throw new AccessDeniedException(); + } + } else { + $category = new ForumCategory(); + $category->range_id = $this->course_id; + } + + $category->name = Request::get('name'); + $category->description = Request::get('description'); + $category->color = Request::get('color'); + + $category->store(); + + PageLayout::postSuccess(sprintf(_('Die Kategorie „%s“ wurde gespeichert.'), $category->name)); + + $this->relocate('course/forum/categories'); + } + + public function delete_action($category_id) + { + if (!$this->is_moderator) { + throw new AccessDeniedException(); + } + + $category = ForumCategory::findOneBySQL("range_id = ? AND category_id = ?", [$this->course_id, $category_id]); + + if (!$category) { + throw new AccessDeniedException(); + } + + $category->delete(); + + PageLayout::postSuccess(_('Die Kategorie wurde gelöscht.')); + + $this->relocate('course/forum/categories'); + } +} diff --git a/app/controllers/course/forum/configs.php b/app/controllers/course/forum/configs.php new file mode 100644 index 0000000..70fdb2b --- /dev/null +++ b/app/controllers/course/forum/configs.php @@ -0,0 +1,34 @@ +is_admin) { + throw new AccessDeniedException(); + } + } + + public function edit_action() + { + $this->config = Context::get()->getConfiguration(); + } + + public function save_action() + { + CSRFProtection::verifyUnsafeRequest(); + + $this->config = Context::get()->getConfiguration(); + + $this->config->store('FORUM_MODERATION_PERMISSION', trim(Request::option('forum_moderation_permission'))); + + $this->config->store('FORUM_HIDE_CATEGORIES_NAVIGATION', Request::bool('forum_hide_categories_navigation')); + + PageLayout::postSuccess(_('Die Einstellungen wurden gespeichert.')); + + $this->relocate('course/forum/topics'); + } +} diff --git a/app/controllers/course/forum/discussion_types.php b/app/controllers/course/forum/discussion_types.php new file mode 100644 index 0000000..da4ea27 --- /dev/null +++ b/app/controllers/course/forum/discussion_types.php @@ -0,0 +1,83 @@ +check('root'); + + Navigation::activateItem('/admin/locations/forum_discussion_types'); + + $actions = new ActionsWidget(); + + $actions->addLink( + _('Neuen Diskussionstyp anlegen'), + $this->url_for('course/forum/discussion_types/edit'), + Icon::create('add', Icon::ROLE_CLICKABLE, ['title' => _('Neuen Diskussionstyp anlegen')]) + )->asDialog('width=700'); + + Sidebar::Get()->addWidget($actions); + } + + public function index_action() + { + $this->discussion_types = ForumDiscussionType::findBySQL("TRUE ORDER BY mkdate DESC"); + } + + public function edit_action(ForumDiscussionType $discussion_type = null) + { + if ($discussion_type->isNew()) { + PageLayout::setTitle(_('Neuen Diskussionstyp anlegen')); + } else { + PageLayout::setTitle(_('Diskussionstyp bearbeiten')); + } + + $icons = []; + + foreach (scandir($GLOBALS['STUDIP_BASE_PATH'] . '/public/assets/images/icons/blue') as $icon_path) { + if (!is_dir( + $GLOBALS['STUDIP_BASE_PATH'] . '/public/assets/images/icons/blue/' + ) . $icon_path && $icon_path[0] !== '.') { + if (stripos($icon_path, '.svg')) { + $icon_path = substr($icon_path, 0, stripos($icon_path, '.svg')); + } + $icons[] = $icon_path; + } + } + + $this->render_vue_app( + Studip\VueApp::create('forum/discussions_types/Edit')->withProps([ + 'icons' => array_unique($icons), + 'discussion_type' => $discussion_type->toRawArray() + ]) + ); + } + + public function save_action(ForumDiscussionType $discussion_type = null) + { + CSRFProtection::verifyUnsafeRequest(); + + $discussion_type->name = Request::get('name'); + $discussion_type->icon = Request::get('icon'); + + $discussion_type->store(); + + PageLayout::postSuccess(sprintf(_('Der Diskussionstyp „%s“ wurde gespeichert.'), $discussion_type->name)); + + $this->relocate('course/forum/discussion_types/index'); + } + + public function delete_action(ForumDiscussionType $discussion_type) + { + $discussion_type->delete(); + + PageLayout::postSuccess(_('Der Diskussionstyp wurde gelöscht.')); + + $this->relocate('course/forum/discussion_types/index'); + } +} diff --git a/app/controllers/course/forum/discussions.php b/app/controllers/course/forum/discussions.php new file mode 100644 index 0000000..745c370 --- /dev/null +++ b/app/controllers/course/forum/discussions.php @@ -0,0 +1,220 @@ +title); + + $auth_user = User::findCurrent(); + + $discussion->view_count += 1; + $discussion->store(); + + $posting_read = ForumPostingRead::findOneBySQL( + "discussion_id = :discussion_id AND user_id = :user_id", + [ + 'discussion_id' => $discussion->getId(), + 'user_id' => User::findCurrent()->user_id + ] + ); + + $user_subscription = ForumSubscription::findOneBySQL( + "subject = :subject AND subject_id = :subject_id AND user_id = :user_id", + [ + 'subject' => 'discussion', + 'subject_id' => $discussion->getId(), + 'user_id' => $auth_user->user_id + ] + ); + + $category = $discussion->getCategory(); + $tags = array_map(fn(ForumTag $tag) => $tag->toRawArray(), $discussion->tags); + $members = array_map(fn(ForumMember $member) => $member->toRawArray(), $discussion->members); + + $this->render_vue_app( + Studip\VueApp::create('forum/discussions/Show') + ->withProps([ + 'auth_user' => [ + 'id' => $auth_user->id, + 'username' => $auth_user->username, + 'name' => $auth_user->getFullName(), + 'avatar_url' => Avatar::getAvatar($auth_user->user_id)->getURL(Avatar::NORMAL), + 'subscription' => $user_subscription ? $user_subscription->toRawArray() : [] + ], + 'discussion' => [ + ...$discussion->transformData(), + 'topic' => $discussion->topic->toRawArray(), + 'tags' => $tags, + 'members' => $members, + 'type' => !empty($discussion->discussion_type) ? $discussion->discussion_type->toRawArray() : [] + ], + 'category' => $category ? $category->toRawArray() : [], + 'read_index' => (int) ($posting_read ? $posting_read->read_index : 0), + 'redirect' => Request::option('redirect'), + 'search_keyword' => $_SESSION['forum'][$this->course_id]['search']['keyword'] ?? '' + ]) + ); + } + + public function edit_action(ForumDiscussion $discussion = null) + { + if (!$this->is_moderator) { + throw new AccessDeniedException(); + } + + if ($discussion->isNew()) { + PageLayout::setTitle(_('Neue Diskussion starten')); + } else { + PageLayout::setTitle(_('Diskussion bearbeiten')); + } + + $topics = DBManager::get()->fetchAll( + " + SELECT + `ft`.`topic_id`, `ft`.`name`, `fc`.`color` + FROM `forum_topics` AS `ft` + LEFT JOIN `forum_categories` AS `fc` USING (`category_id`) + WHERE `ft`.`range_id` = :course_id + ORDER BY `ft`.`position` ASC, `ft`.`mkdate` DESC + ", + ['course_id' => $this->course_id] + ); + + $all_tags = array_map(fn(ForumTag $tag) => $tag->toRawArray(), ForumTag::getForumTags()); + $discussion_tags = array_map(fn(ForumTag $tag) => $tag->toRawArray(), $discussion->tags); + $discussion_types = array_map(fn(ForumDiscussionType $discussion_type) => $discussion_type->toRawArray(), ForumDiscussionType::getForumDiscussionType()); + + $this->render_vue_app( + Studip\VueApp::create('forum/discussions/Edit') + ->withProps([ + 'discussion' => [ + ...$discussion->transformData(), + 'topic_id' => !empty($discussion->topic_id) ? $discussion->topic_id : Request::option('topic_id'), + 'tags' => $discussion_tags + ], + 'topics' => $topics, + 'tags' => $all_tags, + 'discussion_types' => $discussion_types + ]) + ); + } + + public function save_action($discussion_id = null) + { + if (!$this->is_moderator) { + throw new AccessDeniedException(); + } + + CSRFProtection::verifyUnsafeRequest(); + + if ($discussion_id) { + $discussion = ForumDiscussion::find($discussion_id); + } else { + $discussion = new ForumDiscussion(); + } + + $discussion->title = Request::get('title'); + $discussion->closed_at = Request::bool('closed_at', false) ? time() : null; + $discussion->sticky = Request::bool('sticky', false); + + if (Request::get('type_id')) { + $discussion->type_id = Request::get('type_id'); + } + + $topic = json_decode(Request::get('topic'), true); + + if (empty($topic['topic_id'])) { + $newTopic = ForumTopic::create([ + 'range_id' => $this->course_id, + 'name' => $topic['name'] + ]); + + $topic['topic_id'] = $newTopic->topic_id; + } + + $discussion->topic_id = $topic['topic_id']; + $discussion->store(); + + if (!$discussion_id && Request::get('content')) { + ForumPosting::create([ + 'range_id' => $this->course_id, + 'discussion_id' => $discussion->discussion_id, + 'content' => Markup::markAsHtml(Request::get('content')), + 'user_id' => User::findCurrent()->user_id + ]); + } else { + TagRelation::deleteBySQL("range_id = ? AND range_type = 'forum'", [$discussion->discussion_id]); + } + + $tags = json_decode(Request::get('tags'), true); + + foreach ($tags as $tag) { + if (empty($tag['tag_id'])) { + $newTag = Tag::create([ + 'name' => $tag['name'], + ]); + + $tag['tag_id'] = $newTag->id; + } + + TagRelation::create([ + 'tag_id' => $tag['tag_id'], + 'range_id' => $discussion->discussion_id, + 'range_type' => 'forum' + ]); + } + + PageLayout::postSuccess(_('Der Beitrag wurde gespeichert.')); + + $this->relocate( + $discussion_id ? 'course/forum/topics/show/' . $discussion->topic_id : 'course/forum/discussions/show/' . $discussion->discussion_id + ); + } + + public function delete_action($discussion_id) + { + if (!$this->is_moderator) { + throw new AccessDeniedException(); + } + + $discussion = ForumDiscussion::find($discussion_id); + + if (!$discussion) { + throw new AccessDeniedException(); + } + + TagRelation::deleteBySQL("range_id = ? AND range_type = 'forum'", [$discussion->discussion_id]); + $topic_id = $discussion->topic_id; + + $discussion->delete(); + + PageLayout::postSuccess(_('Die Diskussion wurde gelöscht.')); + + $this->relocate('course/forum/topics/show/' . $topic_id); + } +} diff --git a/app/controllers/course/forum/forum_controller.php b/app/controllers/course/forum/forum_controller.php deleted file mode 100644 index 65eec63..0000000 --- a/app/controllers/course/forum/forum_controller.php +++ /dev/null @@ -1,51 +0,0 @@ -validate_args($args, ['option', 'option']); - - parent::before_filter($action, $args); - - $this->flash = Trails\Flash::instance(); - - // Set help keyword for Stud.IP's user-documentation and page title - PageLayout::setHelpKeyword('Basis.Forum'); - PageLayout::setTitle(Context::getHeaderLine() .' - '. _('Forum')); - - // the default for displaying timestamps - $this->time_format_string = "%a %d. %B %Y, %H:%M"; - $this->time_format_string_short = "%d.%m.%Y, %H:%M"; - - //$this->getId() depends on Context::get() - checkObject(); - ForumVisit::setVisit($this->getId()); - if (Request::int('page')) { - ForumHelpers::setPage(Request::int('page')); - } - - $this->seminar_id = $this->getId(); - - $this->no_entries = false; - $this->highlight = []; - $this->highlight_topic = ''; - $this->edit_posting = ''; - $this->js = ''; - } -} diff --git a/app/controllers/course/forum/index.php b/app/controllers/course/forum/index.php deleted file mode 100644 index a4f6fb1..0000000 --- a/app/controllers/course/forum/index.php +++ /dev/null @@ -1,829 +0,0 @@ - - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - */ - -require_once 'forum_controller.php'; - -class Course_Forum_IndexController extends ForumController -{ - /* * * * * * * * * * * * * * * * * * * * * * * * * */ - /* V I E W - A C T I O N S */ - /* * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * redirect to correct page (overview or newest entries), - * depending on whether there are any entries. - */ - function enter_seminar_action() { - if (ForumPerm::has('fav_entry', $this->getId()) - && ForumVisit::getCount($this->getId(), ForumVisit::getVisit($this->getId())) > 0) { - $this->redirect('course/forum/index/newest'); - } else { - $this->redirect('course/forum/index/index'); - } - } - - /** - * the main action for the forum. May be called with a topic_id to be displayed - * and optionally the page to display - * - * @param type $topic_id the topic to display, defaults to the main - * view of the current seminar - * @param type $page the page to be displayed (for thread-view) - */ - function index_action($topic_id = null, $page = null) - { - $nav = Navigation::getItem('course/forum2'); - $nav->setImage(Icon::create('forum', 'info')); - Navigation::activateItem('course/forum2/index'); - - // check, if the root entry is present - ForumEntry::checkRootEntry($this->getId()); - - /* * * * * * * * * * * * * * * * * * * - * V A R I A B L E N F U E L L E N * - * * * * * * * * * * * * * * * * * * */ - - $this->section = 'index'; - - $this->topic_id = $topic_id ? $topic_id : $this->getId(); - $this->constraint = ForumEntry::getConstraints($this->topic_id); - - // check if there has been submitted an invalid id and use seminar_id in case - if (!$this->constraint) { - $this->topic_id = $this->getId(); - $this->constraint = ForumEntry::getConstraints($this->topic_id); - } - - $this->highlight_topic = Request::option('highlight_topic', null); - - // set page to which we shall jump - if ($page) { - ForumHelpers::setPage($page); - } - - // we do not crawl deeper than level 2, we show a page chooser instead - if ($this->constraint['depth'] > 2) { - ForumHelpers::setPage(ForumEntry::getPostingPage($this->topic_id, $this->constraint)); - - $path = ForumEntry::getPathToPosting($this->topic_id); - array_shift($path);array_shift($path);$path_element = array_shift($path); - $this->child_topic = $this->topic_id; - $this->topic_id = $path_element['id']; - $this->constraint = ForumEntry::getConstraints($this->topic_id); - } - - // check if the topic_id matches the currently selected seminar - ForumPerm::checkTopicId($this->getId(), $this->topic_id); - - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * B E R E I C H E / T H R E A D S / P O S T I N G S L A D E N * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - // load list of areas for use in thread-movement - if (ForumPerm::has('move_thread', $this->getId())) { - $this->areas = ForumEntry::getList('flat', $this->getId()); - } - - if ($this->constraint['depth'] > 1) { // POSTINGS - $list = ForumEntry::getList('postings', $this->topic_id); - if (!empty($list['list'])) { - $this->postings = $list['list']; - $this->number_of_entries = $list['count']; - } - } else { - if ($this->constraint['depth'] == 0) { // BEREICHE - $list = ForumEntry::getList('area', $this->topic_id); - } else { - $list = ForumEntry::getList('list', $this->topic_id); - } - - if ($this->constraint['depth'] == 0) { // BEREICHE - $new_list = []; - $this->categories = []; - // iterate over all categories and add the belonging areas to them - foreach (ForumCat::getListWithAreas($this->getId(), false) as $category) { - if ($category['topic_id']) { - $new_list[$category['category_id']][$category['topic_id']] = $list['list'][$category['topic_id']]; - unset($list['list'][$category['topic_id']]); - } else if (ForumPerm::has('add_area', $this->seminar_id)) { - $new_list[$category['category_id']] = []; - } - $this->categories[$category['category_id']] = $category['entry_name']; - } - - if (!empty($list['list'])) { - // append the remaining entries to the standard category - if (!isset($new_list[$this->getId()])) { - $new_list[$this->getId()] = []; - } - $new_list[$this->getId()] = array_merge((array)$new_list[$this->getId()], $list['list']); - } - - // check, if there are any orphaned entries - foreach ($new_list as $key1 => $list_item) { - foreach ($list_item as $key2 => $contents) { - if (empty($contents)) { - // remove the orphaned entry from the list and from the database - unset($new_list[$key1][$key2]); - ForumCat::removeArea($key2); - } - } - } - - $this->list = $new_list; - - } else if ($this->constraint['depth'] == 1) { // THREADS - if (!empty($list['list'])) { - $this->list = [$list['list']]; - } - } - $this->number_of_entries = $list['count']; - } - - // set the visit-date and get the stored last_visitdate - $this->visitdate = ForumVisit::getLastVisit($this->getId()); - - $this->seminar_id = $this->getId(); - - // highlight text if passed some words to highlight - if (Request::getArray('highlight')) { - $this->highlight = Request::optionArray('highlight'); - } - - if (($this->edit_posting = Request::get('edit_posting', null)) - && !ForumPerm::hasEditPerms($this->edit_posting)) { - $this->edit_posting = null; - } - - // trigger a javascript action, like creating an answer or citing a thread - if (Request::submitted('answer')) { - $this->js = 'answer'; - } else if (Request::option('cite')) { - $this->js = 'cite'; - $this->cite_id = $topic_id; - } - - } - - /** - * show newest entries - * - * @param int $page show entries on submitted page - */ - function newest_action($page = null) - { - ForumPerm::check('fav_entry', $this->getId()); - - $nav = Navigation::getItem('course/forum2'); - $nav->setImage(Icon::create('forum', 'info')); - Navigation::activateItem('course/forum2/newest'); - - // set page to which we shall jump - if ($page) { - ForumHelpers::setPage($page); - } - - $this->section = 'newest'; - $this->topic_id = $this->getId(); - - // set the visitdate of the seminar as the last visitdate - $this->visitdate = ForumVisit::getLastVisit($this->getId()); - - $list = ForumEntry::getList('newest', $this->topic_id); - $this->postings = $list['list'] ?? []; - $this->number_of_entries = $list['count'] ?? 0; - $this->show_full_path = true; - - if (empty($this->postings)) { - $this->no_entries = true; - } - - $this->render_action('index'); - } - - /** - * show all latest entries as flat list - * - * @param int $page show entries on submitted page - */ - function latest_action($page = null) - { - ForumPerm::check('fav_entry', $this->getId()); - - $nav = Navigation::getItem('course/forum2'); - $nav->setImage(Icon::create('forum', 'info')); - Navigation::activateItem('course/forum2/latest'); - - // set page to which we shall jump - if ($page) { - ForumHelpers::setPage($page); - } - - $this->section = 'latest'; - $this->topic_id = $this->getId(); - - // set the visitdate of the seminar as the last visitdate - $this->visitdate = ForumVisit::getLastVisit($this->getId()); - - $list = ForumEntry::getList('latest', $this->topic_id); - $this->postings = $list['list']; - $this->number_of_entries = $list['count']; - $this->show_full_path = true; - $this->no_entries = empty($this->postings); - - $this->render_action('index'); - } - - /** - * show the current users favorized entries - * - * @param int $page show entries on submitted page - */ - function favorites_action($page = null) - { - ForumPerm::check('fav_entry', $this->getId()); - - $nav = Navigation::getItem('course/forum2'); - $nav->setImage(Icon::create('forum', 'info')); - Navigation::activateItem('course/forum2/favorites'); - - // set page to which we shall jump - if ($page) { - ForumHelpers::setPage($page); - } - - $this->section = 'favorites'; - $this->topic_id = $this->getId(); - - $list = ForumEntry::getList('favorites', $this->topic_id); - $this->postings = $list['list']; - $this->number_of_entries = $list['count']; - $this->show_full_path = true; - $this->no_entries = empty($this->postings); - - // exploit the visitdate for this view - $this->visitdate = ForumVisit::getLastVisit($this->getId()); - - $this->render_action('index'); - } - - /** - * show search results - * - * @param int $page show entries on submitted page - */ - function search_action($page = null) - { - if (Request::submitted('reset-search')) { - $this->redirect('course/forum/index/index'); - return; - } - - ForumPerm::check('search', $this->getId()); - - $nav = Navigation::getItem('course/forum2'); - $nav->setImage(Icon::create('forum', 'info')); - Navigation::activateItem('course/forum2/index'); - - // set page to which we shall jump - if ($page) { - ForumHelpers::setPage($page); - } - - $this->section = 'search'; - $this->topic_id = $this->getId(); - $this->show_full_path = true; - $this->options = []; - // parse filter-options - foreach (['search_title', 'search_content', 'search_author'] as $option) { - $this->options[$option] = Request::option($option); - } - - $this->searchfor = Request::get('searchfor'); - if (mb_strlen($this->searchfor) < 3) { - $this->flash['messages'] = ['error' => _('Ihr Suchbegriff muss mindestens 3 Zeichen lang sein und darf nur Buchstaben und Zahlen enthalten!')]; - } else { - // get search-results - $list = ForumEntry::getSearchResults($this->getId(), $this->searchfor, $this->options); - - $this->postings = $list['list']; - $this->number_of_entries = $list['count'] ?? 0; - $this->highlight = $list['highlight'] ?? false; - - if (empty($this->postings)) { - $this->flash['messages'] = ['info' => _('Es wurden keine Beiträge gefunden, die zu Ihren Suchkriterien passen!')]; - } - } - - // exploit the visitdate for this view - $this->visitdate = ForumVisit::getLastVisit($this->getId()); - - $this->render_action('index'); - } - - /* * * * * * * * * * * * * * * * * * * * * * * * * */ - /* * * * P O S T I N G - A C T I O N S * * * */ - /* * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * Add a new entry. Has a simple spambot protection and checks - * the parent_id to add the entry to, throwing an exception if missing. - * - * @throws Exception - */ - function add_entry_action() - { - CSRFProtection::verifyUnsafeRequest(); - // Schutz vor Spambots - diese füllen meistens alle Felder aus, auch "versteckte". - // Ist dieses Feld gefüllt, war das vermutlich kein Mensch - if (Request::get('nixda')) { - throw new Exception('Access denied!'); - } - - if (!$parent_id = Request::option('parent')) { - throw new Exception('missing seminar_id/topic_id while adding a new entry!'); - } - - if ($this->seminar_id == $parent_id) { - ForumPerm::check('add_area', $this->getId(), $parent_id); - } else { - ForumPerm::check('add_entry', $this->getId(), $parent_id); - } - - $constraints = ForumEntry::getConstraints($parent_id); - - // if we are answering/citing a posting, we want to add it to the thread - // (which is the parent of passed posting id) - if ($constraints['depth'] == 3) { - $parent_id = ForumEntry::getParentTopicId($parent_id); - } - - $new_id = md5(uniqid(rand())); - - if ($GLOBALS['user']->id == 'nobody') { - $fullname = Request::get('author', 'unbekannt'); - } else { - $fullname = get_fullname($GLOBALS['user']->id); - } - - ForumEntry::insert([ - 'topic_id' => $new_id, - 'seminar_id' => $this->getId(), - 'user_id' => $GLOBALS['user']->id, - 'name' => Request::get('name') ?: '', - 'content' => Studip\Markup::purifyHtml(Request::get('content')), - 'author' => $fullname, - 'author_host' => ($GLOBALS['user']->id == 'nobody') ? getenv('REMOTE_ADDR') : '', - 'anonymous' => Config::get()->FORUM_ANONYMOUS_POSTINGS ? Request::get('anonymous') ? : 0 : 0 - ], $parent_id); - - $this->flash['notify'] = $new_id; - - $this->redirect('course/forum/index/index/' . $new_id .'#'. $new_id); - } - - /** - * Delete the submitted entry. - * - * @param string $topic_id the entry to delete - */ - function delete_entry_action($topic_id) - { - // get the page of the posting to be able to jump there again - $page = ForumEntry::getPostingPage($topic_id); - URLHelper::addLinkParam('page', $page); - $parent = null; - if ( - ForumPerm::hasEditPerms($topic_id) - || ForumPerm::check('remove_entry', $this->getId(), $topic_id) - ) { - CSRFProtection::verifyUnsafeRequest(); - $path = ForumEntry::getPathToPosting($topic_id); - $topic = array_pop($path); - $parent = array_pop($path); - - if ($topic_id != $this->getId()) { - ForumEntry::delete($topic_id); - PageLayout::postSuccess(sprintf(_('Der Eintrag %s wurde gelöscht!'), htmlReady($topic['name']))); - } else { - PageLayout::postWarning(_('Sie können nicht die gesamte Veranstaltung löschen!')); - } - } - - $this->redirect('course/forum/index/index/' . $parent['id'] .'/'. $page); - } - - /** - * Update the submitted entry. - * - * @param string $topic_id id of the entry to update - * @throws AccessDeniedException - */ - function update_entry_action($topic_id) - { - CSRFProtection::verifyUnsafeRequest(); - - $name = Request::get('name', _('Kein Titel')); - $content = Studip\Markup::purifyHtml(Request::get('content', _('Keine Beschreibung'))); - - ForumPerm::check('add_entry', $this->getId(), $topic_id); - - if (ForumPerm::hasEditPerms($topic_id)) { - ForumEntry::update($topic_id, $name, $content); - } else { - throw new AccessDeniedException(_('Sie haben keine Berechtigung, diesen Eintrag zu editieren!')); - } - - if (Request::isXhr()) { - $this->render_text(json_encode([ - 'name' => htmlReady($name), - 'content' => formatReady($content) - ])); - } else { - $this->redirect('course/forum/index/index/' . $topic_id .'#'. $topic_id); - } - } - - /** - * Move the submitted thread to the submitted parent - * - * @param string $thread_id the thread to move - * @param string $destination the threads new parent - */ - function move_thread_action($thread_id, $destination) { - ForumPerm::check('move_thread', $this->getId(), $thread_id); - ForumPerm::check('move_thread', $this->getId(), $destination); - - $current_area = ForumEntry::getParentTopicId($thread_id); - - ForumEntry::move($thread_id, $destination); - - $this->redirect('course/forum/index/index/' . $current_area .'/'. ForumHelpers::getPage()); - } - - /** - * Mark the submitted entry as favorite - * - * @param string $topic_id the entry to mark - */ - function set_favorite_action($topic_id) - { - ForumPerm::check('fav_entry', $this->getId(), $topic_id); - - ForumFavorite::set($topic_id); - - if (Request::isXhr()) { - $this->topic_id = $topic_id; - $this->favorite = true; - $this->render_template('course/forum/index/_favorite'); - } else { - $this->redirect('course/forum/index/index/' . $topic_id .'#'. $topic_id); - } - } - - /** - * Remove the submtted entry as favorite - * - * @param string $topic_id the entry to unmark - */ - function unset_favorite_action($topic_id) { - ForumPerm::check('fav_entry', $this->getId(), $topic_id); - - ForumFavorite::remove($topic_id); - - if (Request::isXhr()) { - $this->topic_id = $topic_id; - $this->favorite = false; - $this->render_template('course/forum/index/_favorite'); - } else { - $this->redirect('course/forum/index/index/' . $topic_id .'#'. $topic_id); - } - } - - /** - * Jump to page in the entries of the submitted parent-entry - * denoted by the submitted context (section) - * - * @param string $topic_id the parent-topic to goto - * @param string $section the type of view (one of index/search) - * @param int $page the page to jump to - */ - function goto_page_action($topic_id, $section, $page) - { - switch ($section) { - case 'index': - $this->redirect('course/forum/index/index/' . $topic_id .'/'. (int)$page .'#'. $topic_id); - break; - - case 'search': - $optionlist = []; - - foreach (['search_title', 'search_content', 'search_author'] as $option) { - if (Request::option($option)) { - $optionlist[] = $option .'='. 1; - } - } - - $this->redirect('course/forum/index/'. $section .'/'. (int)$page - .'/?searchfor='. Request::get('searchfor') .'&'. implode('&', $optionlist)); - break; - - default: - $this->redirect('course/forum/index/'. $section .'/'. (int)$page); - break; - } - } - - /** - * Like the submitted topic - * - * @param string $topic_id the topic to like - */ - function like_action($topic_id) - { - if (!Request::isPost()) { - throw new MethodNotAllowedException(); - } - - ForumPerm::check('like_entry', $this->getId(), $topic_id); - - ForumLike::like($topic_id); - - if (Request::isXhr()) { - $this->topic_id = $topic_id; - $this->render_template('course/forum/index/_like'); - } else { - $this->redirect('course/forum/index/index/' . $topic_id .'#'. $topic_id); - } - } - - /** - * Remove like for the submitted topic - * - * @param string $topic_id the topic to unlike - */ - function dislike_action($topic_id) - { - if (!Request::isPost()) { - throw new MethodNotAllowedException(); - } - - ForumPerm::check('like_entry', $this->getId(), $topic_id); - - ForumLike::dislike($topic_id); - - if (Request::isXhr()) { - $this->topic_id = $topic_id; - $this->render_template('course/forum/index/_like'); - } else { - $this->redirect('course/forum/index/index/' . $topic_id .'#'. $topic_id); - } - } - - /** - * This action is used to close a thread. - * - * @param string $topic_id the topic which will be closed - * @param string $redirect the topic which will be shown after closing the thread - * @param int $page the page number of the topic $redirect - */ - function close_thread_action($topic_id, $redirect, $page = 0) - { - ForumPerm::check('close_thread', $this->getId(), $topic_id); - - ForumEntry::close($topic_id); - - $success_text = _('Das Thema wurde erfolgreich geschlossen.'); - - if (Request::isXhr()) { - $this->render_text(MessageBox::success($success_text)); - } else { - PageLayout::postSuccess($success_text); - $this->redirect('course/forum/index/index/' . $redirect . '/' . $page); - } - } - - /** - * This action is used to open a thread. - * - * @param string $topic_id the topic which will be opened - * @param string $redirect the topic which will be shown after opening the thread - * @param int $page the page number of the topic $redirect - */ - function open_thread_action($topic_id, $redirect, $page = 0) - { - ForumPerm::check('close_thread', $this->getId(), $topic_id); - - ForumEntry::open($topic_id); - - $success_text = _('Das Thema wurde erfolgreich geöffnet.'); - - if (Request::isXhr()) { - $this->render_text(MessageBox::success($success_text)); - } else { - PageLayout::postSuccess($success_text); - $this->redirect('course/forum/index/index/' . $redirect . '/' . $page); - } - } - - /** - * This action is used to mark a thread as sticky. - * - * @param string $topic_id the topic which will be marked as sticky. - * @param string $redirect the topic which will be shown afterwards - * @param int $page the page number of the topic $redirect - */ - function make_sticky_action($topic_id, $redirect, $page = 0) - { - ForumPerm::check('make_sticky', $this->getId(), $topic_id); - - ForumEntry::sticky($topic_id); - - $success_text = _('Das Thema wurde erfolgreich in der Themenliste hervorgehoben.'); - - if (Request::isXhr()) { - $this->render_text(MessageBox::success($success_text)); - } else { - $this->flash['messages'] = ['success' => $success_text]; - $this->redirect('course/forum/index/index/' . $redirect . '/' . $page); - } - } - - /** - * This action is used to remove the sticky attribute from a topic. - * - * @param string $topic_id the topic which will be marked as unsticky. - * @param string $redirect the topic which will be shown afterwards - * @param int $page the page number of the topic $redirect - */ - function make_unsticky_action($topic_id, $redirect, $page = 0) - { - ForumPerm::check('make_sticky', $this->getId(), $topic_id); - - ForumEntry::unsticky($topic_id); - - $success_text = _('Die Hervorhebung des Themas in der Themenliste wurde entfernt.'); - - if (Request::isXhr()) { - $this->render_text(MessageBox::success($success_text)); - } else { - $this->flash['messages'] = ['success' => $success_text]; - $this->redirect('course/forum/index/index/' . $redirect . '/' . $page); - } - } - - /* * * * * * * * * * * * * * * * * * * * * * * * * */ - /* * * * C O N F I G - A C T I O N S * * * */ - /* * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * Add submitted category to current course - */ - function add_category_action() - { - CSRFProtection::verifyUnsafeRequest(); - - ForumPerm::check('add_category', $this->getId()); - - $category_id = ForumCat::add($this->getId(), Request::get('category')); - - ForumPerm::checkCategoryId($this->getId(), $category_id); - - $this->redirect('course/forum/index#cat_'. $category_id); - } - - /* - * Remove submitted category from current course - */ - function remove_category_action($category_id) - { - CSRFProtection::verifyUnsafeRequest(); - - ForumPerm::checkCategoryId($this->getId(), $category_id); - ForumPerm::check('remove_category', $this->getId()); - - $this->flash['messages'] = ['success' => _('Die Kategorie wurde gelöscht!')]; - ForumCat::remove($category_id, $this->getId()); - - if (Request::isXhr()) { - $this->render_template('course/forum/messages'); - } else { - $this->redirect('course/forum/index/index'); - } - - } - - /** - * Change the name of the submitted category - * - * @param string $category_id the category to edit - */ - function edit_category_action($category_id) { - CSRFProtection::verifyUnsafeRequest(); - - ForumPerm::checkCategoryId($this->getId(), $category_id); - ForumPerm::check('edit_category', $this->getId()); - - if (Request::isXhr()) { - ForumCat::setName($category_id, Request::get('name')); - $this->render_nothing(); - } else { - ForumCat::setName($category_id, Request::get('name')); - $this->flash['messages'] = ['success' => _('Der Name der Kategorie wurde geändert.')]; - $this->redirect('course/forum/index/index#cat_' . $category_id); - } - - } - - /** - * Save the ordering of the categories - */ - function savecats_action() - { - ForumPerm::check('sort_category', $this->getId()); - - $pos = 0; - foreach (Request::getArray('categories') as $category_id) { - ForumPerm::checkCategoryId($this->getId(), $category_id); - ForumCat::setPosition($category_id, $pos); - $pos++; - } - - $this->render_nothing(); - } - - /* - * Subscribe to the submitted topic and receive mails on new postings - * - * @param string $topic_id - */ - function abo_action($topic_id) - { - ForumPerm::check('abo', $this->getId(), $topic_id); - - ForumAbo::add($topic_id); - $this->constraint = ForumEntry::getConstraints($topic_id); - - if (Request::isXhr()) { - $this->render_template('course/forum/index/_abo_link'); - } else { - switch ($this->constraint['depth']) { - case 0: $msg = _('Sie haben das gesamte Forum abonniert!');break; - case 1: $msg = _('Sie haben diesen Bereich abonniert.');break; - default: $msg = _('Sie haben dieses Thema abonniert');break; - } - $this->flash['messages'] = ['success' => $msg .' '. _('Sie werden nun über jeden neuen Beitrag informiert.')]; - $this->redirect('course/forum/index/index/' . $topic_id); - } - } - - /** - * Unsubscribe from the passed topic - * - * @param string $topic_id - */ - function remove_abo_action($topic_id) - { - ForumPerm::check('abo', $this->getId(), $topic_id); - - ForumAbo::delete($topic_id); - - if (Request::isXhr()) { - $this->constraint = ForumEntry::getConstraints($topic_id); - $this->render_template('course/forum/index/_abo_link'); - } else { - $this->flash['messages'] = ['success' => _('Abonnement aufgehoben.')]; - $this->redirect('course/forum/index/index/' . $topic_id); - } - } - - /** - * Generate a pdf-export for the whole forum or the passed subtree - * - * @param string $parent_id - */ - function pdfexport_action($parent_id = null) - { - ForumPerm::check('pdfexport', $this->getId(), $parent_id); - - ForumHelpers::createPDF($this->getId(), $parent_id); - } - - public function rescue($exception) - { - if ($exception instanceof AccessDeniedException) { - throw new LoginException(); - } - - return parent::rescue($exception); - } -} diff --git a/app/controllers/course/forum/recent.php b/app/controllers/course/forum/recent.php new file mode 100644 index 0000000..2c3fda2 --- /dev/null +++ b/app/controllers/course/forum/recent.php @@ -0,0 +1,24 @@ +render_vue_app( + Studip\VueApp::create('forum/recent/Index') + ->withProps([ + 'last_visit' => Request::int('last_visit') + ]) + ); + } +} diff --git a/app/controllers/course/forum/search.php b/app/controllers/course/forum/search.php new file mode 100644 index 0000000..84172fb --- /dev/null +++ b/app/controllers/course/forum/search.php @@ -0,0 +1,217 @@ +fetchAll( + "SELECT + `ft`.`topic_id`, `ft`.`name`, `fc`.`color` + FROM `forum_topics` AS `ft` + LEFT JOIN `forum_categories` AS `fc` USING (`category_id`) + WHERE `ft`.`range_id` = :course_id + ORDER BY `ft`.`position` ASC, `ft`.`mkdate` DESC + ", + ['course_id' => $this->course_id] + ); + + $course_members = []; + foreach (Context::get()->members as $member) { + $course_members[] = [ + 'user_id' => $member['user_id'], + 'name' => $member['Vorname'] . ' ' . $member['Nachname'], + 'avatar_url' => Avatar::getAvatar($member['user_id'])->getURL(Avatar::NORMAL), + 'profile_url' => URLHelper::getLink('dispatch.php/profile', ['username' => $member['username']], true) + ]; + } + + $search_object = $this->buildSearchObject(); + $all_tags = array_map(fn(ForumTag $tag) => $tag->toRawArray(), ForumTag::getForumTags()); + $discussion_types = array_map(fn(ForumDiscussionType $discussion_type) => $discussion_type->toRawArray(), ForumDiscussionType::getForumDiscussionType()); + + $this->render_vue_app( + Studip\VueApp::create('forum/search/Index') + ->withProps([ + 'search' => $search_object, + 'discussions' => $this->getResult($search_object), + 'topics' => $topics, + 'discussion_types' => $discussion_types, + 'tags' => $all_tags, + 'course_members' => $course_members, + ]) + ); + } + + private function getResult($search_object): array + { + if ($this->isSearchObjectEmpty($search_object)) { + unset($_SESSION['forum'][$this->course_id]['search']); + return []; + } + + $query = [ + "SELECT + discussions.discussion_id, + COUNT(DISTINCT postings.posting_id) AS 'postings_count' + FROM `forum_discussions` AS `discussions` + LEFT JOIN `forum_postings` AS `postings` USING(`discussion_id`) + LEFT JOIN `tags_relations` ON (`tags_relations`.`range_id` = `discussions`.`discussion_id` AND `range_type` = 'forum') + WHERE `postings`.`range_id` = :course_id ", + [ + 'course_id' => $this->course_id + ] + ]; + + $keyword = $search_object['keyword']; + if ($keyword) { + $query[0] .= " AND (discussions.title LIKE :keyword OR postings.content LIKE :keyword)"; + $query[1]["keyword"] = "%$keyword%"; + } + + if ($search_object['begin']) { + $query[0] .= " AND postings.mkdate >= :begin"; + $query[1]['begin'] = $search_object['begin']; + } + + if ($search_object['end']) { + $query[0] .= " AND postings.mkdate <= :end"; + $query[1]['end'] = $search_object['end']; + } + + if ($search_object['topic_ids']) { + $query[0] .= " AND discussions.topic_id IN (:topic_ids)"; + $query[1]['topic_ids'] = $search_object['topic_ids']; + } + + if ($search_object['discussion_type_ids']) { + $query[0] .= " AND discussions.type_id IN (:type_ids)"; + $query[1]['type_ids'] = $search_object['discussion_type_ids']; + } + + if ($search_object['tag_ids']) { + $query[0] .= " AND tags_relations.tag_id IN (:tag_ids)"; + $query[1]['tag_ids'] = $search_object['tag_ids']; + } + + if ($search_object['user_ids']) { + $query[0] .= " AND postings.user_id IN (:user_ids)"; + $query[1]['user_ids'] = $search_object['user_ids']; + } + + $query[0] .= match ($search_object['discussion_status']) { + 2 => " AND discussions.closed_at IS NULL", // opens + 3 => " AND discussions.closed_at IS NOT NULL", // closed + default => "" + }; + + $result = DBManager::get()->fetchAll( + $query[0]." GROUP BY discussions.discussion_id", + $query[1] + ); + + $discussions = ForumDiscussion::findBySQL("discussion_id IN (:discussion_ids)", ['discussion_ids' => array_column($result, 'discussion_id')]); + + + return array_map(function (ForumDiscussion $discussion) use ($result) { + $postings_count = array_find($result, fn($item) => $item['discussion_id'] === $discussion->discussion_id)['postings_count']; + $members = array_map(fn(ForumMember $member) => $member->toRawArray(), $discussion->members); + $tags = array_map(fn(ForumTag $tag) => $tag->toRawArray(), $discussion->tags); + + return [ + 'id' => $discussion->discussion_id, + 'title' => $discussion->title, + 'closed_at' => $discussion->closed_at ? date('c', $discussion->closed_at) : null, + 'view_count' => (int) $discussion->view_count, + 'sticky' => (bool) $discussion->sticky, + 'mkdate' => date('c', $discussion->mkdate), + 'chdate' => date('c', $discussion->chdate), + 'topic' => $discussion->topic->toRawArray(), + 'category' => $discussion->category ? [ + 'name' => $discussion->category->name, + 'color' => $discussion->category->color, + ] : [], + 'discussion_type' => $discussion->discussion_type ? [ + 'name' => $discussion->discussion_type->name, + 'icon' => $discussion->discussion_type->icon, + ] : [], + 'members' => $members, + 'tags' => $tags, + 'meta' => [ + 'postings_count' => (int) $postings_count, + 'recent_activity' => $discussion->metadata['recent_activity'] ? date('c', $discussion->metadata['recent_activity']) : null, + ] + ]; + }, $discussions); + } + + private function isSearchObjectEmpty($search_object): bool { + if ( + $search_object['keyword'] || + $search_object['begin'] || + $search_object['end'] || + $search_object['discussion_status'] || + $search_object['discussion_type_ids'] || + $search_object['tag_ids'] || + $search_object['topic_ids'] || + $search_object['user_ids'] + ) { + return false; + } + + return true; + } + + private function buildSearchObject(): array + { + $request = Request::getInstance(); + if ( + $request->offsetExists('keyword') || + $request->offsetExists('begin') || + $request->offsetExists('end') || + $request->offsetExists('discussion_status') || + $request->offsetExists('discussion_type_ids') || + $request->offsetExists('tag_ids') || + $request->offsetExists('topic_ids') || + $request->offsetExists('user_ids') + ) { + $search_object = [ + 'keyword' => Request::get('keyword'), + 'begin' => Request::int('begin'), + 'end' => Request::int('end'), + 'discussion_status' => Request::int('discussion_status'), + 'discussion_type_ids' => Request::getArray('discussion_type_ids'), + 'tag_ids' => Request::getArray('tag_ids'), + 'topic_ids' => Request::getArray('topic_ids'), + 'user_ids' => Request::getArray('user_ids') + ]; + + $_SESSION['forum'][$this->course_id]['search'] = $search_object; + return $search_object; + } + + $session_search = $_SESSION['forum'][$this->course_id]['search'] ?? []; + return [ + 'keyword' => $session_search['keyword'] ?? '', + 'begin' => $session_search['begin'] ?? 0, + 'end' => $session_search['end'] ?? 0, + 'discussion_status' => $session_search['discussion_status'] ?? 0, + 'discussion_type_ids' => $session_search['discussion_type_ids'] ?? [], + 'tag_ids' => $session_search['tag_ids'] ?? [], + 'topic_ids' => $session_search['topic_ids'] ?? [], + 'user_ids' => $session_search['user_ids'] ?? [] + ]; + } +} diff --git a/app/controllers/course/forum/subscriptions.php b/app/controllers/course/forum/subscriptions.php new file mode 100644 index 0000000..399c072 --- /dev/null +++ b/app/controllers/course/forum/subscriptions.php @@ -0,0 +1,19 @@ +render_vue_app( + Studip\VueApp::create('forum/subscriptions/Index') + ); + } +} diff --git a/app/controllers/course/forum/topics.php b/app/controllers/course/forum/topics.php new file mode 100644 index 0000000..0070937 --- /dev/null +++ b/app/controllers/course/forum/topics.php @@ -0,0 +1,157 @@ +course_id]['search']); + + Navigation::activateItem('course/forum/topics'); + } + + public function index_action() + { + $this->render_vue_app( + Studip\VueApp::create('forum/topics/Index') + ); + } + + public function show_action($topic_id) + { + $topic = ForumTopic::find($topic_id); + + if (!$topic) { + throw new AccessDeniedException(); + } + + PageLayout::setTitle($topic->name); + + $user_subscription = ForumSubscription::findOneBySQL( + "subject = :subject AND subject_id = :subject_id AND user_id = :user_id", + [ + 'subject' => 'topic', + 'subject_id' => $topic->getId(), + 'user_id' => User::findCurrent()->user_id + ] + ); + + $this->render_vue_app( + Studip\VueApp::create('forum/topics/Show') + ->withProps([ + 'topic' => $topic->transformData(), + 'category' => $topic->category ? $topic->category->transformData() : [], + 'user_subscription' => $user_subscription ? $user_subscription->toRawArray() : [], + 'metadata' => [ + 'postings_count' => (int) $topic->metadata['postings_count'], + 'users_count' => (int) $topic->metadata['users_count'], + 'recent_activity' => date('c', $topic->metadata['recent_activity']) + ] + ]) + ); + } + + public function edit_action($topic_id = null) + { + if (!$this->is_moderator) { + throw new AccessDeniedException(); + } + + if ($topic_id) { + PageLayout::setTitle(_('Thema bearbeiten')); + $topic = ForumTopic::getCourseTopic($this->course_id, $topic_id); + + if (!$topic) { + throw new AccessDeniedException(); + } + } else { + PageLayout::setTitle(_('Neues Thema anlegen')); + $topic = new ForumTopic(); + $topic['category_id'] = Request::get('category_id'); + } + + $categories = DBManager::get()->fetchAll( + "SELECT * FROM `forum_categories` WHERE `range_id` = ? ORDER BY `position` ASC, `mkdate` DESC", + [$this->course_id] + ); + + $this->render_vue_app( + Studip\VueApp::create('forum/topics/Edit') + ->withProps([ + 'topic' => $topic->transformData(), + 'categories' => $categories + ]) + ); + } + + public function save_action($topic_id = null) + { + if (!$this->is_moderator) { + throw new AccessDeniedException(); + } + + CSRFProtection::verifyUnsafeRequest(); + + if ($topic_id) { + $topic = ForumTopic::getCourseTopic($this->course_id, $topic_id); + if (!$topic) { + throw new AccessDeniedException(); + } + } else { + $topic = new ForumTopic(); + $topic->range_id = $this->course_id; + } + + $category = json_decode(Request::get('category'), true); + + if (empty($category['category_id']) && !empty($category['name'])) { + $newCategory = ForumCategory::create([ + 'range_id' => $this->course_id, + 'color' => '#28497C', + 'name' => $category['name'] + ]); + + $category['category_id'] = $newCategory->category_id; + } else { + $topic->category_id = null; + } + + if (!empty($category['category_id'])) { + $topic->category_id = $category['category_id']; + } + + $topic->name = Request::get('name'); + $topic->description = Request::get('description'); + + $topic->store(); + + PageLayout::postSuccess(_('Das Thema wurde gespeichert.')); + + $this->relocate('course/forum/topics/show/' . $topic->topic_id); + } + + public function delete_action($topic_id) + { + if (!$this->is_moderator) { + throw new AccessDeniedException(); + } + + $topic = ForumTopic::getCourseTopic($this->course_id, $topic_id); + + if (!$topic) { + throw new AccessDeniedException(); + } + + $topic->delete(); + + PageLayout::postSuccess(_('Das Thema wurde gelöscht.')); + + $this->relocate('course/forum/topics'); + } +} diff --git a/app/controllers/course/topics.php b/app/controllers/course/topics.php index 36e25ec..e955ba1 100644 --- a/app/controllers/course/topics.php +++ b/app/controllers/course/topics.php @@ -97,9 +97,6 @@ class Course_TopicsController extends AuthenticatedController if (Request::bool('folder')) { $topic->connectWithDocumentFolder(); } - - // create a connection to the module forum (can be anything) - // will update title and description automagically if (Request::bool('forumthread')) { $topic->connectWithForumThread(); } diff --git a/app/controllers/institute/basicdata.php b/app/controllers/institute/basicdata.php index 3a5a751..c1bb1cd 100644 --- a/app/controllers/institute/basicdata.php +++ b/app/controllers/institute/basicdata.php @@ -432,14 +432,6 @@ class Institute_BasicdataController extends AuthenticatedController } } - // delete all contents in forum-modules - foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) { - $plugin->deleteContents($i_id); // delete content irrespective of plugin-activation in the seminar - if ($plugin->isActivated($i_id)) { // only show a message, if the plugin is activated, to not confuse the user - $details[] = sprintf(_('Einträge in %s gelöscht.'), $plugin->getPluginName()); - } - } - // Alle Pluginzuordnungen entfernen PluginManager::getInstance()->deactivateAllPluginsForRange('inst', $i_id); diff --git a/app/controllers/privacy.php b/app/controllers/privacy.php index 5598f65..7aa8780 100644 --- a/app/controllers/privacy.php +++ b/app/controllers/privacy.php @@ -437,7 +437,7 @@ class PrivacyController extends AuthenticatedController 'description' => _('Nachrichten, Kommentare, Blubber, News'), ], 'content' => [ - 'icon' => Icon::create('forum2'), + 'icon' => Icon::create('forum'), 'title' => _('Inhalte'), 'description' => _('Courseware, Dateien, Forum, Wiki, Literaturlisten'), ], diff --git a/app/views/course/forum/admin/childs.php b/app/views/course/forum/admin/childs.php deleted file mode 100644 index c3a8a6b..0000000 --- a/app/views/course/forum/admin/childs.php +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/app/views/course/forum/admin/index.php b/app/views/course/forum/admin/index.php deleted file mode 100644 index 154e44c..0000000 --- a/app/views/course/forum/admin/index.php +++ /dev/null @@ -1,31 +0,0 @@ -addPlainText( - _('Bedienungshinweise'), - _('Sie befinden sich hier in der Administrationsansicht des Forums. ' - . 'Mit den blauen Pfeilen können Sie einen oder mehrere Einträge auswählen, welche dann verschoben werden können. '), - Icon::create('info', 'info_alt') -); -Helpbar::get()->addPlainText( - '', - _('Sie sollten nicht mehr als 20 Einträge gleichzeitig auswählen, da das verschieben sonst sehr lange dauern kann.') -); -?> -
- -
- diff --git a/app/views/course/forum/area/_add_area_form.php b/app/views/course/forum/area/_add_area_form.php deleted file mode 100644 index 4dd6781..0000000 --- a/app/views/course/forum/area/_add_area_form.php +++ /dev/null @@ -1,15 +0,0 @@ - - - -
- -
- - - - url_for('course/forum/index/index#cat_'. $category_id)) ?> -
- - 0 -
- diff --git a/app/views/course/forum/area/_edit_area_form.php b/app/views/course/forum/area/_edit_area_form.php deleted file mode 100644 index 9919d69..0000000 --- a/app/views/course/forum/area/_edit_area_form.php +++ /dev/null @@ -1,7 +0,0 @@ -
-
- - - - url_for('course/forum/index')) ?> -
diff --git a/app/views/course/forum/area/_edit_category_form.php b/app/views/course/forum/area/_edit_category_form.php deleted file mode 100644 index 054c8d6..0000000 --- a/app/views/course/forum/area/_edit_category_form.php +++ /dev/null @@ -1,8 +0,0 @@ -
- - - "javascript:STUDIP.Forum.saveCategoryName('". $category_id ."'); return false;"]) ?> - url_for('course/forum/index/index#cat_'. $category_id), - ['onClick' => "STUDIP.Forum.cancelEditCategoryName('". $category_id ."'); return false;"]) ?> -
diff --git a/app/views/course/forum/area/_js_templates.php b/app/views/course/forum/area/_js_templates.php deleted file mode 100644 index 975f42a..0000000 --- a/app/views/course/forum/area/_js_templates.php +++ /dev/null @@ -1,45 +0,0 @@ - - - - - diff --git a/app/views/course/forum/area/add.php b/app/views/course/forum/area/add.php deleted file mode 100644 index 1449a5d..0000000 --- a/app/views/course/forum/area/add.php +++ /dev/null @@ -1,85 +0,0 @@ -> - - = $visitdate && $entry['user_id'] !== $GLOBALS['user']->id): ?> - asImg([ - 'title' => _('Dieser Eintrag ist neu!'), - ]) ?> - - - 0 ? Icon::ROLE_ATTENTION : Icon::ROLE_INFO)->asImg([ - 'title' => ForumHelpers::getVisitText($num_postings, $entry['topic_id']), - ]) ?> - - - -
- - > - "> - - -
- - -
-
- - - - -
- render_partial('course/forum/area/_edit_area_form', compact('entry')) ?> -
-
- -
- - - - - - - - render_partial('course/forum/index/_last_post.php', compact('entry')) ?> - - - - addLink( - $controller->url_for("course/forum/index/index/{$entry['last_posting']['topic_id']}#{$entry['last_posting']['topic_id']}"), - _('Zur letzten Antwort'), - Icon::create('forum'), - is_array($entry['last_posting']) ? ['class' => 'hidden-small-up'] : ['disabled' => ''] - )->condition(ForumPerm::has('edit_area', $seminar_id) && $issue_id); - } - $action_menu->addLink( - URLHelper::getURL("dispatch.php/course/topics/edit/{$issue_id}"), - _('Zum Ablaufplan'), - Icon::create('info-circle', Icon::ROLE_STATUS_RED), - ['title' => _('Dieser Bereich ist einem Thema zugeordnet und kann hier nicht editiert werden. Die Angaben können im Ablaufplan angepasst werden.')] - )->condition(ForumPerm::has('edit_area', $seminar_id) && !$issue_id) - ->addLink( - $controller->url_for('course/forum/index', ['edit_area' => $entry['topic_id']]), - _('Name/Beschreibung des Bereichs ändern'), - Icon::create('edit'), - [ - 'class' => 'edit-area', - 'onclick' => "STUDIP.Forum.editArea('{$entry['topic_id']}');return false;", - ] - )->condition(ForumPerm::has('remove_area', $seminar_id)) - ->addLink( - $controller->url_for("course/forum/index/delete_entry/{$entry['topic_id']}"), - _('Bereich mitsamt allen Einträgen löschen!'), - Icon::create('trash'), - [ - 'class' => 'delete-area', - 'onclick' => "STUDIP.Forum.deleteArea(this, '{$entry['topic_id']}'); return false;", - ] - ) ?> - - - - diff --git a/app/views/course/forum/configs/edit.php b/app/views/course/forum/configs/edit.php new file mode 100644 index 0000000..59d7b1d --- /dev/null +++ b/app/views/course/forum/configs/edit.php @@ -0,0 +1,52 @@ + +
+ + + + + + +
+ +
+
diff --git a/app/views/course/forum/discussion_types/index.php b/app/views/course/forum/discussion_types/index.php new file mode 100644 index 0000000..8500d90 --- /dev/null +++ b/app/views/course/forum/discussion_types/index.php @@ -0,0 +1,80 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + _('Neue Diskussionstyp anlegen')]) ?> + + +
+ icon) : ?> + icon, ['title' => htmlReady($type->icon)])->asImg(24) ?> + + + + name) ?> + + + addLink( + $controller->url_for('course/forum/discussion_types/edit', $type), + _('Bearbeiten'), + Icon::create('edit', 'clickable', ['title' => _('Diskussionstyp bearbeiten')]), + ['data-dialog' => 'width=700'] + ) + ->addLink( + $controller->url_for('course/forum/discussion_types/delete', $type), + _('Löschen'), + Icon::create('trash', 'clickable',['title' => _('Diskussionstyp löschen')]), + ['data-confirm' => sprintf( + _('Wollen Sie "%s" löschen?'), + $type->name + )] + ); + ?> +
+ +
+
diff --git a/app/views/course/forum/index/_abo_link.php b/app/views/course/forum/index/_abo_link.php deleted file mode 100644 index ccd689a..0000000 --- a/app/views/course/forum/index/_abo_link.php +++ /dev/null @@ -1,23 +0,0 @@ -url_for('course/forum/index/' - . (ForumAbo::has($constraint['topic_id']) ? 'remove_' : '') - . 'abo/'. $constraint['topic_id']); -?> - - - - - - _('Wenn sie diesen Bereich abonnieren, erhalten Sie eine ' - . 'Stud.IP-interne Nachricht sobald in diesem Bereich ' - . 'ein neuer Beitrag erstellt wurde.'), - 'onClick' => $js]) ?> - - $js]) ?> - diff --git a/app/views/course/forum/index/_areas.php b/app/views/course/forum/index/_areas.php deleted file mode 100644 index 4f4fa0f..0000000 --- a/app/views/course/forum/index/_areas.php +++ /dev/null @@ -1,103 +0,0 @@ - -
- $entries) : ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - render_partial('course/forum/area/add', compact('entry')) ?> - - - - render_partial('course/forum/area/_add_area_form') ?> - - - - - - - - - - - - - - - - - - - - - -
- - asImg() ?> - asImg() ?> - - - - - - - - - - 'Name der Kategorie ändern'])->asImg() ?> - - - - - - 'Kategorie entfernen'])->asImg() ?> - - - - - - - - - render_partial('course/forum/area/_edit_category_form', compact('category_id', 'categories')) ?> - - - - -
- -
- - - asImg(["id" => 'tutorAddArea']) ?> - -
- -
- -render_partial('course/forum/area/_js_templates') ?> diff --git a/app/views/course/forum/index/_breadcrumb.php b/app/views/course/forum/index/_breadcrumb.php deleted file mode 100644 index 1627c9f..0000000 --- a/app/views/course/forum/index/_breadcrumb.php +++ /dev/null @@ -1,16 +0,0 @@ - -
- - - - - - - 1) :?>/ - - - - - -
- diff --git a/app/views/course/forum/index/_favorite.php b/app/views/course/forum/index/_favorite.php deleted file mode 100644 index 7325d83..0000000 --- a/app/views/course/forum/index/_favorite.php +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - asImg(['title' => _('Beitrag merken')]) ?> - - - - asImg(['title' => _('Beitrag nicht mehr merken')]) ?> - - diff --git a/app/views/course/forum/index/_js_templates.php b/app/views/course/forum/index/_js_templates.php deleted file mode 100644 index 3ee18ff..0000000 --- a/app/views/course/forum/index/_js_templates.php +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/app/views/course/forum/index/_last_post.php b/app/views/course/forum/index/_last_post.php deleted file mode 100644 index b5d854a..0000000 --- a/app/views/course/forum/index/_last_post.php +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - id || $GLOBALS['perm']->have_perm('root')): ?> - - getFullName() : $entry['last_posting']['user_fullname'] ?? '') ?> - - -
- - - "> - asImg([ - 'title' => _('Direkt zum Beitrag...'), - ]) ?> - - - - diff --git a/app/views/course/forum/index/_like.php b/app/views/course/forum/index/_like.php deleted file mode 100644 index da1c511..0000000 --- a/app/views/course/forum/index/_like.php +++ /dev/null @@ -1,48 +0,0 @@ - - - - - id, $likes) !== false) { - if (sizeof($likes) > 1) { - $text = '' . sprintf(_('Dir und %s weiteren gefällt das.'), (sizeof($likes) - 1)); - $text .= ''; - foreach ($likes as $user_id) { - if ($user_id != $GLOBALS['user']->id) { - $text .= htmlReady(get_fullname($user_id)) .'
'; - } - } - $text .= '
'; - } else { - $text = _('Dir gefällt das.'); - } - } else { - $text = '' . sprintf(_('%s gefällt das.'), sizeof($likes)); - $text .= ''; - foreach ($likes as $user_id) { - $text .= htmlReady(get_fullname($user_id)) .'
'; - } - $text .= '
'; - } - - $text .= '
'; - echo $text; -endif ?> - - -id, $likes); ?> - diff --git a/app/views/course/forum/index/_new_category.php b/app/views/course/forum/index/_new_category.php deleted file mode 100644 index 7048a6c..0000000 --- a/app/views/course/forum/index/_new_category.php +++ /dev/null @@ -1,19 +0,0 @@ - - -
- -
- - - -
- -
- -
-
-
- diff --git a/app/views/course/forum/index/_new_entry.php b/app/views/course/forum/index/_new_entry.php deleted file mode 100644 index 54a920b..0000000 --- a/app/views/course/forum/index/_new_entry.php +++ /dev/null @@ -1,62 +0,0 @@ -flash['new_entry_title'] */ ?> - diff --git a/app/views/course/forum/index/_post.php b/app/views/course/forum/index/_post.php deleted file mode 100644 index 6009787..0000000 --- a/app/views/course/forum/index/_post.php +++ /dev/null @@ -1,270 +0,0 @@ -= $visitdate) || !(isset($visitdate))) ?> - - - - - ForumPerm::hasEditPerms($post['topic_id']), - 'edit_closed' => ForumPerm::has('edit_closed', $constraint['seminar_id']), - 'remove_entry' => ForumPerm::has('remove_entry', $constraint['seminar_id']), -] ?> - - - - -
- - -
- data-topic-id=""> -
-
- -
-
- -
- - - - - getImageTag(Avatar::SMALL, - ['title' => _('Stud.IP')]) ?> - , - - - - getImageTag(Avatar::SMALL, - ['title' => get_username($post['user_id'])]) ?> - - - , - - , - - - - - -
-
- - - > - - - - - - - _('Dieses Thema wurde geschlossen. Sie können daher nicht auf diesen Beitrag antworten.')])->asImg() ?> - - - - - - - - - - - > ', ForumEntry::getFlatPathToPosting($post['topic_id']))), $highlight) ?> - - - - - - - - - -
- - -
- > - - - - > - - render() ?> - -
- - -
-
- - > - - "STUDIP.Forum.saveEntry('". $post['topic_id'] ."'); return false;"]) ?> - - link_for('course/forum/index/index/'. $post['topic_id'] .'#'. $post['topic_id']), - ['onClick' => "STUDIP.Forum.cancelEditEntry('". $post['topic_id'] ."'); return false;"]) ?> - - - > - - - - - url_for('course/forum/index/index/' . $post['topic_id'] .'?cite=1'), [ - 'onClick' => "javascript:STUDIP.Forum.citeEntry('". $post['topic_id'] ."'); return false;", - 'class' => !$perms['edit_closed'] ? 'hideWhenClosed' : '', - 'style' => !$can_edit_closed ? 'display: none' : '' - ]) ?> - - - - url_for('course/forum/index/index/' - . $post['topic_id'] .'/?edit_posting=' . $post['topic_id']), [ - 'onClick' => "STUDIP.Forum.editEntry('". $post['topic_id'] ."'); return false;", - 'class' => !$perms['edit_closed'] ? 'hideWhenClosed' : '', - 'style' => !$can_edit_closed ? 'display: none' : '' - ]) ?> - - - - > - url_for('course/forum/index/delete_entry/' . $post['topic_id']) ?> - - _('Wenn Sie diesen Beitrag löschen wird ebenfalls das gesamte Thema gelöscht. Sind Sie sicher, dass Sie das tun möchten?'), - 'formaction' => $confirmLink, - ] - ) ?> - - _('Möchten Sie diesen Beitrag wirklich löschen?'), - 'formaction' => $confirmLink, - ] - ) ?> - - - - - 'js']) ?> - - -
-
- -
- - - > -
-
-
-
-
- - - - > -
- -
- - id || $GLOBALS['perm']->have_perm('root')): ?> -
- - - getImageTag(Avatar::MEDIUM, - ['title' => get_username($post['user_id'])]) ?> - -
- - - - asImg() ?> - - - - - - - - - _('Online')]) ?> - - _('Abwesend')]) ?> - - _('Offline')]) ?> - - - - - - - - -
- -
- get_studip_perm($constraint['seminar_id'], $post['user_id']))?> -
- -
- Beiträge: -
- - -
- - -
- -
- -
- -
- - - render_partial('course/forum/index/_favorite', ['topic_id' => $post['topic_id'], 'favorite' => $post['fav']]) ?> - - - - - _('Link zu diesem Beitrag')])->asImg() ?> - -
- - - -
- - link_for('course/forum/index/index/' . $post['topic_id'] .'#'. $post['topic_id']), - $post['user_id']) as $applet_data) : ?> -
- -
- -
- - - - _("Dieser Beitrag ist seit Ihrem letzten Besuch hinzugekommen.")])->asImg() ?> - - -
- -
-
-
diff --git a/app/views/course/forum/index/_postings.php b/app/views/course/forum/index/_postings.php deleted file mode 100644 index 1888ae0..0000000 --- a/app/views/course/forum/index/_postings.php +++ /dev/null @@ -1,15 +0,0 @@ -
- -render_partial('course/forum/index/_post', compact('post', 'visitdate', 'section')); - - $posting_num++; -endforeach -?> - -
diff --git a/app/views/course/forum/index/_threads.php b/app/views/course/forum/index/_threads.php deleted file mode 100644 index 6c2fc4a..0000000 --- a/app/views/course/forum/index/_threads.php +++ /dev/null @@ -1,197 +0,0 @@ -
- -
-
-
-
-
- - -
- - $entries) : ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- "> - = $visitdate && $entry['user_id'] != $GLOBALS['user']->id): ?> - asImg([ - 'title' => _('Dieser Eintrag ist neu!'), - ]) ?> - - - 0 ? Icon::ROLE_ATTENTION : Icon::ROLE_INFO)->asImg([ - 'title' => ForumHelpers::getVisitText($num_postings, $entry['topic_id']), - ]) ?> - - -
- - asImg([ - 'title' => _('Dieses Thema ist geschlossen, es können keine neuen Beiträge erstellt werden.'), - 'id' => "img-locked-{$entry['topic_id']}", - 'style' => $entry['closed'] ? '' : 'display: none', - ]) ?> - - asImg([ - 'title' => _('Dieses Thema wurde hervorgehoben.'), - 'id' => "img-sticky-{$entry['topic_id']}", - 'style' => $entry['sticky'] ? '' : 'display: none', - ]) ?> -
-
-
- - - - - - - - - id || $GLOBALS['perm']->have_perm('root')): ?> - - getFullName() : $entry['author']) ?> - - - -
- - -
-
- - - render_partial('course/forum/index/_last_post.php', compact('entry')) ?> - - condition(isset($entry['last_posting'])) - ->addLink( - isset($entry['last_posting']['topic_id']) ? $controller->url_for("course/forum/index/index/{$entry['last_posting']['topic_id']}#{$entry['last_posting']['topic_id']}") : '#no_posting', - _('Zur letzten Antwort'), - Icon::create('forum'), - ['class' => 'hidden-small-up'] - ) - // Make thread sticky/unsticky - ->conditionAll(ForumPerm::has('make_sticky', $seminar_id) && $constraint['depth'] >= 1) - ->condition(!$entry['sticky']) - ->addLink( - $controller->url_for('course/forum/index/make_sticky', $entry['topic_id'], $constraint['topic_id'], 0), - _('Thema hervorheben'), - Icon::create('staple'), - ['id' => "stickyButton-{$entry['topic_id']}"] - ) - ->condition($entry['sticky']) - ->addLink( - $controller->url_for('course/forum/index/make_unsticky', $entry['topic_id'], $constraint['topic_id'], 0), - _('Hervorhebung aufheben'), - Icon::create('staple'), - ['id' => "stickyButton-{$entry['topic_id']}"] - ) - ->conditionAll(null) - // Move thread - ->condition(ForumPerm::has('move_thread', $seminar_id)) - ->addLink( - "javascript:STUDIP.Forum.moveThreadDialog('{$entry['topic_id']}');", - _('Dieses Thema verschieben'), - Icon::create('folder-full'), - ['class' => 'js'] - ) - // Open/close thread - ->conditionAll(ForumPerm::has('close_thread', $seminar_id) && $constraint['depth'] >= 1) - ->condition(!$entry['closed']) - ->addLink( - $controller->url_for('course/forum/index/close_thread', $entry['topic_id'], $constraint['topic_id'], ForumHelpers::getPage()), - _('Thema schließen'), - Icon::create('lock-locked'), - [ - 'id' => "closeButton-{$entry['topic_id']}", - 'onclick' => "STUDIP.Forum.closeThreadFromOverview('{$entry['topic_id']}', '{$constraint['topic_id']}', " . ForumHelpers::getPage() . "); return false;", - ] - ) - ->condition($entry['closed']) - ->addLink( - $controller->url_for('course/forum/index/open_thread', $entry['topic_id'], $constraint['topic_id'], ForumHelpers::getPage()), - _('Thema öffnen'), - Icon::create('lock-unlocked'), - [ - 'id' => "closeButton-{$entry['topic_id']}", - 'onclick' => "STUDIP.Forum.openThreadFromOverview('{$entry['topic_id']}', '{$constraint['topic_id']}', " . ForumHelpers::getPage() . "); return false;", - ] - ) - ->conditionAll(null) - // Delete thread - ->condition(ForumPerm::has('remove_entry', $seminar_id)) - ->addButton( - 'delete', - _('Dieses Thema löschen'), - Icon::create('trash'), - [ - 'formaction' => $controller->url_for("course/forum/index/delete_entry/{$entry['topic_id']}"), - 'data-confirm' => sprintf( - _('Sind sie sicher dass Sie den Eintrag %s löschen möchten?'), - kill_format($entry['name']) - ) - ] - ) - ?> - - - - -
- -
diff --git a/app/views/course/forum/index/index.php b/app/views/course/forum/index/index.php deleted file mode 100644 index 1839783..0000000 --- a/app/views/course/forum/index/index.php +++ /dev/null @@ -1,264 +0,0 @@ - - -render_partial('course/forum/index/_js_templates') ?> - - -
-url_for('course/forum/index/search?backend=search')); - $search->setId('tutorSearchInfobox'); - $search->addNeedle(_('Beiträge durchsuchen'), 'searchfor', true); - $search->addFilter(_('Titel'), 'search_title'); - $search->addFilter(_('Inhalt'), 'search_content'); - $search->addFilter(_('Autor/-in'), 'search_author'); - $sidebar->addWidget($search); -} - -$actions = new ActionsWidget(); - -if ($section == 'index') { - if (ForumPerm::has('abo', $seminar_id)) { - if (ForumAbo::has($constraint['topic_id'])) : - $abo_text = _('Nicht mehr abonnieren'); - $abo_url = $controller->url_for('course/forum/index/remove_abo/' . $constraint['topic_id']); - else : - switch ($constraint['depth']) { - case '0': $abo_text = _('Komplettes Forum abonnieren');break; - case '1': $abo_text = _('Diesen Bereich abonnieren');break; - default: $abo_text = _('Dieses Thema abonnieren');break; - } - - $abo_url = $controller->url_for('course/forum/index/abo/' . $constraint['topic_id']); - endif; - - $actions->addLink($abo_text, $abo_url, Icon::create('link-intern')); - } - - if (ForumPerm::has('close_thread', $seminar_id) && $constraint['depth'] > 1) { - if ($constraint['closed'] == 0) { - $close_url = $controller->url_for('course/forum/index/close_thread/' - . $constraint['topic_id'] .'/'. $constraint['topic_id'] .'/'. ForumHelpers::getPage()); - $close = new LinkElement( - _('Thema schließen'), - $close_url, - Icon::create('lock-locked'), - [ - 'onclick' => 'STUDIP.Forum.closeThreadFromThread(\'' . $constraint['topic_id'] . '\', ' - . ForumHelpers::getPage() . '); return false;', - 'class' => "closeButtons" - ] - ); - $actions->addElement($close, 'closethread'); - } else { - $open_url = $controller->url_for('course/forum/index/open_thread/' - . $constraint['topic_id'] .'/'. $constraint['topic_id'] .'/'. ForumHelpers::getPage()); - $open = new LinkElement( - _('Thema öffnen'), - $open_url, - Icon::create('lock-unlocked'), - [ - 'onclick' => 'STUDIP.Forum.openThreadFromThread(\'' . $constraint['topic_id'] . '\', ' - . ForumHelpers::getPage() . '); return false;', - 'class' => "closeButtons" - ] - ); - $actions->addElement($open, 'closethread'); - } - } - - if (ForumPerm::has('make_sticky', $seminar_id) && $constraint['depth'] > 1) { - if ($constraint['sticky'] == 0) { - $emphasize_url = $controller->url_for('course/forum/index/make_sticky/' - . $constraint['topic_id'] .'/'. $constraint['topic_id'] .'/'. ForumHelpers::getPage()); - $emphasize = new LinkElement( - _('Thema hervorheben'), - $emphasize_url, - Icon::create('staple'), - [ - 'onclick' => 'STUDIP.Forum.makeThreadStickyFromThread(\'' . $constraint['topic_id'] . '\', ' - . ForumHelpers::getPage() . '); return false;', - 'id' => "stickyButton" - ] - ); - $actions->addElement($emphasize, 'emphasize'); - } else { - $unemphasize_url = $controller->url_for('course/forum/index/make_unsticky/' - . $constraint['topic_id'] .'/'. $constraint['topic_id'] .'/'. ForumHelpers::getPage()); - $emphasize = new LinkElement( - _('Hervorhebung aufheben'), - $unemphasize_url, - Icon::create('staple'), - [ - 'onclick' => 'STUDIP.Forum.makeThreadUnstickyFromThread(\'' . $constraint['topic_id'] . '\', ' - . ForumHelpers::getPage() . '); return false;', - 'id' => "stickyButton" - ] - ); - $actions->addElement($emphasize, 'emphasize'); - } - } - - if ($constraint['depth'] == 0 && ForumPerm::has('add_category', $seminar_id)) { - $actions->addLink(_('Neue Kategorie erstellen'), "#create", Icon::create('link-intern')); - } -} - -$sidebar->addWidget($actions); - -if ($section === 'index' && ForumPerm::has('pdfexport', $seminar_id)) { - $export = new ExportWidget(); - $export->addLink(_('Beiträge als PDF exportieren'), - $controller->url_for('course/forum/index/pdfexport/' . $constraint['topic_id']), - Icon::create('file-pdf')); - $sidebar->addWidget($export); -} - -$pagechooser = null; -?> - - -render_partial('course/forum/index/_breadcrumb') ?> - - - ForumEntry::POSTINGS_PER_PAGE) : ?> -
- 0) : ?> - render('shared/pagechooser', [ - 'page' => ForumHelpers::getPage(), - 'num_postings' => $number_of_entries, - 'perPage' => ForumEntry::POSTINGS_PER_PAGE, - 'pagelink' => str_replace('%%s', '%s', str_replace('%', '%%', $controller->url_for('course/forum/index/goto_page/'. $topic_id .'/'. $section - .'/%s/?searchfor=' . ($searchfor ?? '') . (!empty($options) ? '&'. http_build_query($options) : '' )))) - ]); ?> - - -
- - - -
- render_partial('course/forum/messages') ?> -
- - - - - - - - render_partial('course/forum/index/_areas') ?> - - render_partial('course/forum/index/_threads') ?> - - - - - render_partial('course/forum/index/_postings') ?> - - - - -
- -
- - - - -
-
- - - render_partial('course/forum/index/_abo_link', compact('constraint')) ?> - - - - - url_for('course/forum/index/pdfexport'), ['target' => '_blank']) ?> - -
-
- - - render_partial('course/forum/index/_new_category') ?> - - - - -
-
-
- - url_for('course/forum/index/index/'. $topic_id .'?answer=1'), - ['onClick' => 'STUDIP.Forum.answerEntry(); return false;', - 'class' => 'hideWhenClosed',]) ?> - - 1 && ($constraint['closed'] == 1)) : ?> - url_for('course/forum/index/index/' . $topic_id. '?answer=1'), - ['onClick' => 'STUDIP.Forum.answerEntry(); return false;', - 'class' => 'hideWhenClosed', - 'style' => 'display:none;' - ]) ?> - - - 1) : ?> - - url_for('course/forum/index/close_thread/' . $topic_id .'/'. $topic_id .'/'. ForumHelpers::getPage()), [ - 'onClick' => 'STUDIP.Forum.closeThreadFromThread("'. $topic_id .'"); return false;', - 'class' => 'closeButtons'] - ) ?> - - url_for('course/forum/index/open_thread/' . $topic_id .'/'. $topic_id .'/'. ForumHelpers::getPage()), [ - 'onClick' => 'STUDIP.Forum.openThreadFromThread("'. $topic_id .'"); return false;', - 'class' => 'closeButtons'] - ) ?> - - - - 0 && ForumPerm::has('abo', $seminar_id)) : ?> - - render_partial('course/forum/index/_abo_link', compact('constraint')) ?> - - - - - url_for('course/forum/index/pdfexport/' . $topic_id), ['target' => '_blank']) ?> - -
-
-
- - - -seminar_id)) - || (isset($constraint['depth']) && $constraint['depth'] >= 1 && ForumPerm::has('add_entry', $seminar_id)) ): ?> - render_partial('course/forum/index/_new_entry') ?> - -
- - - - - - - - - diff --git a/app/views/course/forum/messages.php b/app/views/course/forum/messages.php deleted file mode 100644 index 246a85d..0000000 --- a/app/views/course/forum/messages.php +++ /dev/null @@ -1,7 +0,0 @@ - $message): ?> - - - - - - diff --git a/cli/Commands/Make/Plugin.php b/cli/Commands/Make/Plugin.php index 1405c6b..ce9af97 100644 --- a/cli/Commands/Make/Plugin.php +++ b/cli/Commands/Make/Plugin.php @@ -26,7 +26,6 @@ final class Plugin extends Command \ExternPagePlugin::class, \FilesystemPlugin::class, \FileUploadHook::class, - \ForumModule::class, \HomepagePlugin::class, \LibraryPlugin::class, \MetricsPlugin::class, diff --git a/db/migrations/6.1.6_forum3.php b/db/migrations/6.1.6_forum3.php new file mode 100644 index 0000000..914fae2 --- /dev/null +++ b/db/migrations/6.1.6_forum3.php @@ -0,0 +1,400 @@ +exec(" + ALTER TABLE `forum_categories` + CHANGE `seminar_id` `range_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + CHANGE `pos` `position` int(11) NOT NULL DEFAULT 0, + CHANGE `entry_name` `name` varchar(255) NOT NULL, + ADD COLUMN `description` text DEFAULT NULL AFTER `name`, + ADD COLUMN `color` VARCHAR(7) AFTER `description`, + ADD COLUMN `chdate` INT(11) DEFAULT NULL AFTER `position`, + ADD COLUMN `mkdate` INT(11) DEFAULT NULL AFTER `chdate` + "); + + \DBManager::get()->exec(" + ALTER TABLE `forum_entries` + ADD KEY `lft` (`lft`), + ADD KEY `rgt` (`rgt`) + "); + + \DBManager::get()->exec(" + CREATE TABLE IF NOT EXISTS `forum_topics` ( + `topic_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `category_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, + `range_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `name` text NOT NULL, + `description` text DEFAULT NULL, + `position` INT(11) NOT NULL DEFAULT 0, + `chdate` INT(11) NOT NULL, + `mkdate` INT(11) NOT NULL, + PRIMARY KEY (`topic_id`) + ) + "); + + \DBManager::get()->exec(" + INSERT INTO `forum_topics` (`topic_id`, `category_id`, `range_id`, `name`, `description`, `position`, `chdate`, `mkdate`) + SELECT `forum_entries`.`topic_id`, + `forum_categories_entries`.`category_id`, + `forum_entries`.`seminar_id`, + `forum_entries`.`name`, + `forum_entries`.`content`, + IFNULL(`forum_categories_entries`.`pos`, 0), + `forum_entries`.`latest_chdate`, + `forum_entries`.`mkdate` + FROM `forum_entries` + LEFT JOIN `forum_categories_entries` ON (`forum_categories_entries`.`topic_id` = `forum_entries`.`topic_id`) + WHERE `forum_entries`.`depth` = 1 + GROUP BY `forum_entries`.`topic_id` + "); + + \DBManager::get()->exec(" + CREATE TABLE IF NOT EXISTS `forum_discussions` ( + `discussion_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `topic_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `type_id` INT(11) DEFAULT NULL, + `title` text NOT NULL, + `sticky` tinyINT(1) NOT NULL DEFAULT 0, + `closed_at` INT(11), + `view_count` INT NOT NULL DEFAULT 0, + `chdate` INT(11) NOT NULL, + `mkdate` INT(11) NOT NULL, + PRIMARY KEY (`discussion_id`), + KEY `topic_id` (`topic_id`) + ) + "); + \DBManager::get()->exec(" + INSERT INTO `forum_discussions` (`discussion_id`, `topic_id`, `sticky`, `title`, `chdate`, `mkdate`) + SELECT `forum_entries`.`topic_id`, + `parent_fe`.`topic_id`, + `forum_entries`.`sticky`, + `forum_entries`.`name`, + `forum_entries`.`latest_chdate`, + `forum_entries`.`mkdate` + FROM `forum_entries` + LEFT JOIN `forum_entries` AS `parent_fe` ON (`parent_fe`.depth = 1 AND `parent_fe`.`lft` < `forum_entries`.`lft` AND `parent_fe`.`rgt` > `forum_entries`.`rgt` AND `parent_fe`.`seminar_id` = `forum_entries`.`seminar_id`) + WHERE `forum_entries`.`depth` = 2 + GROUP BY `forum_entries`.`topic_id` + "); + + \DBManager::get()->exec(" + CREATE TABLE `forum_postings` ( + `posting_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `discussion_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `range_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `parent_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin, + `content` TEXT NOT NULL, + `quote` TEXT, + `user_id` CHAR(32) NOT NULL, + `anonymous` tinyINT(1) NOT NULL DEFAULT 0, + `chdate` INT(11) NOT NULL, + `mkdate` INT(11) NOT NULL, + PRIMARY KEY (`posting_id`), + KEY `discussion_id` (`discussion_id`) + ) + "); + + \DBManager::get()->exec(" + INSERT INTO `forum_postings` (`posting_id`, `discussion_id`, `range_id`, `content`, `user_id`, `anonymous`, `chdate`, `mkdate`) + SELECT `forum_entries`.`topic_id`, + `forum_entries`.`topic_id`, + `forum_entries`.`seminar_id`, + `forum_entries`.`content`, + `forum_entries`.`user_id`, + `forum_entries`.`anonymous`, + `forum_entries`.`latest_chdate`, + `forum_entries`.`mkdate` + FROM `forum_entries` + LEFT JOIN `forum_entries` AS `parent_fe` ON (`parent_fe`.depth = 1 AND `parent_fe`.`lft` < `forum_entries`.`lft` AND `parent_fe`.`rgt` > `forum_entries`.`rgt` AND `parent_fe`.`seminar_id` = `forum_entries`.`seminar_id`) + WHERE `forum_entries`.`depth` = 2 + GROUP BY `forum_entries`.`topic_id` + "); + + \DBManager::get()->exec(" + INSERT INTO `forum_postings` (`posting_id`, `discussion_id`, `range_id`, `content`, `user_id`, `anonymous`, `chdate`, `mkdate`) + SELECT `forum_entries`.`topic_id`, + `parent_fe`.`topic_id`, + `forum_entries`.`seminar_id`, + `forum_entries`.`content`, + `forum_entries`.`user_id`, + `forum_entries`.`anonymous`, + `forum_entries`.`latest_chdate`, + `forum_entries`.`mkdate` + FROM `forum_entries` + LEFT JOIN `forum_entries` AS `parent_fe` ON (`parent_fe`.depth = 2 AND `parent_fe`.`lft` < `forum_entries`.`lft` AND `parent_fe`.`rgt` > `forum_entries`.`rgt` AND `parent_fe`.`seminar_id` = `forum_entries`.`seminar_id`) + WHERE `forum_entries`.`depth` = 3 + GROUP BY `forum_entries`.`topic_id` + "); + + \DBManager::get()->exec(" + CREATE TABLE `forum_discussion_types` ( + `type_id` INT(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varCHAR(255) NOT NULL, + `icon` varCHAR(50) DEFAULT NULL, + `chdate` INT(11) NOT NULL, + `mkdate` INT(11) NOT NULL, + PRIMARY KEY (`type_id`) + ) + "); + + \DBManager::get()->exec(" + CREATE TABLE `forum_posting_reactions` ( + `id` INT(11) unsigned NOT NULL AUTO_INCREMENT, + `posting_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `user_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `emoji` varCHAR(50) DEFAULT NULL, + `chdate` INT(11) NOT NULL, + `mkdate` INT(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `posting_id` (`posting_id`), + KEY `user_id` (`user_id`), + KEY `emoji` (`emoji`) + ) + "); + \DBManager::get()->exec(" + INSERT INTO `forum_posting_reactions` (`posting_id`, `user_id`, `emoji`, `chdate`, `mkdate`) + SELECT `topic_id`, `user_id`, 'THUMBS UP SIGN', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + FROM `forum_likes` + "); // THUMBS UP SIGN, THUMBS DOWN SIGN, ROCKET, GRINNING FACE, SMILING FACE WITH SUNGLASSES, CONFUSED FACE, BLACK HEART SUIT, PARTY POPPER + + \DBManager::get()->exec(" + CREATE TABLE `forum_posting_reads` ( + `discussion_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `user_id` CHAR(32) NOT NULL, + `read_index` INT(11) NOT NULL DEFAULT 0, + `chdate` INT(11) NOT NULL, + PRIMARY KEY (`discussion_id`, `user_id`) + ) + "); + + \DBManager::get()->exec(" + CREATE TABLE `forum_subscriptions` ( + `id` INT(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` CHAR(32) NOT NULL, + `range_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `subject_id` CHAR(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `subject` ENUM('discussion', 'topic') NOT NULL DEFAULT 'discussion', + `notification_type` ENUM('all', 'replies_only', 'none') NOT NULL DEFAULT 'all', + `chdate` INT(11) NOT NULL, + `mkdate` INT(11) NOT NULL, + PRIMARY KEY (`id`) + ) + "); + + $insertConfigSql = "INSERT IGNORE INTO `config` VALUES (:field, :value, :type, :range, :section, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :description)"; + + \DBManager::get()->execute( + $insertConfigSql, + [ + 'field' => 'FORUM_MODERATION_PERMISSION', + 'value' => 'dozent', + 'type' => 'string', + 'range' => 'course', + 'section' => 'Forum', + 'description' => 'Status, den es braucht, um das Forum zu moderieren.' + ] + ); + + \DBManager::get()->execute( + $insertConfigSql, + [ + 'field' => 'FORUM_HIDE_CATEGORIES_NAVIGATION', + 'value' => 0, + 'type' => 'boolean', + 'range' => 'course', + 'section' => 'Forum', + 'description' => 'Bestimmt, ob die Kategorien-Navigation im Forum ausgeblendet wird.' + ] + ); + + \DBManager::get()->execute( + $insertConfigSql, + [ + 'field' => 'FORUM_TILE_LAYOUT', + 'value' => 1, + 'type' => 'boolean', + 'range' => 'user', + 'section' => 'Forum', + 'description' => 'Konfiguration der Ansicht des Forum.' + ] + ); + + \DBManager::get()->exec(" + RENAME TABLE `forum_entries_issues` TO `forum_topics_issues`; + "); + + $insertDiscussionTypeSql = "INSERT IGNORE INTO `forum_discussion_types` VALUES (MD5(:name), :name, :icon, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())"; + + $discussionTypes = [ + [ + 'name' => 'Fragen', + 'icon' => 'question' + ], + [ + 'name' => 'Aufgaben', + 'icon' => 'guestbook' + ], + [ + 'name' => 'Ideen', + 'icon' => 'lightbulb' + ], + [ + 'name' => 'Regeln', + 'icon' => 'info-circle' + ], + [ + 'name' => 'Vorstellung', + 'icon' => 'vcard' + ], + [ + 'name' => 'Organisation', + 'icon' => 'network2' + ] + ]; + + foreach ($discussionTypes as $discussionType) { + \DBManager::get()->execute($insertDiscussionTypeSql, $discussionType); + } + + \DBManager::get()->exec(" + DROP TABLE IF EXISTS + `forum_likes`, + `forum_visits`, + `forum_abo_users`, + `forum_favorites`, + `forum_user_roles`, + `forum_categories_entries`, + `forum_entries` + "); + } + + public function down() + { + $removeConfigSql = "DELETE `config`, `config_values` FROM `config` + LEFT JOIN `config_values` USING (`field`) + WHERE `field` = :field"; + + \DBManager::get()->execute( + $removeConfigSql, + ['field' => 'FORUM_TILE_LAYOUT'] + ); + \DBManager::get()->execute( + $removeConfigSql, + ['field' => 'FORUM_HIDE_CATEGORIES_NAVIGATION'] + ); + \DBManager::get()->execute( + $removeConfigSql, + ['field' => 'FORUM_MODERATION_PERMISSION'] + ); + \DBManager::get()->execute( + $removeConfigSql, + ['field' => 'FORUM_TILE_LAYOUT'] + ); + + \DBManager::get()->exec(" + CREATE TABLE IF NOT EXISTS `forum_entries` ( + `topic_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `seminar_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `user_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `area` tinyint NOT NULL DEFAULT '0', + `mkdate` int unsigned NOT NULL, + `latest_chdate` int unsigned DEFAULT NULL, + `chdate` int unsigned NOT NULL, + `author` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `author_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `lft` int NOT NULL, + `rgt` int NOT NULL, + `depth` int NOT NULL, + `anonymous` tinyint NOT NULL DEFAULT '0', + `closed` tinyint unsigned NOT NULL DEFAULT '0', + `sticky` tinyint unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`topic_id`), + KEY `seminar_id` (`seminar_id`,`lft`), + KEY `seminar_id_2` (`seminar_id`,`rgt`), + KEY `user_id` (`user_id`) + ) + "); + + \DBManager::get()->exec(" + CREATE TABLE IF NOT EXISTS `forum_likes` ( + `topic_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `user_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + PRIMARY KEY (`topic_id`,`user_id`) + ) + "); + + \DBManager::get()->exec(" + CREATE TABLE IF NOT EXISTS `forum_visits` ( + `user_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `seminar_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `visitdate` int unsigned NOT NULL, + `last_visitdate` int unsigned NOT NULL, + PRIMARY KEY (`user_id`,`seminar_id`) + ) + "); + + \DBManager::get()->exec(" + CREATE TABLE IF NOT EXISTS `forum_abo_users` ( + `topic_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `user_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + PRIMARY KEY (`topic_id`,`user_id`) + ) + "); + + \DBManager::get()->exec(" + CREATE TABLE IF NOT EXISTS `forum_favorites` ( + `user_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `topic_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + PRIMARY KEY (`user_id`,`topic_id`) + ) + "); + + \DBManager::get()->exec(" + DROP TABLE IF EXISTS `forum_categories` + "); + + \DBManager::get()->exec(" + CREATE TABLE IF NOT EXISTS `forum_categories` ( + `category_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `seminar_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `entry_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `pos` int NOT NULL DEFAULT '0', + PRIMARY KEY (`category_id`), + KEY `seminar_id` (`seminar_id`) + ) + "); + + \DBManager::get()->exec(" + CREATE TABLE IF NOT EXISTS `forum_categories_entries` ( + `category_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `topic_id` char(32) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `pos` int NOT NULL DEFAULT '0', + PRIMARY KEY (`category_id`,`topic_id`) + ) + "); + + \DBManager::get()->exec(" + RENAME TABLE `forum_topics_issues` TO `forum_entries_issues`; + "); + + \DBManager::get()->exec(" + DROP TABLE IF EXISTS + `forum_subscriptions`, + `forum_posting_reads`, + `forum_posting_reactions`, + `forum_discussion_types`, + `forum_postings`, + `forum_discussions`, + `forum_topics` + "); + } +} diff --git a/lib/activities/CourseContext.php b/lib/activities/CourseContext.php index 1beb4d3..3419ed2 100644 --- a/lib/activities/CourseContext.php +++ b/lib/activities/CourseContext.php @@ -33,7 +33,6 @@ class CourseContext extends Context $course = $this->course; $module_provider = [ - 'CoreForum' => 'ForumProvider', 'CoreParticipants' => 'ParticipantsProvider', 'CoreDocuments' => 'DocumentsProvider', 'CoreWiki' => 'WikiProvider', diff --git a/lib/activities/ForumProvider.php b/lib/activities/ForumProvider.php deleted file mode 100644 index 6a958eb..0000000 --- a/lib/activities/ForumProvider.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @author André Klaßen - * @license GPL 2 or later3 - */ - - -namespace Studip\Activity; - -class ForumProvider implements ActivityProvider -{ - /** - * get the details for the passed activity - * - * @param object $activity the activity to fill with details, passed by reference - */ - public function getActivityDetails($activity) - { - $post = \ForumEntry::getEntry($activity->object_id); - - if (!$post) { - return false; - } - - $activity->content = formatReady($post['content']); - - $url = \URLHelper::getURL('dispatch.php/course/forum/index/index/' . $post['topic_id'] - .'?cid='. $post['seminar_id'] .'&highlight_topic='. $post['topic_id'] - .'#'. $post['topic_id']); - - $activity->object_url = [ - $url => _('Zum Forum der Veranstaltung') - ]; - - return true; - } - - /** - * {@inheritdoc} - */ - public static function getLexicalField() - { - return _('einen Forenbeitrag'); - } - -} diff --git a/lib/activities/InstituteContext.php b/lib/activities/InstituteContext.php index 0f7689d..1f2c3de 100644 --- a/lib/activities/InstituteContext.php +++ b/lib/activities/InstituteContext.php @@ -32,7 +32,6 @@ class InstituteContext extends Context $institute = $this->institute; $module_provider = [ - 'CoreForum' => 'ForumProvider', 'CoreDocuments' => 'DocumentsProvider', 'CoreWiki' => 'WikiProvider', ]; diff --git a/lib/archiv.inc.php b/lib/archiv.inc.php index e40685f..0e0912d 100644 --- a/lib/archiv.inc.php +++ b/lib/archiv.inc.php @@ -64,10 +64,8 @@ function lastActivity ($sem_id) WHERE `range_id` = :id"; } - foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) { - $table = $plugin->getEntryTableInfo(); - $queries[] = 'SELECT MAX(`'. $table['chdate'] .'`) AS chdate FROM `'. $table['table'] .'` WHERE `'. $table['seminar_id'] .'` = :id'; - } + // Forum + $queries[] = 'SELECT MAX(`chdate`) AS chdate FROM `forum_postings` WHERE `range_id` = :id'; $query = "SELECT MAX(chdate) FROM (" . implode(' UNION ', $queries) . ") AS tmp"; } diff --git a/lib/classes/Forum/DTO/ForumMember.php b/lib/classes/Forum/DTO/ForumMember.php new file mode 100644 index 0000000..a6f4ea4 --- /dev/null +++ b/lib/classes/Forum/DTO/ForumMember.php @@ -0,0 +1,53 @@ +get_studip_perm($course_id, $user->user_id); + + return self::fromArray([ + 'id' => $user->user_id, + 'username' => $user->username, + 'name' => $user->getFullName(), + 'avatar_url' => Avatar::getAvatar($user->user_id)->getURL(Avatar::NORMAL), + 'role' => in_array($role, ['dozent', 'tutor']) ? 'moderator' : 'author' + ]); + } + + public function toRawArray(): array + { + return [ + 'id' => $this->id, + 'username' => $this->username, + 'name' => $this->name, + 'avatar_url' => $this->avatar_url, + 'role' => $this->role + ]; + } +} diff --git a/lib/classes/Forum/DTO/ForumTag.php b/lib/classes/Forum/DTO/ForumTag.php new file mode 100644 index 0000000..ca0e045 --- /dev/null +++ b/lib/classes/Forum/DTO/ForumTag.php @@ -0,0 +1,45 @@ + $this->id, + 'name' => $this->name + ]; + } + + public static function getForumTags(): array + { + return DBManager::get()->fetchAll( + "SELECT DISTINCT `tags_relations`.`tag_id`, `tags`.`name` FROM `tags` + LEFT JOIN `tags_relations` ON `tags`.`id` = `tags_relations`.`tag_id` + WHERE `tags_relations`.`range_type` = 'forum' AND `tags`.`active` = TRUE + ORDER BY `tags`.`mkdate` DESC", + [], + function ($tag) { + return self::fromArray([ + 'id' => $tag['tag_id'], + 'name' => $tag['name'] + ]); + } + ); + } +} diff --git a/lib/classes/Forum/Enum/SubscriptionNotificationType.php b/lib/classes/Forum/Enum/SubscriptionNotificationType.php new file mode 100644 index 0000000..ee46714 --- /dev/null +++ b/lib/classes/Forum/Enum/SubscriptionNotificationType.php @@ -0,0 +1,25 @@ +value => [ + 'value' => self::All->value, + 'label' => _('Alle') + ], + self::RepliesOnly->value => [ + 'value' => self::RepliesOnly->value, + 'label' => _('Nur Antworten') + ], + self::None->value => [ + 'value' => self::None->value, + 'label' => _('Keine') + ] + ]; + } +} diff --git a/lib/classes/Forum/Service/DiscussionNotification.php b/lib/classes/Forum/Service/DiscussionNotification.php new file mode 100644 index 0000000..c3dcfe8 --- /dev/null +++ b/lib/classes/Forum/Service/DiscussionNotification.php @@ -0,0 +1,62 @@ +discussion = $discussion; + $this->topic = $discussion->topic; + } + + public function notifySubscribers(): void + { + $subscribers = $this->getSubscribers(); + + foreach ($subscribers as $subscriber) { + $this->sendNotifications($subscriber); + } + } + + protected function getSubscribers(): array + { + return ForumSubscription::findBySQL( + "subject = :subject AND subject_id = :subject_id AND notification_type = :notification_type", + [ + 'subject' => 'topic', + 'subject_id' => $this->topic->topic_id, + 'notification_type' => SubscriptionNotificationType::All->value + ] + ); + } + + protected function sendNotifications(ForumSubscription $subscriber): void + { + $url = URLHelper::getURL('dispatch.php/course/forum/discussions/show/'.$this->discussion->discussion_id, ['cid' => $this->topic->range_id], true); + + $message = sprintf( + _('Es gibt eine neue Diskussion „%1$s“ zum Thema „%2$s“.'), + $this->discussion->title, + $this->topic->name + ); + + PersonalNotifications::add( + $subscriber->user_id, + $url, + $message, + null, + Icon::create('forum') + ); + } +} diff --git a/lib/classes/Forum/Service/PostingNotification.php b/lib/classes/Forum/Service/PostingNotification.php new file mode 100644 index 0000000..5e46535 --- /dev/null +++ b/lib/classes/Forum/Service/PostingNotification.php @@ -0,0 +1,140 @@ +posting = $posting; + $this->topic = $posting->discussion->topic; + $this->discussion = $posting->discussion; + } + + public function notifySubscribers(): void + { + $excludeUserId = null; + if ($this->posting->parent_id) { + $subscriber = $this->notifyParentPostAuthor(); + + if ($subscriber) { + $excludeUserId = $subscriber->user_id; + } + } + + $subscribers = $this->getSubscribers($excludeUserId); + + foreach ($subscribers as $subscriber) { + if ($subscriber->user_id === $this->posting->user_id || $subscriber->notification_type !== SubscriptionNotificationType::All->value) { + continue; + } + + $this->sendNotifications($subscriber); + } + } + + protected function getSubscribers($excludeUserId = null): array + { + $query = [ + "range_id = :range_id AND subject_id IN (:subject_ids)", + [ + 'range_id' => $this->posting->range_id, + 'subject_ids' => [$this->discussion->discussion_id, $this->topic->topic_id] + ] + ]; + + if ($excludeUserId) { + $query[0] .= " AND user_id != :user_id"; + $query[1]['user_id'] = $excludeUserId; + } + + $subscriptions = ForumSubscription::findBySQL(...$query); + + /** + * Allow only one subscription per user. + * 'discussion' subscription has priority over 'topic' subscription + */ + $filteredSubscriptions = []; + foreach ($subscriptions as $subscription) { + $userId = $subscription->user_id; + + if (isset($filtered[$userId])) { + if ($filteredSubscriptions[$userId]->subject === 'discussion') { + continue; + } + + // If current subscription is discussion, replace it with topic + if ($subscription->subject === 'discussion') { + $filteredSubscriptions[$userId] = $subscription; + } + + continue; + } + + $filteredSubscriptions[$userId] = $subscription; + } + + return array_values($filteredSubscriptions); + } + + protected function sendNotifications(ForumSubscription $subscriber): void + { + $url = URLHelper::getURL('dispatch.php/course/forum/discussions/show/'.$this->discussion->discussion_id, ['cid' => $this->topic->range_id], true)."#post_" . $this->posting->posting_id; + + $message = sprintf( + _('Es gibt einen neuen Beitrag zur Diskussion „%s“.'), + $this->discussion->title + ); + + PersonalNotifications::add( + $subscriber->user_id, + $url, + $message, + "post_" . $this->posting->posting_id, + Icon::create('reply') + ); + } + + protected function notifyParentPostAuthor(): ?ForumSubscription + { + $parent = $this->posting->posting; + + $subscriber = ForumSubscription::findOneBySQL( + "range_id = :range_id AND subject_id IN (:subject_ids) AND user_id = :user_id AND notification_type != :notification_type ORDER BY subject", + [ + 'range_id' => $parent->range_id, + 'subject_ids' => [$this->discussion->discussion_id, $this->topic->topic_id], + 'user_id' => $parent->user_id, + 'notification_type' => SubscriptionNotificationType::None->value + ] + ); + + if ($subscriber && $subscriber->user_id !== $this->posting->user_id) { + \PersonalNotifications::add( + $subscriber->user_id, + \URLHelper::getURL('dispatch.php/course/forum/discussions/show/'.$this->posting->discussion_id, ['cid' => $this->topic->range_id], true)."#post_" . $this->posting->posting_id, + sprintf( + _('%s hat ihren Beitrag zur Diskussion „%s“ zitiert.'), + $this->posting->user->getFullName(), + $this->discussion->title + ), + "post_" . $this->posting->posting_id, + \Icon::create('quote') + ); + } + + return $subscriber; + } +} diff --git a/lib/classes/ForumAbo.php b/lib/classes/ForumAbo.php deleted file mode 100644 index 4c4c2ff..0000000 --- a/lib/classes/ForumAbo.php +++ /dev/null @@ -1,187 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 - * @category Stud.IP - */ - -class ForumAbo -{ - /** - * add the passed user as a watcher for the passed topic (including all - * current and future childs) - * - * @param string $topic_id - * @param string $user_id - */ - public static function add($topic_id, $user_id = null) - { - if (!$user_id) $user_id = $GLOBALS['user']->id; - - $stmt = DBManager::get()->prepare("REPLACE INTO forum_abo_users - (topic_id, user_id) VALUEs (?, ?)"); - $stmt->execute([$topic_id, $user_id]); - } - - /** - * remove the passed user as a watcher from the passed topic (including all - * current and future childs) - * - * @param string $topic_id - * @param string $user_id - */ - public static function delete($topic_id, $user_id = null) - { - if (!$user_id) $user_id = $GLOBALS['user']->id; - - $stmt = DBManager::get()->prepare("DELETE FROM forum_abo_users - WHERE topic_id = ? AND user_id = ?"); - $stmt->execute([$topic_id, $user_id]); - } - - /** - * check, if the passed user watches the passed topic. If no user_id is passed, - * the currently logged in user is used - * - * @param string $topic_id - * @param string $user_id - * - * @return boolean returns true if user is watching, false otherwise - */ - public static function has($topic_id, $user_id = null) - { - if (!$user_id) $user_id = $GLOBALS['user']->id; - - $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_abo_users - WHERE topic_id = ? AND user_id = ?"); - $stmt->execute([$topic_id, $user_id]); - - return $stmt->fetchColumn() > 0 ? true : false; - } - - /** - * send out the notification messages for the passed topic. The contents - * and a link directly to the topic are added to the message. - * - * @param string $topic_id - */ - public static function notify($topic_id) - { - // send message to all abo-users - $db = DBManager::get(); - $messaging = new messaging(); - - // get all parent topic-ids, to find out which users to notify - $path = ForumEntry::getPathToPosting($topic_id); - - // fetch all users to notify, exclude current user - $stmt = $db->prepare("SELECT DISTINCT user_id - FROM forum_abo_users - JOIN auth_user_md5 USING (user_id) - WHERE topic_id IN (:topic_ids) - AND user_id != :user_id"); - $stmt->bindValue(':topic_ids', array_keys($path), StudipPDO::PARAM_ARRAY); - $stmt->bindValue(':user_id', $GLOBALS['user']->id); - $stmt->execute(); - - // get details for topic - $topic = ForumEntry::getConstraints($topic_id); - - if (!$topic) { - return; - } - - while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - $user_id = $data['user_id']; - - // don't notify user if view permission is not granted - if (!ForumPerm::has('view', $topic['seminar_id'], $user_id)) { - continue; - } - - $user = User::find($user_id); - - setTempLanguage($data['user_id']); - $notification = sprintf( - _('%s hat einen Beitrag geschrieben'), - $topic['anonymous'] ? _('Anonym') : $topic['author'] - ); - restoreLanguage(); - - PersonalNotifications::add( - $user_id, - URLHelper::getURL( - 'dispatch.php/course/forum/index/index/' . $topic['topic_id'] . '#' . $topic['topic_id'], - ['cid' => $topic['seminar_id']], - true - ), - $notification, - "forumposting_" . $topic['topic_id'], - Icon::create('forum', 'clickable') - ); - - // check if user wants an email for all or selected messages only - if (!$user->isBlocked() && $messaging->user_wants_email($user_id)) { - $title = implode(' >> ', ForumEntry::getFlatPathToPosting($topic_id)); - - $subject = _('[Forum]') . ' ' . ($title ?: _('Neuer Beitrag')); - - $template = $GLOBALS['template_factory']->open('mail/html'); - $htmlTemplate = $GLOBALS['template_factory']->open('mail/forum_notification'); - $content = $htmlTemplate->render( - compact('user_id', 'topic', 'path') - ); - - $htmlMessage = $template->render([ - 'snd_fullname' => '', - 'rec_username' => '', - 'rec_fullname' => $user->getFullName(), - 'message' => $content, - 'attachments' => [], - ]); - - $textMessage = trim(kill_format($content)); - - $userWantsHtml = UserConfig::get($user_id)->MAIL_AS_HTML; - - StudipMail::sendMessage( - $user->email, - $subject, - $textMessage, - $userWantsHtml ? $htmlMessage : null - ); - } - } - } - - /** - * Removes all abos for a given course and user - * - * @param String $course_id Id of the course - * @param String $user_id Id of the user - * @return int number of removed abos - */ - public static function removeForCourseAndUser($course_id, $user_id) - { - $query = "DELETE FROM `forum_abo_users` - WHERE `user_id` = :user_id - AND `topic_id` IN ( - SELECT `topic_id` - FROM `forum_entries` - WHERE `seminar_id` = :course_id - )"; - $statement = DBManager::get()->prepare($query); - $statement->bindValue(':course_id', $course_id); - $statement->bindValue(':user_id', $user_id); - $statement->execute(); - return $statement->rowCount(); - } -} diff --git a/lib/classes/ForumActivity.php b/lib/classes/ForumActivity.php deleted file mode 100644 index 0f6cbf8..0000000 --- a/lib/classes/ForumActivity.php +++ /dev/null @@ -1,149 +0,0 @@ - - * @license https://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - */ - -class ForumActivity -{ - /** - * Post activity for new forum post - * - * @param string $event - * @param string $topic_id - * @param array $post - */ - public static function newEntry($event, $topic_id, $post) - { - $verb = isset($post['depth']) && $post['depth'] === 3 ? 'answered' : 'created'; - - if ($verb === 'created') { - if (isset($post['depth']) && (int)$post['depth'] === 1) { - $summary = _('%s hat im Forum der Veranstaltung "%s" einen Bereich erstellt.'); - } else { - $summary = _('%s hat im Forum der Veranstaltung "%s" ein Thema erstellt.'); - } - } else { - $summary = _('%s hat im Forum der Veranstaltung "%s" auf ein Thema geantwortet.'); - } - - self::post($post, $verb, $summary); - } - - /** - * Post activity for updating a forum post - * @param string $event - * @param string $topic_id - * @param array $post - */ - public static function updateEntry($event, $topic_id, $post) - { - $summary = _('%s hat im Forum der Veranstaltung "%s" einen Beitrag editiert.'); - - if ($post['user_id'] == $GLOBALS['user']->id) { - $content = sprintf( - _('%s hat seinen eigenen Beitrag vom %s editiert.'), - self::getPostUsername($post), - date('d.m.y, H:i', $post['mkdate']) - ); - } else { - $content = sprintf( - _('%s hat den Beitrag von %s vom %s editiert.'), - get_fullname($GLOBALS['user']->id), - self::getPostUsername($post), - date('d.m.y, H:i', $post['mkdate']) - ); - } - - self::post($post, 'edited', $summary, $content); - } - - /** - * Post activity for deleting a forum post - * $param string $event - * @param string $topic_id - * @param array $post - */ - public static function deleteEntry($event, $topic_id, $post) - { - // Remove all previous activities for the post - Studip\Activity\Activity::deleteBySQL( - "provider = ? AND object_type = 'forum' AND object_id = ?", - [Studip\Activity\ForumProvider::class, $topic_id] - ); - - $summary = _('%s hat im Forum der Veranstaltung "%s" einen Beitrag gelöscht.'); - - if ($post['user_id'] == $GLOBALS['user']->id) { - $content = sprintf( - _('%s hat seinen Beitrag vom %s gelöscht.'), - self::getPostUsername($post), - date('d.m.y, H:i', $post['mkdate']) - ); - } else { - $content = sprintf( - _('%s hat den Beitrag von %s vom %s gelöscht.'), - get_fullname($GLOBALS['user']->id), - self::getPostUsername($post), - date('d.m.y, H:i', $post['mkdate']) - ); - } - - self::post($post, 'deleted', $summary, $content); - } - - private static function post($post, $verb, $summary, $content = null) - { - // skip system-created entries like "Allgemeine Diskussionen" - if (!$post['user_id']) { - return; - } - - $range_id = $post['seminar_id']; - $type = get_object_type($range_id); - - $obj = get_object_name($range_id, $type); - - $data = [ - 'provider' => 'Studip\Activity\ForumProvider', - 'context' => $type === 'sem' ? 'course' : 'institute', - 'context_id' => $post['seminar_id'], - 'content' => null, - 'actor_type' => 'user', // who initiated the activity? - 'actor_id' => $post['user_id'], // id of initiator - 'verb' => $verb, // the activity type - 'object_id' => $post['topic_id'], // the id of the referenced object - 'object_type' => 'forum', // type of activity object - 'mkdate' => $post['mkdate'] ?? time() - ]; - - if (!empty($post['anonymous'])) { - $data['actor_type'] = 'anonymous'; - $data['actor_id'] = ''; - } - - Studip\Activity\Activity::create($data); - } - - /** - * Returns the poster's name for a forum post. - * - * @param array $post - * @return string - */ - private static function getPostUsername($post) - { - if (!empty($post['anonymous'])) { - return _('Anonym'); - } - - return get_fullname($post['user_id']); - } -} diff --git a/lib/classes/ForumEntry.php b/lib/classes/ForumEntry.php deleted file mode 100644 index 4726dcb..0000000 --- a/lib/classes/ForumEntry.php +++ /dev/null @@ -1,1418 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 - * @category Stud.IP - */ - -class ForumEntry implements PrivacyObject -{ - const WITH_CHILDS = true; - const WITHOUT_CHILDS = false; - const THREAD_PREVIEW_LENGTH = 100; - const POSTINGS_PER_PAGE = 10; - const FEED_POSTINGS = 100; - - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * H E L P E R - F U N C T I O N S * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * is used for posting-preview. replaces all newlines with spaces - * - * @param string $text the text to work on - * @returns string - */ - public static function br2space($text) - { - return str_replace("\n", ' ', str_replace("\r", '', $text)); - } - - /** - * remove the edit-html from a posting - * - * @param string $description the posting-content - * @return string the content stripped by the edit-mark - */ - public static function killEdit($description) - { - // wurde schon mal editiert - if (preg_match('/^(.*)("; - return $description . $edit; - } - - /** - * convert the edit-html to raw text - * - * @param string $description the posting-content - * @return string the content with the raw text version of the edit-mark - */ - public static function parseEdit($description, $anonymous = false) - { - // TODO figure out if this function can be removed - // has been replaced with getContentAsHTML in core code - $content = ForumEntry::killEdit($description); - $comment = ForumEntry::getEditComment($description, $anonymous); - return $content . ($comment ? "\n\n%%" . $comment .'%%' : ''); - } - - /** - * Get content with appended edit comment as HTML. - * - * @param string $description Database entry of forum entry's body. - * @param bool $anonymous True, if only root is allowed to see - * authors. - * @return string Content and edit comment as HTML. - */ - public static function getContentAsHtml($description, $anonymous = false) - { - $raw_content = ForumEntry::killEdit($description); - - $comment = ForumEntry::getEditComment($description, $anonymous); - $content = formatReady($raw_content); - - if ($comment) { - $content .= '
' . htmlReady($comment) . ''; - } - - return $content; - } - - /** - * Get author and time of an edited forum entry as a string. - * - * @param string $description Database entry of forum entry's body. - * @param bool $anonymous True, if only root is allowed to see - * authors. - * @return string Author and time or empty string if not edited. - */ - public static function getEditComment($description, $anonymous = false) - { - $info = ForumEntry::getEditInfo($description); - if ($info) { - $root = $GLOBALS['perm']->have_perm('root'); - $author = ($anonymous && !$root) ? _('Anonym') : $info['author']; - $time = date('d.m.y - H:i', $info['time']); - return '[' . _('Zuletzt editiert von') . " $author - $time]"; - } - return ''; - } - - /** - * Get author and time of an edited forum entry. - * - * @param string $description Database entry of forum entry's body. - * @return array|bool Associative array containing author and time. - * boolean False if edit tag was not found. - */ - public static function getEditInfo($description) { - if (preg_match('/\s*$/i', $description, $matches)) { - // wurde schon mal editiert - return ['author' => $matches[1], 'time' => $matches[2]]; - } - return false; - } - - /** - * Remove all quote blocks AND the quoted text from a forum post. - * - * @param String $description The string to remove the quote blocks from - * @return String the posting without the [quote]-blocks (not just tags!) - */ - public static function removeQuotes($description) - { - if (Studip\Markup::isHtml($description)) { - // remove all blockquote tags - $dom = new DOMDocument(); - $old_libxml_error = libxml_use_internal_errors(true); - $dom->loadHtml($description, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); - libxml_use_internal_errors($old_libxml_error); - $nodes = iterator_to_array($dom->getElementsByTagName('blockquote')); - - foreach ($nodes as $node) { - $node->parentNode->removeChild($node); - } - - return $dom->saveHTML(); - } else { - $description = preg_replace('/\[quote(=.*)\].*\[\/quote\]/isU', '', $description); - $description = str_replace('[/quote]', '', $description); - } - return $description; - } - - - /** - * calls Stud.IP's kill_format - * - * @param string $text the text to parse - * @return string the text without format-tags - */ - public static function killFormat($text) - { - return kill_format($text); - } - - /** - * returns the entry for the passed topic_id - * - * @param string $topic_id - * @return array | bool array('lft' => ..., 'rgt' => ..., seminar_id => ...) - * - * @throws Exception - */ - public static function getConstraints($topic_id) - { - //very bad performance if topic_id is 0 or false - if (!$topic_id) { - return false; - } - - // look up the range of postings - $range_stmt = DBManager::get()->prepare("SELECT * - FROM forum_entries WHERE topic_id = ?"); - $range_stmt->execute([$topic_id]); - if (!$data = $range_stmt->fetch(PDO::FETCH_ASSOC)) { - return false; - } - - if ($data['depth'] == 1) { - $data['area'] = 1; - } - - return $data; - } - - /** - * return the topic_id of the parent element, false if there is none (ie the - * passed topic_id is already the upper-most node in the tree) - * - * @param string $topic_id the topic_id for which the parent shall be found - * - * @return string the topic_id of the parent element or false - */ - public static function getParentTopicId($topic_id) - { - $path = ForumEntry::getPathToPosting($topic_id); - array_pop($path); - $data = array_pop($path); - - return $data['id'] ?? false; - } - - - /** - * get the topic_ids of all childs of the passed topic including itself - * - * @param string $topic_id the topic_id to find the childs for - * @return array a list if topic_ids - */ - public static function getChildTopicIds($topic_id) - { - $constraints = ForumEntry::getConstraints($topic_id); - if (!$constraints) { - return []; - } - - $stmt = DBManager::get()->prepare("SELECT topic_id - FROM forum_entries WHERE lft >= ? AND rgt <= ? - AND seminar_id = ?"); - $stmt->execute([$constraints['lft'], $constraints['rgt'], $constraints['seminar_id']]); - - return $stmt->fetchAll(PDO::FETCH_COLUMN); - } - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * D A T A - R E T R I E V A L * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * get the page the passed posting is on - * - * @param string $topic_id - * @return int - */ - public static function getPostingPage($topic_id, $constraint = null) - { - if (!$constraint) { - $constraint = ForumEntry::getConstraints($topic_id); - } - - if (!$constraint) { - return 0; - } - // this calculation only works for postings - if ($constraint['depth'] <= 2) { - return ForumHelpers::getPage(); - } - - if ($parent_id = ForumEntry::getParentTopicId($topic_id)) { - $parent_constraint = ForumEntry::getConstraints($parent_id); - - if ($parent_constraint) { - return ceil((($constraint['lft'] - $parent_constraint['lft'] + 3) / 2) / ForumEntry::POSTINGS_PER_PAGE); - } - } - - return 0; - } - - /** - * return the id for the oldest unread child-posting for the passed topic. - * - * @param string $parent_id - * @return string id of oldest unread posting - */ - public static function getLastUnread($parent_id) - { - $constraint = ForumEntry::getConstraints($parent_id); - - if (!$constraint) { - return null; - } - - // take users visitdate into account - $visitdate = ForumVisit::getLastVisit($constraint['seminar_id']); - - // get the first unread entry - $stmt = DBManager::get()->prepare("SELECT * FROM forum_entries - WHERE lft > ? AND rgt < ? AND seminar_id = ? - AND mkdate >= ? - ORDER BY mkdate ASC LIMIT 1"); - $stmt->execute([$constraint['lft'], $constraint['rgt'], $constraint['seminar_id'], $visitdate]); - $last_unread = $stmt->fetch(PDO::FETCH_ASSOC); - - return $last_unread ? $last_unread['topic_id'] : null; - } - - /** - * retrieve the the latest posting under $parent_id - * or false if the postings itself is the latest - * - * @param string $parent_id the node to lookup the childs in - * @return array | bool the data for the latest postings or false - */ - public static function getLatestPosting($parent_id) - { - $constraint = ForumEntry::getConstraints($parent_id); - if (!$constraint) { - return false; - } - - $stmt = DBManager::get()->prepare("SELECT * FROM forum_entries - WHERE lft > ? AND rgt < ? AND seminar_id = ? - ORDER BY mkdate DESC LIMIT 1"); - $stmt->execute([$constraint['lft'], $constraint['rgt'], $constraint['seminar_id']]); - - return $stmt->fetch(PDO::FETCH_ASSOC) ?: false; - } - - /** - * returns a hashmap with arrays containing id and name with the entries - * which lead to the passed topic - * - * @param string $topic_id the topic to get the path for - * - * @return array - */ - public static function getPathToPosting($topic_id) - { - $data = ForumEntry::getConstraints($topic_id); - if (!$data) { - return []; - } - - $ret = []; - - $stmt = DBManager::get()->prepare("SELECT * FROM forum_entries - WHERE lft <= ? AND rgt >= ? AND seminar_id = ? ORDER BY lft ASC"); - $stmt->execute([$data['lft'], $data['rgt'], $data['seminar_id']]); - - while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - $ret[$data['topic_id']] = $data; - $ret[$data['topic_id']]['id'] = $data['topic_id']; - } - // set the name of the first entry to the name of the category the entry is in - if (count($ret) > 1) { - $tmp = array_slice($ret, 1, 1); - $area = array_pop($tmp); - $top = current($ret); - $ret[$top['id']]['name'] = ForumCat::getCategoryNameForArea($area['id']) ?: _('Allgemein'); - } - - return $ret; - } - - /** - * returns a hashmap where key is topic_id and value a posting-title from the - * entries which lead to the passed topic. - * - * WARNING: This function ommits postings with an empty title. For a full - * list please use ForumEntry::getPathToPosting()! - * - * @param string $topic_id the topic to get the path for - * - * @return array - */ - public static function getFlatPathToPosting($topic_id) - { - // use only the part of the path until the thread, no posting title - $postings = array_slice(self::getPathToPosting($topic_id), 0, 3); - $ret = []; - foreach ($postings as $post) { - if ($post['name']) { - $ret[$post['id']] = $post['name']; - } - } - - return $ret; - } - - /** - * fill the passed postings with additional data - * - * @param array $postings - * @return array - */ - public static function parseEntries($postings) - { - $posting_list = []; - - // retrieve the postings - foreach ($postings as $data) { - // we throw away all formatting stuff, tags, etc, leaving the important bit of information - $desc_short = ForumEntry::br2space(ForumEntry::killFormat($data['content'])); - if (mb_strlen($desc_short) > (ForumEntry::THREAD_PREVIEW_LENGTH + 2)) { - $desc_short = mb_substr($desc_short, 0, ForumEntry::THREAD_PREVIEW_LENGTH) . '...'; - } - - $posting_list[$data['topic_id']] = [ - 'author' => $data['author'], - 'topic_id' => $data['topic_id'], - 'name' => formatReady($data['name']), - 'name_raw' => $data['name'], - 'content' => ForumEntry::getContentAsHtml($data['content'], $data['anonymous']), - 'content_raw' => ForumEntry::killEdit($data['content']), - 'content_short' => $desc_short, - 'chdate' => $data['chdate'], - 'mkdate' => $data['mkdate'], - 'user_id' => $data['user_id'], - 'raw_title' => $data['name'], - 'raw_description' => ForumEntry::killEdit($data['content']), - 'fav' => (!empty($data['fav']) && ($data['fav'] == 'fav')), - 'depth' => $data['depth'], - 'anonymous' => $data['anonymous'], - 'closed' => $data['closed'], - 'sticky' => $data['sticky'], - 'seminar_id' => $data['seminar_id'] - ]; - } // retrieve the postings - - return $posting_list; - } - - /** - * Get all entries for the passed parent_id. - * Returns an array of the following structure: - * Array ( - * 'list' => Array ( - * 'author' => - * 'topic_id' => - * 'name' => formatReady() - * 'name_raw' => - * 'content' => formatReady() - * 'content_raw' => - * 'content_short' => - * 'chdate' => - * 'mkdate' => - * 'user_id' => - * 'raw_title' => - * 'raw_description' => - * 'fav' => - * 'depth' => - * 'sticky' => - * 'closed' => - * 'seminar_id' => - * ) - * 'count' => - * ) - * - * @param string $parent_id id of parent-element to get entries for. - * @param boolean $with_childs if true, the whole subtree is fetched - * @param string $add for additional constraints in the WHERE-part of the query - * @param string $sort_order can be ASC or DESC - * @param int $start can be used for pagination, is used for the LIMIT-part of the query - * @param int $limit number of entries to fetch, defaults to ForumEntry::POSTINGS_PER_PAGE - * - * @return array - * - * @throws Exception if the retrieval failed, an Exception is thrown - */ - public static function getEntries($parent_id, $with_childs = false, $add = '', - $sort_order = 'DESC', $start = 0, $limit = ForumEntry::POSTINGS_PER_PAGE) - { - $constraint = ForumEntry::getConstraints($parent_id); - if (!$constraint) { - return []; - } - $seminar_id = $constraint['seminar_id']; - $depth = $constraint['depth'] + 1; - - // count the entries and set correct page if necessary - if ($with_childs) { - $count_stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries - LEFT JOIN forum_favorites as ou ON (ou.topic_id = forum_entries.topic_id AND ou.user_id = ?) - WHERE (forum_entries.seminar_id = ? - AND forum_entries.seminar_id != forum_entries.topic_id - AND lft > ? AND rgt < ?) " - . ($depth > 2 ? " OR forum_entries.topic_id = ". DBManager::get()->quote($parent_id) : '') - . $add - . " ORDER BY forum_entries.mkdate $sort_order"); - $count_stmt->execute([$GLOBALS['user']->id, $seminar_id, $constraint['lft'], $constraint['rgt']]); - $count = $count_stmt->fetchColumn(); - } else { - $count_stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries - LEFT JOIN forum_favorites as ou ON (ou.topic_id = forum_entries.topic_id AND ou.user_id = ?) - WHERE ((depth = ? AND forum_entries.seminar_id = ? - AND forum_entries.seminar_id != forum_entries.topic_id - AND lft > ? AND rgt < ?) " - . ($depth > 2 ? " OR forum_entries.topic_id = ". DBManager::get()->quote($parent_id) : '') - . ') '. $add - . " ORDER BY forum_entries.mkdate $sort_order"); - $count_stmt->execute([$GLOBALS['user']->id, $depth, $seminar_id, $constraint['lft'], $constraint['rgt']]); - $count = $count_stmt->fetchColumn(); - } - - // use the last page if the requested page does not exist - if ($start > $count) { - $page = ceil($count / ForumEntry::POSTINGS_PER_PAGE); - ForumHelpers::setPage($page); - $start = max(1, $page - 1) * ForumEntry::POSTINGS_PER_PAGE; - } - - if ($with_childs) { - $stmt = DBManager::get()->prepare("SELECT forum_entries.*, IF(ou.topic_id IS NOT NULL, 'fav', NULL) as fav - FROM forum_entries - LEFT JOIN forum_favorites as ou ON (ou.topic_id = forum_entries.topic_id AND ou.user_id = ?) - WHERE (forum_entries.seminar_id = ? - AND forum_entries.seminar_id != forum_entries.topic_id - AND lft > ? AND rgt < ?) " - . ($depth > 2 ? " OR forum_entries.topic_id = ". DBManager::get()->quote($parent_id) : '') - . $add - . " ORDER BY forum_entries.mkdate $sort_order" - . ($limit ? " LIMIT $start, $limit" : '')); - $stmt->execute([$GLOBALS['user']->id, $seminar_id, $constraint['lft'], $constraint['rgt']]); - } else { - $stmt = DBManager::get()->prepare("SELECT forum_entries.*, IF(ou.topic_id IS NOT NULL, 'fav', NULL) as fav - FROM forum_entries - LEFT JOIN forum_favorites as ou ON (ou.topic_id = forum_entries.topic_id AND ou.user_id = ?) - WHERE ((depth = ? AND forum_entries.seminar_id = ? - AND lft > ? AND rgt < ?) " - . ($depth > 2 ? " OR forum_entries.topic_id = ". DBManager::get()->quote($parent_id) : '') - . ') '. $add - . " ORDER BY forum_entries.mkdate $sort_order" - . ($limit ? " LIMIT $start, $limit" : '')); - $stmt->execute([$GLOBALS['user']->id, $depth, $seminar_id, $constraint['lft'], $constraint['rgt']]); - } - - if (!$stmt) { - throw new Exception("Error while retrieving postings in " . __FILE__ . " on line " . __LINE__); - } - - return ['list' => ForumEntry::parseEntries($stmt->fetchAll(PDO::FETCH_ASSOC)), 'count' => $count]; - } - - - /** - * Takes a posting-array like the one generated by ForumEntry::getList() - * and adds the child-posting with the freshest creation-date to it. - * - * @param array $postings - * @return array - */ - public static function getLastPostings($postings) - { - foreach ($postings as $key => $posting) - { - $last_posting = []; - - if ($data = ForumEntry::getLatestPosting($posting['topic_id'])) { - $last_posting['topic_id'] = $data['topic_id']; - $last_posting['date'] = $data['mkdate']; - $last_posting['user_id'] = $data['user_id']; - $last_posting['user_fullname'] = $data['author']; - $last_posting['username'] = get_username($data['user_id']); - $last_posting['anonymous'] = $data['anonymous']; - - // we throw away all formatting stuff, tags, etc, so we have just the important bit of information - $text = ForumEntry::removeQuotes($data['name']); - $text = ForumEntry::br2space(ForumEntry::killFormat($text)); - - if (mb_strlen($text) > 42) { - $text = mb_substr($text, 0, 40) . '...'; - } - - $last_posting['text'] = $text; - } - - $postings[$key]['last_posting'] = $last_posting; - if (!$postings[$key]['last_unread'] = ForumEntry::getLastUnread($posting['topic_id'])) { - $postings[$key]['last_unread'] = $last_posting['topic_id'] ?? ''; - } - $postings[$key]['num_postings'] = ForumEntry::countEntries($posting['topic_id']); - - unset($last_posting); - } - - return $postings; - } - - /** - * get a list of postings of a special type - * - * @param string $type one of 'area', 'list', 'postings', 'latest', 'favorites', 'dump', 'flat' - * @param string $parent_id the are to fetch from - * @return array array('list' => ..., 'count' => ...); - */ - public static function getList($type, $parent_id) - { - $start = (ForumHelpers::getPage() - 1) * ForumEntry::POSTINGS_PER_PAGE; - switch ($type) { - case 'area': - $list = ForumEntry::getEntries($parent_id, ForumEntry::WITHOUT_CHILDS, '', 'DESC', 0, 1000); - $postings = $list['list']; - - $postings = ForumEntry::getLastPostings($postings); - return ['list' => $postings, 'count' => $list['count']]; - - case 'list': - $constraint = ForumEntry::getConstraints($parent_id); - - if (!$constraint) { - return []; - } - - // purpose of the following query is to retrieve the threads - // for an area ordered by the mkdate of their latest posting - $stmt = DBManager::get()->prepare("SELECT SQL_CALC_FOUND_ROWS - fe.*, IF(ou.topic_id IS NOT NULL, 'fav', NULL) as fav - FROM forum_entries AS fe - LEFT JOIN forum_favorites as ou ON (ou.topic_id = fe.topic_id AND ou.user_id = :user_id) - WHERE fe.seminar_id = :seminar_id AND fe.lft > :left - AND fe.rgt < :right AND fe.depth = 2 - ORDER BY sticky DESC, latest_chdate DESC - LIMIT $start, ". ForumEntry::POSTINGS_PER_PAGE); - $stmt->bindParam(':seminar_id', $constraint['seminar_id']); - $stmt->bindParam(':left', $constraint['lft'], PDO::PARAM_INT); - $stmt->bindParam(':right', $constraint['rgt'], PDO::PARAM_INT); - $stmt->bindParam(':user_id', $GLOBALS['user']->id); - $stmt->execute(); - - $postings = $stmt->fetchAll(PDO::FETCH_ASSOC); - $count = DBManager::get()->query("SELECT FOUND_ROWS()")->fetchColumn(); - $postings = ForumEntry::parseEntries($postings); - $postings = ForumEntry::getLastPostings($postings); - - return ['list' => $postings, 'count' => $count]; - - case 'postings': - return ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, '', 'ASC', $start); - - case 'newest': - $constraint = ForumEntry::getConstraints($parent_id); - if (!$constraint) { - return []; - } - $last_visit_date = ForumVisit::getLastVisit($constraint['seminar_id']); - - // get postings - $stmt = DBManager::get()->prepare("SELECT forum_entries.*, IF(ou.topic_id IS NOT NULL, 'fav', NULL) as fav - FROM forum_entries - LEFT JOIN forum_favorites as ou ON (ou.topic_id = forum_entries.topic_id AND ou.user_id = :user_id) - WHERE seminar_id = :seminar_id AND lft > :left - AND rgt < :right AND (mkdate >= :mkdate OR chdate >= :mkdate) - ORDER BY mkdate ASC - LIMIT $start, ". ForumEntry::POSTINGS_PER_PAGE); - - $stmt->bindParam(':seminar_id', $constraint['seminar_id']); - $stmt->bindParam(':left', $constraint['lft']); - $stmt->bindParam(':right', $constraint['rgt']); - $stmt->bindParam(':mkdate', $last_visit_date); - $stmt->bindParam(':user_id', $GLOBALS['user']->id); - $stmt->execute(); - - $postings = $stmt->fetchAll(PDO::FETCH_ASSOC); - - $postings = ForumEntry::parseEntries($postings); - - // count found postings - $stmt_count = DBManager::get()->prepare("SELECT COUNT(*) - FROM forum_entries - WHERE seminar_id = :seminar_id AND lft > :left - AND rgt < :right AND mkdate >= :mkdate - ORDER BY mkdate ASC"); - - $stmt_count->bindParam(':seminar_id', $constraint['seminar_id']); - $stmt_count->bindParam(':left', $constraint['lft']); - $stmt_count->bindParam(':right', $constraint['rgt']); - $stmt_count->bindParam(':mkdate', $last_visit_date); - $stmt_count->execute(); - - - // return results - return ['list' => $postings, 'count' => $stmt_count->fetchColumn()]; - - case 'latest': - return ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, 'AND depth > 1', 'DESC', $start); - - case 'favorites': - $add = "AND ou.topic_id IS NOT NULL"; - return ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, $add, 'DESC', $start); - - case 'dump': - $constraint = ForumEntry::getConstraints($parent_id); - if (!$constraint) { - return []; - } - $seminar_id = $constraint['seminar_id']; - $depth = $constraint['depth'] + 1; - - $stmt = DBManager::get()->prepare("SELECT * FROM forum_entries - WHERE (forum_entries.seminar_id = ? - AND forum_entries.seminar_id != forum_entries.topic_id - AND lft > ? AND rgt < ?) " - . ($depth > 2 ? " OR forum_entries.topic_id = ". DBManager::get()->quote($parent_id) : '') - . " ORDER BY forum_entries.lft ASC"); - $stmt->execute([$seminar_id, $constraint['lft'], $constraint['rgt']]); - - return ForumEntry::parseEntries($stmt->fetchAll(PDO::FETCH_ASSOC)); - - case 'flat': - $constraint = ForumEntry::getConstraints($parent_id); - if (!$constraint) { - return []; - } - $stmt = DBManager::get()->prepare("SELECT SQL_CALC_FOUND_ROWS * FROM forum_entries - WHERE lft > ? AND rgt < ? AND seminar_id = ? AND depth = ? - ORDER BY name ASC"); - $stmt->execute([$constraint['lft'], $constraint['rgt'], $constraint['seminar_id'], $constraint['depth'] + 1]); - - $count = DBManager::get()->query("SELECT FOUND_ROWS()")->fetchColumn(); - - $posting_list = []; - - // speed up things a bit by leaving out the formatReady fields - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $data) { - // we throw away all formatting stuff, tags, etc, leaving the important bit of information - $desc_short = ForumEntry::br2space(ForumEntry::killFormat($data['content'])); - if (mb_strlen($desc_short) > (ForumEntry::THREAD_PREVIEW_LENGTH + 2)) { - $desc_short = mb_substr($desc_short, 0, ForumEntry::THREAD_PREVIEW_LENGTH) . '...'; - } - $posting_list[$data['topic_id']] = [ - 'author' => $data['author'], - 'topic_id' => $data['topic_id'], - 'name_raw' => $data['name'], - 'content_raw' => ForumEntry::killEdit($data['content']), - 'content_short' => $desc_short, - 'chdate' => $data['chdate'], - 'mkdate' => $data['mkdate'], - 'user_id' => $data['user_id'], - 'raw_title' => $data['name'], - 'raw_description' => ForumEntry::killEdit($data['content']), - 'fav' => (!empty($data['fav']) && $data['fav'] == 'fav'), - 'depth' => $data['depth'], - 'seminar_id' => $data['seminar_id'] - ]; - } - - return ['list' => $posting_list, 'count' => $count]; - - case 'depth_to_large': - $constraint = ForumEntry::getConstraints($parent_id); - if (!$constraint) { - return []; - } - $stmt = DBManager::get()->prepare("SELECT SQL_CALC_FOUND_ROWS * FROM forum_entries - WHERE lft > ? AND rgt < ? AND seminar_id = ? AND depth > 3 - ORDER BY name ASC"); - $stmt->execute([$constraint['lft'], $constraint['rgt'], $constraint['seminar_id']]); - - $count = DBManager::get()->query("SELECT FOUND_ROWS()")->fetchColumn(); - - return ['list' => $stmt->fetchAll(PDO::FETCH_ASSOC), 'count' => $count]; - } - - throw new InvalidArgumentException("Invalid type {$type}"); - } - - /** - * Get the latest forum entries for the passed entries childs - * - * @param string $parent_id - * @param int $start_date timestamp - * @param int $end_date timestamp - * - * @return array list of postings - */ - public static function getLatestSince($parent_id, $start_date, $end_date) - { - $constraint = ForumEntry::getConstraints($parent_id); - if (!$constraint) { - return []; - } - $stmt = DBManager::get()->prepare("SELECT SQL_CALC_FOUND_ROWS * FROM forum_entries - WHERE lft > ? AND rgt < ? AND seminar_id = ? - AND mkdate BETWEEN ? AND ? - ORDER BY name ASC"); - $stmt->execute([$constraint['lft'], $constraint['rgt'], $constraint['seminar_id'], $start_date, $end_date]); - - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - /** - ** returns a list of postings for the passed search-term - * - * @param string $parent_id the area to search in (can be a whole seminar) - * @param string $_searchfor the term to search for - * @param array $options filter-options: search_title, search_content, search_author - * @return array array('list' => ..., 'count' => ...); - */ - public static function getSearchResults($parent_id, $_searchfor, $options) - { - $start = (ForumHelpers::getPage() - 1) * ForumEntry::POSTINGS_PER_PAGE; - - // if there are quoted parts, they should not be separated - $suchmuster = '/".*"/U'; - preg_match_all($suchmuster, $_searchfor, $treffer); - array_walk($treffer[0], function(&$value) { $value = trim($value, '"'); }); - - // remove the quoted parts from $_searchfor - $_searchfor = trim(preg_replace($suchmuster, '', $_searchfor)); - - // split the searchstring $_searchfor at every space - $parts = explode(' ', $_searchfor); - - foreach ($parts as $key => $val) { - if ($val == '') { - unset($parts[$key]); - } - } - - if (!empty($parts)) { - $_searchfor = array_merge($parts, $treffer[0]); - } else { - $_searchfor = $treffer[0]; - } - - // make an SQL-statement out of the searchstring - $search_string = []; - foreach ($_searchfor as $key => $val) { - if (!$val) { - unset($_searchfor[$key]); - } else { - $search_word = '%'. $val .'%'; - $zw_search_string = []; - if ($options['search_title']) { - $zw_search_string[] = "name LIKE " . DBManager::get()->quote($search_word); - } - - if ($options['search_content']) { - $zw_search_string[] = "content LIKE " . DBManager::get()->quote($search_word); - } - - if ($options['search_author']) { - $zw_search_string[] = "(anonymous = 0 AND author LIKE " . DBManager::get()->quote($search_word) . ')'; - } - - if (!empty($zw_search_string)) { - $search_string[] = '(' . implode(' OR ', $zw_search_string) . ')'; - } - } - } - - if (!empty($search_string)) { - $add = "AND (" . implode(' AND ', $search_string) . ")"; - return array_merge( - ['highlight' => $_searchfor], - ForumEntry::getEntries($parent_id, ForumEntry::WITH_CHILDS, $add, 'DESC', $start) - ); - } - - return ['num_postings' => 0, 'list' => []]; - } - - /** - * returns the entry for the passed topic_id - * - * @param string $topic_id - * @return array | false hash-array with the entries fields - */ - public static function getEntry($topic_id) - { - return ForumEntry::getConstraints($topic_id); - } - - /** - * Count the number of child-elements that the passed entry has and return it. - * - * @param string $parent_id - * - * @return int the number of child entries for the passed entry - */ - public static function countEntries($parent_id) - { - $data = ForumEntry::getConstraints($parent_id); - if (!$data) { - return 0; - } - return max((($data['rgt'] - $data['lft'] - 1) / 2) + 1, 0); - } - - /** - * Count the number of postings in a given course and return it. - * - * @param string $course_id the id of the given course - * - * @return int the number of postings in the course - */ - public static function countPostings($course_id) - { - $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries - WHERE seminar_id = ? AND depth >= 2"); - $stmt->execute([$course_id]); - - return $stmt->fetchColumn(0); - } - - /** - * Count all entries the passed user has ever written and return the result - * - * @staticvar type $entries - * - * @param string $user_id - * - * @return int number of entries user has ever written - */ - public static function countUserEntries($user_id, $seminar_id = null) - { - static $entries; - - if (empty($entries[$user_id])) { - $stmt = DBManager::get()->prepare("SELECT COUNT(*) - FROM forum_entries - WHERE user_id = ? AND seminar_id = IFNULL(?, seminar_id)"); - $stmt->execute([$user_id, $seminar_id]); - - $entries[$user_id] = $stmt->fetchColumn(); - } - - return $entries[$user_id]; - } - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * D A T A - C R E A T I O N * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * insert a node into the table - * - * @param array $data an array containing the following fields: - * topic_id the id of the new topic - * seminar_id the id of the seminar to add the topic to - * user_id the id of the user who created the topic - * name the title of the entry - * content the content of the entry - * author the author's name as a plaintext string - * author_host ip-address of creator - * @param string $parent_id the node to add the topic to - * - * @return void - */ - public static function insert($data, $parent_id) - { - $constraint = ForumEntry::getConstraints($parent_id); - - if (!$constraint) { - return; - } - - // #TODO: Zusammenfassen in eine Transaktion!!! - DBManager::get()->exec('UPDATE forum_entries SET lft = lft + 2 - WHERE lft > '. $constraint['rgt'] ." AND seminar_id = '". $constraint['seminar_id'] ."'"); - DBManager::get()->exec('UPDATE forum_entries SET rgt = rgt + 2 - WHERE rgt >= '. $constraint['rgt'] ." AND seminar_id = '". $constraint['seminar_id'] ."'"); - - $stmt = DBManager::get()->prepare("INSERT INTO forum_entries - (topic_id, seminar_id, user_id, name, content, mkdate, latest_chdate, - chdate, author, author_host, lft, rgt, depth, anonymous) - VALUES (? ,?, ?, ?, ?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), ?, ?, ?, ?, ?, ?)"); - $stmt->execute([$data['topic_id'], $data['seminar_id'], $data['user_id'], - $data['name'], $data['content'], $data['author'], $data['author_host'], - $constraint['rgt'], $constraint['rgt'] + 1, $constraint['depth'] + 1, $data['anonymous'] ?? 0]); - - // update "latest_chdate" for easier sorting of actual threads - DBManager::get()->exec("UPDATE forum_entries SET latest_chdate = UNIX_TIMESTAMP() - WHERE topic_id = '" . $constraint['topic_id'] . "'"); - - NotificationCenter::postNotification('ForumAfterInsert', $data['topic_id'], $data); - } - - - /** - * update the passed topic - * - * @param string $topic_id the id of the topic to update - * @param string $name the new name - * @param string $content the new content - * - * @return void - */ - public static function update($topic_id, $name, $content) - { - $post = ForumEntry::getConstraints($topic_id); - if (!$post) { - return; - } - - if (time() - $post['mkdate'] > 5 * 60) { - $content = ForumEntry::appendEdit($content); - } - - $stmt = DBManager::get()->prepare("UPDATE forum_entries - SET name = ?, content = ?, chdate = UNIX_TIMESTAMP(), latest_chdate = UNIX_TIMESTAMP() - WHERE topic_id = ?"); - $stmt->execute([$name, $content, $topic_id]); - - // update "latest_chdate" for easier sorting of actual threads - $parent_id = ForumEntry::getParentTopicId($topic_id); - DBManager::get()->exec("UPDATE forum_entries SET latest_chdate = UNIX_TIMESTAMP() - WHERE topic_id = '" . $parent_id . "'"); - - $post['name'] = $name; - $post['content'] = $content; - - NotificationCenter::postNotification('ForumAfterUpdate', $topic_id, $post); - } - - /** - * delete an entry and all his descendants from the mptt-table - * - * @param string $topic_id the id of the entry to delete - * - * @return void - */ - public static function delete($topic_id) - { - $post = ForumEntry::getConstraints($topic_id); - - if ($post) { - NotificationCenter::postNotification('ForumBeforeDelete', $topic_id, $post); - - // #TODO: Zusammenfassen in eine Transaktion!!! - // get all entry-ids to delete them from the category-reference-table - $stmt = DBManager::get()->prepare("SELECT topic_id FROM forum_entries - WHERE seminar_id = ? AND lft >= ? AND rgt <= ? AND depth = 1"); - $stmt->execute([$post['seminar_id'], $post['lft'], $post['rgt']]); - $ids = $stmt->fetchAll(PDO::FETCH_COLUMN); - - if ($ids != false && !is_array($ids)) { - $ids = [$ids]; - } - - if (!empty($ids)) { - $stmt = DBManager::get()->prepare("DELETE FROM forum_categories_entries - WHERE topic_id IN (:ids)"); - $stmt->bindParam(':ids', $ids, StudipPDO::PARAM_ARRAY); - $stmt->execute(); - } - - // delete all entries - $stmt = DBManager::get()->prepare("DELETE FROM forum_entries - WHERE seminar_id = ? AND lft >= ? AND rgt <= ?"); - - $stmt->execute([$post['seminar_id'], $post['lft'], $post['rgt']]); - - // update lft and rgt - $diff = $post['rgt'] - $post['lft'] + 1; - $stmt = DBManager::get()->prepare("UPDATE forum_entries SET lft = lft - $diff - WHERE lft > ? AND seminar_id = ?"); - $stmt->execute([$post['rgt'], $post['seminar_id']]); - - $stmt = DBManager::get()->prepare("UPDATE forum_entries SET rgt = rgt - $diff - WHERE rgt > ? AND seminar_id = ?"); - $stmt->execute([$post['rgt'], $post['seminar_id']]); - - } - - $parent = ForumEntry::getConstraints(ForumEntry::getParentTopicId($topic_id)); - - if ($parent) { - // set the latest_chdate to the latest child's chdate - $stmt = DBManager::get()->prepare("SELECT chdate FROM forum_entries - WHERE lft > ? AND rgt < ? AND seminar_id = ? - ORDER BY chdate DESC LIMIT 1"); - $stmt->execute([$parent['lft'] ?? null, $parent['rgt'] ?? null, $parent['seminar_id'] ?? null]); - $chdate = $stmt->fetchColumn(); - - $stmt_insert = DBManager::get()->prepare("UPDATE forum_entries - SET chdate = ? WHERE topic_id = ?"); - if ($chdate) { - $stmt_insert->execute([$chdate, $parent['topic_id']]); - } else { - $stmt_insert->execute([$parent['chdate'], $parent['topic_id']]); - } - } - } - - /** - * move the passed topic to the passed area - * - * @param string $topic_id the topic to move - * @param string $destination the area_id where the topic is moved to - * - * @return void - */ - public static function move($topic_id, $destination) - { - // #TODO: Zusammenfassen in eine Transaktion!!! - $constraints = ForumEntry::getConstraints($topic_id); - if ($constraints) { - // move the affected entries "outside" the tree - $stmt = DBManager::get()->prepare("UPDATE forum_entries - SET lft = lft * -1, rgt = rgt * -1 - WHERE seminar_id = ? AND lft >= ? AND rgt <= ?"); - $stmt->execute([$constraints['seminar_id'], $constraints['lft'], $constraints['rgt']]); - - // update the lft and rgt values of the parent to reflect the "deletion" - $diff = $constraints['rgt'] - $constraints['lft'] + 1; - $stmt = DBManager::get()->prepare("UPDATE forum_entries SET lft = lft - ? - WHERE lft > ? AND seminar_id = ?"); - $stmt->execute([$diff, $constraints['rgt'], $constraints['seminar_id']]); - - $stmt = DBManager::get()->prepare("UPDATE forum_entries SET rgt = rgt - ? - WHERE rgt > ? AND seminar_id = ?"); - $stmt->execute([$diff, $constraints['rgt'], $constraints['seminar_id']]); - - // make some space by updating the lft and rgt values of the target node - $constraints_destination = ForumEntry::getConstraints($destination); - - if ($constraints_destination) { - $size = $constraints['rgt'] - $constraints['lft'] + 1; - - $stmt = DBManager::get()->prepare("UPDATE forum_entries SET lft = lft + ? - WHERE lft > ? AND seminar_id = ?"); - $stmt->execute([$size, $constraints_destination['rgt'], $constraints_destination['seminar_id']]); - - $stmt = DBManager::get()->prepare("UPDATE forum_entries SET rgt = rgt + ? - WHERE rgt >= ? AND seminar_id = ?"); - $stmt->execute([$size, $constraints_destination['rgt'], $constraints_destination['seminar_id']]); - } - } - //move the entries from "outside" the tree to the target node - $constraints_destination = ForumEntry::getConstraints($destination); - - if ($constraints_destination) { - // update the depth to reflect the new position in the tree - // determine if we need to add, subtract or even do nothing to/from the depth - $depth_mod = $constraints_destination['depth'] - $constraints['depth'] + 1; - - $stmt = DBManager::get()->prepare("UPDATE forum_entries - SET depth = depth + ? - WHERE seminar_id = ? AND lft < 0"); - $stmt->execute([$depth_mod, $constraints_destination['seminar_id']]); - - // if the depth is larger than 3, fix it - $stmt = DBManager::get()->prepare("UPDATE forum_entries - SET depth = 3 - WHERE seminar_id = ? AND depth > 3 AND lft < 0"); - $stmt->execute([$constraints_destination['seminar_id']]); - - // move the tree to its destination - $diff = ($constraints_destination['rgt'] - ($constraints['rgt'] - $constraints['lft'])) - 1 - $constraints['lft']; - - $stmt = DBManager::get()->prepare("UPDATE forum_entries - SET lft = (lft * -1) + ?, rgt = (rgt * -1) + ? - WHERE seminar_id = ? AND lft < 0"); - $stmt->execute([$diff, $diff, $constraints_destination['seminar_id']]); - - if ($depth_mod != 0) { - self::fix_ordering($topic_id); - } - } - } - - private static function fix_ordering($parent_id) - { - $db = DBManager::get(); - - $entry = ForumEntry::getConstraints($parent_id); - if (!$entry) { - return; - } - - $stmt= $db->prepare('SELECT topic_id FROM forum_entries - WHERE lft > ? AND rgt < ? AND depth = 3 - AND seminar_id = ? - ORDER BY mkdate'); - - $stmt->execute([$entry['lft'], $entry['rgt'], $entry['seminar_id']]); - - $lft = $entry['lft'] + 1; - $rgt = $lft + 1; - - $inner_stmt = $db->prepare("UPDATE forum_entries SET lft=?, rgt=? - WHERE topic_id = ?"); - while ($topic_id = $stmt->fetchColumn()) { - $inner_stmt->execute([$lft, $rgt, $topic_id]); - - $lft += 2; - $rgt += 2; - } - } - - /** - * close the passed topic - * - * @param string $topic_id the topic to close - * - * @return void - */ - public static function close($topic_id) - { - // close all entries belonging to the topic - $stmt = DBManager::get()->prepare("UPDATE forum_entries - SET closed = 1 - WHERE topic_id = ?"); - $stmt->execute([$topic_id]); - } - - /** - * open the passed topic - * - * @param string $topic_id the topic to open - * - * @return void - */ - public static function open($topic_id) - { - // open all entries belonging to the topic - $stmt = DBManager::get()->prepare("UPDATE forum_entries - SET closed = 0 - WHERE topic_id = ?"); - $stmt->execute([$topic_id]); - } - - /** - * make the passed topic sticky - * - * @param string $topic_id the topic to make sticky - * - * @return void - */ - public static function sticky($topic_id) - { - // open all entries belonging to the topic - $stmt = DBManager::get()->prepare("UPDATE forum_entries - SET sticky = 1 - WHERE topic_id = ?"); - $stmt->execute([$topic_id]); - } - - /** - * make the passed topic unsticky - * - * @param string $topic_id the topic to make unsticky - * - * @return void - */ - public static function unsticky($topic_id) - { - // open all entries belonging to the topic - $stmt = DBManager::get()->prepare("UPDATE forum_entries - SET sticky = 0 - WHERE topic_id = ?"); - $stmt->execute([$topic_id]); - } - - /** - * check, if the default root-node for this seminar exists and make sure - * the default category exists as well - * - * @param string $seminar_id - * - * @return void - */ - public static function checkRootEntry($seminar_id) - { - setTempLanguage(); - - // check, if the root entry in the topic tree exists - $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries - WHERE topic_id = ? AND seminar_id = ?"); - $stmt->execute([$seminar_id, $seminar_id]); - if ($stmt->fetchColumn() == 0) { - $stmt = DBManager::get()->prepare("INSERT INTO forum_entries - (topic_id, seminar_id, user_id, name, content, author, author_host, mkdate, chdate, lft, rgt, depth) - VALUES (?, ?, '', 'Übersicht', '', '', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 1, 0)"); - $stmt->execute([$seminar_id, $seminar_id]); - } - - // make sure, that the category "Allgemein" exists - $stmt = DBManager::get()->prepare("INSERT IGNORE INTO forum_categories - (category_id, seminar_id, entry_name) VALUES (?, ?, ?)"); - $stmt->execute([$seminar_id, $seminar_id, _('Allgemein')]); - - // make sure that the default area "Allgemeine Diskussionen" exists, if there is nothing else present - $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM forum_entries - WHERE seminar_id = ? AND depth = 1"); - $stmt->execute([$seminar_id]); - - // add default area - if ($stmt->fetchColumn() == 0) { - $data = [ - 'topic_id' => md5(uniqid()), - 'seminar_id' => $seminar_id, - 'user_id' => '', - 'name' => _('Allgemeine Diskussion'), - 'content' => _('Hier ist Raum für allgemeine Diskussionen'), - 'author' => '', - 'author_host' => '' - ]; - ForumEntry::insert($data, $seminar_id); - } - - restoreLanguage(); - } - - /** - * returns the ten most active seminars - * - * @return array - */ - public static function getTopTenSeminars() - { - return DBManager::get()->query("SELECT a.seminar_id, b.name AS display, - count( a.seminar_id ) AS count FROM forum_entries a - INNER JOIN seminare b USING ( seminar_id ) - WHERE b.visible = 1 - AND a.mkdate > UNIX_TIMESTAMP( NOW( ) - INTERVAL 2 WEEK ) - GROUP BY a.seminar_id - ORDER BY count DESC - LIMIT 10")->fetchAll(PDO::FETCH_ASSOC); - } - - /** - * count all entries that exists in the whole installation and return it. - * - * @return int - */ - public static function countAllEntries() - { - return count_table_rows('forum_entries'); - } - - /** - * updates the user-entries and replaces the old user-id by the new one - * - * @param string $user_from - * @param string $user_to - */ - public static function migrateUser($user_from, $user_to) - { - $stmt = DBManager::get()->prepare("UPDATE forum_entries SET user_id = ? WHERE user_id = ?"); - $stmt->execute([$user_to, $user_from]); - - $stmt = DBManager::get()->prepare("UPDATE IGNORE forum_favorites SET user_id = ? WHERE user_id = ?"); - $stmt->execute([$user_to, $user_from]); - - $stmt = DBManager::get()->prepare("UPDATE IGNORE forum_visits SET user_id = ? WHERE user_id = ?"); - $stmt->execute([$user_to, $user_from]); - - $stmt = DBManager::get()->prepare("UPDATE IGNORE forum_likes SET user_id = ? WHERE user_id = ?"); - $stmt->execute([$user_to, $user_from]); - - $stmt = DBManager::get()->prepare("UPDATE IGNORE forum_abo_users SET user_id = ? WHERE user_id = ?"); - $stmt->execute([$user_to, $user_from]); - } - - /** - * returns the complete seminar or only the passed sub-tree as a html-string - * - * @param string $seminar_id - * - * @return string - */ - public static function getDump($seminar_id, $parent_id = null) - { - $seminar_name = get_object_name($seminar_id, 'sem'); - $content = '

'. _('Forum') .': ' . $seminar_name['name'] .'

'; - $data = ForumEntry::getList('dump', $parent_id ?: $seminar_id); - - foreach ($data as $entry) { - if ($entry['depth'] == 1) { - $content .= '

'. _('Bereich') .': '. $entry['name'] .'

'; - $content .= $entry['content'] .'

'; - } else if ($entry['depth'] == 2) { - $content .= '

'. _('Thema') .': '. $entry['name'] .'

'; - $content .= '' . sprintf(_('erstellt von %s am %s'), htmlReady($entry['author']), - strftime('%A %d. %B %Y, %H:%M', (int)$entry['mkdate'])) . '
'; - $content .= $entry['content'] .'

'; - } else if ($entry['depth'] == 3) { - $content .= ''.$entry['name'] .'
'; - $content .= '' . sprintf(_('erstellt von %s am %s'), htmlReady($entry['author']), - strftime('%A %d. %B %Y, %H:%M', (int)$entry['mkdate'])) . '
'; - $content .= $entry['content'] .'

'; - } - } - - return $content; - } - - public static function isClosed($topic_id) - { - foreach(ForumEntry::getPathToPosting($topic_id) as $entry) { - if ($entry['closed']) { - return true; - } - } - - return false; - } - - /** - * Export available data of a given user into a storage object - * (an instance of the StoredUserData class) for that user. - * - * @param StoredUserData $storage object to store data into - */ - public static function exportUserData(StoredUserData $storage) - { - $field_data = DBManager::get()->fetchAll("SELECT * FROM forum_entries WHERE user_id = ?", [$storage->user_id]); - if ($field_data) { - $storage->addTabularData(_('Forum Einträge'), 'forum_entries', $field_data); - } - } - -} diff --git a/lib/classes/ForumFavorite.php b/lib/classes/ForumFavorite.php deleted file mode 100644 index 9ffebf5..0000000 --- a/lib/classes/ForumFavorite.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 - * @category Stud.IP - */ - -class ForumFavorite { - - /** - * Set the topic denoted by the passed id as favorite for the - * currently logged in user - * - * @param string $topic_id - */ - static function set($topic_id) { - $stmt = DBManager::get()->prepare("REPLACE INTO - forum_favorites (topic_id, user_id) - VALUES (?, ?)"); - $stmt->execute([$topic_id, $GLOBALS['user']->id]); - } - - /** - * Remove the topic denoted by the passed id as favorite for the - * currently logged in user - * - * @param string $topic_id - */ - static function remove($topic_id) { - $stmt = DBManager::get()->prepare("DELETE FROM forum_favorites - WHERE topic_id = ? AND user_id = ?"); - $stmt->execute([$topic_id, $GLOBALS['user']->id]); - } -} \ No newline at end of file diff --git a/lib/classes/ForumHelpers.php b/lib/classes/ForumHelpers.php deleted file mode 100644 index 0c036cf..0000000 --- a/lib/classes/ForumHelpers.php +++ /dev/null @@ -1,278 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 - * @category Stud.IP - */ - -class ForumHelpers { - - /** - * The page for the current script run, modified by a global page-handle - * @var int - */ - static $page = 1; - - /** - * helper_function for highlight($text, $highlight) - * - * @param string $text - * @param array $highlight - * @return string - */ - public static function do_highlight($text, $highlight) - { - foreach ($highlight as $hl) { - $text = preg_replace( - '/' . preg_quote(htmlReady($hl), '/') . '/i', - '$0', - $text - ); - } - return $text; - } - - /** - * This function highlights Text HTML-safe - * (tags or words in tags are not highlighted, words between tags ARE highlighted) - * - * @param string $text the text where to words shall be highlighted, may contain tags - * @param array $highlight an array of words to be highlighted - * @return string the highlighted text - */ - public static function highlight($text, $highlight) - { - if (empty($highlight)) { - return $text; - } - - $data = []; - $treffer = []; - - // split text at every tag - $pattern = '/<[^<]*>/U'; - preg_match_all($pattern, $text, $treffer, PREG_OFFSET_CAPTURE); - - if (sizeof($treffer[0]) == 0) { - return self::do_highlight($text, $highlight); - } - - // cycle trough the text between the tags and highlight all hits - $last_pos = 0; - foreach ($treffer[0] as $taginfo) { - $size = mb_strlen($taginfo[0]); - if ($taginfo[1] != 0) { - $data[] = self::do_highlight(mb_substr($text, $last_pos, $taginfo[1] - $last_pos), $highlight); - } - - $data[] = mb_substr($text, $taginfo[1], $size); - $last_pos = $taginfo[1] + $size; - } - - // don't miss the last portion of a posting - if ($last_pos < mb_strlen($text)) { - $data[] = self::do_highlight(mb_substr($text, $last_pos, mb_strlen($text) - $last_pos), $highlight); - } - - return implode('', $data); - } - - /** - * Returns a human-readable version of the passed global Stud.IP permission. - * - * @param string $perm - * @return string - */ - public static function translate_perm($perm) - { - return match ($perm) { - 'root' => _('Root'), - 'admin' => _('Administrator/-in'), - 'dozent' => _('Lehrende/-r'), - 'tutor' => _('Tutor/-in'), - 'autor' => _('Autor/-in'), - 'user' => _('Leser/-in'), - default => '', - }; - } - - /** - * return the currently chosen page - * - * @return int - */ - public static function getPage() - { - return self::$page; - } - - /** - * set the current page - * - * @param int $page_num the page - */ - public static function setPage($page_num) - { - self::$page = $page_num; - } - - /** - * Return an info-text explaining the visit-status of the passed topic_di - * which has the passed number of new entries. - * - * @param string $num_entries the number of new entries - * @param string $topic_id the id of the topic - * - * @return string a human readable, localized text - */ - public static function getVisitText($num_entries, $topic_id) - { - if ($num_entries > 0) { - $text = sprintf(_('Seit Ihrem letzten Besuch gibt es %s neue Beiträge'), $num_entries); - } else { - $all_entries = ForumEntry::countPostings($topic_id); - - if ($all_entries == 0) { - $text = sprintf(_('Es gibt bisher keine Beiträge.')); - } else if ($all_entries == 1) { - $text = sprintf(_('Seit Ihrem letzten Besuch gab es nichts Neues.' - . ' Es ist ein alter Beitrag vorhanden.')); - } else { - $text = sprintf(_('Seit Ihrem letzten Besuch gab es nichts Neues.' - . ' Es sind %s alte Beiträge vorhanden.'), $all_entries); - } - } - - return $text; - } - - /** - * return the online status of the passed user, one of three possible - * states is returned: - * - available - * - away - * - offline - * - * @staticvar type $online_status - * - * @param string $user_id - * - * @return string - */ - public static function getOnlineStatus($user_id) - { - static $online_status; - - // check if the corresponding user's profile is visible - if (get_visibility_by_id($user_id) == false) { - return 'offline'; - } - - if ($GLOBALS['user']->id == $user_id) { - return 'available'; - } - - if (!$online_status) { - $online_users = get_users_online(10); - foreach ($online_users as $username => $data) { - if ($data['last_action'] >= 300) { - $online_status[$data['user_id']] = 'away'; - } else { - $online_status[$data['user_id']] = 'available'; - } - } - } - - return $online_status[$user_id] ?? 'offline'; - } - - /** - * Create a pdf of all postings belonging to the passed seminar located - * under the passed topic_id. The PDF is dispatched automatically. - * - * BEWARE: This function never returns, it dies after the PDF has been - * (succesfully or not) dispatched. - * - * @param string $seminar_id - * @param string $parent_id - */ - public static function createPdf($seminar_id, $parent_id = null) - { - $seminar_name = get_object_name($seminar_id, 'sem'); - $data = ForumEntry::getList('dump', $parent_id ?: $seminar_id); - $first_page = true; - - $document = new ExportPDF(); - $document->SetTitle(_('Forum')); - $document->setHeaderTitle(sprintf(_("Forum \"%s\""), $seminar_name['name'])); - $document->addPage(); - - foreach ($data as $entry) { - if (Config::get()->FORUM_ANONYMOUS_POSTINGS && $entry['anonymous']) { - $author = _('anonym'); - } else { - $author = $entry['author']; - } - if ($entry['depth'] == 1) { - if (!$first_page) { - $document->addPage(); - } - $first_page = false; - $document->addContent('!! '. _('Bereich') . ": {$entry['name_raw']}\n"); - $document->addContent($entry['content_raw']); - $document->addContent("\n\n"); - } else if ($entry['depth'] == 2) { - $document->addContent('! '. _('Thema') . ": {$entry['name_raw']}\n"); - $document->addContent('%%' . sprintf(_('erstellt von %s am %s'), $author, - strftime('%A %d. %B %Y, %H:%M', (int)$entry['mkdate'])) . '%%' . "\n"); - $document->addContent($entry['content_raw']); - $document->addContent("\n--\n"); - } else if ($entry['depth'] == 3) { - $document->addContent("**{$entry['name_raw']}**\n"); - $document->addContent('%%' . sprintf(_('erstellt von %s am %s'), $author, - strftime('%A %d. %B %Y, %H:%M', (int)$entry['mkdate'])) . '%%' . "\n"); - $document->addContent($entry['content_raw']); - $document->addContent("\n--\n"); - } - } - - $document->dispatch($seminar_name['name'] ." - Forum"); - die; - } - - - /** - * Returns the id of the currently selected seminar or false, if no seminar - * is selected - * - * @return mixed seminar_id or false - */ - public static function getSeminarId() - { - return Context::getId(); - } - - /** - * replace in the passed text every %%% with <% and every ### with %> - * This is used to work around a limitation of the Button-API in combination - * with the underscore.js way of inserting template vars. - * - * The Button-API correctly replaces < > with tags, but underscore.js is - * unable to find them in their tag-represenation - * - * @param string $text the text to apply the replacements on - * - * @return string the modified text - */ - public static function replace($text) - { - return str_replace('%%%', '<%', str_replace('###', '%>', $text)); - } -} diff --git a/lib/classes/ForumIssue.php b/lib/classes/ForumIssue.php deleted file mode 100644 index 8be894f..0000000 --- a/lib/classes/ForumIssue.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 - * @category Stud.IP - */ - -class ForumIssue -{ - /** - * Get the id of the topic linked to the issue denoted by the passed id. - * - * @param string $issue_id - * @return string the id of the linked topic - */ - static function getThreadIdForIssue($issue_id) - { - $stmt = DBManager::get()->prepare("SELECT topic_id FROM forum_entries_issues - WHERE issue_id = ?"); - $stmt->execute([$issue_id]); - - return ($stmt->fetchColumn()); - } - - - /** - * Get the id of the issue linked to the topic denoted by the passed id. - * - * @param string $topic_id - * @return string the id of the linked topic - */ - static function getIssueIdForThread($topic_id) - { - $stmt = DBManager::get()->prepare("SELECT issue_id FROM forum_entries_issues - WHERE topic_id = ?"); - $stmt->execute([$topic_id]); - - return ($stmt->fetchColumn()); - } - - - /** - * Create/Update the linked posting for the passed issue_id - * - * @param string $seminar_id - * @param string $issue_id issue id to link to - * @param string $title (new) title of the posting - * @param string $content (new) content of the posting - */ - static function setThreadForIssue($seminar_id, $issue_id, $title, $content) - { - if ($topic_id = self::getThreadIdForIssue($issue_id)) { // update - ForumEntry::update($topic_id, $title ?: _('Ohne Titel'), $content); - - } else { // create - // make sure the forum is set up properly - ForumEntry::checkRootEntry($seminar_id); - - $topic_id = md5(uniqid(rand())); - - ForumEntry::insert([ - 'topic_id' => $topic_id, - 'seminar_id' => $seminar_id, - 'user_id' => $GLOBALS['user']->id, - 'name' => $title ?: _('Ohne Titel'), - 'content' => $content, - 'author' => get_fullname($GLOBALS['user']->id), - 'author_host' => ($GLOBALS['user']->id == 'nobody') ? getenv('REMOTE_ADDR') : '' - ], $seminar_id); - - $stmt = DBManager::get()->prepare("INSERT INTO forum_entries_issues - (issue_id, topic_id) VALUES (?, ?)"); - $stmt->execute([$issue_id, $topic_id]); - } - } - - /** - * Remove the link for the posting denoted by the passed topic_id - * - * @param object $notification - * @param string $topic_id - */ - static function unlinkIssue($notification, $topic_id) - { - $stmt = DBManager::get()->prepare("DELETE FROM forum_entries_issues - WHERE topic_id = ?"); - $stmt->execute([$topic_id]); - } -} diff --git a/lib/classes/ForumLike.php b/lib/classes/ForumLike.php deleted file mode 100644 index ad83274..0000000 --- a/lib/classes/ForumLike.php +++ /dev/null @@ -1,105 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 - * @category Stud.IP - */ - -class ForumLike { - - /** - * Set the posting denoted by the passed topic_id as liked for the - * currently logged in user - * - * @param string $topic_id - */ - public static function like($topic_id) - { - $stmt = DBManager::get()->prepare("REPLACE INTO - forum_likes (topic_id, user_id) - VALUES (?, ?)"); - $stmt->execute([$topic_id, $GLOBALS['user']->id]); - - // get posting owner - $data = ForumEntry::getConstraints($topic_id); - if (!$data) { - return; - } - - // notify owner of posting about the like - setTempLanguage($data['user_id']); - $notification = sprintf(_('%s gefällt einer deiner Forenbeiträge!'), $GLOBALS['user']->getFullName()); - restoreLanguage(); - - PersonalNotifications::add( - $data['user_id'], - URLHelper::getURL('dispatch.php/course/forum/index/index/' . $topic_id .'?highlight_topic='. $topic_id .'#'. $topic_id), - $notification, - $topic_id, - Icon::create('forum') - ); - } - - /** - * Revoke the liking of the posting denoted by the passed topic_id for the - * currently logged in user - * - * @param string $topic_id - */ - static function dislike($topic_id) { - $stmt = DBManager::get()->prepare("DELETE FROM forum_likes - WHERE topic_id = ? AND user_id = ?"); - $stmt->execute([$topic_id, $GLOBALS['user']->id]); - } - - /** - * Get the user_id for all likers of the topic denoted by the passed id - * - * @param string $topic_id - * @return array an array of user_id's - */ - static function getLikes($topic_id) { - $stmt = DBManager::get()->prepare("SELECT - auth_user_md5.user_id FROM forum_likes - LEFT JOIN auth_user_md5 USING (user_id) - LEFT JOIN user_info USING (user_id) - WHERE topic_id = ?"); - $stmt->execute([$topic_id]); - - return $stmt->fetchAll(PDO::FETCH_COLUMN); - } - - /** - * count the number of likes the user has received - system-wide - * - * @staticvar type $entries - * @param string $user_id the user's id to count the received likes for - * - * @return int the number of likes received - */ - static function receivedForUser($user_id) - { - static $entries; - - if (empty($entries[$user_id])) { - $stmt = DBManager::get()->prepare("SELECT COUNT(*) - FROM forum_entries - LEFT JOIN forum_likes USING (topic_id) - WHERE forum_entries.user_id = ? - AND forum_likes.topic_id IS NOT NULL - AND forum_likes.user_id != ?"); - $stmt->execute([$user_id, $user_id]); - - $entries[$user_id] = $stmt->fetchColumn(); - } - - return $entries[$user_id]; - } -} diff --git a/lib/classes/ForumPerm.php b/lib/classes/ForumPerm.php deleted file mode 100644 index 2832978..0000000 --- a/lib/classes/ForumPerm.php +++ /dev/null @@ -1,217 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 - * @category Stud.IP - */ - -class ForumPerm { - - /** - * Check, if the a user has the passed permission in a seminar. - * Possible permissions are: - * edit_category - Editing the name of a category
- * add_category - Adding a new category
- * remove_category - Removing an existing category
- * sort_category - Sorting categories
- * edit_area - Editing an area (title + content)
- * add_area - Adding a new area
- * remove_area - Removing an area and all belonging threads
- * sort_area - Sorting of areas in categories and between categories
- * search - Searching in postings
- * edit_entry - Editing of foreign threads/postings
- * add_entry - Creating a new thread/posting
- * remove_entry - Removing of foreign threads/postings
- * fav_entry - Marking a Posting as "favorite"
- * like_entry - Liking a posting
- * move_thread - Moving a thrad between ares
- * close_thread - Close or open a thread
- * make_sticky - Make a thread sticky
- * abo - Signing up for mail-notifications for new entries
- * forward_entry - Forwarding an existing entry as a message
- * pdfexport - Exporting parts of the forum as PDF
- * admin - Allowed to mass-administrate the forum
- * view - Allowed to view the forum at all
- * edit_closed - Editing entries in a closed thread - * - * @param string $perm one of the modular permissions - * @param string $seminar_id the seminar to check for - * @param string $user_id the user to check for - * @return boolean true, if the user has the perms, false otherwise - */ - public static function has($perm, $seminar_id, $user_id = null) - { - static $permissions = []; - - // if no user-id is passed, use the current user (for your convenience) - if (!$user_id) { - $user_id = $GLOBALS['user']->id; - } - - // get the status for the user in the passed seminar - if (empty($permissions[$seminar_id][$user_id])) { - $permissions[$seminar_id][$user_id] = $GLOBALS['perm']->get_studip_perm($seminar_id, $user_id); - } - - $status = $permissions[$seminar_id][$user_id]; - - // take care of the not logged in user - if ($user_id == 'nobody' || $status == false) { - // which status has nobody - read only or read/write? - if (get_object_type($seminar_id) == 'sem') { - $course = Course::find($seminar_id); - - if ($course->schreibzugriff == 0) { - $status = 'nobody_write'; - } else if ($course->lesezugriff == 0) { - $status = 'nobody_read'; - } else { - return false; - } - } else { - return false; - } - } - - // root and admins have all possible perms - if (in_array($status, words('root admin')) !== false) { - return true; - } - - // eCULT Notlösung - if ($status == 'tutor' && $seminar_id == '30e0b89dcc9173d5fccf9f22b13b87bd') { - $status = 'autor'; - } - - // check the status and the passed permission - if (($status == 'dozent' || $status == 'tutor') && in_array($perm, - words('edit_category add_category remove_category sort_category ' - . 'edit_area add_area remove_area sort_area ' - . 'search edit_entry add_entry remove_entry fav_entry like_entry move_thread ' - . 'make_sticky close_thread abo forward_entry pdfexport view edit_closed') - ) !== false) { - return true; - } else if ($status == 'autor' && in_array($perm, words('search add_entry fav_entry like_entry forward_entry abo pdfexport view')) !== false) { - return true; - } else if ($status == 'user' && in_array($perm, words('search forward_entry pdfexport view')) !== false) { - return true; - } else if ($status == 'nobody_write' && in_array($perm, words('search add_entry pdfexport view')) !== false) { - return true; - } else if ($status == 'nobody_read' && in_array($perm, words('search pdfexport view')) !== false) { - return true; - } - - // user has no permission - return false; - } - - /** - * If the user has not the passed perm in a seminar, an AccessDeniedException - * is thrown. - * An optional topic_id can be passed which is checked against the passed - * seminar if the topic_id belongs to that seminar - * - * @param string $perm for the list of possible perms and their function see @ForumPerm::hasPerm() - * @param string $seminar_id the seminar to check for - * @param string $topic_id if passed, this topic_id is checked if it belongs to the passed seminar - * - * @throws AccessDeniedException - */ - public static function check($perm, $seminar_id, $topic_id = null) - { - if (!self::has($perm, $seminar_id)) { - throw new AccessDeniedException(sprintf( - _("Sie haben keine Berechtigung für diese Aktion! Benötigte Berechtigung: %s"), - $perm) - ); - } - - // check the topic id (if any) - if ($topic_id) { - self::checkTopicId($seminar_id, $topic_id); - } - } - - /** - * Check if the current user is allowed to edit the topic - * denoted by the passed id - * - * @staticvar array $perms - * - * @param string $topic_id the id for the topic to check for - * - * @return bool true if the user has the necessary perms, false otherwise - */ - public static function hasEditPerms($topic_id) - { - static $perms = []; - - if (empty($perms[$topic_id])) { - // find out if the posting is the last in the thread - $constraints = ForumEntry::getConstraints($topic_id); - if (!$constraints) { - return false; - } - $stmt = DBManager::get()->prepare("SELECT user_id, seminar_id - FROM forum_entries WHERE topic_id = ?"); - $stmt->execute([$topic_id]); - - $data = $stmt->fetch(); - - $closed = ForumEntry::isClosed($topic_id); - - $perms[$topic_id] = (($GLOBALS['user']->id == $data['user_id'] && $GLOBALS['user']->id != 'nobody') || - ForumPerm::has('edit_entry', $constraints['seminar_id'])) - && (!$closed || $closed && ForumPerm::has('edit_closed', $constraints['seminar_id'])); - } - - return $perms[$topic_id]; - } - - /** - * check if the passed category_id belongs to the passed seminar_id. - * Throws an AccessDenied denied exception if this is not the case - * - * @param string $seminar_id id of the seminar, the category should belong to - * @param string $category_id the id of the category to check - */ - public static function checkCategoryId($seminar_id, $category_id) - { - $data = ForumCat::get($category_id); - - if ($data['seminar_id'] != $seminar_id) { - throw new AccessDeniedException(sprintf( - _('Forum: Sie haben keine Berechtigung auf die Kategorie mit der ID %s zuzugreifen!'), - $category_id - )); - } - } - - /** - * check if the passed topic_id belongs to the passed seminar_id. - * Throws an AccessDenied denied exception if this is not the case - * - * @param string $seminar_id id of the seminar, the category should belong to - * @param string $topic_id the id of the topic to check - */ - public static function checkTopicId($seminar_id, $topic_id) - { - $data = ForumEntry::getConstraints($topic_id); - - if (!$data || $data['seminar_id'] !== $seminar_id) { - throw new AccessDeniedException(sprintf( - _('Forum: Sie haben keine Berechtigung auf den Eintrag mit der ID %s zuzugreifen!'), - $topic_id - )); - } - } -} diff --git a/lib/classes/ForumVisit.php b/lib/classes/ForumVisit.php deleted file mode 100644 index 08535b2..0000000 --- a/lib/classes/ForumVisit.php +++ /dev/null @@ -1,171 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 - * @category Stud.IP - */ - -class ForumVisit { - - /** - * This is the maximum number of seconds that unread entries are - * marked as new. - */ - const LAST_VISIT_MAX = 7776000; // 90 days - - /** - * return number of new entries since last visit up to 3 month ago - * - * @param string $parent_id the seminar_id for the entries - * @param string $visitdate count all entries newer than this timestamp - * - * @return int the number of entries - */ - static function getCount($parent_id, $visitdate) - { - if ($visitdate < time() - ForumVisit::LAST_VISIT_MAX) { - $visitdate = time() - ForumVisit::LAST_VISIT_MAX; - } - - $constraints = ForumEntry::getConstraints($parent_id); - - if (!$constraints) { - return 0; - } - - $query = "SELECT COUNT(*) - FROM forum_entries - WHERE lft >= :lft - AND rgt <= :rgt - AND user_id != :user_id - AND (user_id != '' OR author != '') - AND seminar_id = :seminar_id - AND topic_id != seminar_id - AND chdate > :lastvisit"; - $stmt = DBManager::get()->prepare($query); - - $stmt->bindValue(':user_id', $GLOBALS['user']->id); - $stmt->bindValue(':lft', $constraints['lft']); - $stmt->bindValue(':rgt', $constraints['rgt']); - $stmt->bindValue(':seminar_id', $constraints['seminar_id']); - $stmt->bindValue(':lastvisit', $visitdate); - - $stmt->execute(); - - return $stmt->fetchColumn(); - } - - /** - * Set the seminar denoted by the passed id as visited by the currently - * logged in user - * - * @param string $seminar_id - */ - static function setVisit($seminar_id) { - $type = get_object_type($seminar_id, words('fak inst sem')); - if ($type === 'fak') { - $type = 'inst'; - } - if (self::getVisit($seminar_id) < object_get_visit($seminar_id, $type, false, false)) { - self::setVisitdates($seminar_id); - } - } - - /** - * Stores the visitdate in last_visitdate and sets the current time for as new visitdate - * - * @param string $seminar_id the seminar that has been entered - */ - static function setVisitdates($seminar_id) { - $stmt = DBManager::get()->prepare('SELECT visitdate FROM forum_visits - WHERE user_id = ? AND seminar_id = ?'); - $stmt->execute([$GLOBALS['user']->id, $seminar_id]); - $visitdate = $stmt->fetchColumn(); - - $stmt = DBManager::get()->prepare("REPLACE INTO forum_visits - (user_id, seminar_id, visitdate, last_visitdate) - VALUES (?, ?, UNIX_TIMESTAMP(), ?)"); - $stmt->execute([$GLOBALS['user']->id, $seminar_id, $visitdate]); - - } - - - /** - * returns visitdate and last_visitdate for the passed seminar and the - * currently logged in user - * - * @staticvar array $visit - * - * @param string $seminar_id the seminar to fetch the visitdates for - * @return mixed an array containing visitdate and last_visitdate - */ - private static function getVisitDates($seminar_id) - { - static $visit = []; - - // no costly checking for root or nobody necessary - if ($GLOBALS['perm']->have_perm('root') || $GLOBALS['user']->id == 'nobody') { - $tstamp = mktime(23, 59, 00, date('m'), 31, date('y')); - return ['visit' => $tstamp, 'last_visitdate' => $tstamp]; - } - - if (!isset($visit[$seminar_id])) { - $visit[$seminar_id] = []; - } - if (!isset($visit[$seminar_id][$GLOBALS['user']->id])) { - $stmt = DBManager::get()->prepare("SELECT visitdate, last_visitdate FROM forum_visits - WHERE seminar_id = ? AND user_id = ?"); - $stmt->execute([$seminar_id, $GLOBALS['user']->id]); - $visit[$seminar_id][$GLOBALS['user']->id] = $stmt->fetch(PDO::FETCH_ASSOC); - - // no entry for this seminar yet present - if (!$visit[$seminar_id][$GLOBALS['user']->id]) { - // set visitdate to current time - $visit[$seminar_id][$GLOBALS['user']->id] = [ - 'visit' => time() - ForumVisit::LAST_VISIT_MAX, - 'last_visitdate' => time() - ForumVisit::LAST_VISIT_MAX - ]; - } - - // prevent visit-dates from being older than LAST_VISIT_MAX allows - foreach ($visit[$seminar_id][$GLOBALS['user']->id] as $type => $date) { - if ($date < time() - ForumVisit::LAST_VISIT_MAX) { - $visit[$seminar_id][$GLOBALS['user']->id][$type] = time() - ForumVisit::LAST_VISIT_MAX; - } - } - } - - return $visit[$seminar_id][$GLOBALS['user']->id]; - } - - /** - * return the last_visitdate for the passed seminar and currently logged in user - * - * @param string $seminar_id the seminar to get the last_visitdate for - * @return int a timestamp - */ - static function getLastVisit($seminar_id) - { - $visit = self::getVisitDates($seminar_id); - return $visit['last_visitdate']; - } - - /** - * return the visitdate for the passed seminar and currently logged in user - * - * @param string $seminar_id the seminar to get the visitdate for - * @return int a timestamp - */ - static function getVisit($seminar_id) - { - $visit = self::getVisitDates($seminar_id); - return $visit['visitdate'] ?? 0; - } -} diff --git a/lib/classes/JsonApi/Models/ForumCat.php b/lib/classes/JsonApi/Models/ForumCat.php deleted file mode 100644 index 25a0207..0000000 --- a/lib/classes/JsonApi/Models/ForumCat.php +++ /dev/null @@ -1,46 +0,0 @@ - 'Course', - 'foreign_key' => 'course_id', - ); - parent::configure($config); - } - - public static function getCategories(\Course $course) - { - return self::findBySQL('seminar_id = ? ORDER BY pos ASC', [$course->id]); - } - - public function deleteCategory($categoryId, $seminarId) - { - //delete category... - $stmt = \DBManager::get()->prepare('DELETE FROM - forum_categories - WHERE category_id = ?'); - $stmt->execute(array($categoryId)); - - //... and set all it's entries to default category - $stmt2 = \DBManager::get()->prepare('UPDATE - forum_categories_entries - SET category_id = ?, pos = 999 - WHERE category_id = ?'); - $stmt2->execute(array($seminarId, $categoryId)); - - return $stmt && $stmt2; - } -} diff --git a/lib/classes/JsonApi/Models/ForumEntry.php b/lib/classes/JsonApi/Models/ForumEntry.php deleted file mode 100644 index e52cc3e..0000000 --- a/lib/classes/JsonApi/Models/ForumEntry.php +++ /dev/null @@ -1,146 +0,0 @@ -= ? AND forum_entries.seminar_id = ? AND forum_entries.depth = 1 ORDER BY forum_entries.lft ASC', - [(int) $targetEntry['lft'], (int) $targetEntry['rgt'], $targetEntry['seminar_id']] - ); - - if (count($parentEntries) > 0) { - $category = ForumCat::findOneBySQL( - 'LEFT JOIN forum_categories_entries AS fce USING (category_id) - WHERE fce.topic_id = ?', - [$parentEntries[0]->id] - ); - } - - if (empty($category)) { - $category = ForumCat::findOneBySql( - "seminar_id = ? AND category_id = seminar_id", - [$targetEntry->seminar_id] - ); - } - - return $category; - } - - public static function getCategories($seminarId, $asObjects = false) - { - $stmt = \DBManager::get()->prepare('SELECT * FROM forum_categories - WHERE seminar_id = ? ORDER BY pos ASC'); - $stmt->execute([$seminarId]); - $ret = $stmt->fetchGrouped(\PDO::FETCH_ASSOC); - - return $asObjects ? ForumCat::getCatObjects($ret) : $ret; - } - - public static function getEntriesFromCat(ForumCat $targetCategory) - { - return ForumEntry::findBySQL( - 'LEFT JOIN forum_categories_entries ' - .'ON forum_categories_entries.topic_id = forum_entries.topic_id ' - .'WHERE forum_categories_entries.category_id = ? ' - .'ORDER BY forum_entries.lft DESC', - [$targetCategory->id] - ); - } - - public static function getChildEntries($topicId) - { - $targetEntry = ForumEntry::find($topicId); - - return ForumEntry::findBySQL( - 'lft > ? AND rgt < ? AND seminar_id = ? ', - [$targetEntry->lft, $targetEntry->rgt, $targetEntry->seminar_id] - ); - } - - public function storeWith(\SimpleORMap $parent, ForumEntry $entry) - { - if ($this->is_new) { - if ($parent instanceof \JsonApi\Models\ForumCat) { - $stmt = \DBManager::get()->prepare('INSERT INTO forum_categories_entries - (category_id, topic_id) - VALUES (?, ?)'); - $stmt->execute([$parent->id, $this->id]); - $constraint = $entry; - } elseif ($parent instanceof \JsonApi\Models\ForumEntry) { - $constraint = ForumEntry::find($parent->id); - } - - if (is_null($constraint)) { - throw new \InvalidArgumentException('There must be a parent'); - } - - \DBManager::get()->exec('UPDATE forum_entries SET lft = lft + 2 - WHERE lft > '.$constraint['rgt']." AND seminar_id = '".$constraint['seminar_id']."'"); - \DBManager::get()->exec('UPDATE forum_entries SET rgt = rgt + 2 - WHERE rgt >= '.$constraint['rgt']." AND seminar_id = '".$constraint['seminar_id']."'"); - - $this->lft = $constraint['rgt']; - $this->rgt = $constraint['rgt'] + 1; - $this->depth = $constraint['depth'] + 1; - - \NotificationCenter::postNotification('ForumAfterInsert', $this->topic_id, $this->toArray()); - - // update "latest_chdate" for easier sorting of actual threads - \DBManager::get()->exec("UPDATE forum_entries SET latest_chdate = UNIX_TIMESTAMP() - WHERE topic_id = '".$constraint['topic_id']."'"); - } - - return $this->store(); - } - - public function deleteEntry($topicId) - { - $targetEntry = ForumEntry::find($topicId); - $childEntries = ForumEntry::getChildEntries($topicId); - - //delete an entry and his appended entries... - $stmt_one = \DBManager::get()->prepare('DELETE FROM forum_entries - WHERE seminar_id = ? AND lft >= ? AND rgt <= ?'); - $stmt_one->execute([$targetEntry->seminar_id, $targetEntry->lft, $targetEntry->rgt]); - - //...and update affected entries - $diff = $targetEntry->lft - $targetEntry->rgt; - $stmt_two = \DBManager::get()->prepare("UPDATE forum_entries SET lft = lft - ${diff} - WHERE lft > ? AND seminar_id = ?"); - $stmt_two->execute([$targetEntry->rgt, $targetEntry->seminar_id]); - - $stmt_three = \DBManager::get()->prepare("UPDATE forum_entries SET rgt = rgt - ${diff} - WHERE rgt > ? AND seminar_id = ?"); - $stmt_three->execute([$targetEntry->rgt, $targetEntry->seminar_id]); - - //... delete categories_entries-row if exists - - $stmt_four = \DBManager::get()->prepare('DELETE FROM forum_categories_entries - WHERE topic_id = ?'); - $stmt_four->execute([$topicId]); - - return $stmt_one && $stmt_two && $stmt_three && $stmt_four; - } - - protected static function configure($config = []) - { - $config['db_table'] = 'forum_entries'; - - $config['belongs_to']['category'] = [ - 'class_name' => '\JsonApi\Models\ForumEntry', - 'assoc_func' => 'getCatFromEntry', - ]; - parent::configure($config); - } -} diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 86c6d92..4df0677 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -671,24 +671,56 @@ class RouteMap private function addAuthenticatedForumRoutes(RouteCollectorProxy $group): void { - $group->get('/courses/{id}/forum-categories', Routes\Forum\ForumCategoriesIndex::class); - - $group->get('/forum-entries/{id}', Routes\Forum\ForumEntriesShow::class); - $group->get('/forum-entries/{id}/entries', Routes\Forum\ForumEntryEntriesIndex::class); - - $group->get('/forum-categories/{id}', Routes\Forum\ForumCategoriesShow::class); - - $group->get('/forum-categories/{id}/entries', Routes\Forum\ForumCategoryEntriesIndex::class); - - $group->post('/forum-entries/{id}/entries', Routes\Forum\ForumEntryEntriesCreate::class); - $group->post('/forum-categories/{id}/entries', Routes\Forum\ForumCategoryEntriesCreate::class); - $group->post('/courses/{id}/forum-categories', Routes\Forum\ForumCategoriesCreate::class); - - $group->patch('/forum-categories/{id}', Routes\Forum\ForumCategoriesUpdate::class); - $group->patch('/forum-entries/{id}', Routes\Forum\ForumEntriesUpdate::class); - - $group->delete('/forum-categories/{id}', Routes\Forum\ForumCategoriesDelete::class); - $group->delete('/forum-entries/{id}', Routes\Forum\ForumEntriesDelete::class); + $group->group('/courses/{course_id}', function ($forum) { + $forum->get('/forum-configs', Routes\Forum\ForumConfigIndex::class); + $forum->get('/forum-categories', Routes\Forum\ForumCategoryIndex::class); + $forum->get('/forum-discussions', Routes\Forum\ForumDiscussionIndex::class); + $forum->get('/forum-topics', Routes\Forum\ForumTopicIndex::class); + $forum->get('/forum-subscriptions', Routes\Forum\ForumSubscriptionIndex::class); + }); + + $group->group('/forum-subscriptions', function ($forum) { + $forum->post('', Routes\Forum\ForumSubscriptionStore::class); + $forum->get('/{subscription_id}', Routes\Forum\ForumSubscriptionShow::class); + $forum->delete('/{subscription_id}', Routes\Forum\ForumSubscriptionDelete::class); + }); + + $group->group('/forum-topics', function ($forum) { + $forum->get('/{topic_id}', Routes\Forum\ForumTopicShow::class); + $forum->get('/{topic_id}/discussions', Routes\Forum\ForumTopicDiscussions::class); + $forum->patch('/sort', Routes\Forum\ForumTopicUpdateSort::class); + }); + + $group->group('/forum-categories', function ($forum) { + $forum->get('/{category_id}', Routes\Forum\ForumCategoryShow::class); + $forum->get('/{category_id}/topics', Routes\Forum\ForumCategoryTopics::class); + $forum->patch('/sort', Routes\Forum\ForumCategoryUpdateSort::class); + }); + + $group->group('/forum-discussion-types', function ($forum) { + $forum->get('', Routes\Forum\ForumDiscussionTypeIndex::class); + $forum->get('/{type_id}', Routes\Forum\ForumDiscussionTypeShow::class); + $forum->get('/{type_id}/discussions', Routes\Forum\ForumDiscussionTypeDiscussions::class); + }); + + $group->group('/forum-discussions', function ($forum) { + $forum->get('/{discussion_id}', Routes\Forum\ForumDiscussionShow::class); + $forum->get('/{discussion_id}/postings', Routes\Forum\ForumDiscussionPostings::class); + }); + + $group->group('/forum-postings', function ($forum) { + $forum->post('', Routes\Forum\ForumPostingStore::class); + $forum->get('/{posting_id}', Routes\Forum\ForumPostingShow::class); + $forum->get('/{posting_id}/reactions', Routes\Forum\ForumPostingReactions::class); + $forum->patch('/{posting_id}', Routes\Forum\ForumPostingUpdate::class); + $forum->delete('/{posting_id}', Routes\Forum\ForumPostingDelete::class); + }); + + $group->group('/forum-posting-reactions', function ($forum) { + $forum->post('', Routes\Forum\ForumPostingReactionStore::class); + $forum->get('/{reaction_id}', Routes\Forum\ForumPostingReactionShow::class); + $forum->delete('/{reaction_id}', Routes\Forum\ForumPostingReactionDelete::class); + }); } private function addAuthenticatedStockImagesRoutes(RouteCollectorProxy $group): void diff --git a/lib/classes/JsonApi/Routes/Forum/AbstractEntriesCreate.php b/lib/classes/JsonApi/Routes/Forum/AbstractEntriesCreate.php deleted file mode 100644 index 734b9d8..0000000 --- a/lib/classes/JsonApi/Routes/Forum/AbstractEntriesCreate.php +++ /dev/null @@ -1,65 +0,0 @@ -getParentObject($parentId); - - return $this->createEntry($title, $content, $parent, $user); - } - - protected function getParentObject($parentId) - { - if ($parent = ForumCat::find($parentId)) { - return $parent; - } - - return ForumEntry::find($parentId); - } - - protected function createEntry($title, $content, $parent, $user) - { - //Do we create id's like this? - $topicId = md5(uniqid(rand())); - if (empty($title)) { - $title = $parent->name; - } - $data = [ - 'topic_id' => $topicId, - 'seminar_id' => $parent->seminar_id, - 'user_id' => $user->id, - 'name' => $title, - 'content' => $content, - 'author' => $user->getFullName(), - 'anonymous' => 0, - ]; - - $entry = new ForumEntry(); - $entry->setData($data); - $entry->storeWith($parent, $entry); - - return $entry; - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumAuthority.php b/lib/classes/JsonApi/Routes/Forum/ForumAuthority.php deleted file mode 100644 index acc5d2a..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumAuthority.php +++ /dev/null @@ -1,25 +0,0 @@ -id, $user->id)) { - return false; - } - - if ($topic) { - try { - \ForumPerm::checkTopicId($course->id, $topic->id); - } catch (\AccessDeniedException $e) { - return false; - } - } - - return true; - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesCreate.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoriesCreate.php deleted file mode 100644 index 280e437..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesCreate.php +++ /dev/null @@ -1,61 +0,0 @@ -validate($request); - - if (!$course = \Course::find($args['id'])) { - throw new RecordNotFoundException(); - } - - if (!ForumAuthority::has($this->getUser($request), 'view', $course)) { - throw new AuthorizationFailedException(); - } - if (!$category = $this->createCategoryFromJSON($course->id, $json)) { - throw new InternalServerError('Could not create the category.'); - } - - return $this->getCreatedResponse($category); - } - - protected function createCategoryFromJSON($courseId, $json) - { - $title = self::arrayGet($json, 'data.attributes.title'); - - $category = new ForumCat(); - $category->seminar_id = $courseId; - $category->entry_name = $title; - $category->store(); - - return $category; - } - - protected function validateResourceDocument($json, $data) - { - $title = self::arrayGet($json, 'data.attributes.title', ''); - if (empty($title)) { - return 'Categorys must have a title. '; - } - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesDelete.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoriesDelete.php deleted file mode 100644 index 2441a1f..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesDelete.php +++ /dev/null @@ -1,42 +0,0 @@ -seminar_id)) { - throw new RecordNotFoundException('Course does not exist.'); - } - if (!ForumAuthority::has($this->getUser($request), 'view', $course)) { - throw new AuthorizationFailedException(); - } - if (!$this->deleteCategory($category)) { - throw new RecordNotFoundException(); - } - - return $this->getCodeResponse(204); - } - - protected static function deleteCategory($category) - { - return $category->deleteCategory($category->category_id, $category->seminar_id); - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesIndex.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoriesIndex.php deleted file mode 100644 index ea5d17d..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesIndex.php +++ /dev/null @@ -1,44 +0,0 @@ -getUser($request), 'view', $course)) { - throw new AuthorizationFailedException(); - } - - if (!$categories = \JsonApi\Models\ForumCat::getCategories($course)) { - throw new RecordNotFoundException(); - } - - list($offset, $limit) = $this->getOffsetAndLimit(); - - return $this->getPaginatedContentResponse( - array_slice($categories, $offset, $limit), - count($categories) - ); - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesShow.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoriesShow.php deleted file mode 100644 index 190efb2..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesShow.php +++ /dev/null @@ -1,33 +0,0 @@ -seminar_id)) { - throw new RecordNotFoundException('could not find course'); - } - - if (!ForumAuthority::has($this->getUser($request), 'view', $course)) { - throw new AuthorizationFailedException(); - } - - return $this->getContentResponse($category); - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesUpdate.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoriesUpdate.php deleted file mode 100644 index e09339e..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumCategoriesUpdate.php +++ /dev/null @@ -1,63 +0,0 @@ -validate($request); - - if (!$category = ForumCat::find($args['id'])) { - throw new RecordNotFoundException('Category has not been found.'); - } - if (!$course = \Course::find($category->seminar_id)) { - throw new RecordNotFoundException('Course does not exist.'); - } - - if (!ForumAuthority::has($this->getUser($request), 'view', $course)) { - throw new AuthorizationFailedException(); - } - if (!$category = $this->updateCategoryFromJSON($category, $json)) { - throw new InternalServerError('Could not update the category.'); - } - - return $this->getContentResponse($category); - } - - protected function updateCategoryFromJSON($category, $json) - { - $title = self::arrayGet($json, 'data.attributes.title'); - $category->entry_name = $title; - if ($category->isDirty()) { - $category->store(); - - return $category; - } - } - - protected function validateResourceDocument($json, $data) - { - $title = self::arrayGet($json, 'data.attributes.title', ''); - if (empty($title)) { - return 'Categories must have a title. '; - } - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoryEntriesCreate.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoryEntriesCreate.php deleted file mode 100644 index 7199e39..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumCategoryEntriesCreate.php +++ /dev/null @@ -1,43 +0,0 @@ -validate($request); - $categoryId = $args['id']; - - if (!ForumCat::exists($categoryId)) { - throw new RecordNotFoundException('Could not find category.'); - } - - $courseId = ForumCat::find($categoryId)->seminar_id; - $course = \Course::find($courseId); - - if (!$course) { - throw new RecordNotFoundException('Could not find course.'); - } - - if (!ForumAuthority::has($this->getUser($request), 'view', $course)) { - throw new AuthorizationFailedException(); - } - - if (!$entry = $this->createEntryFromJSON($this->getUser($request), $categoryId, $json)) { - throw new InternalServerError('Could not create forum entry.'); - } - - return $this->getCreatedResponse($entry); - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoryEntriesIndex.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoryEntriesIndex.php deleted file mode 100644 index 1ddc4c0..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumCategoryEntriesIndex.php +++ /dev/null @@ -1,44 +0,0 @@ -seminar_id)) { - throw new RecordNotFoundException('Could not find course.'); - } - - if (!ForumAuthority::has($this->getUser($request), 'view', $course)) { - throw new AuthorizationFailedException(); - } - - $entries = ForumEntry::getEntriesFromCat($category); - - list($offset, $limit) = $this->getOffsetAndLimit(); - - return $this->getPaginatedContentResponse( - array_slice($entries, $offset, $limit), - count($entries) - ); - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoryIndex.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoryIndex.php new file mode 100644 index 0000000..9e8375c --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumCategoryIndex.php @@ -0,0 +1,37 @@ +getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + $categories = ForumCategory::findBySQL("range_id = ? ORDER BY position ASC, mkdate DESC", [$course->id]); + + return $this->getPaginatedContentResponse( + array_slice($categories, ...$this->getOffsetAndLimit()), + count($categories) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoryShow.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoryShow.php new file mode 100644 index 0000000..46bba9d --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumCategoryShow.php @@ -0,0 +1,36 @@ +range_id)) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($category); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoryTopics.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoryTopics.php new file mode 100644 index 0000000..f597c52 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumCategoryTopics.php @@ -0,0 +1,46 @@ +range_id)) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + $topics = $category->topics ?? \SimpleORMapCollection::createFromArray([]); + + return $this->getPaginatedContentResponse( + $topics->limit(...$this->getOffsetAndLimit()), + count($topics) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumCategoryUpdateSort.php b/lib/classes/JsonApi/Routes/Forum/ForumCategoryUpdateSort.php new file mode 100644 index 0000000..3e2c597 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumCategoryUpdateSort.php @@ -0,0 +1,62 @@ +validate($request); + $course_id = self::arrayGet($json, 'data.relationships.range.data.id'); + + if (!$course = \Course::find($course_id)) { + throw new RecordNotFoundException(); + } + + if (!\CoreForum::isModerator($course->id)) { + throw new AuthorizationFailedException(); + } + + $category_ids = self::arrayGet($json, 'data.attributes.category-ids'); + + ForumCategory::findEachBySQL( + function (ForumCategory $category) use ($category_ids) { + $category->position = (int) array_search($category->category_id, $category_ids); + $category->store(); + }, + "category_id IN (:category_ids) AND range_id = :course_id", + [ + "category_ids" => $category_ids, + "course_id" => $course->id + ] + ); + + return $this->getCodeResponse(204); + } + + protected function validateResourceDocument($json, $data) + { + $required_keys = [ + 'data.attributes.category-ids' => 'Missing `data.attributes.category-ids`', + 'data.relationships.range.data.id' => 'Missing `data.relationships.range.data.id`', + ]; + + foreach ($required_keys as $key => $error_message) { + if (!self::arrayHas($json, $key)) { + return $error_message; + } + } + + return null; + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumConfigIndex.php b/lib/classes/JsonApi/Routes/Forum/ForumConfigIndex.php new file mode 100644 index 0000000..d3be0ce --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumConfigIndex.php @@ -0,0 +1,32 @@ +getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + return $this->getMetaResponse([ + 'is-admin' => \CoreForum::isAdmin($course->id), + 'is-moderator' => \CoreForum::isModerator($course->id), + 'anonymous-post' => (bool) \Config::get()->FORUM_ANONYMOUS_POSTINGS, + 'tile-layout' => (bool) \UserConfig::get($user->user_id)->FORUM_TILE_LAYOUT + ]); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumDiscussionIndex.php b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionIndex.php new file mode 100644 index 0000000..c8e8477 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionIndex.php @@ -0,0 +1,60 @@ +getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + $filtering = $this->getQueryParameters()->getFilteringParameters() ?: []; + $last_visit = $filtering['last-visit'] ?? 0; + + if ($last_visit) { + $recent_posts = ForumPosting::getRecentPosts($course->id, $last_visit); + $discussions = ForumDiscussion::findBySQL( + "discussion_id IN (:discussion_ids)", + [ + 'discussion_ids' => array_column($recent_posts, 'discussion_id') + ] + ); + } else { + $discussions = ForumDiscussion::findBySQL( + "JOIN forum_topics USING(topic_id) WHERE forum_topics.range_id = :course_id ORDER BY position ASC, mkdate DESC", + ['course_id' => $course->id] + ); + } + + return $this->getPaginatedContentResponse( + array_slice($discussions, ...$this->getOffsetAndLimit()), + count($discussions) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumDiscussionPostings.php b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionPostings.php new file mode 100644 index 0000000..320344d --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionPostings.php @@ -0,0 +1,51 @@ +range_id)) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + $postings = ForumPosting::findBySQL("discussion_id = :discussion_id ORDER BY mkdate ASC", ['discussion_id' => $discussion->discussion_id]); + + ForumPostingRead::updateUserReadPoint($user->user_id, $discussion->discussion_id, count($postings)); + + return $this->getPaginatedContentResponse( + array_slice($postings, ...$this->getOffsetAndLimit()), + count($postings) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumDiscussionShow.php b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionShow.php new file mode 100644 index 0000000..784a181 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionShow.php @@ -0,0 +1,39 @@ +range_id)) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($discussion); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeDiscussions.php b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeDiscussions.php new file mode 100644 index 0000000..162f23a --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeDiscussions.php @@ -0,0 +1,31 @@ +discussions ?? \SimpleORMapCollection::createFromArray([]); + + return $this->getPaginatedContentResponse( + $discussions->limit(...$this->getOffsetAndLimit()), + count($discussions) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeIndex.php b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeIndex.php new file mode 100644 index 0000000..b6a9999 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeIndex.php @@ -0,0 +1,25 @@ +getPaginatedContentResponse( + array_slice($discussion_types, ...$this->getOffsetAndLimit()), + count($discussion_types) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeShow.php b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeShow.php new file mode 100644 index 0000000..66ff9e5 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumDiscussionTypeShow.php @@ -0,0 +1,25 @@ +getContentResponse($discussion_type); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumEntriesDelete.php b/lib/classes/JsonApi/Routes/Forum/ForumEntriesDelete.php deleted file mode 100644 index 41de02d..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumEntriesDelete.php +++ /dev/null @@ -1,37 +0,0 @@ -seminar_id)) { - throw new RecordNotFoundException('could not find course'); - } - if (!ForumAuthority::has($this->getUser($request), 'view', $course)) { - throw new AuthorizationFailedException(); - } - if (!$entry->deleteEntry($entry->topic_id)) { - throw new RecordNotFoundException(); - } - - return $this->getCodeResponse(204); - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumEntriesShow.php b/lib/classes/JsonApi/Routes/Forum/ForumEntriesShow.php deleted file mode 100644 index 12d1034..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumEntriesShow.php +++ /dev/null @@ -1,33 +0,0 @@ -seminar_id)) { - throw new RecordNotFoundException('Could not find course.'); - } - if (!ForumAuthority::has($this->getUser($request), 'view', $course, $entry)) { - throw new AuthorizationFailedException(); - } - - return $this->getContentResponse($entry); - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumEntriesUpdate.php b/lib/classes/JsonApi/Routes/Forum/ForumEntriesUpdate.php deleted file mode 100644 index 21eeba4..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumEntriesUpdate.php +++ /dev/null @@ -1,72 +0,0 @@ -validate($request); - - if (!$entry = ForumEntry::find($args['id'])) { - throw new RecordNotFoundException('Entry has not been found.'); - } - if (!$course = \Course::find($entry->seminar_id)) { - throw new RecordNotFoundException('Course does not exist.'); - } - - if (!ForumAuthority::has($this->getUser($request), 'view', $course)) { - throw new AuthorizationFailedException(); - } - if (!$entry = $this->updateEntryFromJSON($entry, $json)) { - throw new InternalServerError('Could not update the entry.'); - } - - return $this->getContentResponse($entry); - } - - protected function updateEntryFromJSON($entry, $json) - { - $title = self::arrayGet($json, 'data.attributes.title'); - $content = self::arrayGet($json, 'data.attributes.content'); - if (!empty($title)) { - $entry->name = $title; - } - if (!empty($content)) { - $content = \Studip\Markup::purifyHtml($content); - $entry->content = $content; - } - if ($entry->isDirty()) { - $entry->store(); - } - - return $entry; - } - - protected function validateResourceDocument($json, $data) - { - $title = self::arrayGet($json, 'data.attributes.title'); - $content = self::arrayGet($json, 'data.attributes.content'); - - if (empty($title) && empty($content)) { - return 'You must change entry-data to update.'; - } - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumEntryEntriesCreate.php b/lib/classes/JsonApi/Routes/Forum/ForumEntryEntriesCreate.php deleted file mode 100644 index d3562b5..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumEntryEntriesCreate.php +++ /dev/null @@ -1,38 +0,0 @@ -validate($request); - $user = $this->getUser($request); - - if (!$related = ForumEntry::find($args['id'])) { - throw new RecordNotFoundException(); - } - if (!$course = \Course::find($related->seminar_id)) { - throw new RecordNotFoundException('Could not find course.'); - } - - if (!ForumAuthority::has($this->getUser($request), 'view', $course)) { - throw new AuthorizationFailedException(); - } - if (!$entry = $this->createEntryFromJSON($user, $related->id, $json)) { - throw new InternalServerError('could not create forum-entry'); - } - - return $this->getCreatedResponse($entry); - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumEntryEntriesIndex.php b/lib/classes/JsonApi/Routes/Forum/ForumEntryEntriesIndex.php deleted file mode 100644 index 5b03e99..0000000 --- a/lib/classes/JsonApi/Routes/Forum/ForumEntryEntriesIndex.php +++ /dev/null @@ -1,45 +0,0 @@ -seminar_id)) { - throw new RecordNotFoundException('could not find course'); - } - - if (!ForumAuthority::has($this->getUser($request), 'view', $course, $entry)) { - throw new AuthorizationFailedException(); - } - - if (!$entries = ForumEntry::getChildEntries($entry->id)) { - throw new RecordNotFoundException('could not find forum-entries'); - } - - list($offset, $limit) = $this->getOffsetAndLimit(); - $total = count($entries); - $data = array_slice($entries, $offset, $limit); - - return $this->getPaginatedContentResponse($data, $total); - } -} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumPostingDelete.php b/lib/classes/JsonApi/Routes/Forum/ForumPostingDelete.php new file mode 100644 index 0000000..9929b14 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumPostingDelete.php @@ -0,0 +1,38 @@ +getUser($request); + + $posting = ForumPosting::findOneBySQL( + "posting_id = :posting_id AND user_id = :user_id", + [ + 'posting_id' => $args['posting_id'], + 'user_id' => $user->user_id + ] + ); + + if (!$posting) { + throw new RecordNotFoundException(); + } + + if ($posting->discussion->closed_at) { + throw new AuthorizationFailedException(); + } + + $posting->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumPostingReactionDelete.php b/lib/classes/JsonApi/Routes/Forum/ForumPostingReactionDelete.php new file mode 100644 index 0000000..6e5ff35 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumPostingReactionDelete.php @@ -0,0 +1,32 @@ +getUser($request); + + $posting_reaction = ForumPostingReaction::findOneBySQL( + "id = :reaction_id AND user_id = :user_id", + [ + 'reaction_id' => $args['reaction_id'], + 'user_id' => $user->user_id + ] + ); + + if (!$posting_reaction) { + throw new RecordNotFoundException(); + } + + $posting_reaction->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumPostingReactionShow.php b/lib/classes/JsonApi/Routes/Forum/ForumPostingReactionShow.php new file mode 100644 index 0000000..a6a0299 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumPostingReactionShow.php @@ -0,0 +1,26 @@ +getContentResponse($posting_reaction); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumPostingReactionStore.php b/lib/classes/JsonApi/Routes/Forum/ForumPostingReactionStore.php new file mode 100644 index 0000000..58b7a8b --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumPostingReactionStore.php @@ -0,0 +1,77 @@ +validate($request); + $user = $this->getUser($request); + + $posting = ForumPosting::find(self::arrayGet($json, 'data.relationships.posting.data.id')); + + if (!$posting) { + throw new BadRequestException(); + } + + if (!$course = \Course::find($posting->range_id)) { + throw new RecordNotFoundException(); + } + + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + $posting_reaction = ForumPostingReaction::create([ + 'posting_id' => $posting->posting_id, + 'user_id' => $user->user_id, + 'emoji' => self::arrayGet($json, 'data.attributes.emoji') + ]); + + if ($user->user_id !== $posting->user_id) { + \PersonalNotifications::add( + $posting->user_id, + \URLHelper::getURL('dispatch.php/course/forum/discussions/show/'.$posting->discussion_id, ['cid' => $posting->range_id], true)."#post_" . $posting->posting_id, + sprintf(_("%s hat auf deinen Beitrag reagiert."), $user->getFullName()), + null, + self::arrayGet($json, 'data.meta.emoji-icon') + ); + } + + return $this->getCreatedResponse($posting_reaction); + } + + protected function validateResourceDocument($json, $data) + { + $required_keys = [ + 'data.attributes.emoji' => 'Missing `data.attributes.emoji`', + 'data.meta.emoji-icon' => 'Missing `data.meta.emoji-icon`', + 'data.relationships.posting.data.id' => 'Missing `data.relationships.posting.data.id`', + ]; + + foreach ($required_keys as $key => $error_message) { + if (!self::arrayHas($json, $key)) { + return $error_message; + } + } + + return null; + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumPostingReactions.php b/lib/classes/JsonApi/Routes/Forum/ForumPostingReactions.php new file mode 100644 index 0000000..d5dde95 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumPostingReactions.php @@ -0,0 +1,43 @@ +range_id)) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + $reactions = $posting->reactions ?? \SimpleORMapCollection::createFromArray([]); + + return $this->getPaginatedContentResponse( + $reactions->limit(...$this->getOffsetAndLimit()), + count($reactions) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumPostingShow.php b/lib/classes/JsonApi/Routes/Forum/ForumPostingShow.php new file mode 100644 index 0000000..ec64bb6 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumPostingShow.php @@ -0,0 +1,40 @@ +range_id)) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($posting); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumPostingStore.php b/lib/classes/JsonApi/Routes/Forum/ForumPostingStore.php new file mode 100644 index 0000000..b6ecf6e --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumPostingStore.php @@ -0,0 +1,100 @@ +validate($request); + $user = $this->getUser($request); + + $discussion = ForumDiscussion::find(self::arrayGet($json, 'data.relationships.discussion.data.id')); + $course = \Course::find($discussion->range_id); + + if (!$discussion || !$course) { + throw new RecordNotFoundException(); + } + + if ( + !CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC) || + $discussion->closed_at + ) { + throw new AuthorizationFailedException(); + } + + $parent_id = self::arrayGet($json, 'data.relationships.posting.data.id'); + + $psoting = ForumPosting::create([ + 'range_id' => $discussion->range_id, + 'parent_id' => $parent_id ?? null, + 'discussion_id' => $discussion->discussion_id, + 'content' => Markup::markAsHtml(self::arrayGet($json, 'data.attributes.content')), + 'anonymous' => (self::arrayGet($json, 'data.attributes.anonymous') && \Config::get()->FORUM_ANONYMOUS_POSTINGS), + 'user_id' => $user->user_id + ]); + + $subscription = ForumSubscription::findOneBySQL( + "user_id = :user_id AND subject_id IN (:subject_ids)", + [ + 'user_id' => $user->user_id, + 'subject_ids' => [$discussion->discussion_id, $discussion->topic_id] + ] + ); + + if (!$subscription) { + $subscription = new ForumSubscription(); + $subscription->user_id = $user->user_id; + $subscription->range_id = $discussion->range_id; + $subscription->subject_id = $discussion->discussion_id; + $subscription->subject = 'discussion'; + $subscription->notification_type = SubscriptionNotificationType::All->value; + $subscription->store(); + } + + ForumPostingRead::updateUserReadPoint($user->user_id, $discussion->discussion_id); + + return $this->getCreatedResponse($psoting); + } + + protected function validateResourceDocument($json, $data) + { + $required_keys = [ + 'data.attributes.content' => 'Missing `data.attributes.content`', + 'data.attributes.anonymous' => 'Missing `data.attributes.anonymous`', + 'data.relationships.discussion.data.id' => 'Missing `data.relationships.discussion.data.id`', + ]; + + foreach ($required_keys as $key => $error_message) { + if (!self::arrayHas($json, $key)) { + return $error_message; + } + } + + return null; + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumPostingUpdate.php b/lib/classes/JsonApi/Routes/Forum/ForumPostingUpdate.php new file mode 100644 index 0000000..b8470fa --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumPostingUpdate.php @@ -0,0 +1,69 @@ +validate($request); + $user = $this->getUser($request); + + $posting = ForumPosting::findOneBySQL( + "posting_id = :posting_id AND user_id = :user_id", + [ + 'posting_id' => $args['posting_id'], + 'user_id' => $user->user_id + ] + ); + + if (!$posting) { + throw new RecordNotFoundException(); + } + + if ($posting->discussion->closed_at) { + throw new AuthorizationFailedException(); + } + + $posting->content = Markup::markAsHtml(self::arrayGet($json, 'data.attributes.content')); + $posting->anonymous = (self::arrayGet($json, 'data.attributes.anonymous') && \Config::get()->FORUM_ANONYMOUS_POSTINGS); + $posting->store(); + + return $this->getCreatedResponse($posting); + } + + protected function validateResourceDocument($json, $data) + { + $required_keys = [ + 'data.attributes.content' => 'Missing `data.attributes.content`', + 'data.attributes.anonymous' => 'Missing `data.attributes.anonymous`', + ]; + + foreach ($required_keys as $key => $error_message) { + if (!self::arrayHas($json, $key)) { + return $error_message; + } + } + + return null; + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionDelete.php b/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionDelete.php new file mode 100644 index 0000000..fef6ac0 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionDelete.php @@ -0,0 +1,32 @@ +getUser($request); + + $subscription = ForumSubscription::findOneBySQL( + "id = :subscription_id AND user_id = :user_id", + [ + 'subscription_id' => $args['subscription_id'], + 'user_id' => $user->user_id + ] + ); + + if (!$subscription) { + throw new RecordNotFoundException(); + } + + $subscription->delete(); + + return $this->getCodeResponse(204); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionIndex.php b/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionIndex.php new file mode 100644 index 0000000..5771d19 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionIndex.php @@ -0,0 +1,45 @@ +getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + $subscriptions = ForumSubscription::findBySQL( + "range_id = :course_id AND user_id = :user_id ORDER BY mkdate DESC", + [ + 'course_id' => $course->id, + 'user_id' => $user->user_id + ] + ); + + return $this->getPaginatedContentResponse( + array_slice($subscriptions, ...$this->getOffsetAndLimit()), + count($subscriptions) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionShow.php b/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionShow.php new file mode 100644 index 0000000..dd0854d --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionShow.php @@ -0,0 +1,36 @@ +getUser($request); + + $subscription = ForumSubscription::findOneBySQL( + "id = :id AND user_id = :user_id", + [ + 'id' => $args['subscription_id'], + 'user_id' => $user->user_id + ] + ); + + if (!$subscription) { + throw new RecordNotFoundException(); + } + + return $this->getContentResponse($subscription); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionStore.php b/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionStore.php new file mode 100644 index 0000000..ea3f308 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumSubscriptionStore.php @@ -0,0 +1,86 @@ +validate($request); + $user = $this->getUser($request); + $subjectType = $this->mapSubjectType(self::arrayGet($json, 'data.relationships.subject.data.type')); + + if ($subjectType === 'discussion') { + $discussion = ForumDiscussion::find(self::arrayGet($json, 'data.relationships.subject.data.id')); + + if (!$discussion || $discussion->closed_at) { + throw new AuthorizationFailedException(); + } + } + + if (!self::arrayHas($json, 'data.id')) { + $subscription = new ForumSubscription(); + $subscription->user_id = $user->user_id; + } else { + $subscription = ForumSubscription::findOneBySQL( + "id = :id AND user_id = :user_id", + [ + 'id' => self::arrayGet($json, 'data.id'), + 'user_id' => $user->user_id + ] + ); + + if (!$subscription) { + throw new BadRequestException(); + } + } + + $subscription->range_id = self::arrayGet($json, 'data.relationships.range.data.id'); + $subscription->subject_id = self::arrayGet($json, 'data.relationships.subject.data.id'); + $subscription->subject = $subjectType; + $subscription->notification_type = self::arrayGet($json, 'data.attributes.notification-type'); + + $subscription->store(); + + return $this->getCreatedResponse($subscription); + } + + protected function validateResourceDocument($json, $data) + { + $required_keys = [ + 'data' => 'Missing `data`', + 'data.attributes' => 'Missing `data.attributes`', + 'data.attributes.notification-type' => 'Missing `data.attributes.notification-type`', + 'data.relationships' => 'Missing `data.relationships`', + 'data.relationships.range.data.id' => 'Missing `data.relationships.range.data.id`', + 'data.relationships.subject.data.id' => 'Missing `data.relationships.subject.data.id`', + 'data.relationships.subject.data.type' => 'Missing `data.relationships.subject.data.type`', + ]; + + foreach ($required_keys as $key => $error_message) { + if (!self::arrayHas($json, $key)) { + return $error_message; + } + } + + return null; + } + + private function mapSubjectType($type): string + { + return match ($type) { + 'forum-discussions' => 'discussion', + 'forum-topics' => 'topic' + }; + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumTopicDiscussions.php b/lib/classes/JsonApi/Routes/Forum/ForumTopicDiscussions.php new file mode 100644 index 0000000..68bd09a --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumTopicDiscussions.php @@ -0,0 +1,50 @@ +range_id)) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + $discussions = $topic->discussions ?? \SimpleORMapCollection::createFromArray([]); + + return $this->getPaginatedContentResponse( + $discussions->limit(...$this->getOffsetAndLimit()), + count($discussions) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumTopicIndex.php b/lib/classes/JsonApi/Routes/Forum/ForumTopicIndex.php new file mode 100644 index 0000000..0379bfa --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumTopicIndex.php @@ -0,0 +1,38 @@ +getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + $topics = ForumTopic::getCourseTopics($course->id); + + return $this->getPaginatedContentResponse( + array_slice($topics, ...$this->getOffsetAndLimit()), + count($topics) + ); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumTopicShow.php b/lib/classes/JsonApi/Routes/Forum/ForumTopicShow.php new file mode 100644 index 0000000..d4170df --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumTopicShow.php @@ -0,0 +1,33 @@ +range_id)) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!CourseAuthority::canShowCourse($user, $course, CourseAuthority::SCOPE_BASIC)) { + throw new AuthorizationFailedException(); + } + + return $this->getContentResponse($topic); + } +} diff --git a/lib/classes/JsonApi/Routes/Forum/ForumTopicUpdateSort.php b/lib/classes/JsonApi/Routes/Forum/ForumTopicUpdateSort.php new file mode 100644 index 0000000..4926487 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Forum/ForumTopicUpdateSort.php @@ -0,0 +1,61 @@ +validate($request); + $course_id = self::arrayGet($json, 'data.relationships.range.data.id'); + + if (!$course = \Course::find($course_id)) { + throw new RecordNotFoundException(); + } + + if (!\CoreForum::isModerator($course->id)) { + throw new AuthorizationFailedException(); + } + + $topic_ids = self::arrayGet($json, 'data.attributes.topic-ids'); + + ForumTopic::findEachBySQL( + function (ForumTopic $topic) use ($topic_ids) { + $topic->position = (int) array_search($topic->topic_id, $topic_ids); + $topic->store(); + }, + "topic_id IN (:topic_ids) AND range_id = :course_id", + [ + "topic_ids" => $topic_ids, + "course_id" => $course->id + ] + ); + + return $this->getCodeResponse(204); + } + + protected function validateResourceDocument($json, $data) + { + $required_keys = [ + 'data.attributes.topic-ids' => 'Missing `data.attributes.topic-ids`', + 'data.relationships.range.data.id' => 'Missing `data.relationships.range.data.id`', + ]; + + foreach ($required_keys as $key => $error_message) { + if (!self::arrayHas($json, $key)) { + return $error_message; + } + } + + return null; + } +} diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index b3c3179..c5acad7 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -39,8 +39,15 @@ class SchemaMap \Degree::class => Schemas\Degree::class, \FeedbackElement::class => Schemas\FeedbackElement::class, \FeedbackEntry::class => Schemas\FeedbackEntry::class, - \JsonApi\Models\ForumCat::class => Schemas\ForumCategory::class, - \JsonApi\Models\ForumEntry::class => Schemas\ForumEntry::class, + \Forum\ForumCategory::class => \JsonApi\Schemas\Forum\ForumCategory::class, + \Forum\ForumTopic::class => \JsonApi\Schemas\Forum\ForumTopic::class, + \Forum\ForumDiscussion::class => \JsonApi\Schemas\Forum\ForumDiscussion::class, + \Forum\ForumDiscussionType::class => \JsonApi\Schemas\Forum\ForumDiscussionType::class, + \Forum\ForumPosting::class => \JsonApi\Schemas\Forum\ForumPosting::class, + \Forum\ForumPostingReaction::class => \JsonApi\Schemas\Forum\ForumPostingReaction::class, + \Forum\ForumSubscription::class => \JsonApi\Schemas\Forum\ForumSubscription::class, + \Forum\DTO\ForumMember::class => \JsonApi\Schemas\Forum\ForumMember::class, + \Forum\DTO\ForumTag::class => \JsonApi\Schemas\Forum\ForumTag::class, \Institute::class => Schemas\Institute::class, \InstituteMember::class => Schemas\InstituteMember::class, \LtiTool::class => Schemas\LtiTool::class, diff --git a/lib/classes/JsonApi/Schemas/Activity.php b/lib/classes/JsonApi/Schemas/Activity.php index fbdd6e7..3839793 100644 --- a/lib/classes/JsonApi/Schemas/Activity.php +++ b/lib/classes/JsonApi/Schemas/Activity.php @@ -89,7 +89,7 @@ class Activity extends SchemaProvider { $mapping = [ 'documents' => \FileRef::class, - 'forum' => \JsonApi\Models\ForumEntry::class, + 'forum' => \Forum\ForumPosting::class, 'message' => \Message::class, 'news' => \StudipNews::class, 'participants' => \Course::class, diff --git a/lib/classes/JsonApi/Schemas/Forum/ForumCategory.php b/lib/classes/JsonApi/Schemas/Forum/ForumCategory.php new file mode 100644 index 0000000..551201a --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Forum/ForumCategory.php @@ -0,0 +1,74 @@ +id; + } + + public function getAttributes($category, ContextInterface $context): iterable + { + return [ + 'name' => $category->name, + 'description' => $category->description, + 'color' => $category->color, + 'position' => (int) $category->position, + 'mkdate' => date('c', $category->mkdate), + 'chdate' => date('c', $category->chdate) + ]; + } + + public function hasResourceMeta($category): bool + { + return true; + } + + public function getResourceMeta($category) + { + $metaData = $category->getMetaData(); + + return [ + 'topics-count' => (int) $metaData['topics_count'], + 'discussions-count' => (int) $metaData['discussions_count'], + 'postings-count' => (int) $metaData['postings_count'], + 'recent-postings-count' => (int) $metaData['recent_postings_count'], + 'user-read-index' => (int) $metaData['user_read_index'], + 'users-count' => (int) $metaData['users_count'], + 'recent-activity' => $metaData['recent_activity'] ? date('c', $metaData['recent_activity']) : '', + ]; + } + + public function getRelationships($category, ContextInterface $context): iterable + { + $relationships = []; + $relationships = $this->addTopicsRelationship($relationships, $category, $this->shouldInclude($context, self::REL_TOPICS)); + + return $relationships; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function addTopicsRelationship($relationships, $category, $withTopics = false) + { + if ($withTopics) { + $relationships[self::REL_TOPICS] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($category, self::REL_TOPICS) + ], + self::RELATIONSHIP_DATA => $category->topics + ]; + } + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/Forum/ForumDiscussion.php b/lib/classes/JsonApi/Schemas/Forum/ForumDiscussion.php new file mode 100644 index 0000000..d095325 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Forum/ForumDiscussion.php @@ -0,0 +1,154 @@ +discussion_id; + } + + public function getAttributes($discussion, ContextInterface $context): iterable + { + return [ + 'title' => $discussion->title, + 'closed-at' => $discussion->closed_at ? date('c', $discussion->closed_at) : null, + 'sticky' => (bool) $discussion->sticky, + 'view-count' => (int) $discussion->view_count, + 'mkdate' => date('c', $discussion->mkdate), + 'chdate' => date('c', $discussion->chdate) + ]; + } + + public function hasResourceMeta($discussion): bool + { + return true; + } + + public function getResourceMeta($discussion) + { + $metaData = $discussion->getMetaData(); + + return [ + 'postings-count' => (int) $metaData['postings_count'], + 'recent-postings-count' => (int) $metaData['recent_postings_count'], + 'user-read-index' => (int) $metaData['user_read_index'], + 'recent-activity' => $metaData['recent_activity'] ? date('c', $metaData['recent_activity']) : '' + ]; + } + + public function getRelationships($discussion, ContextInterface $context): iterable + { + $relationships = []; + + $relationships = $this->addPostingsRelationship($relationships, $discussion, $this->shouldInclude($context, self::REL_POSTINGS)); + $relationships = $this->addTopicRelationship($relationships, $discussion, $this->shouldInclude($context, self::REL_TOPIC)); + $relationships = $this->addCategoryRelationship($relationships, $discussion, $this->shouldInclude($context, self::REL_CATEGORY)); + $relationships = $this->addDiscussionTypeRelationship($relationships, $discussion, $this->shouldInclude($context, self::REL_DISCUSSION_TYPE)); + $relationships = $this->addMembersRelationship($relationships, $discussion, $this->shouldInclude($context, self::REL_MEMBERS)); + $relationships = $this->addTagsRelationship($relationships, $discussion, $this->shouldInclude($context, self::REL_TAGS)); + + return $relationships; + } + + private function addPostingsRelationship(array $relationships, $discussion, bool $withPostings = false) + { + if ($withPostings) { + $relationships[self::REL_POSTINGS] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($discussion, self::REL_POSTINGS) + ], + self::RELATIONSHIP_DATA => $discussion->postings + ]; + } + + return $relationships; + } + + private function addTopicRelationship(array $relationships, $discussion, bool $withTopic = false) + { + if ($withTopic) { + $relationships[self::REL_TOPIC] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($discussion->topic) + ], + self::RELATIONSHIP_DATA => $discussion->topic + ]; + } + + return $relationships; + } + + private function addCategoryRelationship(array $relationships, $discussion, bool $withCategory = false) + { + $category = $discussion->category; + if ($withCategory && $category) { + + $relationships[self::REL_CATEGORY] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($category) + ], + self::RELATIONSHIP_DATA => $category + ]; + } + + return $relationships; + } + + private function addDiscussionTypeRelationship(array $relationships, $discussion, bool $withDiscussionType = false) + { + $discussionType = $discussion->discussion_type; + + if ($withDiscussionType && $discussionType) { + $relationships[self::REL_DISCUSSION_TYPE] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($discussionType) + ], + self::RELATIONSHIP_DATA => $discussionType + ]; + } + + return $relationships; + } + + private function addMembersRelationship(array $relationships, $discussion, bool $withMembers = false) + { + if ($withMembers) { + $relationships[self::REL_MEMBERS] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($discussion, self::REL_MEMBERS) + ], + self::RELATIONSHIP_DATA => $discussion->members + ]; + } + + return $relationships; + } + + private function addTagsRelationship(array $relationships, $discussion, bool $withTags = false) + { + if ($withTags) { + $relationships[self::REL_TAGS] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($discussion, self::REL_TAGS) + ], + self::RELATIONSHIP_DATA => $discussion->tags + ]; + } + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/Forum/ForumDiscussionType.php b/lib/classes/JsonApi/Schemas/Forum/ForumDiscussionType.php new file mode 100644 index 0000000..7bdf6bb --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Forum/ForumDiscussionType.php @@ -0,0 +1,54 @@ +type_id; + } + + public function getAttributes($discussionType, ContextInterface $context): iterable + { + return [ + 'name' => $discussionType->name, + 'icon' => $discussionType->icon, + 'mkdate' => date('c', $discussionType->mkdate), + 'chdate' => date('c', $discussionType->chdate), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getRelationships($discussionType, ContextInterface $context): iterable + { + $relationships = []; + + $relationships = $this->addDiscussionsRelationship($relationships, $discussionType, $this->shouldInclude($context, self::REL_DISCUSSIONS)); + + return $relationships; + } + + private function addDiscussionsRelationship($relationships, $discussionType, $withDiscussions = false) + { + if ($withDiscussions) { + $relationships[self::REL_DISCUSSIONS] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($discussionType, self::REL_DISCUSSIONS) + ], + self::RELATIONSHIP_DATA => $discussionType->discussions + ]; + } + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/Forum/ForumMember.php b/lib/classes/JsonApi/Schemas/Forum/ForumMember.php new file mode 100644 index 0000000..71a0226 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Forum/ForumMember.php @@ -0,0 +1,30 @@ +id; + } + + public function getAttributes($member, ContextInterface $context): iterable + { + return [ + 'username' => $member->username, + 'name' => $member->name, + 'role' => $member->role, + 'avatar_url' => $member->avatar_url + ]; + } + + public function getRelationships($resource, ContextInterface $context): iterable + { + return []; + } +} diff --git a/lib/classes/JsonApi/Schemas/Forum/ForumPosting.php b/lib/classes/JsonApi/Schemas/Forum/ForumPosting.php new file mode 100644 index 0000000..5506f55 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Forum/ForumPosting.php @@ -0,0 +1,123 @@ +posting_id; + } + + public function getAttributes($posting, ContextInterface $context): iterable + { + return [ + 'content' => formatReady($posting->content), + 'anonymous' => (bool) $posting->anonymous, + 'mkdate' => date('c', $posting->mkdate), + 'chdate' => date('c', $posting->chdate) + ]; + } + + public function hasResourceMeta($posting): bool + { + return true; + } + + public function getResourceMeta($posting) + { + return [ + self::REL_OPENGRAPH_URLS => array_map(fn($og) => [ + 'url' => $og['url'], + 'is-opengraph' => (bool) $og['is_opengraph'], + 'title' => $og['title'], + 'description' => $og['description'], + 'image' => $og['image'], + ], $posting->getOpenGraphURLs()->toArray()) + ]; + } + + public function getRelationships($posting, ContextInterface $context): iterable + { + $relationships = []; + $relationships = $this->addAuthorRelationship($relationships, $posting, $this->shouldInclude($context, self::REL_AUTHOR)); + $relationships = $this->addDiscussionRelationship($relationships, $posting, $this->shouldInclude($context, self::REL_DISCUSSION)); + $relationships = $this->addPostingRelationship($relationships, $posting, $this->shouldInclude($context, self::REL_POSTING)); + $relationships = $this->addReactionsRelationship($relationships, $posting, $this->shouldInclude($context, self::REL_REACTIONS)); + + return $relationships; + } + + private function addAuthorRelationship($relationships, $posting, $withAuthor = false) + { + $author = $posting->author; + + if ($withAuthor && $author) { + $relationships[self::REL_AUTHOR] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($author) + ], + self::RELATIONSHIP_DATA => $author + ]; + } + + return $relationships; + } + + private function addDiscussionRelationship($relationships, $posting, $withDiscussion = false) + { + if ($withDiscussion) { + $relationships[self::REL_DISCUSSION] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($posting->discussion) + ], + self::RELATIONSHIP_DATA => $posting->discussion + ]; + } + + return $relationships; + } + + private function addPostingRelationship($relationships, $posting, $withPosting = false) + { + $posting = $posting->posting; + + if ($withPosting && $posting) { + $relationships[self::REL_POSTING] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($posting) + ], + self::RELATIONSHIP_DATA => $posting + ]; + } + + return $relationships; + } + + private function addReactionsRelationship($relationships, $posting, $withReactions = false) + { + if ($withReactions) { + $relationships[self::REL_REACTIONS] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($posting, self::REL_REACTIONS) + ], + self::RELATIONSHIP_DATA => $posting->reactions + ]; + } + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/Forum/ForumPostingReaction.php b/lib/classes/JsonApi/Schemas/Forum/ForumPostingReaction.php new file mode 100644 index 0000000..1b7bfa9 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Forum/ForumPostingReaction.php @@ -0,0 +1,71 @@ +id; + } + + public function getAttributes($postingReaction, ContextInterface $context): iterable + { + return [ + 'emoji' => $postingReaction->emoji, + 'mkdate' => date('c', $postingReaction->mkdate), + 'chdate' => date('c', $postingReaction->chdate) + ]; + } + + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getRelationships($postingReaction, ContextInterface $context): iterable + { + $relationships = []; + + $relationships = $this->addPostingRelationship($relationships, $postingReaction, $this->shouldInclude($context, self::REL_POSTING)); + $relationships = $this->addUserRelationship($relationships, $postingReaction, $this->shouldInclude($context, self::REL_USER)); + + return $relationships; + } + + + private function addPostingRelationship(array $relationships, $postingReaction, bool $withPosting = false) + { + if ($withPosting) { + $relationships[self::REL_POSTING] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($postingReaction->posting) + ], + self::RELATIONSHIP_DATA => $postingReaction->posting + ]; + } + + return $relationships; + } + + private function addUserRelationship(array $relationships, $discussion, bool $withUser = false) + { + if ($withUser) { + $relationships[self::REL_USER] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($discussion->user) + ], + self::RELATIONSHIP_DATA => $discussion->user + ]; + } + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/Forum/ForumSubscription.php b/lib/classes/JsonApi/Schemas/Forum/ForumSubscription.php new file mode 100644 index 0000000..ecfb699 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Forum/ForumSubscription.php @@ -0,0 +1,84 @@ +id; + } + + public function getAttributes($subscription, ContextInterface $context): iterable + { + return [ + 'notification-type' => $subscription->notification_type, + 'mkdate' => date('c', $subscription->mkdate), + 'chdate' => date('c', $subscription->chdate) + ]; + } + + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getRelationships($subscription, ContextInterface $context): iterable + { + $isPrimary = $context->getPosition()->getLevel() === 0; + $includeList = $context->getIncludePaths(); + + $relationships = []; + if ($isPrimary) { + $relationships = $this->addUserRelationship($relationships, $subscription, $includeList); + $relationships = $this->addRangeRelationship($relationships, $subscription, $includeList); + $relationships = $this->addSubjectRelationship($relationships, $subscription, $includeList); + } + + return $relationships; + } + + private function addUserRelationship(array $relationships, $subscription, array $includeList) + { + $relationships[self::REL_USER] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($subscription->user) + ], + self::RELATIONSHIP_DATA => $subscription->user + ]; + + return $relationships; + } + + private function addSubjectRelationship(array $relationships, $subscription, array $includeList) + { + $relationships[self::REL_SUBJECT] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($subscription->subject_object) + ], + self::RELATIONSHIP_DATA => $subscription->subject_object + ]; + + return $relationships; + } + + private function addRangeRelationship(array $relationships, $subscription, $includeList) + { + $relationships[self::REL_RANGE] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($subscription->range), + ], + self::RELATIONSHIP_DATA => $subscription->range, + ]; + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/Forum/ForumTag.php b/lib/classes/JsonApi/Schemas/Forum/ForumTag.php new file mode 100644 index 0000000..00bf2e9 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Forum/ForumTag.php @@ -0,0 +1,27 @@ +id; + } + + public function getAttributes($tag, ContextInterface $context): iterable + { + return [ + 'name' => $tag->name + ]; + } + + public function getRelationships($resource, ContextInterface $context): iterable + { + return []; + } +} diff --git a/lib/classes/JsonApi/Schemas/Forum/ForumTopic.php b/lib/classes/JsonApi/Schemas/Forum/ForumTopic.php new file mode 100644 index 0000000..1d310c2 --- /dev/null +++ b/lib/classes/JsonApi/Schemas/Forum/ForumTopic.php @@ -0,0 +1,107 @@ +topic_id; + } + + /** + * @inheritdoc + * + * @param \Forum\ForumTopic $topic + */ + public function getAttributes($topic, ContextInterface $context): iterable + { + return [ + 'name' => $topic->name, + 'description' => $topic->description, + 'position' => (int) $topic->position, + 'mkdate' => date('c', $topic->mkdate), + 'chdate' => date('c', $topic->chdate) + ]; + } + + /** + * @inheritdoc + * + * @param \Forum\ForumTopic $topic + */ + public function hasResourceMeta($topic): bool + { + return true; + } + + /** + * @inheritdoc + * + * @param \Forum\ForumTopic $topic + */ + public function getResourceMeta($topic) + { + $metaData = $topic->getMetaData(); + + return [ + 'discussions-count' => (int) $metaData['discussions_count'], + 'postings-count' => (int) $metaData['postings_count'], + 'recent-postings-count' => (int) $metaData['recent_postings_count'], + 'user-read-index' => (int) $metaData['user_read_index'], + 'users-count' => (int) $metaData['users_count'], + 'recent-activity' => $metaData['recent_activity'] ? date('c', $metaData['recent_activity']) : '', + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param \Forum\ForumTopic $topic + */ + public function getRelationships($topic, ContextInterface $context): iterable + { + $relationships = []; + $relationships = $this->addCategoryRelationship($relationships, $topic, $this->shouldInclude($context, self::REL_CATEGORY)); + $relationships = $this->addDiscussionsRelationship($relationships, $topic, $this->shouldInclude($context, self::REL_DISCUSSION)); + + return $relationships; + } + + private function addCategoryRelationship($relationships, $topic, $withCategory = false) + { + if ($withCategory) { + $relationships[self::REL_CATEGORY] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($topic, self::REL_CATEGORY) + ], + self::RELATIONSHIP_DATA => $topic->category + ]; + } + + return $relationships; + } + + private function addDiscussionsRelationship($relationships, $topic, $withDiscussions = false) + { + if ($withDiscussions) { + $relationships[self::REL_DISCUSSION] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($topic, self::REL_DISCUSSION) + ], + self::RELATIONSHIP_DATA => $topic->dicussions + ]; + } + + return $relationships; + } +} diff --git a/lib/classes/JsonApi/Schemas/ForumCategory.php b/lib/classes/JsonApi/Schemas/ForumCategory.php deleted file mode 100644 index 4111464..0000000 --- a/lib/classes/JsonApi/Schemas/ForumCategory.php +++ /dev/null @@ -1,76 +0,0 @@ -id; - } - - public function getAttributes($category, ContextInterface $context): iterable - { - return [ - 'title' => $category->entry_name, - 'position' => (int) $category->pos, - ]; - } - - public function getRelationships($category, ContextInterface $context): iterable - { - $isPrimary = $context->getPosition()->getLevel() === 0; - $includeList = $context->getIncludePaths(); - - $relationships = []; - if ($isPrimary) { - $relationships = $this->addCourseRelationship($category, $isPrimary, $includeList); - $relationships = $this->addEntryRelationship($category, $isPrimary, $includeList); - } - - return $relationships; - } - - public function addCourseRelationship($category, $isPrimary, $includeList) - { - $data = $isPrimary && in_array(self::REL_COURSE, $includeList) - ? \Course::find($category->seminar_id) - : \Course::buildExisting(['id' => $category->seminar_id]); - $link = $this->createLinkToResource($data); - $relationships = [ - self::REL_COURSE => [ - self::RELATIONSHIP_LINKS => [Link::RELATED => $link], - self::RELATIONSHIP_DATA => $data, - ], - ]; - - return $relationships; - } - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - private function addEntryRelationship($category, $isPrimary, $includeList) - { - $data = Entry::getEntriesFromCat($category); - $link = $this->getRelationshipRelatedLink($category, self::REL_ENTRY); - $relationships[self::REL_ENTRY] = [ - self::RELATIONSHIP_DATA => $data, - self::RELATIONSHIP_LINKS => [ - Link::RELATED => $link, - ], - ]; - - return $relationships; - } -} diff --git a/lib/classes/JsonApi/Schemas/ForumEntry.php b/lib/classes/JsonApi/Schemas/ForumEntry.php deleted file mode 100644 index e99be46..0000000 --- a/lib/classes/JsonApi/Schemas/ForumEntry.php +++ /dev/null @@ -1,78 +0,0 @@ -topic_id; - } - - public function getAttributes($entry, ContextInterface $context): iterable - { - return [ - 'title' => $entry->name, - 'area' => (int) $entry->area, - 'content' => $entry->content, - ]; - } - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function getRelationships($entry, ContextInterface $context): iterable - { - $isPrimary = $context->getPosition()->getLevel() === 0; - $includeList = $context->getIncludePaths(); - - $relationships = []; - if ($isPrimary) { - $relationships = $this->addCategoryRelationship($relationships, $entry, $includeList); - $relationships = $this->addChildEntryRelationship($relationships, $entry, $includeList); - } - - return $relationships; - } - - private function addCategoryRelationship($relationships, $entry, $includeList) - { - $cat_link = $this->createLinkToResource($entry->category); - $cat_data = in_array(self::REL_CAT, $includeList) - ? ForumCat::find($entry->category->id) - : ForumCat::buildExisting(['id' => $entry->category->id]); - - $relationships[self::REL_CAT] = [ - self::RELATIONSHIP_LINKS => [ - Link::RELATED => $cat_link, - ], - self::RELATIONSHIP_DATA => $cat_data, - ]; - - return $relationships; - } - - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - private function addChildEntryRelationship($relationships, $entry, $includeList) - { - $relationships[self::REL_ENTRY] = [ - self::RELATIONSHIP_DATA => $entry->getChildEntries($entry->id), - - self::RELATIONSHIP_LINKS => [ - Link::RELATED => $this->getRelationshipRelatedLink($entry, self::REL_ENTRY), - ], - ]; - - return $relationships; - } -} diff --git a/lib/classes/Privacy.php b/lib/classes/Privacy.php index b38838c..d28ecf4 100644 --- a/lib/classes/Privacy.php +++ b/lib/classes/Privacy.php @@ -42,7 +42,7 @@ class Privacy ], 'content' => [ FileRef::class, - ForumEntry::class, + \Forum\ForumPosting::class, WikiPage::class, Courseware\Unit::class, Courseware\StructuralElement::class, diff --git a/lib/classes/Score.php b/lib/classes/Score.php index f6370ee..35c62c0 100644 --- a/lib/classes/Score.php +++ b/lib/classes/Score.php @@ -204,7 +204,7 @@ class Score $forum = PluginEngine::getPlugin(CoreForum::class); if ($forum && $forum->isEnabled()) { - $tables[] = ['table' => 'forum_entries']; + $tables[] = ['table' => 'forum_postings']; } $blubber = PluginEngine::getPlugin(Blubber::class); diff --git a/lib/classes/Siteinfo.php b/lib/classes/Siteinfo.php index 4a969ee..8ca1c08 100644 --- a/lib/classes/Siteinfo.php +++ b/lib/classes/Siteinfo.php @@ -473,37 +473,6 @@ class SiteinfoMarkupEngine { LIMIT 10"; $template->type = "seminar"; break; - case "mostpostings": - $template->heading = _("die aktivsten Veranstaltungen (Postings der letzten zwei Wochen)"); - $seminars = []; - - // get TopTen of seminars from all ForumModules and add up the - // count for seminars with more than one active ForumModule - // to get a combined toplist - foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) { - $new_seminars = $plugin->getTopTenSeminars(); - foreach ($new_seminars as $sem) { - if (!isset($seminars[$sem['seminar_id']])) { - $seminars[$sem['seminar_id']] = $sem; - } else { - $seminars[$sem['seminar_id']]['count'] += $sem['count']; - } - } - } - - // sort the seminars by the number of combined postings - usort($seminars, function($a, $b) { - if ($a['count'] === $b['count']) { - return 0; - } - return ($a['count'] > $b['count']) ? -1 : 1; - }); - - // fill the template and returned the rendered code - $template->lines = $seminars; - $template->type = "seminar"; - - break; case "mostvisitedhomepages": $template->heading = _("die beliebtesten Profile (Besucher)"); $sql = "SELECT auth_user_md5.user_id, @@ -586,16 +555,9 @@ class SiteinfoMarkupEngine { "constraint" => Config::get()->RESOURCES_ENABLE]; if ($key === 'posting') { - $count = 0; - - // sum up number of postings for all availabe ForumModules - foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) { - $count += $plugin->getNumberOfPostings(); - } - $template->title = _('Forenbeiträge'); $template->detail = _('Anzahl Beiträge aller verwendeten Foren'); - $template->count = $count; + $template->count = \Forum\ForumPosting::countBySql(); } else { // iterate over the other indicators if (in_array($key,array_keys($indicator))) { diff --git a/lib/classes/StudipKing.php b/lib/classes/StudipKing.php index ae5a14e..c77908b 100644 --- a/lib/classes/StudipKing.php +++ b/lib/classes/StudipKing.php @@ -115,23 +115,7 @@ class StudipKing { private static function forum_kings() { - $kings = []; - - // sum up postings for all users from all ForumModules available - foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) { - $table = $plugin->getEntryTableInfo(); - $query = "SELECT user_id AS id, COUNT(*) AS num FROM ". $table['table'] ." GROUP BY user_id"; - $new_kings = self::select_kings($query); - foreach ($new_kings as $user_id => $num) { - if (!isset($kings[$user_id])) { - $kings[$user_id] = $num; - } else { - $kings[$user_id] += $num; - } - } - } - - return $kings; + return self::select_kings("SELECT user_id AS id, COUNT(*) AS num FROM (SELECT user_id FROM forum_postings) as `postings` GROUP BY user_id"); } private static function files_kings() diff --git a/lib/classes/UserManagement.php b/lib/classes/UserManagement.php index 961a872..255bba1 100644 --- a/lib/classes/UserManagement.php +++ b/lib/classes/UserManagement.php @@ -1221,8 +1221,9 @@ class UserManagement "DELETE FROM priorities WHERE user_id = ?", "DELETE FROM help_tour_user WHERE user_id = ?", "DELETE FROM personal_notifications_user WHERE user_id = ?", - "DELETE FROM forum_abo_users WHERE user_id = ?", - "DELETE FROM forum_favorites WHERE user_id = ?", + "DELETE FROM forum_subscriptions WHERE user_id = ?", + "DELETE FROM forum_posting_reads WHERE user_id = ?", + "DELETE FROM forum_posting_reactions WHERE user_id = ?", "DELETE FROM comments WHERE user_id = ?", "DELETE questionnaires FROM questionnaires LEFT JOIN questionnaire_assignments qa USING (`questionnaire_id`) WHERE qa.range_id = ?", @@ -1235,7 +1236,7 @@ class UserManagement "DELETE etask_tests, ea FROM etask_tests LEFT JOIN etask_assignments ea ON (`test_id` = ea.test_id) WHERE ea.range_type = 'user' AND user_id = ?", "DELETE FROM etask_assignments WHERE range_type = 'user' AND range_id = ?", - "UPDATE forum_entries SET author = '' WHERE user_id = ?", + "UPDATE forum_postings SET anonymous = 1 WHERE user_id = ?", "UPDATE auth_user_md5 SET visible = 'never' WHERE user_id = ?", "REPLACE INTO `user_info` (`user_id`, `hobby`, `lebenslauf`, `publi`, `schwerp`, `Home`, `privatnr`, `privatcell`, `privadr`, `score`, `geschlecht`, `mkdate`, `chdate`, `title_front`, `title_rear`, `preferred_language`, `smsforward_copy`, `smsforward_rec`, `email_forward`, `motto`, `lock_rule`) VALUES(?, '', '', '', '', '', '', '', '', 0, 0, 0, 0, '', '', NULL, 1, '', 0, '', '');" diff --git a/lib/classes/forms/ColorInput.php b/lib/classes/forms/ColorInput.php new file mode 100644 index 0000000..43dac93 --- /dev/null +++ b/lib/classes/forms/ColorInput.php @@ -0,0 +1,18 @@ +open('forms/color_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } +} diff --git a/lib/classes/globalsearch/GlobalSearchForum.php b/lib/classes/globalsearch/GlobalSearchForum.php index 915fe23..845fcaa 100644 --- a/lib/classes/globalsearch/GlobalSearchForum.php +++ b/lib/classes/globalsearch/GlobalSearchForum.php @@ -49,7 +49,7 @@ class GlobalSearchForum extends GlobalSearchModule implements GlobalSearchFullte if (!$GLOBALS['perm']->have_perm('admin')) { $seminaruser = " AND EXISTS ( SELECT 1 FROM `seminar_user` - WHERE `forum_entries`.`seminar_id` = `seminar_user`.`seminar_id` + WHERE `forum_postings`.`range_id` = `seminar_user`.`seminar_id` AND `seminar_user`.`user_id` = " . DBManager::get()->quote($GLOBALS['user']->id) . " ) "; } @@ -81,11 +81,10 @@ class GlobalSearchForum extends GlobalSearchModule implements GlobalSearchFullte $anonymous = ""; } - $sql = "SELECT SQL_CALC_FOUND_ROWS `forum_entries`.* - FROM `forum_entries` + $sql = "SELECT SQL_CALC_FOUND_ROWS `forum_postings`.* + FROM `forum_postings` WHERE {$anonymous} ( - `name` LIKE {$query} - OR `content` LIKE {$query} + `content` LIKE {$query} ) {$semester_condition} {$seminaruser} diff --git a/lib/cronjobs/garbage_collector.php b/lib/cronjobs/garbage_collector.php index 961e086..4a029ef 100644 --- a/lib/cronjobs/garbage_collector.php +++ b/lib/cronjobs/garbage_collector.php @@ -154,13 +154,6 @@ class GarbageCollectorJob extends CronJob $statement->execute(); } - // Remove outdated entries from forum_visits - $query = "DELETE FROM `forum_visits` - WHERE GREATEST(`visitdate`, `last_visitdate`) < UNIX_TIMESTAMP() - :threshold"; - DBManager::get()->execute($query, [ - ':threshold' => ForumVisit::LAST_VISIT_MAX, - ]); - // clean db cache $cache = new Studip\Cache\DbCache(); $cache->purge(); diff --git a/lib/models/Course.php b/lib/models/Course.php index 0357b66..7b285a8 100644 --- a/lib/models/Course.php +++ b/lib/models/Course.php @@ -378,10 +378,8 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe ['course' => $course->id] ); - //Delete forum entries: - foreach (PluginEngine::getPlugins(ForumModule::class) as $forum_tool) { - $forum_tool->deleteContents($course->id); - } + //Delete forum contents: + CoreForum::deleteCourseContents($course->id); //Delete all files: $folder = Folder::findTopFolder($course->id); diff --git a/lib/models/CourseTopic.php b/lib/models/CourseTopic.php index 72d2167..fb31efc 100644 --- a/lib/models/CourseTopic.php +++ b/lib/models/CourseTopic.php @@ -50,13 +50,17 @@ class CourseTopic extends SimpleORMap 'class_name' => User::class, 'foreign_key' => 'author_id' ]; - - $config['additional_fields']['forum_thread_url']['get'] = 'getForumThreadURL'; + $config['has_and_belongs_to_many']['forum_topics'] = [ + 'class_name' => \Forum\ForumTopic::class, + 'thru_table' => 'forum_topics_issues', + 'on_delete' => 'delete', + 'on_store' => 'store' + ]; $config['registered_callbacks']['before_create'][] = 'cbDefaultValues'; $config['registered_callbacks']['after_store'][] = 'cbUpdateConnectedContentModules'; - $config['registered_callbacks']['before_delete'][] = 'cbUnlinkConnectedContentModules'; + $config['additional_fields']['forum_thread_url']['get'] = 'getForumThreadURL'; $config['i18n_fields']['title'] = true; $config['i18n_fields']['description'] = true; @@ -113,37 +117,27 @@ class CourseTopic extends SimpleORMap } /** - * set or update connection with forum thread - */ + * set or update connection with forum thread + */ public function connectWithForumThread() { - if ($this->seminar_id) { - $course = Course::find($this->seminar_id); - try { - $forum_module = $course->getTool(CoreForum::class); - if ($forum_module instanceof ForumModule) { - $forum_module->setThreadForIssue($this->id, $this->title, $this->description); - return true; - } - } catch (\Studip\Exception $e) { - return false; - } + if ($this->seminar_id && !$this->forum_thread_url) { + $forum_topic = new \Forum\ForumTopic(); + $forum_topic['range_id'] = $this->seminar_id; + $forum_topic['name'] = $this['title']; + $forum_topic['description'] = $this['description']; + $forum_topic->store(); + + $this->forum_topics[] = $forum_topic; + $this->store(); } return false; } public function getForumThreadURL() { - if ($this->seminar_id) { - $course = Course::find($this->seminar_id); - try { - $forum_module = $course->getTool(CoreForum::class); - if ($forum_module instanceof ForumModule) { - return html_entity_decode($forum_module->getLinkToThread($this->id)); - } - } catch (\Studip\Exception $e) { - return ''; - } + if (count($this->forum_topics) > 0) { + return URLHelper::getURL('dispatch.php/course/forum/topics/show/'. $this->forum_topics[0]['topic_id'], ['cid' => $this->forum_topics[0]['range_id']]); } return ''; } @@ -152,25 +146,15 @@ class CourseTopic extends SimpleORMap { if ($this->isFieldDirty('title') || $this->isFieldDirty('description')) { if ($this->forum_thread_url) { - $this->connectWithForumThread(); + foreach ($this->forum_topics as $forum_topic) { + $forum_topic['name'] = $this['title']; + $forum_topic['description'] = $this['description']; + $forum_topic->store(); + } } } } - /** - * Removes link information for forum topic and remove forum topic as well - * if it is empty. - */ - protected function cbUnlinkConnectedContentModules() - { - $query = "DELETE fei, fe - FROM `forum_entries_issues` AS fei - LEFT JOIN `forum_entries` AS fe - ON fei.`topic_id` = fe.`topic_id` AND fe.`rgt` = fe.`lft` + 1 - WHERE `issue_id` = ?"; - DBManager::get()->execute($query, [$this->id]); - } - protected function cbDefaultValues() { if (empty($this->content['priority'])) { diff --git a/lib/models/Forum/ForumCategory.php b/lib/models/Forum/ForumCategory.php new file mode 100644 index 0000000..341011f --- /dev/null +++ b/lib/models/Forum/ForumCategory.php @@ -0,0 +1,107 @@ + ForumTopic::class, + 'foreign_key' => 'category_id', + 'assoc_foreign_key' => 'category_id', + 'order_by' => 'ORDER BY position ASC, mkdate DESC', + ]; + + $config['additional_fields']['metadata']['get'] = 'getMetaData'; + + $config['registered_callbacks']['after_delete'][] = 'onDelete'; + + parent::configure($config); + } + + public function getMetaData(): array + { + $user_id = \User::findCurrent()->user_id; + $object_user_visit = \ObjectUserVisit::findOneBySQL( + "object_id = :object_id AND plugin_id = :plugin_id AND user_id = :user_id", + [ + 'object_id' => $this->range_id, + 'plugin_id' => \PluginEngine::getPlugin(\CoreForum::class)->getPluginId(), + 'user_id' => $user_id, + ] + ); + + return DBManager::get()->fetchOne( + "SELECT + COUNT(DISTINCT`forum_topics`.`topic_id`) AS 'topics_count', + COUNT(DISTINCT `forum_discussions`.`discussion_id`) AS 'discussions_count', + COUNT(DISTINCT `forum_postings`.`posting_id`) AS 'postings_count', + COUNT(DISTINCT `forum_postings`.`user_id`) AS 'users_count', + MAX(`forum_postings`.`mkdate`) AS 'recent_activity', + ( + SELECT SUM(fpr.read_index) + FROM forum_topics ft2 + LEFT JOIN forum_discussions fd2 USING (`topic_id`) + JOIN forum_posting_reads fpr + ON fpr.discussion_id = fd2.discussion_id + AND fpr.user_id = :user_id + WHERE ft2.category_id = :category_id + ) AS 'user_read_index', + ( + SELECT + COUNT(DISTINCT fp.posting_id) + FROM forum_topics ft + JOIN forum_discussions fd USING(topic_id) + JOIN forum_postings fp ON fp.discussion_id = fd.discussion_id AND fp.mkdate > :last_visit + WHERE ft.category_id = :category_id + ) AS 'recent_postings_count' + FROM `forum_topics` + LEFT JOIN `forum_discussions` USING (`topic_id`) + LEFT JOIN `forum_postings` USING (`discussion_id`) + WHERE `forum_topics`.`category_id` = :category_id", + [ + 'category_id' => $this->category_id, + 'user_id' => $user_id, + 'last_visit' => $object_user_visit->last_visitdate ?? 0 + ] + ); + } + + public function transformData(): array + { + return [ + 'category_id' => $this->category_id, + 'range_id' => $this->range_id, + 'name' => $this->name, + 'description' => $this->description, + 'color' => $this->color, + 'position' => $this->position, + 'chdate' => date('c', $this->chdate), + 'mkdate' => date('c', $this->mkdate) + ]; + } + + public function onDelete() + { + DBManager::get()->execute( + "Update `forum_topics` SET `category_id` = null WHERE `category_id` = ?", + [$this->category_id] + ); + } +} diff --git a/lib/models/Forum/ForumDiscussion.php b/lib/models/Forum/ForumDiscussion.php new file mode 100644 index 0000000..53a0e4d --- /dev/null +++ b/lib/models/Forum/ForumDiscussion.php @@ -0,0 +1,201 @@ + ForumTopic::class, + 'foreign_key' => 'topic_id', + 'assoc_foreign_key' => 'topic_id' + ]; + + $config['belongs_to']['discussion_type'] = [ + 'class_name' => ForumDiscussionType::class, + 'foreign_key' => 'type_id', + 'assoc_foreign_key' => 'type_id' + ]; + + $config['has_many']['postings'] = [ + 'class_name' => ForumPosting::class, + 'foreign_key' => 'discussion_id', + 'assoc_foreign_key' => 'discussion_id' + ]; + + $config['has_many']['subscribers'] = [ + 'class_name' => ForumSubscription::class, + 'foreign_key' => 'discussion_id', + 'assoc_foreign_key' => 'discussion_id' + ]; + + $config['additional_fields']['range_id']['get'] = 'getRangeId'; + $config['additional_fields']['category']['get'] = 'getCategory'; + $config['additional_fields']['tags']['get'] = 'getTags'; + $config['additional_fields']['users']['get'] = 'getUsers'; + $config['additional_fields']['members']['get'] = 'getMembers'; + $config['additional_fields']['metadata']['get'] = 'getMetaData'; + + $config['registered_callbacks']['after_create'][] = 'onCreate'; + $config['registered_callbacks']['after_delete'][] = 'onDelete'; + + parent::configure($config); + } + + public function getTags() + { + return DBManager::get()->fetchAll( + "SELECT DISTINCT `tags_relations`.`tag_id`, `tags`.`name` FROM `tags` + LEFT JOIN `tags_relations` ON `tags`.`id` = `tags_relations`.`tag_id` + WHERE `tags_relations`.`range_id` = :discussion_id AND `tags`.`active` = TRUE + ORDER BY `tags`.`mkdate` DESC", + ['discussion_id' => $this->discussion_id], + function ($tag) { + return ForumTag::fromArray([ + 'id' => $tag['tag_id'], + 'name' => $tag['name'] + ]); + } + ); + } + + public function getCategory(): ?ForumCategory + { + return ForumCategory::findOneBySQL("JOIN forum_topics USING (category_id) WHERE forum_topics.topic_id = :topic_id", ['topic_id' => $this->topic_id]); + } + + public function getRangeId(): string + { + return $this->topic->range_id; + } + + public function getUsers($last_visit = null): array + { + $query = [ + "JOIN forum_postings USING(user_id) + WHERE forum_postings.discussion_id = :discussion_id + AND forum_postings.anonymous = FALSE", + ['discussion_id' => $this->discussion_id] + ]; + + if ($last_visit) { + $query[0] .= " AND forum_postings.mkdate > :last_visit"; + $query[1]["last_visit"] = $last_visit; + } + + $users = User::findBySQL($query[0]." ORDER BY forum_postings.mkdate DESC", $query[1]); + + $unique_users = []; + foreach ($users as $user) { + $unique_users[$user->user_id] = $user; + } + + return array_values($unique_users); + } + + public function getMembers($last_visit = null): array + { + $users = $this->getUsers($last_visit); + + $members = []; + foreach ($users as $user) { + $members[] = ForumMember::fromUser($user, $this->range_id); + } + + return $members; + } + + public function transformData(): array + { + return [ + 'discussion_id' => $this->discussion_id, + 'topic_id' => $this->topic_id, + 'type_id' => $this->type_id, + 'title' => $this->title, + 'sticky' => (int) $this->sticky, + 'closed_at' => $this->closed_at ? date('c', $this->closed_at) : '', + 'view_count' => (int) $this->view_count, + 'chdate' => date('c', $this->chdate), + 'mkdate' => date('c', $this->mkdate) + ]; + } + + public function getMetaData(): array + { + $user_id = \User::findCurrent()->user_id; + $object_user_visit = \ObjectUserVisit::findOneBySQL( + "object_id = :object_id AND plugin_id = :plugin_id AND user_id = :user_id", + [ + 'object_id' => $this->topic->range_id, + 'plugin_id' => \PluginEngine::getPlugin(\CoreForum::class)->getPluginId(), + 'user_id' => $user_id, + ] + ); + + return DBManager::get()->fetchOne( + "SELECT + COUNT(`posting_id`) 'postings_count', + MAX(`mkdate`) 'recent_activity', + ( + SELECT + `read_index` + FROM `forum_posting_reads` + WHERE `discussion_id` = :discussion_id AND `user_id` = :user_id + ) 'user_read_index', + ( + SELECT + COUNT(DISTINCT fp.posting_id) + FROM forum_postings fp + WHERE fp.discussion_id = :discussion_id AND fp.mkdate > :last_visit + ) AS 'recent_postings_count' + FROM `forum_postings` WHERE `discussion_id` = :discussion_id", + [ + 'discussion_id' => $this->discussion_id, + 'user_id' => $user_id, + 'last_visit' => $object_user_visit->last_visitdate ?? 0 + ] + ); + } + + public function onCreate() + { + $discussionNotification = new DiscussionNotification($this); + $discussionNotification->notifySubscribers(); + } + + public function onDelete() + { + ForumSubscription::deleteBySQL("subject_id = ?", [$this->discussion_id]); + ForumPosting::deleteBySQL("discussion_id = ?", [$this->discussion_id]); + ForumPostingRead::deleteBySQL("discussion_id = ?", [$this->discussion_id]); + } +} diff --git a/lib/models/Forum/ForumDiscussionType.php b/lib/models/Forum/ForumDiscussionType.php new file mode 100644 index 0000000..fe44add --- /dev/null +++ b/lib/models/Forum/ForumDiscussionType.php @@ -0,0 +1,36 @@ + ForumDiscussion::class, + 'foreign_key' => 'type_id' , + 'assoc_foreign_key' => 'type_id' + ]; + + parent::configure($config); + } + + public static function getForumDiscussionType(): array + { + return self::findBySQL("TRUE ORDER BY `mkdate` DESC"); + } +} diff --git a/lib/models/Forum/ForumPosting.php b/lib/models/Forum/ForumPosting.php new file mode 100644 index 0000000..bb14703 --- /dev/null +++ b/lib/models/Forum/ForumPosting.php @@ -0,0 +1,118 @@ + ForumDiscussion::class, + 'foreign_key' => 'discussion_id', + 'assoc_foreign_key' => 'discussion_id' + ]; + + $config['belongs_to']['posting'] = [ + 'class_name' => ForumPosting::class, + 'foreign_key' => 'parent_id', + 'assoc_foreign_key' => 'posting_id' + ]; + + $config['belongs_to']['user'] = [ + 'class_name' => User::class, + 'foreign_key' => 'user_id', + 'assoc_foreign_key' => 'user_id' + ]; + + $config['has_many']['reactions'] = [ + 'class_name' => ForumPostingReaction::class, + 'foreign_key' => 'posting_id', + 'assoc_foreign_key' => 'posting_id' + ]; + + $config['additional_fields']['author']['get'] = 'getAuthor'; + $config['registered_callbacks']['after_create'][] = 'onCreate'; + $config['registered_callbacks']['after_delete'][] = 'onDelete'; + + parent::configure($config); + } + + public function getAuthor(): ?ForumMember + { + if ($this->anonymous && $this->user_id !== User::findCurrent()->user_id) { + return ForumMember::fromArray(); + } + + $user = $this->user; + if ($user) { + return ForumMember::fromUser($user, $this->range_id); + } + + return null; + } + + public static function getRecentPosts($course_id, int $last_visit = 0): array + { + $query = [ + "SELECT + forum_discussions.*, + COUNT(DISTINCT forum_postings.posting_id) AS 'posts' + FROM forum_topics + JOIN forum_discussions USING(topic_id) + JOIN forum_postings USING(discussion_id) + WHERE forum_topics.range_id = :course_id + ", + [ + 'course_id' => $course_id + ] + ]; + + if ($last_visit) { + $query[0] .= " AND forum_postings.mkdate > :last_visit"; + $query[1]["last_visit"] = $last_visit; + } + + return \DBManager::get()->fetchAll( + $query[0]." GROUP BY discussion_id ORDER BY forum_postings.mkdate DESC", + $query[1] + ); + } + + public function getOpenGraphURLs() + { + $content = preg_replace("~(.*)~si", '', $this['content']); + return \OpenGraph::extract($content); + } + + public function onCreate() + { + $postingNotification = new PostingNotification($this); + $postingNotification->notifySubscribers(); + } + + public function onDelete() + { + ForumPostingReaction::deleteBySQL("posting_id = ?", [$this->posting_id]); + } +} diff --git a/lib/models/Forum/ForumPostingReaction.php b/lib/models/Forum/ForumPostingReaction.php new file mode 100644 index 0000000..f7141a9 --- /dev/null +++ b/lib/models/Forum/ForumPostingReaction.php @@ -0,0 +1,49 @@ + ForumPosting::class, + 'foreign_key' => 'posting_id', + 'assoc_foreign_key' => 'posting_id' + ]; + + $config['belongs_to']['user'] = [ + 'class_name' => User::class, + 'foreign_key' => 'user_id', + 'assoc_foreign_key' => 'user_id' + ]; + + parent::configure($config); + } +} diff --git a/lib/models/Forum/ForumPostingRead.php b/lib/models/Forum/ForumPostingRead.php new file mode 100644 index 0000000..8232236 --- /dev/null +++ b/lib/models/Forum/ForumPostingRead.php @@ -0,0 +1,64 @@ + ForumDiscussion::class, + 'foreign_key' => 'discussion_id', + 'assoc_foreign_key' => 'discussion_id' + ]; + + $config['belongs_to']['user'] = [ + 'class_name' => User::class, + 'foreign_key' => 'user_id', + 'assoc_foreign_key' => 'user_id' + ]; + + parent::configure($config); + } + + public static function updateUserReadPoint($user_id, $discussion_id, int $read_index = 0): ForumPostingRead + { + $postingRead = ForumPostingRead::findOneBySQL( + "discussion_id = :discussion_id AND user_id = :user_id", + [ + 'discussion_id' => $discussion_id, + 'user_id' => $user_id + ] + ); + + if (!$postingRead) { + $postingRead = new ForumPostingRead(); + $postingRead->discussion_id = $discussion_id; + $postingRead->user_id = $user_id; + } + + if (!$read_index) { + $read_index = $postingRead->read_index + 1; + } + + $postingRead->read_index = $read_index; + + $postingRead->store(); + + return $postingRead; + } +} diff --git a/lib/models/Forum/ForumSubscription.php b/lib/models/Forum/ForumSubscription.php new file mode 100644 index 0000000..a805975 --- /dev/null +++ b/lib/models/Forum/ForumSubscription.php @@ -0,0 +1,53 @@ + User::class, + 'foreign_key' => 'user_id', + 'assoc_foreign_key' => 'user_id' + ]; + + $config['belongs_to']['range'] = [ + 'class_name' => Course::class, + 'foreign_key' => 'range_id', + 'assoc_foreign_key' => 'Seminar_id' + ]; + + $config['additional_fields']['subject_object']['get'] = 'getSubjectObject'; + + parent::configure($config); + } + + public function getSubjectObject(): ForumDiscussion | ForumTopic + { + return match ($this->subject) { + 'topic' => ForumTopic::find($this->subject_id), + 'discussion' => ForumDiscussion::find($this->subject_id) + }; + } +} diff --git a/lib/models/Forum/ForumTopic.php b/lib/models/Forum/ForumTopic.php new file mode 100644 index 0000000..42227fd --- /dev/null +++ b/lib/models/Forum/ForumTopic.php @@ -0,0 +1,153 @@ + ForumCategory::class, + 'foreign_key' => 'category_id', + 'assoc_foreign_key' => 'category_id' + ]; + + $config['has_many']['discussions'] = [ + 'class_name' => ForumDiscussion::class, + 'foreign_key' => 'topic_id', + 'assoc_foreign_key' => 'topic_id', + ]; + + $config['additional_fields']['users']['get'] = 'getUsers'; + $config['additional_fields']['metadata']['get'] = 'getMetaData'; + $config['registered_callbacks']['after_delete'][] = 'onDelete'; + + parent::configure($config); + } + + public static function getCourseTopics($course_id) + { + return self::findBySQL( + "range_id = :course_id + GROUP BY CASE WHEN category_id IS NULL THEN topic_id ELSE category_id END + ORDER BY position ASC, mkdate DESC", + ["course_id" => $course_id] + ); + } + + public static function getCourseTopic($course_id, $topic_id) + { + return self::findOneBySQL("range_id = ? AND topic_id = ?", [$course_id, $topic_id]); + } + + public function getUsers($last_visit = null): array + { + $query = [ + "JOIN forum_postings USING(user_id) + JOIN forum_discussions USING(discussion_id) + WHERE forum_discussions.topic_id = :topic_id ", + ['topic_id' => $this->topic_id] + ]; + + if ($last_visit) { + $query[0] .= " AND forum_postings.mkdate > :last_visit"; + $query[1]["last_visit"] = $last_visit; + } + + $users = User::findBySQL($query[0]." ORDER BY forum_postings.mkdate DESC", $query[1]); + + $unique_users = []; + foreach ($users as $user) { + $unique_users[$user->user_id] = $user; + } + + return array_values($unique_users); + } + + public function getMetaData(): array + { + $user_id = User::findCurrent()->user_id; + $object_user_visit = \ObjectUserVisit::findOneBySQL( + "object_id = :object_id AND plugin_id = :plugin_id AND user_id = :user_id", + [ + 'object_id' => $this->range_id, + 'plugin_id' => \PluginEngine::getPlugin(\CoreForum::class)->getPluginId(), + 'user_id' => $user_id, + ] + ); + + return DBManager::get()->fetchOne( + "SELECT + COUNT(DISTINCT `forum_discussions`.`discussion_id`) AS 'discussions_count', + COUNT(DISTINCT `forum_postings`.`posting_id`) AS 'postings_count', + COUNT(DISTINCT `forum_postings`.`user_id`) AS 'users_count', + MAX(`forum_postings`.`mkdate`) AS 'recent_activity', + ( + SELECT + SUM(fpr.read_index) + FROM forum_discussions fd2 + JOIN forum_posting_reads fpr + ON fpr.discussion_id = fd2.discussion_id + AND fpr.user_id = :user_id + WHERE fd2.topic_id = :topic_id + ) AS 'user_read_index', + ( + SELECT + COUNT(DISTINCT fp.posting_id) + FROM forum_topics ft + JOIN forum_discussions fd USING(topic_id) + JOIN forum_postings fp ON fp.discussion_id = fd.discussion_id AND fp.mkdate > :last_visit + WHERE ft.topic_id = :topic_id + ) AS 'recent_postings_count' + FROM `forum_discussions` + LEFT JOIN `forum_postings` USING (`discussion_id`) + WHERE `forum_discussions`.`topic_id` = :topic_id", + [ + 'topic_id' => $this->topic_id, + 'user_id' => $user_id, + 'last_visit' => $object_user_visit->last_visitdate ?? 0 + ] + ); + } + + public function transformData(): array + { + return [ + 'topic_id' => $this->topic_id, + 'category_id' => $this->category_id, + 'range_id' => $this->range_id, + 'name' => $this->name, + 'description' => $this->description, + 'position' => $this->position, + 'chdate' => date('c', $this->chdate), + 'mkdate' => date('c', $this->mkdate) + ]; + } + + public function onDelete() + { + ForumSubscription::deleteBySQL("subject_id = ?", [$this->topic_id]); + ForumDiscussion::deleteBySQL("topic_id = ?", [$this->topic_id]); + } +} diff --git a/lib/models/ForumCat.php b/lib/models/ForumCat.php deleted file mode 100644 index 069f097..0000000 --- a/lib/models/ForumCat.php +++ /dev/null @@ -1,258 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 - * @category Stud.IP - * - * @property string $id alias column for category_id - * @property string $category_id database column - * @property string $seminar_id database column - * @property string $entry_name database column - * @property int $pos database column - */ - -class ForumCat extends SimpleORMap -{ - /** - * Configures this model. - * - * @param array $config Configuration array - */ - protected static function configure($config = []) - { - $config['db_table'] = 'forum_categories'; - parent::configure($config); - } - - /** - * Return a list of all areas with their categories. - * Empty categories are excluded by default - * - * @param string $seminar_id the seminar_id the retrieve the categories for - * @param string $exclude_null if false, empty categories are returned as well - * @return array list of categories - */ - public static function getListWithAreas($seminar_id, $exclude_null = true) - { - $stmt = DBManager::get()->prepare("SELECT * FROM forum_categories AS fc - LEFT JOIN forum_categories_entries AS fce USING (category_id) - WHERE seminar_id = ? " - . ($exclude_null ? 'AND fce.topic_id IS NOT NULL ' : '') - . "ORDER BY fc.pos ASC, fce.pos ASC"); - - $stmt->execute([$seminar_id]); - - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - /** - * Returns the name of the associated category for an area denoted by the - * passed topic_id - * - * @param string $topic_id - * @return string the name of the category - */ - public static function getCategoryNameForArea($topic_id) - { - $stmt = DBManager::get()->prepare("SELECT fc.entry_name FROM forum_categories AS fc - LEFT JOIN forum_categories_entries AS fce USING (category_id) - WHERE fce.topic_id = ?"); - $stmt->execute([$topic_id]); - - return $stmt->fetchColumn(); - } - - - /** - * Adds a new category with the passed name to the passed seminar and - * returns the id of the newly created category - * - * @param string $seminar_id - * @param string $name the name of the new category - * @return string the id of the newly created category - */ - public static function add($seminar_id, $name) - { - $stmt = DBManager::get()->prepare("INSERT INTO forum_categories - (category_id, seminar_id, entry_name) - VALUES (?, ?, ?)"); - - $category_id = md5(uniqid(rand())); - - $stmt->execute([$category_id, $seminar_id, $name]); - - return $category_id; - } - - - /** - * Remove the category with the passed id. The seminar_id is used only - * to be certain. - * - * @param string $category_id The ID of the category to be deleted - * @param string $seminar_id Seminar-ID the category belongs to - */ - public static function remove($category_id, $seminar_id) - { - // delete the category itself - $stmt = DBManager::get()->prepare("DELETE FROM - forum_categories - WHERE category_id = ?"); - $stmt->execute([$category_id]); - - // set all entries to default category - $stmt = DBManager::get()->prepare("UPDATE - forum_categories_entries - SET category_id = ?, pos = 999 - WHERE category_id = ?"); - $stmt->execute([$seminar_id, $category_id]); - } - - - /** - * Set the position for the passed category to the passed value - * - * @param string $category_id the ID of the category to update - * @param int $pos the new position - */ - public static function setPosition($category_id, $pos) - { - $stmt = DBManager::get()->prepare("UPDATE - forum_categories - SET pos = ? WHERE category_id = ?"); - $stmt->execute([$pos, $category_id]); - } - - - /** - * Add the passed area to the passed category and remove it from all - * other categories. - * - * @param string $category_id the ID of the category - * @param string $area_id the ID of the area to add the category to - */ - public static function addArea($category_id, $area_id) - { - // remove area from all other categories - $stmt = DBManager::get()->prepare("DELETE FROM - forum_categories_entries - WHERE topic_id = ?"); - $stmt->execute([$area_id]); - - // add area to this category, make sure it is at the end - $stmt = DBManager::get()->prepare("SELECT COUNT(*) FROM - forum_categories_entries - WHERE category_id = ?"); - $stmt->execute([$category_id]); - $new_pos = $stmt->fetchColumn() + 1; - - $stmt = DBManager::get()->prepare("REPLACE INTO - forum_categories_entries - (category_id, topic_id, pos) VALUES (?, ?, ?)"); - $stmt->execute([$category_id, $area_id, $new_pos]); - } - - - /** - * Remove the passed area from all categories. - * - * @param string $area_id the ID of the area to be removed - */ - public static function removeArea($area_id) - { - $stmt = DBManager::get()->prepare("DELETE FROM - forum_categories_entries - WHERE topic_id = ?"); - $stmt->execute([$area_id]); - } - - - /** - * Set the position for the passed category to the passed value - * - * @param string $area_id the ID of the area to update - * @param int $pos the new position - */ - public static function setAreaPosition($area_id, $pos) - { - $stmt = DBManager::get()->prepare("UPDATE - forum_categories_entries - SET pos = ? WHERE topic_id = ?"); - $stmt->execute([$pos, $area_id]); - } - - - /** - * Set the name for the passed category - * - * @param string $category_id the ID of the category to update - * @param string $name the name to set - */ - public static function setName($category_id, $name) - { - $stmt = DBManager::get()->prepare("UPDATE - forum_categories - SET entry_name = ? WHERE category_id = ?"); - $stmt->execute([$name, $category_id]); - } - - /** - * Return the data for the passed category_id - * - * @param string $category_id - * - * @return array the data for the passed category_id - */ - public static function get($category_id) - { - $stmt = DBManager::get()->prepare("SELECT * FROM forum_categories - WHERE category_id = ?"); - $stmt->execute([$category_id]); - - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - /** - * Return the areas for the passed category_id - * - * @param string $category_id - * @param int $start limit start (optional) - * @param int $num number of entries to fetch (optional, default is 20) - * - * @return array the data for the passed category_id - */ - public static function getAreas($category_id, $start = null, $num = 20) - { - $category = self::get($category_id); - - $limit = ''; - if ($start !== null && $num) { - $limit = " LIMIT $start, $num"; - } - - if ($category_id == $category['seminar_id']) { - $stmt = DBManager::get()->prepare("SELECT fe.* FROM forum_entries AS fe - LEFT JOIN forum_categories_entries AS fce USING (topic_id) - WHERE seminar_id = ? AND depth = 1 AND ( - fce.category_id = ? OR fce.category_id IS NULL - ) ORDER BY category_id DESC, pos ASC" . $limit); - $stmt->execute([$category_id, $category_id]); - } else { - $stmt = DBManager::get()->prepare("SELECT forum_entries.* FROM forum_categories_entries - LEFT JOIN forum_entries USING(topic_id) - WHERE category_id = ? - ORDER BY pos ASC" . $limit); - - $stmt->execute([$category_id]); - } - - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } -} diff --git a/lib/models/PersonalNotifications.php b/lib/models/PersonalNotifications.php index 1d753eb..36a6187 100644 --- a/lib/models/PersonalNotifications.php +++ b/lib/models/PersonalNotifications.php @@ -98,7 +98,7 @@ class PersonalNotifications extends SimpleORMap * this html-element the notification will be marked as read, so the user * does not need to handle the information twice. Optional. Default: null * @param null|Icon|string $avatar : either an Icon or a URL of an - * image for the notification. Best size: 40px x 40px + * image for the notification. Best size: 40px x 40px or an HTML emoji code. * @return boolean : true on success */ public static function add($user_ids, $url, $text, $html_id = null, $avatar = null, $dialog = false) diff --git a/lib/models/User.php b/lib/models/User.php index 18ab436..af4f75f 100644 --- a/lib/models/User.php +++ b/lib/models/User.php @@ -1203,11 +1203,6 @@ class User extends AuthUserMd5 implements Range, PrivacyObject, Studip\Calendar\ // Restliche Daten übertragen - // ForumsModule migrieren - foreach (PluginEngine::getPlugins(ForumModule::class) as $plugin) { - $plugin->migrateUser($old_id, $new_id); - } - // Dateieintragungen und Ordner // TODO (mlunzena) should post a notification $query = "UPDATE IGNORE file_refs SET user_id = ? WHERE user_id = ?"; diff --git a/lib/modules/CoreForum.php b/lib/modules/CoreForum.php index c9a23d1..0c2b5e5 100644 --- a/lib/modules/CoreForum.php +++ b/lib/modules/CoreForum.php @@ -1,189 +1,92 @@ - * @copyright 2011 ELAN e.V. - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP + * @author Murtaza Sultani + * @author Rasmus Fuhse + * @license GPL2 or any later version + * @since Stud.IP 6.1 */ -// Notifications -NotificationCenter::addObserver('CoreForum', 'overviewDidClear', 'OverviewDidClear'); -NotificationCenter::addObserver('CoreForum', 'removeAbosForUserAndCourse', 'UserDidLeaveCourse'); - -NotificationCenter::addObserver('ForumActivity', 'newEntry', 'ForumAfterInsert'); -NotificationCenter::addObserver('ForumActivity', 'updateEntry', 'ForumAfterUpdate'); -NotificationCenter::addObserver('ForumActivity', 'deleteEntry', 'ForumBeforeDelete'); +use Forum\ForumPosting; -NotificationCenter::addObserver('ForumIssue', 'unlinkIssue', 'ForumBeforeDelete'); - -class CoreForum extends CorePlugin implements ForumModule +class CoreForum extends CorePlugin implements StudipModule { - /* interface method */ public function getTabNavigation($course_id) { - $navigation = new Navigation(_('Forum'), 'dispatch.php/course/forum/index'); - $navigation->setImage(Icon::create('forum', 'info_alt')); + $navigation = new Navigation(_('Forum'), 'dispatch.php/course/forum/topics'); - // add main third-level navigation-item - $navigation->addSubNavigation('index', new Navigation(_('Übersicht'), 'dispatch.php/course/forum/index')); + $navigation->setImage(Icon::create('forum', 'info_alt')); - if (ForumPerm::has('fav_entry', $course_id)) { - $navigation->addSubNavigation('newest', new Navigation(_("Neue Beiträge"), 'dispatch.php/course/forum/index/newest')); - $navigation->addSubNavigation('latest', new Navigation(_("Letzte Beiträge"), 'dispatch.php/course/forum/index/latest')); - $navigation->addSubNavigation('favorites', new Navigation(_('Gemerkte Beiträge'), 'dispatch.php/course/forum/index/favorites')); + $navigation->addSubNavigation( + 'topics', + new Navigation(_('Themenübersicht'), 'dispatch.php/course/forum/topics') + ); - // mass-administrate the forum - if (ForumPerm::has('admin', $course_id)) { - $navigation->addSubNavigation('admin', new Navigation(_('Administration'), 'dispatch.php/course/forum/admin')); - } + if (!CourseConfig::get($course_id)->FORUM_HIDE_CATEGORIES_NAVIGATION) { + $navigation->addSubNavigation( + 'categories', + new Navigation(_('Kategorien'), 'dispatch.php/course/forum/categories') + ); } - return ['forum2' => $navigation]; + $navigation->addSubNavigation( + 'subscriptions', + new Navigation(_('Abonnierte Diskussionen'), 'dispatch.php/course/forum/subscriptions') + ); + + return ['forum' => $navigation]; } - /* interface method */ - public function getIconNavigation($course_id, $last_visit, $user_id = null) + public function getIconNavigation($course_id, $last_visit, $user_id) { + $recent_posts_count = 0; + $navigation_title = _('Forum'); + if ($GLOBALS['perm']->have_studip_perm('user', $course_id)) { - $num_entries = ForumVisit::getCount($course_id, ForumVisit::getVisit($course_id)); - $text = ForumHelpers::getVisitText($num_entries, $course_id); - } else { - $num_entries = 0; - $text = 'Forum'; + $recent_posts = ForumPosting::getRecentPosts($course_id, $last_visit); + $recent_posts_count = array_sum(array_column($recent_posts, 'posts')); + + if ($recent_posts_count > 0) { + $navigation_title = sprintf(_('%s neue Beiträge seit Ihrem letzten Besuch.'), $recent_posts_count); + } else { + $navigation_title = _('Keine neuen Beiträge seit Ihrem letzten Besuch.'); + } } - $navigation = new Navigation('forum', 'dispatch.php/course/forum/index/enter_seminar'); - $navigation->setBadgeNumber($num_entries); - $navigation->setLinkAttributes(['title' => $text]); + $navigation = new Navigation(_("Forum")); + $navigation->setBadgeNumber($recent_posts_count); + + $navigation->setLinkAttributes(['title' => $navigation_title]); - if ($num_entries > 0) { + if ($recent_posts_count > 0) { $navigation->setImage(Icon::create('forum', Icon::ROLE_ATTENTION)); + $navigation->setURL('dispatch.php/course/forum/recent', ['last_visit' => $last_visit]); } else { $navigation->setImage(Icon::create('forum')); + $navigation->setURL('dispatch.php/course/forum/topics'); } return $navigation; } - /** - * This method is called, whenever an user clicked to clear the visit timestamps - * and set everything as visited - * - * @param object $notification - * @param string $user_id - */ - public static function overviewDidClear($notification, $user_id) - { - $query = "REPLACE INTO `forum_visits` - SELECT `user_id`, `Seminar_id`, UNIX_TIMESTAMP(), UNIX_TIMESTAMP() - FROM `seminar_user` - WHERE `user_id` = ?"; - DBManager::get()->execute($query, [$user_id]); - } - - /** - * This method is called whenever a user is removed from a course and thus - * the forum abos will be removed. - * - * @param object $notification - * @param string $course_id - * @param string $user_id - */ - public static function removeAbosForUserAndCourse($notification, $course_id, $user_id) - { - ForumAbo::removeForCourseAndUser($course_id, $user_id); - } - public function getInfoTemplate($course_id) { - return null; - } - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - /* * IMPLEMENTATION OF METHODS FROM FORUMMODULE-INTERFACE * */ - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - public function getLinkToThread($issue_id) - { - if ($topic_id = ForumIssue::getThreadIdForIssue($issue_id)) { - return URLHelper::getLink('dispatch.php/course/forum/index/index/' . $topic_id); - } - - return false; - } - - public function setThreadForIssue($issue_id, $title, $content) - { - ForumIssue::setThreadForIssue(Context::getId(), $issue_id, $title, $content); - } - - public function getNumberOfPostingsForUser($user_id, $seminar_id = null) - { - return ForumEntry::countUserEntries($user_id, $seminar_id); - } - - public function getNumberOfPostingsForIssue($issue_id) - { - $topic_id = ForumIssue::getThreadIdForIssue($issue_id); - - return $topic_id ? ForumEntry::countEntries($topic_id) : 0; - } - - public function getNumberOfPostingsForSeminar($seminar_id) - { - return floor(ForumEntry::countEntries($seminar_id)); - } - - public function getNumberOfPostings() - { - return ForumEntry::countAllEntries(); - } - - public function getEntryTableInfo() - { - return [ - 'table' => 'forum_entries', - 'content' => 'content', - 'chdate' => 'chdate', - 'seminar_id' => 'seminar_id', - 'user_id' => 'user_id' - ]; + // TODO: Implement getInfoTemplate() method. } - public function getTopTenSeminars() + public static function isAdmin($course_id): bool { - return ForumEntry::getTopTenSeminars(); + return $GLOBALS['perm']->have_perm('root') || $GLOBALS['perm']->have_studip_perm('dozent', $course_id); } - public function migrateUser($user_from, $user_to) + public static function isModerator($course_id): bool { - ForumEntry::migrateUser($user_from, $user_to); + return self::isAdmin($course_id) || + CourseConfig::get($course_id)->FORUM_MODERATION_PERMISSION === $GLOBALS['perm']->get_studip_perm($course_id) || + CourseConfig::get($course_id)->FORUM_MODERATION_PERMISSION === 'all'; } - public function deleteContents($seminar_id) - { - ForumEntry::delete($seminar_id); - } - - public function getDump($seminar_id) - { - return ForumEntry::getDump($seminar_id); - } - - public static function getDescription() - { - return _('Textbasierte und zeit- und ortsunabhängige '. - 'Diskursmöglichkeit. Lehrende können parallel zu '. - 'Veranstaltungsthemen Fragen stellen, die von den Studierenden '. - 'per Meinungsaustausch besprochen werden.'); - } /** * {@inheritdoc} @@ -201,11 +104,17 @@ class CoreForum extends CorePlugin implements ForumModule 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Forum', 'pictures' => [ - 0 => ['source' => 'Uebersicht.jpg', 'title' => _('Übersicht')], - 1 => ['source' => 'Beitrag.jpg', 'title' => _('Beitrag')], - 2 => ['source' => 'Beitrag_verfassen.jpg', 'title' => _('Beitrag verfassen')], + ['source' => 'Lehrendensicht_-_Kategorien_mit_Bereichen_und_Beitraegen.jpg'], + ['source' => 'Studentische_Sicht_-_Kategorien_mit_Bereichen_und_Beitraegen.jpg'], + ['source' => 'Einen_Forumsbeitrag_erstellen.jpg'], ] ] ]; } + + public static function deleteCourseContents($course_id) + { + \Forum\ForumCategory::deleteBySQL("range_id = ?", [$course_id]); + \Forum\ForumTopic::deleteBySQL("range_id = ?", [$course_id]); + } } diff --git a/lib/navigation/AdminNavigation.php b/lib/navigation/AdminNavigation.php index fe31b72..3c92b62 100644 --- a/lib/navigation/AdminNavigation.php +++ b/lib/navigation/AdminNavigation.php @@ -176,6 +176,14 @@ class AdminNavigation extends Navigation 'dispatch.php/admin/tags/index' ) ); + + $navigation->addSubNavigation( + 'forum_discussion_types', + new Navigation( + _('Forum (Diskussionstypen)'), + 'dispatch.php/course/forum/discussion_types' + ) + ); } $this->addSubNavigation('locations', $navigation); diff --git a/lib/object.inc.php b/lib/object.inc.php index a8bd5d7..a053bf4 100644 --- a/lib/object.inc.php +++ b/lib/object.inc.php @@ -359,6 +359,7 @@ function object_type_to_id($type) 'schedule' => 'CoreSchedule', 'scm' => 'CoreScm', 'wiki' => 'CoreWiki', + 'forum' => 'CoreForum', 'elearning_interface' => 'CoreElearningInterface', 'ilias_interface' => 'IliasInterfaceModule', 'participants' => 'CoreParticipants' @@ -397,6 +398,7 @@ function object_id_to_type($id) 'schedule' => 'CoreSchedule', 'scm' => 'CoreScm', 'wiki' => 'CoreWiki', + 'forum' => 'CoreForum', 'elearning_interface' => 'CoreElearningInterface', 'ilias_interface' => 'IliasInterfaceModule', 'participants' => 'CoreParticipants' diff --git a/lib/plugins/core/ForumModule.php b/lib/plugins/core/ForumModule.php deleted file mode 100644 index e72ffc2..0000000 --- a/lib/plugins/core/ForumModule.php +++ /dev/null @@ -1,140 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-3.0.html GPL version 3 - * @category Stud.IP - */ - -interface ForumModule extends StandardPlugin -{ - /** - * Issues can be connected with an entry in a forum. This method - * has to return an url to the connected topic for the passed issue_id. - * If no topic is connected, it has to return "false" - * - * @param string $issue_id - * @return mixed URL or false - */ - function getLinkToThread($issue_id); - - /** - * This method is called in case of an creation OR an update of an issue. - * Normally one would update the title and the content of the linked topic - * when called - * - * @param string $issue_id - * @param string $title the title of the issue - * @param string $content the description of the issue - */ - function setThreadForIssue($issue_id, $title, $content); - - /** - * Return the number of postings the connected topic contains for - * the issue with the passed id - * - * @param type $issue_id - * - * @return int - */ - function getNumberOfPostingsForIssue($issue_id); - - /** - * Return the number of postings for the passed user - * - * @param type $user_id - * - * @return int - */ - function getNumberOfPostingsForUser($user_id); - - /** - * Return the number of postings for the passed seminar - * - * @param type $seminar_id - * - * @return int - */ - function getNumberOfPostingsForSeminar($seminar_id); - - /** - * Return the number of all postings served by your module. The - * results are used for statistics. - * - * @return int - */ - function getNumberOfPostings(); - - /** - * This function is called whenever Stud.IP needs to directly operate - * on your entries-table. Your entries-table MUST have at least fields - * for a date (a change-date is preferred, but make-date will suffice), - * posting-content, seminar_id and user_id. - * - * The returning array must have the following structure: - * Array ( - * 'table' => 'your_entry_table, - * 'content' => 'your_content_field', - * 'chdate' => 'your_date_field', - * 'seminar_id' => 'your_seminar_id_field', - * 'user_id' => 'your_user_id_field' - * ) - * - * @return array - */ - function getEntryTableInfo(); - - /** - * The caller expects an array of the ten seminars with the most postings - * in your module. - * - * Return an array of the following structure: - * Array ( - * Array ( - * 'seminar_id' => - * 'display' => - * 'count' => - * ) - * ) - * - * @return array - */ - function getTopTenSeminars(); - - /** - * Is called when the data of a user is moved to another user. - * Update all user_ids with the passed new one. - * - * @param string $user_from the user_id of the user who has the data - * @param string $user_to the user_id of the user who shall receive the data - */ - function migrateUser($user_from, $user_to); - - /** - * Clean up everything for the passed seminar, because the seminar - * is beeing deleted. - * - * @param string $seminar_id - */ - function deleteContents($seminar_id); - - /** - * Return a complete HTML-Dump of all entries in the forum-module. This is - * used for archiving purposes, so make it pretty! - * - * @param string $seminar_id - * - * @return string a single-page HTML-view of all contents in one string - */ - function getDump($seminar_id); -} diff --git a/package-lock.json b/package-lock.json index c552be9..1c56c59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "6.1.0", "license": "GPL-2.0", "dependencies": { - "@vojtechlanka/vue-tags-input": "^3.1.1" + "@vojtechlanka/vue-tags-input": "^3.1.1", + "jsonapi-serializer": "^3.6.9" }, "devDependencies": { "@axe-core/playwright": "^4.6.1", @@ -6367,7 +6368,6 @@ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", "dev": true, - "optional": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -6381,7 +6381,6 @@ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", "dev": true, - "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" @@ -7008,43 +7007,6 @@ "node": ">=10.13.0" } }, - "node_modules/copy-webpack-plugin": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.0.tgz", - "integrity": "sha512-FgR/h5a6hzJqATDGd9YG41SeDViH+0bkHn6WNXCi5zKAZkeESeSxLySSsFLHqLEVCh0E+rITmCf0dusXWYukeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-parent": "^6.0.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^6.0.2", - "tinyglobby": "^0.2.12" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/core-js": { "version": "3.31.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz", @@ -8216,7 +8178,6 @@ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, - "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -8435,7 +8396,6 @@ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "optional": true, "engines": { "node": ">= 0.4" } @@ -8445,7 +8405,6 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, - "optional": true, "engines": { "node": ">= 0.4" } @@ -8461,7 +8420,6 @@ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, - "optional": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -9539,7 +9497,6 @@ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "dev": true, - "optional": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", @@ -9586,7 +9543,6 @@ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, - "optional": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -9764,7 +9720,6 @@ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "optional": true, "engines": { "node": ">= 0.4" }, @@ -9844,7 +9799,6 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, - "optional": true, "engines": { "node": ">= 0.4" }, @@ -9880,7 +9834,6 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, - "optional": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -10189,6 +10142,12 @@ "node": ">=0.8.19" } }, + "node_modules/inflected": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/inflected/-/inflected-1.1.7.tgz", + "integrity": "sha512-3lz7idKIPmKvz0wqlu1PUPSg5strJnCh2v2NldPQy13Fmd6WsWQ5yExDoiIX48lQ9mo8N7ztdDlkZxOauZ/E5g==", + "license": "MIT" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -11378,6 +11337,19 @@ "node": ">=6" } }, + "node_modules/jsonapi-serializer": { + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/jsonapi-serializer/-/jsonapi-serializer-3.6.9.tgz", + "integrity": "sha512-LeRPlP93Mz6+Klu13OKcnXNLvtH1gbeo/yfThqihAMw7vUBCWWs6jHImpR/tQwzAxJi7F1+bfVJxeHoNCrbZiQ==", + "license": "MIT", + "dependencies": { + "inflected": "^1.1.6", + "lodash": "^4.16.3" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -11551,8 +11523,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash-es": { "version": "4.17.21", @@ -11686,7 +11657,6 @@ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, - "optional": true, "engines": { "node": ">= 0.4" } diff --git a/package.json b/package.json index cce9fac..6944c9c 100644 --- a/package.json +++ b/package.json @@ -155,6 +155,7 @@ "output": "./.reports/eslint-report.xml" }, "dependencies": { - "@vojtechlanka/vue-tags-input": "^3.1.1" + "@vojtechlanka/vue-tags-input": "^3.1.1", + "jsonapi-serializer": "^3.6.9" } } diff --git a/public/assets/images/forum/forum-keyvisual-positive.svg b/public/assets/images/forum/forum-keyvisual-positive.svg new file mode 100644 index 0000000..2dafbf3 --- /dev/null +++ b/public/assets/images/forum/forum-keyvisual-positive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/add-reaction.svg b/public/assets/images/icons/black/add-reaction.svg new file mode 100644 index 0000000..cdbfeaf --- /dev/null +++ b/public/assets/images/icons/black/add-reaction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/pin.svg b/public/assets/images/icons/black/pin.svg new file mode 100644 index 0000000..403a300 --- /dev/null +++ b/public/assets/images/icons/black/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/quote.svg b/public/assets/images/icons/black/quote.svg new file mode 100644 index 0000000..1719331 --- /dev/null +++ b/public/assets/images/icons/black/quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/quote2.svg b/public/assets/images/icons/black/quote2.svg new file mode 100644 index 0000000..161cebb --- /dev/null +++ b/public/assets/images/icons/black/quote2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/subscription-all.svg b/public/assets/images/icons/black/subscription-all.svg new file mode 100644 index 0000000..800ecff --- /dev/null +++ b/public/assets/images/icons/black/subscription-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/subscription-end.svg b/public/assets/images/icons/black/subscription-end.svg new file mode 100644 index 0000000..57e237f --- /dev/null +++ b/public/assets/images/icons/black/subscription-end.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/subscription-none.svg b/public/assets/images/icons/black/subscription-none.svg new file mode 100644 index 0000000..a5260aa --- /dev/null +++ b/public/assets/images/icons/black/subscription-none.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/black/subscription-quotes.svg b/public/assets/images/icons/black/subscription-quotes.svg new file mode 100644 index 0000000..61add1b --- /dev/null +++ b/public/assets/images/icons/black/subscription-quotes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/add-reaction.svg b/public/assets/images/icons/blue/add-reaction.svg new file mode 100644 index 0000000..75997f3 --- /dev/null +++ b/public/assets/images/icons/blue/add-reaction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/pin.svg b/public/assets/images/icons/blue/pin.svg new file mode 100644 index 0000000..958cd0f --- /dev/null +++ b/public/assets/images/icons/blue/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/quote.svg b/public/assets/images/icons/blue/quote.svg new file mode 100644 index 0000000..f4661e5 --- /dev/null +++ b/public/assets/images/icons/blue/quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/quote2.svg b/public/assets/images/icons/blue/quote2.svg new file mode 100644 index 0000000..97b7272 --- /dev/null +++ b/public/assets/images/icons/blue/quote2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/subscription-all.svg b/public/assets/images/icons/blue/subscription-all.svg new file mode 100644 index 0000000..a9bdc0c --- /dev/null +++ b/public/assets/images/icons/blue/subscription-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/subscription-end.svg b/public/assets/images/icons/blue/subscription-end.svg new file mode 100644 index 0000000..d9f6583 --- /dev/null +++ b/public/assets/images/icons/blue/subscription-end.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/subscription-none.svg b/public/assets/images/icons/blue/subscription-none.svg new file mode 100644 index 0000000..8b2f6ad --- /dev/null +++ b/public/assets/images/icons/blue/subscription-none.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/blue/subscription-quotes.svg b/public/assets/images/icons/blue/subscription-quotes.svg new file mode 100644 index 0000000..477b75e --- /dev/null +++ b/public/assets/images/icons/blue/subscription-quotes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/add-reaction.svg b/public/assets/images/icons/green/add-reaction.svg new file mode 100644 index 0000000..daf0741 --- /dev/null +++ b/public/assets/images/icons/green/add-reaction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/pin.svg b/public/assets/images/icons/green/pin.svg new file mode 100644 index 0000000..0510655 --- /dev/null +++ b/public/assets/images/icons/green/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/quote.svg b/public/assets/images/icons/green/quote.svg new file mode 100644 index 0000000..0c94106 --- /dev/null +++ b/public/assets/images/icons/green/quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/quote2.svg b/public/assets/images/icons/green/quote2.svg new file mode 100644 index 0000000..ea1095e --- /dev/null +++ b/public/assets/images/icons/green/quote2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/subscription-all.svg b/public/assets/images/icons/green/subscription-all.svg new file mode 100644 index 0000000..40f3e4d --- /dev/null +++ b/public/assets/images/icons/green/subscription-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/subscription-end.svg b/public/assets/images/icons/green/subscription-end.svg new file mode 100644 index 0000000..decbb13 --- /dev/null +++ b/public/assets/images/icons/green/subscription-end.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/subscription-none.svg b/public/assets/images/icons/green/subscription-none.svg new file mode 100644 index 0000000..44aa356 --- /dev/null +++ b/public/assets/images/icons/green/subscription-none.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/green/subscription-quotes.svg b/public/assets/images/icons/green/subscription-quotes.svg new file mode 100644 index 0000000..414391d --- /dev/null +++ b/public/assets/images/icons/green/subscription-quotes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/add-reaction.svg b/public/assets/images/icons/grey/add-reaction.svg new file mode 100644 index 0000000..4d8fde0 --- /dev/null +++ b/public/assets/images/icons/grey/add-reaction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/pin.svg b/public/assets/images/icons/grey/pin.svg new file mode 100644 index 0000000..b987ec9 --- /dev/null +++ b/public/assets/images/icons/grey/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/quote.svg b/public/assets/images/icons/grey/quote.svg new file mode 100644 index 0000000..400ac85 --- /dev/null +++ b/public/assets/images/icons/grey/quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/quote2.svg b/public/assets/images/icons/grey/quote2.svg new file mode 100644 index 0000000..41d576c --- /dev/null +++ b/public/assets/images/icons/grey/quote2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/subscription-all.svg b/public/assets/images/icons/grey/subscription-all.svg new file mode 100644 index 0000000..777b726 --- /dev/null +++ b/public/assets/images/icons/grey/subscription-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/subscription-end.svg b/public/assets/images/icons/grey/subscription-end.svg new file mode 100644 index 0000000..88deffc --- /dev/null +++ b/public/assets/images/icons/grey/subscription-end.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/subscription-none.svg b/public/assets/images/icons/grey/subscription-none.svg new file mode 100644 index 0000000..9330871 --- /dev/null +++ b/public/assets/images/icons/grey/subscription-none.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/grey/subscription-quotes.svg b/public/assets/images/icons/grey/subscription-quotes.svg new file mode 100644 index 0000000..ec7200f --- /dev/null +++ b/public/assets/images/icons/grey/subscription-quotes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/add-reaction.svg b/public/assets/images/icons/red/add-reaction.svg new file mode 100644 index 0000000..ce083e4 --- /dev/null +++ b/public/assets/images/icons/red/add-reaction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/pin.svg b/public/assets/images/icons/red/pin.svg new file mode 100644 index 0000000..b1e869d --- /dev/null +++ b/public/assets/images/icons/red/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/quote.svg b/public/assets/images/icons/red/quote.svg new file mode 100644 index 0000000..3191c5a --- /dev/null +++ b/public/assets/images/icons/red/quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/quote2.svg b/public/assets/images/icons/red/quote2.svg new file mode 100644 index 0000000..218d4b6 --- /dev/null +++ b/public/assets/images/icons/red/quote2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/subscription-all.svg b/public/assets/images/icons/red/subscription-all.svg new file mode 100644 index 0000000..bd57981 --- /dev/null +++ b/public/assets/images/icons/red/subscription-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/subscription-end.svg b/public/assets/images/icons/red/subscription-end.svg new file mode 100644 index 0000000..fe68849 --- /dev/null +++ b/public/assets/images/icons/red/subscription-end.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/subscription-none.svg b/public/assets/images/icons/red/subscription-none.svg new file mode 100644 index 0000000..e4e136d --- /dev/null +++ b/public/assets/images/icons/red/subscription-none.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/red/subscription-quotes.svg b/public/assets/images/icons/red/subscription-quotes.svg new file mode 100644 index 0000000..d724b86 --- /dev/null +++ b/public/assets/images/icons/red/subscription-quotes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/add-reaction.svg b/public/assets/images/icons/white/add-reaction.svg new file mode 100644 index 0000000..4b378cd --- /dev/null +++ b/public/assets/images/icons/white/add-reaction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/pin.svg b/public/assets/images/icons/white/pin.svg new file mode 100644 index 0000000..5ab8bec --- /dev/null +++ b/public/assets/images/icons/white/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/quote.svg b/public/assets/images/icons/white/quote.svg new file mode 100644 index 0000000..f188165 --- /dev/null +++ b/public/assets/images/icons/white/quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/quote2.svg b/public/assets/images/icons/white/quote2.svg new file mode 100644 index 0000000..debca48 --- /dev/null +++ b/public/assets/images/icons/white/quote2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/subscription-all.svg b/public/assets/images/icons/white/subscription-all.svg new file mode 100644 index 0000000..73fccbd --- /dev/null +++ b/public/assets/images/icons/white/subscription-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/subscription-end.svg b/public/assets/images/icons/white/subscription-end.svg new file mode 100644 index 0000000..8444a64 --- /dev/null +++ b/public/assets/images/icons/white/subscription-end.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/subscription-none.svg b/public/assets/images/icons/white/subscription-none.svg new file mode 100644 index 0000000..8b704dd --- /dev/null +++ b/public/assets/images/icons/white/subscription-none.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/white/subscription-quotes.svg b/public/assets/images/icons/white/subscription-quotes.svg new file mode 100644 index 0000000..2f2fecc --- /dev/null +++ b/public/assets/images/icons/white/subscription-quotes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/add-reaction.svg b/public/assets/images/icons/yellow/add-reaction.svg new file mode 100644 index 0000000..33904d2 --- /dev/null +++ b/public/assets/images/icons/yellow/add-reaction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/pin.svg b/public/assets/images/icons/yellow/pin.svg new file mode 100644 index 0000000..ef2a97f --- /dev/null +++ b/public/assets/images/icons/yellow/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/quote.svg b/public/assets/images/icons/yellow/quote.svg new file mode 100644 index 0000000..bc16425 --- /dev/null +++ b/public/assets/images/icons/yellow/quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/quote2.svg b/public/assets/images/icons/yellow/quote2.svg new file mode 100644 index 0000000..ca6f4bc --- /dev/null +++ b/public/assets/images/icons/yellow/quote2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/subscription-all.svg b/public/assets/images/icons/yellow/subscription-all.svg new file mode 100644 index 0000000..fda754d --- /dev/null +++ b/public/assets/images/icons/yellow/subscription-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/subscription-end.svg b/public/assets/images/icons/yellow/subscription-end.svg new file mode 100644 index 0000000..bd5f165 --- /dev/null +++ b/public/assets/images/icons/yellow/subscription-end.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/subscription-none.svg b/public/assets/images/icons/yellow/subscription-none.svg new file mode 100644 index 0000000..e6a4d30 --- /dev/null +++ b/public/assets/images/icons/yellow/subscription-none.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/icons/yellow/subscription-quotes.svg b/public/assets/images/icons/yellow/subscription-quotes.svg new file mode 100644 index 0000000..7323021 --- /dev/null +++ b/public/assets/images/icons/yellow/subscription-quotes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js index 4af6ed9..86d1f9e 100644 --- a/resources/assets/javascripts/init.js +++ b/resources/assets/javascripts/init.js @@ -30,7 +30,6 @@ import Files from './lib/files.js'; import FilesDashboard from './lib/files_dashboard.js'; import Folders from './lib/folders.js'; import Forms from './lib/forms.js'; -import Forum from './lib/forum.js'; import Fullcalendar from './lib/fullcalendar.js'; import Fullscreen from './lib/fullscreen.js'; import GlobalSearch from './lib/global_search.js'; @@ -111,7 +110,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, { FilesDashboard, Folders, Forms, - Forum, Fullcalendar, Fullscreen, Gettext, diff --git a/resources/assets/javascripts/lib/forum.js b/resources/assets/javascripts/lib/forum.js deleted file mode 100644 index d7a625b..0000000 --- a/resources/assets/javascripts/lib/forum.js +++ /dev/null @@ -1,853 +0,0 @@ -import { $gettext } from "./gettext"; -import eventBus from "./event-bus.ts"; - -eventBus.on('studip:set-locale', () => { - Forum.warning_text = $gettext('Wenn Sie die Seite verlassen, gehen ihre Änderungen verloren!'); -}); - -const Forum = { - confirmDialog: null, - current_area_id: null, - current_category_id: null, - seminar_id: null, - warning_text: 'Wenn Sie die Seite verlassen, gehen ihre Änderungen verloren!', - clipboard: {}, - - getTemplate: _.memoize(function(name) { - return _.template(jQuery("script." + name).html()); - }), - - init: function () { - jQuery('html').addClass('forum'); - - // make categories and areas sortable - jQuery('#sortable_areas').sortable({ - axis: 'y', - items: ">*.movable", - handle: 'caption', - stop: function () { - var categories = {}; - categories.categories = {}; - jQuery(this).find('table').each(function () { - var name = jQuery(this).data('category-id'); - categories.categories[name] = name; - }); - - jQuery.ajax({ - type: 'POST', - url: STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/savecats?cid=' + STUDIP.Forum.seminar_id), - data: categories - }); - } - }); - - jQuery('tbody.sortable').sortable({ - axis: 'y', - items: ">*:not(.sort-disabled)", - connectWith: 'tbody.sortable', - handle: '.drag-handle', - helper: function (e, ui) { - ui.children().each(function () { - jQuery(this).width(jQuery(this).width()); - }); - return ui; - }, - - stop: function () { - STUDIP.Forum.saveAreaOrder(); - } - }); - - STUDIP.Forum.confirmDialog = STUDIP.Forum.getTemplate('confirm_dialog'); - - STUDIP.Forum.attachEventHandlers(); - }, - - approveDelete: function () { - if (STUDIP.Forum.current_area_id) { - // hide the area in the dom - jQuery('tr[data-area-id=' + STUDIP.Forum.current_area_id + ']').remove(); - STUDIP.Forum.closeDialog(); - - // ajax call to make the deletion permanent - jQuery.ajax(STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/delete_entry/' - + STUDIP.Forum.current_area_id + '?cid=' + STUDIP.Forum.seminar_id), { - method: 'post', - data: {'security_token' : STUDIP.CSRF_TOKEN.value}, - success: function (html) { - jQuery('#message_area').html(html); - } - }); - - STUDIP.Forum.current_area_id = null; - } - - if (STUDIP.Forum.current_category_id) { - // hide the table in the dom - jQuery('table[data-category-id=' + STUDIP.Forum.current_category_id + ']').fadeOut(); - STUDIP.Forum.closeDialog(); - - // move all areas to the default category - jQuery('table[data-category-id=' + STUDIP.Forum.current_category_id + '] tr.movable').each(function () { - jQuery('table[data-category-id=' + STUDIP.Forum.seminar_id + ']').append(jQuery(this)); - }); - - // ajax call to make the deletion permanent - jQuery.ajax(STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/remove_category/' - + STUDIP.Forum.current_category_id + '?cid=' + STUDIP.Forum.seminar_id), { - method: 'post', - data: {'security_token' : STUDIP.CSRF_TOKEN.value}, - success: function (html) { - jQuery('#message_area').html(html); - } - }); - - STUDIP.Forum.current_category_id = null; - } - }, - - deleteCategory: function (category_id) { - STUDIP.Forum.showDialog($gettext('Sind sie sicher, dass Sie diese Kategorie entfernen möchten? ') - + $gettext('Alle Bereiche werden dann nach "Allgemein" verschoben!'), - 'javascript:STUDIP.Forum.approveDelete()', - 'table[data-category-id=' + category_id +'] td.areaentry'); - - STUDIP.Forum.current_category_id = category_id; - }, - - editCategoryName: function (category_id) { - var template = STUDIP.Forum.getTemplate('edit_category'); - - // remove any other open edit fields before adding a new one - jQuery('table[data-category-id=' + category_id + '] span.edit_category').remove(); - - jQuery('table[data-category-id=' + category_id + '] span.category_name').hide() - .parent().append(template({ - category_id : category_id, - name : jQuery('table[data-category-id=' + category_id + '] span.category_name').text().trim() - })); - // jQuery('table[data-category-id=' + category_id + '] span.heading_edit').show(); - }, - - cancelEditCategoryName: function (category_id) { - jQuery('table[data-category-id=' + category_id + '] span.edit_category').remove(); - jQuery('table[data-category-id=' + category_id + '] span.category_name').show(); - - // reset the input field with the unchanged name - jQuery('table[data-category-id=' + category_id + '] span.heading_edit input[type=text]').val( - jQuery('table[data-category-id=' + category_id + '] span.category_name').text().trim() - ); - }, - - saveCategoryName: function (category_id) { - var name = {}; - name.name = jQuery('table[data-category-id=' + category_id + '] span.edit_category input[type=text]').val(); - - if (!jQuery.trim(name.name).length) { - jQuery('table[data-category-id=' + category_id + '] span.edit_category input[type=text]').val(''); - return; - } - - // display the new name immediately - jQuery('table[data-category-id=' + category_id + '] span.category_name').text(name.name); - - jQuery('table[data-category-id=' + category_id + '] span.edit_category').remove(); - jQuery('table[data-category-id=' + category_id + '] span.category_name').show(); - - jQuery.ajax(STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/edit_category/' + category_id + '?cid=' + STUDIP.Forum.seminar_id), { - type: 'POST', - data: name - }); - }, - - saveAreaOrder: function() { - // iterate over each category and get the areas there - var areas = {}; - areas.areas = {}; - jQuery('#sortable_areas').find('table').each(function () { - var category_id = jQuery(this).data('category-id'); - - areas.areas[category_id] = {}; - - jQuery(this).find('tr').each(function () { - var area_id = jQuery(this).data('area-id'); - areas.areas[category_id][area_id] = area_id; - }); - }); - - jQuery.ajax({ - type: 'POST', - url: STUDIP.URLHelper.getURL('dispatch.php/course/forum/area/save_order?cid=' + STUDIP.Forum.seminar_id), - data: areas - }); - }, - - deleteArea: function (element, area_id) { - STUDIP.Forum.showDialog($gettext('Sind sie sicher, dass Sie diesen Bereich löschen möchten? ') - + $gettext('Es werden auch alle Beiträge in diesem Bereich gelöscht!'), - 'javascript:STUDIP.Forum.approveDelete()', - 'tr[data-area-id=' + area_id +'] td.areaentry'); - - STUDIP.Forum.current_area_id = area_id; - }, - - addArea: function (category_id) { - var template = STUDIP.Forum.getTemplate('add_area'); - - this.cancelAddArea(); - - jQuery('table[data-category-id=' + category_id + '] tr.add_area').hide(); - - $(template({ - category_id : category_id, - })).appendTo('table[data-category-id=' + category_id + ']'); - - // #FIXME: there should be a better way to initialize a single form - STUDIP.Forms.initialize(); - }, - - doAddArea: function() { - // store the area only if the validity check has passed - var values = $(this).serializeObject(); - - // disable submit and cancel buttons, there is no turning back now - $('.button', this).prop('disabled', true); - - jQuery.ajax(STUDIP.URLHelper.getURL('dispatch.php/course/forum/area/add/' + values.category_id + '?cid=' + STUDIP.Forum.seminar_id), { - type: 'POST', - data: values, - success: function(data) { - // remove the add-form and enable the addition of another area - $('table[data-category-id=' + values.category_id +'] tr.new_area').remove(); - $('table[data-category-id=' + values.category_id +'] tr.add_area').show(); - - // insert the new area at the end of the list (more precisely: add the exact position where the add-form has been) - $(data).appendTo('table[data-category-id=' + values.category_id + ']'); - - STUDIP.Forum.saveAreaOrder(); - } - }); - - return false; - }, - - cancelAddArea: function () { - jQuery('tr.new_area').remove(); - jQuery('tr.add_area').show(); - }, - - editArea: function (area_id) { - - var template = STUDIP.Forum.getTemplate('edit_area'); - - // disable iconbar - STUDIP.ActionMenu.closeAll(); - jQuery('tr[data-area-id=' + area_id + '] .action-menu').hide(); - - // show edit form - jQuery('tr[data-area-id=' + area_id + '] span.areadata').hide() - .parent().append(template({ - area_id : area_id, - name : jQuery('tr[data-area-id=' + area_id + '] span.areaname').text().trim(), - content : jQuery('tr[data-area-id=' + area_id + '] div.areacontent').data('content') - })); - }, - - cancelEditArea: function (area_id) { - jQuery('tr[data-area-id=' + area_id + '] span.edit_area').remove(); - jQuery('tr[data-area-id=' + area_id + '] span.areadata').show(); - - // enable iconbar - jQuery('tr[data-area-id=' + area_id + '] .action-menu').show(); - }, - - saveArea: function (area_id) { - var name = {}; - name.name = jQuery('tr[data-area-id=' + area_id + '] span.edit_area input[type=text]').val(); - name.content = jQuery('tr[data-area-id=' + area_id + '] span.edit_area textarea').val(); - - // display the new name immediately - jQuery('tr[data-area-id=' + area_id + '] span.areaname').text(name.name); - - // store the modified raw-content used for possible subsequent edits - jQuery('tr[data-area-id=' + area_id + '] div.areacontent').data('content', name.content); - - // store the modified area and get formatted content-text from server - jQuery.ajax(STUDIP.URLHelper.getURL('dispatch.php/course/forum/area/edit/' + area_id + '?cid=' + STUDIP.Forum.seminar_id), { - type: 'POST', - data: name, - success: function(data) { - // shorten the description to 150 chars max - if (data.content.length > 150) { - jQuery('tr[data-area-id=' + area_id + '] div.areacontent').text(data.content.substr(0, 150)).append('…'); - } else { - jQuery('tr[data-area-id=' + area_id + '] div.areacontent').text(data.content); - } - - jQuery('tr[data-area-id=' + area_id + '] span.areaname_edit').hide(); - jQuery('tr[data-area-id=' + area_id + '] span.areaname').parent().parent().show(); - - // remove edit form - jQuery('tr[data-area-id=' + area_id + '] span.edit_area').remove(); - - // enable iconbar - jQuery('tr[data-area-id=' + area_id + '] .action-icons').show(); - } - }); - - }, - - saveEntry: function(topic_id) { - var $ = jQuery; - - var spanSelector = 'span[data-edit-topic=' + topic_id +']'; - - var name = $(spanSelector + ' input[name=name]'); - name.data('reset', name.val()); - - var textarea = $(spanSelector + ' textarea[name=content]'); - - // make sure HTML stays HTML - // usually the wysiwyg editor does this automatically, - // but since there is no submit event the editor does not - // get notified - textarea.val(STUDIP.wysiwyg.markAsHtml(textarea.val())); - - // remember current textarea value - textarea.data('reset', textarea.val()); - - jQuery.ajax(STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/update_entry/' + topic_id + '?cid=' + STUDIP.Forum.seminar_id), { - type: 'POST', - data: jQuery('form[data-topicid='+ topic_id +']').serializeObject(), - - error: function(data) { - alert('Server meldet: ' + data.statusText); - }, - - success: function (data) { - var json = jQuery.parseJSON(data); - // set the new name and content - jQuery('span[data-topic-name=' + topic_id +']').html(json.name); - jQuery('span[data-topic-content=' + topic_id +']').html(json.content); - STUDIP.Markup.element('span[data-topic-content=' + topic_id +']'); - - // hide the other stuff - jQuery('span[data-edit-topic=' + topic_id +']').hide(); - jQuery('span[data-show-topic=' + topic_id +']').show(); - - } - }); - }, - - editEntry: function (topic_id) { - jQuery('span[data-edit-topic]').hide(); - jQuery('span[data-show-topic]').show(); - - jQuery('span[data-show-topic=' + topic_id +']').hide(); - jQuery('span[data-edit-topic=' + topic_id +']').show().find('textarea').focus(); - }, - - cancelEditEntry: function (topic_id) { - jQuery('span[data-edit-topic=' + topic_id +'] input[name=name]').val( - jQuery('span[data-edit-topic=' + topic_id +'] input[name=name]').data('reset') - ); - - jQuery('span[data-edit-topic=' + topic_id +'] textarea[name=content]').val( - jQuery('span[data-edit-topic=' + topic_id +'] textarea[name=content]').data('reset') - ); - - jQuery('span[data-edit-topic=' + topic_id +']').hide(); - jQuery('span[data-show-topic=' + topic_id +']').show(); - }, - - newEntry: function() { - jQuery('#new_entry_button').hide(); - - jQuery('body').animate({scrollTop: jQuery('div.forum_new_entry').offset().top - 40}, 'slow'); - jQuery('html').animate({scrollTop: jQuery('div.forum_new_entry').offset().top - 40}, 'slow'); - }, - - cancelNewEntry: function(callback) { - $(window).off('beforeunload'); - - if ($('div.forum_new_entry').length) { - STUDIP.Dialog.confirm( - $gettext('Sind sie sicher, dass Sie ihren bisherigen Beitrag verwerfen wollen?'), - function() { - $('div.forum_new_entry').remove(); - callback(); - }, - function() {} - ); - } else { - callback(); - } - - jQuery('#new_entry_button').show(); - - return false; - }, - - answerEntry: function() { - $(window).on('beforeunload', function() { - return STUDIP.Forum.warning_text; - }); - - if (!$('div[data-id=global]').length) { - STUDIP.Forum.cancelNewEntry(function() { - var tmpl = STUDIP.Forum.getTemplate('new_entry_box'); - jQuery('#new_entry_button').parent().append(tmpl({ - topic_id: 'global' - })); - - STUDIP.Forum.newEntry(); - }); - } - }, - - citeEntry: function(topic_id) { - $(window).on('beforeunload', function() { - return STUDIP.Forum.warning_text; - }); - - /* Only recreate input-form, if it is different than the current one */ - if (!$('div[data-id=' + topic_id + ']').length) { - STUDIP.Forum.cancelNewEntry(function() { - var tmpl = STUDIP.Forum.getTemplate('new_entry_box'); - $('#forumposting_'+ topic_id).parent().append(tmpl({ - topic_id: topic_id - })); - - // watch out for anonymous postings - var anonymous = jQuery('.anonymous_post[data-profile=' + topic_id + ']').length > 0; - - var name = anonymous - ? $gettext('Anonym') - : jQuery('span.username[data-profile=' + topic_id + ']').text().trim(); - - // add content from cited posting in [quote]-tags - var originalContent = jQuery( - 'span[data-edit-topic=' + topic_id +'] textarea[name=content]' - ).val(); - - var content = STUDIP.Forum.quote(originalContent, name); - - var box = jQuery('div.forum_new_entry[data-id=' + topic_id + ']'); - $(box).find('textarea').val(content); - $(box).insertAfter('form[data-topicid=' + topic_id + ']'); - $(box).addClass('cite_box'); - - $(box).find('input[type=hidden][name=parent]').val(topic_id); - - STUDIP.Forum.newEntry(); - }); - } - }, - - quote: function(text, name) { - // If quoting is changed update these functions: - // - StudipFormat::markupQuote - // lib/classes/StudipFormat.php - // - quotes_encode lib/visual.inc.php - // - STUDIP.Forum.citeEntry > quote - // public/plugins_packages/core/Forum/javascript/forum.js - // - studipQuotePlugin > insertStudipQuote - // public/assets/javascripts/ckeditor/plugins/studip-quote/plugin.js - - var author = ''; - if (name) { - var writtenBy = $gettext('%s hat geschrieben:'); - author = '
' - + writtenBy.replace('%s', name) - + '
'; - } - return '
' + author + text + '

 

'; - }, - - forwardEntry: function(topic_id) { - var title = 'WG: ' + jQuery('span[data-edit-topic=' + topic_id +'] [name=name]').attr('value'); - var content = jQuery('span[data-edit-topic=' + topic_id +'] textarea[name=content]').val().trim(); - var is_html = STUDIP.wysiwyg.isHtml(content); - var nl = is_html ? '
' : "\n"; - var text = $gettext('Die Senderin/der Sender dieser Nachricht möchte Sie auf den folgenden Beitrag aufmerksam machen. ') - + nl + nl - + $gettext('Link zum Beitrag: ') - + nl - + STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/index/' - + topic_id + '?cid=' + STUDIP.Forum.seminar_id + '&again=yes#' + topic_id) - + nl + nl - + content - + nl + nl; - if (is_html) { - text = STUDIP.wysiwyg.markAsHtml(text); - } - STUDIP.Dialog.fromURL(STUDIP.URLHelper.getURL('dispatch.php/messages/write'), { - data: { - default_body: text, - default_subject: title - }, - method: 'post' - }); - }, - - postToUrl: function(path, params) { - // create a form - var form = jQuery('
'); - for (var key in params) { - jQuery(form).append(''); - } - - // append it to the body-element - jQuery('body').append(form); - - // submit it - jQuery(form).submit(); - }, - - moveThreadDialog: function (topic_id) { - var element = jQuery('tr[data-area-id=' + topic_id +'] td.areaentry').addClass('selected'), - content = jQuery('#dialog_' + topic_id).html(); - - STUDIP.Dialog.show(content, { - title: $gettext('Beitrag verschieben'), - width: 400, - height: 400, - origin: element - }); - - element.on('dialog-close', function () { - $(this).removeClass('selected').off('dialog-close'); - }); - }, - - loadAction: function(element, action) { - jQuery(element).load(STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/' - + action + '?cid=' + STUDIP.Forum.seminar_id)) - }, - - showDialog: function(question, confirm, highlight_element) { - if (highlight_element !== null) { - // STUDIP.Forum.highlightedElement = highlight_element; - jQuery(highlight_element).addClass('selected'); - } - - jQuery('body').append(STUDIP.Forum.confirmDialog({ - question: question, - confirm: confirm - })); - }, - - closeDialog: function() { - jQuery('#forum td.selected').removeClass('selected'); - jQuery('div.modaloverlay').remove(); - }, - - setFavorite: function(topic_id) { - jQuery('#favorite_' + topic_id).load(STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/set_favorite/' - + topic_id + '?cid=' + STUDIP.Forum.seminar_id)); - jQuery('a.marked[data-topic-id=' + topic_id +']').show(); - return false; - }, - - unsetFavorite: function(topic_id) { - jQuery('#favorite_' + topic_id).load(STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/unset_favorite/' - + topic_id + '?cid=' + STUDIP.Forum.seminar_id)); - jQuery('a.marked[data-topic-id=' + topic_id +']').hide(); - return false; - }, - - adminLoadChilds: function(topic_id) { - // if there is already data present, remove it (to "close" the current node in the tree) - if (jQuery('li[data-id=' + topic_id + '] ul').length) { - jQuery('li[data-id=' + topic_id + '] ul').remove(); - return; - } - - // jQuery('li[data-id=' + topic_id + '] > a.tooltip2').showAjaxNotification(); - - // load children from server and show them - jQuery.ajax(STUDIP.URLHelper.getURL('dispatch.php/course/forum/admin/childs/' + topic_id), { - dataType: 'html', - success: function(response) { - jQuery('li[data-id=' + topic_id + ']').append(response); - - // jQuery('li[data-id=' + topic_id + '] a.tooltip2').hideAjaxNotification(); - - // clean up icons - STUDIP.Forum.checkCutPaste(); - } - }); - }, - - cut : function(topic_id) { - // remove all childs from clipboard - jQuery('li[data-id=' + topic_id +'] li.selected').each(function(){ - var tid = jQuery(this).data('id'); - jQuery(this).removeClass('selected'); - delete STUDIP.Forum.clipboard[tid]; - }); - - // add this element to clipboard and mark it as selected - jQuery('li[data-id=' + topic_id +']').addClass('selected'); - jQuery('li[data-id=' + topic_id + '] > a[data-role=cut]').hide(); - jQuery('li[data-id=' + topic_id + '] > a[data-role=cancel_cut]').show(); - STUDIP.Forum.clipboard[topic_id] = topic_id; - - // iterate over every li and remove the paste icon from all li's in the clipboard' - jQuery('#forum li').each(function() { - var tid = jQuery(this).data('id'); - if (tid !== null && !STUDIP.Forum.clipboard[tid]) { - jQuery(this).find('a[data-role=paste]').show(); - } else { - jQuery(this).find('a[data-role=paste]').hide(); - } - }); - - // clean up icons (if necessary) - STUDIP.Forum.checkCutPaste(); - }, - - cancelCut: function(topic_id) { - // remove the selected element from the clipboard and unmark it - jQuery('li[data-id=' + topic_id +']').removeClass('selected'); - jQuery('li[data-id=' + topic_id + '] a[data-role=cut]').show(); - jQuery('li[data-id=' + topic_id + '] > a[data-role=cancel_cut]').hide(); - - delete STUDIP.Forum.clipboard[topic_id]; - - // all children are now valid paste-targets again - jQuery('li[data-id=' + topic_id + '] a[data-role=paste]').show(); - - if (Object.keys(STUDIP.Forum.clipboard).length == 0) { - jQuery('a[data-role=paste]').hide(); - } - - // clean up icons (if necessary) - STUDIP.Forum.checkCutPaste(); - }, - - paste: function(topic_id) { - // jQuery('li[data-id=' + topic_id + '] > a.tooltip2').showAjaxNotification(); - - jQuery.ajax(STUDIP.URLHelper.getURL('dispatch.php/course/forum/admin/move/' + topic_id), { - data : { - 'topics' : STUDIP.Forum.clipboard - }, - type: 'POST', - success: function() { - // jQuery('li[data-id=' + topic_id + '] a.tooltip2').hideAjaxNotification(); - - // remove all pasted entries, they are now elsewhere - for (var id in STUDIP.Forum.clipboard) { - jQuery('li[data-id=' + id + ']').remove(); - } - - // reload childs after succesful moving - jQuery('li[data-id=' + topic_id + '] ul').remove(); - STUDIP.Forum.adminLoadChilds(topic_id); - // reset icons after succesful moving - STUDIP.Forum.clipboard = {}; - jQuery('a[data-role=cut]').show(); - jQuery('a[data-role=cancel_cut]').hide(); - jQuery('a[data-role=paste]').hide(); - jQuery('li.selected').removeClass('selected'); - } - }); - }, - - checkCutPaste: function() { - jQuery('li.selected').find('li').each(function(){ - var tid = jQuery(this).data('id'); - delete STUDIP.Forum.clipboard[tid]; - - jQuery(this).removeClass('selected'); - jQuery(this).find('a[data-role=cut]').hide(); - jQuery(this).find('a[data-role=cancel_cut]').hide(); - jQuery(this).find('a[data-role=paste]').hide(); - }); - }, - - wrapActionElementText: function (element) { - if (jQuery('span', element).length > 0) { - return; - } - var img = jQuery('img', element).remove(); - var text = jQuery(element).text().trim(); - var span = jQuery('').text(text); - - $(element).empty().append(img, span); - }, - - openThreadFromOverview: function(topic_id, parent_topic_id, page) { - var buttonText = $gettext('Thema schließen'); - var element = jQuery('#closeButton-' + topic_id); - - STUDIP.Forum.wrapActionElementText(element); - - jQuery('img', element).attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/lock-locked.svg'); - jQuery('span', element).text(buttonText); - jQuery(element).attr('onclick', 'STUDIP.Forum.closeThreadFromOverview("' + topic_id + '", "' + parent_topic_id + '", ' + page + '); return false;'); - jQuery('#img-locked-' + topic_id).hide(); - - STUDIP.Forum.openThread(topic_id, parent_topic_id, page, false); - - STUDIP.ActionMenu.closeAll(); - }, - - openThreadFromThread: function(topic_id, page) { - var buttonText = $gettext('Thema schließen'); - jQuery('.closeButtons').text(buttonText); - jQuery('.closeButtons').attr('onclick', 'STUDIP.Forum.closeThreadFromThread("' + topic_id + '", ' + page + '); return false;'); - jQuery('.closeButtons').closest("li").css('background-image', "url(" + STUDIP.ASSETS_URL + 'images/icons/blue/lock-locked.svg' + ")"); - jQuery('.hideWhenClosed').show(); - - STUDIP.Forum.openThread(topic_id, topic_id, page, true); - }, - - openThread: function(topic_id, redirect, page, showSuccessMessage) { - jQuery.ajax({ - type: 'GET', - url: STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/open_thread/' + topic_id + '/' + redirect + '/' + page), - success: function(data) { - if (showSuccessMessage == true) { - jQuery('#message_area').html(data); - } - } - }); - - return false; - }, - - closeThreadFromOverview: function(topic_id, parent_topic_id, page) { - var buttonText = $gettext('Thema öffnen'); - var element = jQuery('#closeButton-' + topic_id); - - STUDIP.Forum.wrapActionElementText(element); - - jQuery('img', element).attr('src', STUDIP.ASSETS_URL + 'images/icons/blue/lock-unlocked.svg'); - jQuery('span', element).text(buttonText); - jQuery(element).attr('onclick', 'STUDIP.Forum.openThreadFromOverview("' + topic_id + '", ' + page + '); return false;'); - - jQuery('#img-locked-' + topic_id).show(); - - STUDIP.Forum.closeThread(topic_id, parent_topic_id, page, false); - - STUDIP.ActionMenu.closeAll(); - }, - - closeThreadFromThread: function(topic_id, page) { - var buttonText = $gettext('Thema öffnen'); - jQuery('.closeButtons').text(buttonText); - jQuery('.closeButtons').attr('onclick', 'STUDIP.Forum.openThreadFromThread("' + topic_id + '", '+ page +'); return false;'); - jQuery('.closeButtons').closest("li").css('background-image', "url(" + STUDIP.ASSETS_URL + 'images/icons/blue/lock-unlocked.svg' + ")"); - jQuery('.hideWhenClosed').hide(); - - STUDIP.Forum.cancelNewEntry(); - - STUDIP.Forum.closeThread(topic_id, topic_id, page, true); - }, - - closeThread: function(topic_id, redirect, page, showSuccessMessage) { - - jQuery.ajax({ - type: 'GET', - url: STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/close_thread/' + topic_id + '/' + redirect + '/' + page), - success: function(data) { - if (showSuccessMessage == true) { - jQuery('#message_area').html(data); - } - } - }); - - return false; - }, - - makeThreadStickyFromThread: function(topic_id) { - jQuery.ajax({ - type: 'GET', - url: STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/make_sticky/' + topic_id + '/' + topic_id + '/0'), - success: function(data) { - jQuery('#message_area').html(data); - var linkText = $gettext('Hervorhebung aufheben'); - jQuery('#stickyButton').text(linkText); - jQuery('#stickyButton').attr('onclick', 'STUDIP.Forum.makeThreadUnstickyFromThread("' + topic_id + '"); return false;'); - } - }); - - return false; - }, - - makeThreadUnstickyFromThread: function(topic_id) { - jQuery.ajax({ - type: 'GET', - url: STUDIP.URLHelper.getURL('dispatch.php/course/forum/index/make_unsticky/' + topic_id + '/' + topic_id + '/0'), - success: function(data) { - jQuery('#message_area').html(data); - var linkText = $gettext('Thema hervorheben'); - jQuery('#stickyButton').text(linkText); - jQuery('#stickyButton').attr('onclick', 'STUDIP.Forum.makeThreadStickyFromThread("' + topic_id + '"); return false;'); - } - }); - - return false; - }, - attachEventHandlers: function () { - $(document).on('submit', 'form.add_area_form', STUDIP.Forum.doAddArea); - } -}; - - -// TODO: make TIC and add this to the Stud.IP-Core -/** - * found at stackoverflow.com - * http://stackoverflow.com/questions/946534/insert-text-into-textarea-with-jquery/946556#946556 - */ -jQuery.fn.extend({ - insertAtCaret: function (myValue) { - return this.each(function () { - if (document.selection) { - //For browsers like Internet Explorer - this.focus(); - var sel = document.selection.createRange(); - sel.text = myValue; - this.focus(); - } else if (this.selectionStart || this.selectionStart === '0') { - //For browsers like Firefox and Webkit based - var startPos = this.selectionStart; - var endPos = this.selectionEnd; - var scrollTop = this.scrollTop; - this.value = this.value.substring(0, startPos) + myValue - + this.value.substring(endPos, this.value.length); - this.focus(); - this.selectionStart = startPos + myValue.length; - this.selectionEnd = startPos + myValue.length; - this.scrollTop = scrollTop; - } else { - this.value += myValue; - this.focus(); - } - }); - } -}); - -/** - * Thanks to Tobias Cohen for this function - * http://stackoverflow.com/questions/1184624/convert-form-data-to-js-object-with-jquery - */ -jQuery.fn.serializeObject = function() { - var o = {}; - var a = this.serializeArray(); - jQuery.each(a, function() { - if (o[this.name] !== undefined) { - if (!o[this.name].push) { - o[this.name] = [o[this.name]]; - } - o[this.name].push(this.value || ''); - } else { - o[this.name] = this.value || ''; - } - }); - return o; -}; - -export default Forum; diff --git a/resources/assets/javascripts/lib/jsonapiUtils.js b/resources/assets/javascripts/lib/jsonapiUtils.js new file mode 100644 index 0000000..b8e1a1d --- /dev/null +++ b/resources/assets/javascripts/lib/jsonapiUtils.js @@ -0,0 +1,15 @@ +import { Deserializer } from 'jsonapi-serializer'; + +const deserializer = new Deserializer({ + keyForAttribute: 'snake_case', + typeAsAttribute: true +}); + +export const deserializeJSONAPIResponse = async response => { + try { + return await deserializer.deserialize(response); + } catch (error) { + console.error('Failed to deserialize JSON:API response', error); + throw error; + } +}; diff --git a/resources/assets/javascripts/lib/number_formatter.js b/resources/assets/javascripts/lib/number_formatter.js new file mode 100644 index 0000000..a58c4c5 --- /dev/null +++ b/resources/assets/javascripts/lib/number_formatter.js @@ -0,0 +1,21 @@ +export const numberFormatter = (number, digits) => { + const lookup = [ + { value: 1, symbol: "" }, + { value: 1e3, symbol: "k" }, + { value: 1e6, symbol: "M" }, + { value: 1e9, symbol: "G" }, + { value: 1e12, symbol: "T" }, + { value: 1e15, symbol: "P" }, + { value: 1e18, symbol: "E" } + ]; + const regexp = /\.0+$|(?<=\.[0-9]*[1-9])0+$/; + const item = lookup.findLast(item => number >= item.value); + + if (!item) { + return "0"; + } + + let formattedNumber = (number / item.value).toFixed(digits).replace(regexp, "").replace(".", ","); + + return formattedNumber + item.symbol; +} diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss index ecf0584..dbed895 100644 --- a/resources/assets/stylesheets/scss/forms.scss +++ b/resources/assets/stylesheets/scss/forms.scss @@ -121,7 +121,7 @@ form.default { vertical-align: top; input[type=date], input[type=email], input[type=number], - input[type=password], input[type=text], input[type=time], input[type=tel], input[type=url], + input[type=password], input[type=text], input[type=time], input[type=tel], input[type=url], input[type=color], textarea, select, .ck.ck-editor { display: block; margin-top: 0.5ex; @@ -450,7 +450,7 @@ form.default { width: auto; } input, textarea, select, button { - display: inline-block;; + display: inline-block; } } diff --git a/resources/assets/stylesheets/scss/forum.scss b/resources/assets/stylesheets/scss/forum.scss index 0810bae..a3ab33d 100644 --- a/resources/assets/stylesheets/scss/forum.scss +++ b/resources/assets/stylesheets/scss/forum.scss @@ -1,364 +1,1654 @@ -/* An enhanced style for the forum, Web 2.0 - like*/ +$card-min-width: 250px; +$card-max-width: 300px; + +.forum { + hr { + border-top: 1px solid $color--divider; + border-bottom: none; + border-left: none; + border-right: none; + } -/* mixins */ -@mixin rounded($radius: 3px) { - border-radius: $radius; - -moz-border-radius: $radius; - -webkit-border-radius: $radius; -} + .forum-table { + &.--discussions-index, + &.--subscription-index, + &.--topics-index { + .details-xs { + display: none; + margin-top: 5px; + margin-bottom: 0; + } + tbody { + tr:last-child { + td { + border-bottom: none; + } + } + } + dl { + margin: 0; + + dt { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } + + dd { + margin-left: 0; + } + } -$shadow: 1px 1px 3px rgba(0, 0, 0, 0.2); -@media print { - #forum { - .searchbar, div[data-type="page_chooser"], - .likes, dl.postprofile { - display: none; - } - div.title { - p.author { - margin-bottom: 0; + a.navigation-link::after { + position: absolute; + inset: 0; + content: ''; + } + + } + &.--subscription-index { + .subscription-button { + background-color: transparent; + border-color: transparent; } } - div.postbody { - width: 100%; + .footer-actions-container { + display: flex; + align-items: center; + gap: 10px; } } -} -.ui-dialog { - box-shadow: $shadow -} + .title-with-actions { + display: block; + + &__content { + display: inline-flex; + align-items: start; + gap: 10px; + + h3 { + flex: 1; + } + } + + &__link { + display: inline-flex; + align-items: start; + gap: 5px; + } -#forum { - img.button, input[type=image] { - vertical-align: middle; + &__title { + flex: 1; + font-size: 16px; + font-weight: normal; + } + + &__actions-xs { + display: none; + } } - form { - display: inline; + .unread-items-badge { + padding: 1px 4px; + background: $red; + color: $white; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 5px; + font-size: smaller; } - span.highlight { - background-color: var(--activity-color-40); - border: 1px solid var(--activity-color-40); - @include rounded; + .text-highlight { + &::selection, + *::selection { + background-color: $yellow; + color: $color--font-primary; + } } - .searchbar { - text-align: left; - input[name=searchfor] { - width: 90%; + .profile-image-container { + width: 50px; + height: 50px; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + border-radius: 50%; + border: 1.5px solid white; + + img { + width: 100%; + height: 100%; + object-fit: cover; } + } + &__container { + display: grid; + gap: 20px; + grid-template-columns: 5fr 2fr; } - .forum_header { - background-color: var(--content-color); - color: var(--white); - margin: 0; - padding: 0; - .button { + .card { + box-sizing: border-box; + border: solid 1px $color--content-box-border; + margin: 0 0 15px; + min-width: auto; + + &__header { + background-color: $color--fieldset-header; + font-size: 16px; + padding: 10px; + + &--with-actions { + display: flex; + justify-content: space-between; + gap: 10px; + + .actions { + display: flex; + gap: 10px; + } + } + } + + &__title { margin: 0; } + &__body { + padding: 10px; + } } - .heading { - display: block; - margin: 1px 4px 4px 6px; - text-transform: uppercase; + .header { + display: flex; + background-color: $color--fieldset-header; + + &.--sticky-top { + position: sticky; + top: 50px; + z-index: 10; + } + + .flag { + width: 10px; + } + + &__content { + padding: 10px 15px; + flex: 1 1 0%; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + + &--with-actions { + .actions { + display: flex; + align-items: center; + gap: 10px; + } + } + + .go-back-link { + margin-top: 4px; + } + + h2 { + margin: 0; + font-size: 18px; + font-weight: 500; + } + } } - table.forum { - td.selected { - background-color: var(--activity-color-40); + .icon-button { + display: flex; + align-items: center; + justify-content: center; + background: white; + border: 1px solid $base-color; + color: $base-color; + border-radius: 5px; + padding: 7px; + cursor: pointer; + + &.--with-label { + padding-left: 10px; + padding-right: 10px; + } + + &:hover { + background-color: $content-color-10; + } + + &:disabled { + border-color: $color--button-inactive-border; + opacity: 0.5; + cursor: not-allowed; + } + + .label { + margin-left: 5px; } } - td.postings { - vertical-align: middle; - text-align: center; - width: 80px; + button.style-less { + background: no-repeat; + border: none; + padding: 0; } - td.answer { - width: 300px; + .button { + &.--with-icon { + display: inline-flex; + gap: 10px; + align-items: center; + } - img { - vertical-align: text-bottom; + .icon-hover { + display: none; } - } - .area_title { - padding: 0 5px; - font-weight: bold; - text-transform: uppercase; + &:hover { + .icon-default { + display: none; + } + .icon-hover { + display: inline-block; + } + } } - .area_input { - display: block; - padding: 0 5px; + .v-select { + margin: 0; + min-width: 160px; } - .add_area_form { - display: block; - padding: 0 5px; - text-align: center; + .empty-forum { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 10px; + color: $color--font-primary; + padding: 0 20px; + + .forum-illustration { + padding: 10px; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 90%; + height: auto; + } + } + + p { + color: $color--font-secondary; + } + + .buttons-container { + margin-top: 20px; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 15px; + + button { + margin: 0; + } + } } - td.add_area { - font-weight: bold; - font-size: 16pt; - text-align: right; - padding-right: 12px; + .topic-overview { + display: flex; - img { - margin-bottom: -3px; + .flag { + width: 5px; + margin-right: 15px; + } + + .content { + flex: 1 1 0%; } - span { - font-size: 10pt; + h3 { + margin-top: 0; + margin-bottom: 5px; + font-size: 16px; font-weight: normal; } + + p { + color: $color--font-secondary; + } } - td.add_area:hover { - cursor: pointer; + .discussion-overview { + h3 { + margin-top: 0; + font-size: 16px; + font-weight: normal; + } + + p { + color: $color--font-secondary; + } - span { - color: var(--red-80); + .discussion-category { + display: flex; + align-items: center; + gap: 5px; } } - .icon img { - vertical-align: middle; + .forum-form { + flex: 1; + padding: 5px 10px; + .multi-select-input .vs__dropdown-toggle { + max-height: fit-content; + } + + fieldset:not(.undecorated) > :not(legend, table) { + max-width: calc(100% - 1px); + } + + .inputs-container { + display: flex; + justify-content: space-between; + gap: 20px; + flex-wrap: wrap; + } } - span.areaname { - display: block; - margin-right: 55px; - font-weight: bold; + .tags-container { + padding: 0; + list-style-type: none; + display: flex; + gap: 6px; + flex-wrap: wrap; + + &__tag { + display: flex; + align-items: center; + justify-content: center; + color: $color--font-secondary; + font-size: 10px; + border: 1px solid $color--action-menu-border; + padding: 1px 5px; + border-radius: 4px; + } } - span.threadauthor { - float: left; - width: 70%; + table.topics td { + vertical-align: top; } - .posting { - height: 100%; - margin: 0 0 10px 0; - padding: 5px; - background-color: var(--content-color-20); + .topic-cards-container { + padding: 0; + list-style-type: none; + display: grid; + grid-template-columns: repeat(auto-fit, minmax($card-min-width, $card-max-width)); + grid-gap: 15px; + + &.--fill-free-space { + grid-template-columns: repeat(auto-fit, minmax($card-min-width, 1fr)); + } + + .card-group { + background-color: $color--tile-background; + border: 1px solid $color--button-inactive-border; + padding-top: 4px; + padding-right: 4px; + margin-left: 4px; + margin-bottom: 4px; + + .topic-card { + margin-left: -4px; + margin-bottom: -4px; + } + } } - .real_posting { + .topic-card { + background-color: $color--tile-background; + border: 1px solid $color--button-inactive-border; + outline: transparent; + height: 100%; + min-height: 180px; display: flex; + transition: 0.2s; + + &__flag { + width: 10px; + } + + &.--new-topic { + display: flex; + justify-content: center; + align-items: center; + + a { + height: fit-content; + } + } + + .unread-items-badge { + position: relative; + z-index: 1; + } + + &__title { + margin-top: 0; + margin-bottom: 10px; + font-size: 16px; + font-weight: normal; + } + + p { + opacity: 60%; + } + + &__content { + flex: 1; + padding: 10px 15px; + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 150px; + overflow: hidden; + } + + &__footer { + display: flex; + justify-content: space-between; + gap: 5px; + position: relative; + z-index: 1; + } + + + &:hover:not(.--new-topic), + &:focus:not(.--new-topic) { + background-color: $color--tile-background-hover; + } + + &.--with-hover-style:hover, + &.--with-hover-style:focus { + outline-color: var(--forum-topic-card-hover-border-color); + outline-style: solid; + border-color: var(--forum-topic-card-hover-border-color); + } } - @keyframes border-pulsate { - 0% { border-color: rgba(255, 255, 153, 1); } - 50% { border-color: rgba(255, 255, 153, 0); } - 100% { border-color: rgba(255, 255, 153, 1); } + .post { + display: flex; + transition: background-color 1s ease; + + &.--highlight { + background-color: $content-color-10; + } + + &__body { + padding: 25px; + flex: 1; + display: flex; + gap: 15px; + } + + &__unread { + width: 5px; + margin-right: 15px; + background-color: $red; + } + + &__content { + flex: 1; + } + + &__text { + p { + color: $color--font-primary; + } + + img { + max-width: 100% !important; + height: auto !important; + } + } + + &__author-name-container { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + color: $color--font-secondary; + + .author-name { + font-size: 14px; + font-weight: bold; + } + + &.--xl { + display: flex; + } + + &.--xs { + display: none; + } + } + + &__author-avatar { + position: sticky; + top: 50px; + z-index: 1; + + .dropdown__content { + right: auto; + left: 0; + } + } + + &__footer { + margin-top: 10px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + } + + .opengraph-urls { + margin-top: 10px; + margin-bottom: 20px; + } } - div.highlight { - border: 4px solid var(--activity-color-40); - animation: border-pulsate 2s 5; + .forum-quote { + blockquote { + border: none; + position: relative; + padding-left: 2rem; + color: $color--font-primary; + white-space: pre-line; + background-color: $color--tile-background !important; + + &:before { + color: $color--font-secondary; + position: absolute; + font-size: 4rem; + width: 2rem; + height: 2rem; + content: '“'; + left: 4px; + top: -1rem; + } + + a { + background: unset !important; + color: unset !important; + } + + a.ck-link_selected { + background: unset !important; + color: unset !important; + } + } } - .postbody { + .post-reactions { position: relative; - padding: 0pt 5px; - margin: 5px 0 0 0; - flex: 1; - min-width: 0; - text-align: left; - } + display: inline-flex; + align-items: center; + justify-content: center; + + &__create-button { + display: inline-flex; + cursor: pointer; + background: none; + border: navajowhite; + padding: 0; + align-items: center; + gap: 5px; + + p { + margin: 0; + } - .buttons { - clear: both; - width: 100%; - text-align: center; - padding-top: 5px; + .add-reaction-icon { + width: 20px; + height: auto; + fill: $color--font-secondary; + transition: fill 0.2s; + } + + &:hover { + .add-reaction-icon { + fill: $base-color; + } + } + } + + &__container { + position: absolute; + top: -50px; + left: 12px; + transform: translateX(-50%); + z-index: 10; + display: flex; + background: white; + border: 1px solid $base-color; + border-radius: 5px; + cursor: pointer; + overflow: hidden; + box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; + + button { + background: none; + border: none; + padding: 6px 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + transition: 0.3s; + + &.--active { + background-color: $base-color-20; + } + } + + button:hover { + cursor: pointer; + background-color: $content-color-10; + } + } } - div.title { - text-align: left; - float: left; - width: 100%; + .post-form-container { + padding: 25px; } - .title { - font-weight: bold; + .discussion { + background-color: $color--main-navigation-background; + + &__status, + &__body, + &__form-container { + padding: 25px; + } + + &__body { + display: flex; + gap: 15px; + } + + &__status { + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: space-between; + gap: 10px; + color: $color--font-secondary; + } + + .post__author-image { + width: 50px !important; + height: 50px !important; + } } - div.postbody span.icons { - float: right; - min-width: 3%; + .posts-container { + display: grid; + grid-template-columns: repeat(1, minmax(0, 1fr)); + + hr { + width: 100%; + } } - div.postbody .content { - overflow: hidden; - clear: both; + .post-form { + max-width: calc(100% - 1px); + &__author { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 15px; + } + + &__author-name { + color: $color--font-secondary; + font-weight: 600; + } + + &__author-image { + width: 25px; + height: 25px; + } + + .quote-container { + position: relative; + + .remove-quote-button { + display: none; + position: absolute; + cursor: pointer; + bottom: 0; + right: 0; + padding: 3px; + align-items: center; + justify-content: center; + background-color: $color--tile-background; + border: 1px solid $color--action-menu-border; + border-radius: 4px; + transition: background-color 1s ease; + z-index: 1; + + &:hover, + &:focus { + background-color: $color--tile-background-hover; + } + } + + &:hover, + &:focus { + .remove-quote-button { + display: flex; + } + } + } } - p.author { - margin: 2px 0px 8px 0px; + .user-avatar-dropdown { + &__preview { + background: none; + border: none; + padding: 0; + cursor: pointer; + + &:hover, + &:focus, + &.active { + position: relative; + z-index: 1; + + img.user-profile { + box-shadow: 0 2px 3px rgb(0 0 0 / 0.2); + } + } + + img.user-profile { + width: 25px; + height: auto; + border-radius: 100%; + border: 1.5px solid white; + transition: all .4s ease-in-out; + } + } } - .content { - clear: both; + .forum-users-dropdown { + padding: 10px; + min-width: 200px; + max-width: 80vw; + max-height: 50vh; + overflow-y: scroll; + + hr { + margin: 10px 0; + } } - span.username { - font-weight: bold; + .user-group { + &--moderators { + img.user-profile { + border-color: $base-color; + } + } + + .user-avatar { + img.user-profile { + width: 40px; + height: 40px; + } + } + + &__title { + font-size: 14px; + font-weight: bold; + color: $color--font-primary; + margin: 0; + } + + &__list { + margin-top: 10px; + list-style: none; + padding: 0; + + li { + position: relative; + .user-avatar { + margin-left: -10px !important; + margin-right: -10px !important; + overflow: hidden; + } + } + + .user-item { + margin-left: -10px !important; + margin-right: -10px !important; + padding: 4px 10px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + transition: background-color 0.2s ease-out; + + &:hover, + &:focus { + background-color: $color--tile-background-hover; + } + + &__user { + display: flex; + align-items: center; + gap: 10px; + + img { + width: 25px; + height: 25px; + border-radius: 100%; + border: 1.5px solid white; + transition: all .4s ease-in-out; + } + + p { + text-wrap: auto; + margin: 0; + } + } + } + + .hide-avatar, + .show-avatar { + cursor: pointer; + background: transparent; + border: none; + margin: -4px; + padding: 4px; + } + + .hide-avatar { + position: absolute; + right: 0; + top: 10px; + transition: background-color 0.2s ease; + + &:hover { + background-color: $content-color-10; + } + } + } } - .postprofile { - @media only screen and (max-width: 768px) { - display: none !important; + + .forum-members { + list-style-type: none; + display: flex; + align-items: center; + padding: 0; + + li { + &:not(:first-child) { + margin-left: -10px; + } + + .moderator img.user-profile { + border: 2px solid $base-color; + } + + .remained-users { + &__button { + padding: 0; + margin: 0; + background: none; + border: none; + } + + &__count { + cursor: pointer; + font-size: 14px; + color: $color--font-secondary; + background-color: $color--fieldset-header; + width: 25px; + height: 25px; + border-radius: 100%; + border: 1.5px solid white; + display: flex; + align-items: center; + justify-content: center; + } + } } - border-left: 1px solid var(--white); - margin: 0; - padding: 4px; - height: 100%; - width: 180px; - dd, dt { - padding: 0pt; - margin: 0pt; + + .user-avatar { + img.user-profile { + box-shadow: none !important; + } + + &__actions { + li { + margin-left: 0; + } + } } + .user-group__list li { + margin-left: 0; + } } - span.buttons { - display: block; - clear: both; - text-align: center; - width: 78%; + .with-ballon-action { + margin: -5px; + padding: 10px 5px; } - .clear { - display: block; - clear: both; + .ballon-action { + z-index: 10; + position: absolute; + display: none; + background: white; + border: 1px solid $base-color; + color: $base-color; + border-radius: 5px; + gap: 5px; + cursor: pointer; + overflow: hidden; + box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; + + button { + background: none; + border: none; + padding: 6px; + display: flex; + align-items: center; + justify-content: center; + } + + button:hover { + cursor: pointer; + background-color: $content-color-10; + } } - textarea { - width: 100%; - height: 20em; + .timeline-container { + position: sticky; + top: 50px; } - .editor_toolbar { + .discussion-timeline-table { width: 100%; + height: 350px; + + tr td:first-child { + width: 6px; + background-color: $color--tile-title-background; + } + + tr:first-child td:first-child { + background-color: $base-color; + } + + tr td:nth-child(2) { + padding: 0 10px; + } + + time, p { + color: $color--font-secondary; + margin: 0; + } + + time { + font-weight: 600; + } + + p { + font-size: small; + } + + td.bg-new-activity { + background-color: $red !important; + } + } + + .drag-handle { + display: inline-block; + width: 6px; + height: 20px; + margin-top: 5px; + background-size: auto 20px; + cursor: move; + } + + .studip-icons-container { + display: flex; + flex-wrap: wrap; + gap: 15px; + padding: 15px; + justify-content: space-between; + border: thin solid $color--button-inactive-border; + max-width: calc(48em - 30px); + + .icon { + border-radius: 5px; + padding: 0.5rem; + cursor: pointer; + border: 1px solid $light-gray-color-20; + outline: transparent; + background: none; + transition: background-color 0.2s ease; + + &:hover:not(.active) { + background-color: $light-gray-color-20; + } + + &.disabled { + opacity: 0.25; + } + + &.active { + border-color: $green-40; + outline-color: $green-40; + outline-style: solid; + } + } } - a.marked div { + .post-reactions-container { + margin-top: 10px; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + + .post-reaction { + padding: 4px 6px; + background: $color--gray-7; + border-radius: 6px; + border: thin solid $color--gray-5; + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + + &.--active { + background-color: $base-color-20; + border-color: $base-color-60; + } + } + + .html-emoji { + font-family: apple color emoji, segoe ui emoji, notocoloremoji, segoe ui symbol, android emoji, emojisymbols, emojione mozilla; + } + } + + .forum-subscriptions-dropdown { + text-align: left; + white-space: pre-wrap; + + .dropdown__items { + max-width: 300px; + li { + display: flex; + align-items: center; + gap: 15px; + padding: 10px 15px; + + .subscription-option { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 5px; + } + + .option-title { + font-size: 14px; + color: $base-color; + font-weight: 400; + margin: 0; + } + + p { + color: $color--font-secondary; + margin-top: 6px; + font-size: small; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } + + &.all { + background-color: $green-20; + cursor: default; + } + + &.replies_only { + background-color: $activity-color-20; + cursor: default; + } + + &.none { + background-color: $dark-gray-color-20; + cursor: default; + } + + &.--active { + background-color: $dark-gray-color-10; + cursor: default; + } + + &.--disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + } + + .subscription-button { + white-space: nowrap; + display: inline-flex; + gap: 6px; + align-items: center; + } + } + + .discussion-closed { + display: inline-flex; + align-items: center; + gap: 5px; + opacity: 0.6; + } + + .use-utility-classes { + .flex { + display: flex; + } + .flex-1 { + flex: 1; + } + .flex-col { + flex-direction: column; + } + .inline-flex { + display: inline-flex; + } + .items-start { + align-items: start; + } + .items-center { + align-items: center; + } + .items-baseline { + align-items: baseline; + } + .space-between { + justify-content: space-between; + } + .justify-start { + justify-content: start; + } + .gap-5 { + gap: 5px; + } + .gap-10 { + gap: 10px; + } + .gap-20 { + gap: 20px; + } + .gap-40 { + gap: 40px; + } + .mr-10 { + margin-right: 10px; + } + .mr-5 { + margin-right: 5px; + } + .w-100 { + width: 100%; + } + .p-10 { + padding: 10px; + } + .py-10 { + padding: 10px 0; + } + .border-b-0 { + border-bottom: 0 !important; + } + .mb-1 { + margin-bottom: 1rem !important; + } + .mt-10 { + margin-top: 10px; + } + .mb-10 { + margin-bottom: 10px; + } + + .align-baseline { + vertical-align: baseline; + } + + .bg-light-gray { + background-color: $light-gray-color-40; + } + + .line-clamp-1 { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .nowrap { + white-space: nowrap; + } + + .p-0 { + padding: 0; + } + + .m-0 { + margin: 0; + } + + .list-none { + list-style-type: none; + } + + .max-w-full { + max-width: 100%; + } + + .cursor-pointer { + cursor: pointer; + } + .relative { + position: relative; + } + + .z-1 { + z-index: 1; + } + .color-font-secondary { + color: $color--font-secondary; + } + } + + .vs__actions { cursor: pointer; - @include background-icon(staple, $size: 32px); - position: absolute; - top: -10px; - right: 10px; - height: 32px; - width: 32px; - transform: rotate(140deg); + align-items: center !important; + } + .vs__selected { + margin: 4px 4px 0 !important; + padding: 1px 5px !important; + background: transparent; + align-items: center; + gap: 5px; + border: thin solid $color--divider; + border-radius: 5px; + } + .vs__search, + .vs__search:focus, + .vs__selected-options { + font-size: inherit; } - .new_posting { - position: absolute; - top: 10px; - right: 10px; + .discussion-badges-container { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-top: 5px; + margin-bottom: 10px; } - div.action-icons { - display: none; + .badge { + display: inline-flex; + align-items: center; + gap: 5px; + border: thin solid $color--divider; + padding: 1px 5px; + border-radius: 5px; + font-size: smaller; + + .action { + cursor: pointer; + padding: 0; + background: transparent; + border: 0; + display: flex; + align-items: center; + justify-content: center; + } } - dd.posting_icons { - padding-top: 5px; - img { - vertical-align: bottom; + .topic-badge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 1px 5px; + font-size: smaller; + } + + .search-container { + background-color: $color--tile-background; + padding: 30px; + + h1 { + font-weight: 400; } - a { - margin-right: 5px; + hr { + margin: 20px 0; + border-color: $color--gray-5; + } + + .search-controls { + margin-top: 10px; + display: flex; + gap: 10px; + + .search-input-container { + flex: 1; + + input { + max-width: unset; + } + } + + button { + padding-top: 5px; + padding-bottom: 5px; + height: fit-content; + } + + input.search-input { + max-width: unset; + } + } + + .filter-summary-container { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin-top: 10px; + margin-bottom: 10px; + } + + .badge { + border-color: $color--gray-5; + } + + .toggle-filter-button { + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; + background: none; + border: none; + padding: 0; + } + + .filter-controls { + margin-top: 20px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 10px; + + .date-inputs-container { + display: flex; + gap: 10px; + align-items: start; + } + + .vs__dropdown-toggle { + max-height: fit-content; + } + + label { + margin: 0; + } } } - a.tooltip2 { - color: black; - cursor: help; - display: inline-block; - outline: none; - position: relative; - text-decoration: none; - - span { - display: inline-block; - margin-bottom: 9px; - background-image: -moz-linear-gradient(top, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0)); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(rgba(255, 255, 255, 0.5)), to(rgba(255, 255, 255, 0))); - background-image: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0)); - background-image: -o-linear-gradient(top, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0)); - background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0)); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80ffffff', endColorstr='#00ffffff', GradientType=0); - background-color: var(--dark-gray-color-20); - border: 2px solid var(--dark-gray-color-30); - border-radius: 4px; - top: 20%; - bottom: 0; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 0 rgba(255, 255, 255, 0.5) inset; - font-size: 10pt; - font-weight: normal; - margin-left: 0px; - opacity: .95; - padding: 10px; - position: absolute; - text-align: left; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4); - visibility: hidden; - white-space: normal; - width: 400px; - z-index: 999; - clear: both; - } - } - - a.tooltip2:hover span { - visibility: visible; - } - - li.selected { - background-color: var(--base-color-20); - } - - div.posting.bg2 { - flex: 1; + .search-result-container { + margin-top: 20px; + + h2 { + font-weight: 400; + } } - #tutorBreadcrumb { - float: left; - margin-bottom: 1em; - font-size: 1.4em; + ul.breadcrumb { + padding: 0; + list-style: none; + + li { + display: inline; + font-size: 18px; + } + + li+li:before { + padding: 0 8px; + content: "/"; + } } - #page-chooser { - float: right; - padding-right: 10px; - margin-bottom: 1em; + /* + vue Transition --start-- + */ + .fade-enter-active, + .fade-leave-active { + transition: opacity 0.5s ease; + } + + .fade-enter-from, + .fade-leave-to { + opacity: 0; + } + + .fade-up-enter-active { + transition: all 0.2s ease-out; + } + + .fade-up-leave-active { + transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1); + } + + .fade-up-enter-from, + .fade-up-leave-to { + transform: translateY(20px); + opacity: 0; + } + /* + vue Transition --end-- + */ + + @media (max-width: 1200px) { + &__container { + grid-template-columns: 1fr; + } + + .timeline-container { + display: none; + } + } + + @media only screen and (max-width: 600px) { + .topic-cards-container { + grid-template-columns: 1fr; + } + + .forum-table { + &.--discussions-index, + &.--subscription-index, + &.--topics-index { + th:not(:first-child), + td:not(:first-child), + col:not(:first-child) { + display: none; + } + + .details-xs { + display: flex; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; + + dl { + display: flex; + gap: 15px; + flex-wrap: wrap; + } + } + } + } + + .title-with-actions { + display: flex; + justify-content: space-between; + gap: 10px; + + &__content { + flex: 1; + } + + &__actions-xs { + display: inline-block; + } + } + + .discussion__status { + padding: 15px; + } + + .post { + &__body { + padding: 10px; + flex-direction: column; + } + + &__author-image { + width: 35px; + height: 35px; + } + + &__author { + display: flex; + gap: 10px; + } + + &__author-name-container { + &.--xl { + display: none; + } + + &.--xs { + display: flex; + flex: 1; + flex-direction: column; + align-items: start; + justify-content: space-around; + height: 100%; + } + } + } + + .post-form-container { + padding: 10px; + } + + .discussion .forum-members img { + width: 30px !important; + height: 30px !important; + } + + .header { + position: unset !important; + + &__content { + padding: 10px; + flex-wrap: wrap; + + .actions { + flex: 1; + justify-content: end; + } + } + } + + .search-container { + padding: 15px; + } + + .empty-forum { + padding: 0 5px; + } + + .forum-form { + padding: 5px ; + } } } -.forum_new_entry { - form.default { - footer { - margin-bottom: 10px; +.fullscreen-mode .forum { + .timeline-container { + top: 120px; + } + + .post, + .discussion { + &__author-image { + top: 120px; } } } + +.studip-dialog.no-default-buttons .ui-dialog-content form > :not(footer[data-dialog-button]):last-of-type { + margin-bottom: 15px; +} diff --git a/resources/assets/stylesheets/scss/links.scss b/resources/assets/stylesheets/scss/links.scss index 33da274..03fe33a 100644 --- a/resources/assets/stylesheets/scss/links.scss +++ b/resources/assets/stylesheets/scss/links.scss @@ -43,3 +43,14 @@ a.link-edit { a img { border: 0; } + +a.styleless { + color: unset; + text-decoration: unset; + + &:hover, + &:active, + &:active { + color: unset; + } +} diff --git a/resources/assets/stylesheets/scss/personal-notifications.scss b/resources/assets/stylesheets/scss/personal-notifications.scss index f506e28..6ce6d27 100644 --- a/resources/assets/stylesheets/scss/personal-notifications.scss +++ b/resources/assets/stylesheets/scss/personal-notifications.scss @@ -206,18 +206,18 @@ @include background-icon('accept', 'clickable'); background-repeat: no-repeat; background-position: right 8px center; - + &:hover { @include background-icon('accept', 'attention'); } - + margin: 0; } a.enable-desktop-notifications { @include background-icon('notification', 'clickable'); background-repeat: no-repeat; background-position: right 8px center; - + &:hover { @include background-icon('notification', 'attention'); } @@ -236,4 +236,10 @@ } .item:hover .options.hidden { visibility: visible; } } + + .html-emoji { + font-family: apple color emoji, segoe ui emoji, notocoloremoji, segoe ui symbol, android emoji, emojisymbols, emojione mozilla; + font-size: 20px; + margin-right: 10px; + } } diff --git a/resources/assets/stylesheets/scss/select.scss b/resources/assets/stylesheets/scss/select.scss index 3285081..a5f51b7 100644 --- a/resources/assets/stylesheets/scss/select.scss +++ b/resources/assets/stylesheets/scss/select.scss @@ -46,3 +46,40 @@ form.default .studip-v-select .vs__selected { padding: 0; margin: 2px 2px 0; } + + + +.vs { + &__open-indicator, + &__clear, + &__deselect { + fill: $base-color !important; + } + + &__dropdown-toggle, + &__dropdown-menu { + border: 1px solid $light-gray-color-40 !important; + } + + &__dropdown-menu { + margin-top: 5px !important; + box-shadow: none !important; + } + + &__deselect, + &__clear { + height: 15px; + width: 15px; + $icon: icon-path(decline, clickable); + background-image: url("#{$icon}") !important; + background-size: 15px 15px !important; + + svg { + display: none; + } + } +} + +.multiselect__tags { + background-color: red !important; +} diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index 21ce5ab..16075ae 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -650,3 +650,253 @@ input.allow-plaintext-toggle { } } } + +.links-preview { + &__item { + min-height: 150px; + border: solid 1px $color--content-box-border; + border-radius: 5px; + overflow: hidden; + } + + &__controls { + display: flex; + justify-content: end; + + button { + cursor: pointer; + background-color: transparent; + border: none; + padding: 7px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; + + &:hover { + background-color: $content-color-10; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + } +} + +.og-preview { + display: grid; + grid-template-columns: 1fr 2fr; + grid-gap: 10px; + + &__image-container { + max-height: 150px; + + img { + width: 100%; + height: 100%; + -o-object-fit: cover; + object-fit: cover; + } + } + + &__details { + padding: 20px 10px; + } + + &__title { + color: $color--font-primary; + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + &__description { + color: $color--font-inactive; + margin-top: 5px; + font-size: small; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } +} + +@media only screen and (max-width: 600px) { + .og-preview { + grid-template-columns: 1fr; + + &__details { + padding: 5px 10px; + } + } +} + +.dropdown { + position: relative; + width: min-content; + + &__content { + position: absolute; + right: 0; + left: auto; + z-index: 10; + display: flex; + flex-direction: column; + margin-top: 4px; + list-style: none; + background: white; + border: thin solid $color--action-menu-border; + border-radius: 5px; + box-shadow: 2px 2px 0 $color--action-menu-shadow; + overflow: hidden; + min-width: 200px; + max-width: 90vw; + } + + &__header { + padding: 10px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + } + + &__title { + margin: 0; + font-weight: 500; + font-size: 16px; + } + + &__close-button { + position: absolute; + right: 10px; + top: 10px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: transparent; + height: 20px; + width: 20px; + cursor: pointer; + } + + &__items { + padding: 0; + list-style: none; + width: max-content; + display: flex; + flex-direction: column; + + li { + padding: 6px 10px; + transition: background-color 0.2s ease-out; + cursor: pointer; + + &:not(:first-child) { + border-top: thin solid $color--table-border; + } + + &:hover, + &:focus { + background-color: $color--tile-background-hover; + } + } + } +} + +.user-avatar { + padding: 10px; + min-width: 200px; + max-width: 80vw; + max-height: 80vh; + + hr { + margin: 10px 0; + } + + &__header { + display: flex; + align-items: center; + gap: 10px; + + img.user-profile { + width: 50px; + height: 50px; + border-radius: 100%; + border: 1.5px solid white; + transition: all .4s ease-in-out; + } + + .user-info { + flex: 1; + + .user-name { + text-wrap: auto; + font-size: 14px; + margin-top: 0; + margin-bottom: 5px; + font-weight: bold; + color: $color--font-primary; + } + + p { + font-size: smaller; + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-wrap: auto; + } + } + } + &__actions { + list-style: none; + padding: 0; + + .action-item { + cursor: pointer; + background: none; + border: none; + display: flex; + align-items: center; + gap: 10px; + width: fit-content; + padding: 5px 10px; + } + + li { + transition: background-color 0.2s ease-out; + + &:hover, + &:focus { + background-color: $color--tile-background-hover; + } + } + } +} +/* + vue Transition:fade-down --start-- +*/ +.fade-down-enter-active { + transition: all 0.2s ease-out; +} + +.fade-down-leave-active { + transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1); +} + +.fade-down-enter-from, +.fade-down-leave-to { + transform: translateY(-20px); + opacity: 0; +} +/* + vue Transition:fade-down --end-- +*/ + diff --git a/resources/vue/apps/forum/categories/Edit.vue b/resources/vue/apps/forum/categories/Edit.vue new file mode 100644 index 0000000..b3ca722 --- /dev/null +++ b/resources/vue/apps/forum/categories/Edit.vue @@ -0,0 +1,92 @@ + + + + diff --git a/resources/vue/apps/forum/categories/Index.vue b/resources/vue/apps/forum/categories/Index.vue new file mode 100644 index 0000000..4d7be3e --- /dev/null +++ b/resources/vue/apps/forum/categories/Index.vue @@ -0,0 +1,270 @@ + + + diff --git a/resources/vue/apps/forum/categories/Show.vue b/resources/vue/apps/forum/categories/Show.vue new file mode 100644 index 0000000..c9a411d --- /dev/null +++ b/resources/vue/apps/forum/categories/Show.vue @@ -0,0 +1,125 @@ + + + diff --git a/resources/vue/apps/forum/discussions/Edit.vue b/resources/vue/apps/forum/discussions/Edit.vue new file mode 100644 index 0000000..4531ce1 --- /dev/null +++ b/resources/vue/apps/forum/discussions/Edit.vue @@ -0,0 +1,164 @@ + + + + + + diff --git a/resources/vue/apps/forum/discussions/Show.vue b/resources/vue/apps/forum/discussions/Show.vue new file mode 100644 index 0000000..b41b909 --- /dev/null +++ b/resources/vue/apps/forum/discussions/Show.vue @@ -0,0 +1,325 @@ + + + + + diff --git a/resources/vue/apps/forum/discussions_types/Edit.vue b/resources/vue/apps/forum/discussions_types/Edit.vue new file mode 100644 index 0000000..df6fa28 --- /dev/null +++ b/resources/vue/apps/forum/discussions_types/Edit.vue @@ -0,0 +1,91 @@ + + + + diff --git a/resources/vue/apps/forum/recent/Index.vue b/resources/vue/apps/forum/recent/Index.vue new file mode 100644 index 0000000..5b8a788 --- /dev/null +++ b/resources/vue/apps/forum/recent/Index.vue @@ -0,0 +1,71 @@ + + + diff --git a/resources/vue/apps/forum/search/Index.vue b/resources/vue/apps/forum/search/Index.vue new file mode 100644 index 0000000..9e7c36a --- /dev/null +++ b/resources/vue/apps/forum/search/Index.vue @@ -0,0 +1,282 @@ + + + diff --git a/resources/vue/apps/forum/subscriptions/Index.vue b/resources/vue/apps/forum/subscriptions/Index.vue new file mode 100644 index 0000000..6ed2a84 --- /dev/null +++ b/resources/vue/apps/forum/subscriptions/Index.vue @@ -0,0 +1,233 @@ + + + diff --git a/resources/vue/apps/forum/topics/Edit.vue b/resources/vue/apps/forum/topics/Edit.vue new file mode 100644 index 0000000..845e062 --- /dev/null +++ b/resources/vue/apps/forum/topics/Edit.vue @@ -0,0 +1,125 @@ + + + + diff --git a/resources/vue/apps/forum/topics/Index.vue b/resources/vue/apps/forum/topics/Index.vue new file mode 100644 index 0000000..830f76e --- /dev/null +++ b/resources/vue/apps/forum/topics/Index.vue @@ -0,0 +1,100 @@ + + + diff --git a/resources/vue/apps/forum/topics/Show.vue b/resources/vue/apps/forum/topics/Show.vue new file mode 100644 index 0000000..12751a6 --- /dev/null +++ b/resources/vue/apps/forum/topics/Show.vue @@ -0,0 +1,129 @@ + + + diff --git a/resources/vue/components/Dropdown.vue b/resources/vue/components/Dropdown.vue new file mode 100644 index 0000000..338e23f --- /dev/null +++ b/resources/vue/components/Dropdown.vue @@ -0,0 +1,55 @@ + + + diff --git a/resources/vue/components/LinksPreview.vue b/resources/vue/components/LinksPreview.vue new file mode 100644 index 0000000..7946002 --- /dev/null +++ b/resources/vue/components/LinksPreview.vue @@ -0,0 +1,51 @@ + + + diff --git a/resources/vue/components/StudipDateTime.vue b/resources/vue/components/StudipDateTime.vue index 61f498b..4f4ab72 100644 --- a/resources/vue/components/StudipDateTime.vue +++ b/resources/vue/components/StudipDateTime.vue @@ -1,55 +1,64 @@ - + + + diff --git a/resources/vue/components/StudipSwitch.vue b/resources/vue/components/StudipSwitch.vue new file mode 100644 index 0000000..666f302 --- /dev/null +++ b/resources/vue/components/StudipSwitch.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/resources/vue/components/UserAvatar.vue b/resources/vue/components/UserAvatar.vue new file mode 100644 index 0000000..b9c0b1c --- /dev/null +++ b/resources/vue/components/UserAvatar.vue @@ -0,0 +1,102 @@ + + diff --git a/resources/vue/components/forum/EmptyForum.vue b/resources/vue/components/forum/EmptyForum.vue new file mode 100644 index 0000000..bc7d25d --- /dev/null +++ b/resources/vue/components/forum/EmptyForum.vue @@ -0,0 +1,40 @@ + + + diff --git a/resources/vue/components/forum/ForumApp.vue b/resources/vue/components/forum/ForumApp.vue new file mode 100644 index 0000000..60c8f8d --- /dev/null +++ b/resources/vue/components/forum/ForumApp.vue @@ -0,0 +1,34 @@ + + + diff --git a/resources/vue/components/forum/ForumMembers.vue b/resources/vue/components/forum/ForumMembers.vue new file mode 100644 index 0000000..7e0e385 --- /dev/null +++ b/resources/vue/components/forum/ForumMembers.vue @@ -0,0 +1,134 @@ + + + diff --git a/resources/vue/components/forum/Loader.vue b/resources/vue/components/forum/Loader.vue new file mode 100644 index 0000000..17584bb --- /dev/null +++ b/resources/vue/components/forum/Loader.vue @@ -0,0 +1,19 @@ + + + diff --git a/resources/vue/components/forum/SelectTagsInput.vue b/resources/vue/components/forum/SelectTagsInput.vue new file mode 100644 index 0000000..2582852 --- /dev/null +++ b/resources/vue/components/forum/SelectTagsInput.vue @@ -0,0 +1,35 @@ + + + diff --git a/resources/vue/components/forum/SelectUserInput.vue b/resources/vue/components/forum/SelectUserInput.vue new file mode 100644 index 0000000..68bd283 --- /dev/null +++ b/resources/vue/components/forum/SelectUserInput.vue @@ -0,0 +1,30 @@ + + + diff --git a/resources/vue/components/forum/SubscriptionDropdown.vue b/resources/vue/components/forum/SubscriptionDropdown.vue new file mode 100644 index 0000000..c641011 --- /dev/null +++ b/resources/vue/components/forum/SubscriptionDropdown.vue @@ -0,0 +1,207 @@ + + + diff --git a/resources/vue/components/forum/UserAvatarDropdown.vue b/resources/vue/components/forum/UserAvatarDropdown.vue new file mode 100644 index 0000000..dec1658 --- /dev/null +++ b/resources/vue/components/forum/UserAvatarDropdown.vue @@ -0,0 +1,44 @@ + + diff --git a/resources/vue/components/forum/categories/CategoryItem.vue b/resources/vue/components/forum/categories/CategoryItem.vue new file mode 100644 index 0000000..80463fa --- /dev/null +++ b/resources/vue/components/forum/categories/CategoryItem.vue @@ -0,0 +1,212 @@ + + + diff --git a/resources/vue/components/forum/categories/Create.vue b/resources/vue/components/forum/categories/Create.vue new file mode 100644 index 0000000..fa79752 --- /dev/null +++ b/resources/vue/components/forum/categories/Create.vue @@ -0,0 +1,27 @@ + + + + diff --git a/resources/vue/components/forum/discussions/Create.vue b/resources/vue/components/forum/discussions/Create.vue new file mode 100644 index 0000000..f4d3fca --- /dev/null +++ b/resources/vue/components/forum/discussions/Create.vue @@ -0,0 +1,30 @@ + + + + diff --git a/resources/vue/components/forum/discussions/DiscussionIndex.vue b/resources/vue/components/forum/discussions/DiscussionIndex.vue new file mode 100644 index 0000000..748d291 --- /dev/null +++ b/resources/vue/components/forum/discussions/DiscussionIndex.vue @@ -0,0 +1,290 @@ + + + diff --git a/resources/vue/components/forum/discussions/DiscussionTimeline.vue b/resources/vue/components/forum/discussions/DiscussionTimeline.vue new file mode 100644 index 0000000..c394d01 --- /dev/null +++ b/resources/vue/components/forum/discussions/DiscussionTimeline.vue @@ -0,0 +1,88 @@ + + + + diff --git a/resources/vue/components/forum/discussions/SelectDiscussionType.vue b/resources/vue/components/forum/discussions/SelectDiscussionType.vue new file mode 100644 index 0000000..c6e71d4 --- /dev/null +++ b/resources/vue/components/forum/discussions/SelectDiscussionType.vue @@ -0,0 +1,35 @@ + + + diff --git a/resources/vue/components/forum/enums/SubscriptionNotificationType.ts b/resources/vue/components/forum/enums/SubscriptionNotificationType.ts new file mode 100644 index 0000000..01deab1 --- /dev/null +++ b/resources/vue/components/forum/enums/SubscriptionNotificationType.ts @@ -0,0 +1,5 @@ +export enum SubscriptionNotificationType { + All = 'all', + RepliesOnly = 'replies_only', + None = 'none', +} diff --git a/resources/vue/components/forum/helpers/index.js b/resources/vue/components/forum/helpers/index.js new file mode 100644 index 0000000..2e0b210 --- /dev/null +++ b/resources/vue/components/forum/helpers/index.js @@ -0,0 +1,16 @@ +export const highlightText = (keyword, containerSelector)=> { + const elements = document.querySelectorAll(containerSelector); + + for (const element of elements) { + const re = new RegExp(keyword, "gi"); + element.innerHTML = element.innerHTML.replace(re, match => `${match}`); + } +} + +export const removeHighlight = (containerSelector) => { + const markedElements = document.querySelectorAll(containerSelector); + for (const element of markedElements) { + const textNode = document.createTextNode(element.textContent); + element.replaceWith(textNode); + } +} diff --git a/resources/vue/components/forum/helpers/transformers.js b/resources/vue/components/forum/helpers/transformers.js new file mode 100644 index 0000000..abbe6ae --- /dev/null +++ b/resources/vue/components/forum/helpers/transformers.js @@ -0,0 +1,18 @@ +export const subscriptionTransformer = subscription => { + // rename object key + if (subscription.subject.title) { + subscription.subject.name = subscription.subject.title; + } + + return { + ...subscription, + subject: subscription.subject + }; +}; + +export const topicTransformer = topic => { + return { + ...topic, + ...topic.category + } +}; diff --git a/resources/vue/components/forum/helpers/urls.js b/resources/vue/components/forum/helpers/urls.js new file mode 100644 index 0000000..eb8538c --- /dev/null +++ b/resources/vue/components/forum/helpers/urls.js @@ -0,0 +1,17 @@ +export const getTopicURL = id => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/topics/show/${id}`); +export const getTopicEditURL = id => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/topics/edit/${id}`); +export const getTopicDeleteURL = id => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/topics/delete/${id}`); + +// Discussions +export const getDiscussionCreateURL = () => STUDIP.URLHelper.getURL('dispatch.php/course/forum/discussions/edit'); +export const getDiscussionURL = (discussion_id, params = {}) => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/discussions/show/${discussion_id}`, params); + +export const getCategoryURL = id => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/categories/show/${id}`); +export const getCategoryCreateURL = () => STUDIP.URLHelper.getURL('dispatch.php/course/forum/categories/edit'); +export const getCategoryEditURL = id => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/categories/edit/${id}`); +export const getCategoryDeleteURL = id => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/categories/delete/${id}`); + +export const getSearchURL = (hashtags='') => STUDIP.URLHelper.getURL(`dispatch.php/course/forum/search?${hashtags}`); + + +export const userProfileURL = username => STUDIP.URLHelper.getURL('dispatch.php/profile', {username}); diff --git a/resources/vue/components/forum/posts/Post.vue b/resources/vue/components/forum/posts/Post.vue new file mode 100644 index 0000000..f954b94 --- /dev/null +++ b/resources/vue/components/forum/posts/Post.vue @@ -0,0 +1,235 @@ + + + diff --git a/resources/vue/components/forum/posts/PostContent.vue b/resources/vue/components/forum/posts/PostContent.vue new file mode 100644 index 0000000..93111f3 --- /dev/null +++ b/resources/vue/components/forum/posts/PostContent.vue @@ -0,0 +1,66 @@ + + + + diff --git a/resources/vue/components/forum/posts/PostCreateForm.vue b/resources/vue/components/forum/posts/PostCreateForm.vue new file mode 100644 index 0000000..095413a --- /dev/null +++ b/resources/vue/components/forum/posts/PostCreateForm.vue @@ -0,0 +1,161 @@ + + + diff --git a/resources/vue/components/forum/posts/PostEditForm.vue b/resources/vue/components/forum/posts/PostEditForm.vue new file mode 100644 index 0000000..9c50f90 --- /dev/null +++ b/resources/vue/components/forum/posts/PostEditForm.vue @@ -0,0 +1,107 @@ + + + diff --git a/resources/vue/components/forum/posts/PostReactions.vue b/resources/vue/components/forum/posts/PostReactions.vue new file mode 100644 index 0000000..816ed15 --- /dev/null +++ b/resources/vue/components/forum/posts/PostReactions.vue @@ -0,0 +1,141 @@ + + + diff --git a/resources/vue/components/forum/posts/reactions.js b/resources/vue/components/forum/posts/reactions.js new file mode 100644 index 0000000..805bf55 --- /dev/null +++ b/resources/vue/components/forum/posts/reactions.js @@ -0,0 +1,35 @@ +export const REACTION_ICONS = { + 'THUMBS UP SIGN' : { + icon: '👍', + value: 'THUMBS UP SIGN' + }, + 'THUMBS DOWN SIGN' : { + icon: '👎', + value: 'THUMBS DOWN SIGN' + }, + 'ROCKET' : { + icon: '🚀', + value: 'ROCKET' + }, + 'GRINNING FACE': { + icon: '😀', + value: 'GRINNING FACE' + }, + 'SMILING FACE WITH SUNGLASSES': { + icon: '😎', + value: 'SMILING FACE WITH SUNGLASSES' + }, + 'CONFUSED FACE': { + icon: '😕', + value: 'CONFUSED FACE' + }, + 'BLACK HEART SUIT': { + icon: '♥', + value: 'BLACK HEART SUIT' + }, + 'PARTY POPPER': { + icon: '🎉', + value: 'PARTY POPPER' + } +} + diff --git a/resources/vue/components/forum/topics/CreateTopic.vue b/resources/vue/components/forum/topics/CreateTopic.vue new file mode 100644 index 0000000..d6d3d5f --- /dev/null +++ b/resources/vue/components/forum/topics/CreateTopic.vue @@ -0,0 +1,37 @@ + + + + diff --git a/resources/vue/components/forum/topics/SelectTopicInput.vue b/resources/vue/components/forum/topics/SelectTopicInput.vue new file mode 100644 index 0000000..7dabc95 --- /dev/null +++ b/resources/vue/components/forum/topics/SelectTopicInput.vue @@ -0,0 +1,53 @@ + + + diff --git a/resources/vue/components/forum/topics/TopicItem.vue b/resources/vue/components/forum/topics/TopicItem.vue new file mode 100644 index 0000000..2fdfe74 --- /dev/null +++ b/resources/vue/components/forum/topics/TopicItem.vue @@ -0,0 +1,203 @@ + + + diff --git a/resources/vue/components/forum/topics/TopicsIndex.vue b/resources/vue/components/forum/topics/TopicsIndex.vue new file mode 100644 index 0000000..598d691 --- /dev/null +++ b/resources/vue/components/forum/topics/TopicsIndex.vue @@ -0,0 +1,228 @@ + + + diff --git a/resources/vue/composables/useDetectOutsideClick.js b/resources/vue/composables/useDetectOutsideClick.js new file mode 100644 index 0000000..ee7db14 --- /dev/null +++ b/resources/vue/composables/useDetectOutsideClick.js @@ -0,0 +1,18 @@ +import {onBeforeUnmount, onMounted} from "vue"; + +export default function useDetectOutsideClick(component, callback) { + if (!component) return; + function listener(event) { + if (event.target !== component.value && event.composedPath().includes(component.value)) { + return; + } + if (typeof callback === 'function') { + callback(); + } + } + + onMounted(() => { window.addEventListener('click', listener) }); + onBeforeUnmount(() => { window.removeEventListener('click', listener) }); + + return {listener}; +} diff --git a/resources/vue/composables/useSortable.js b/resources/vue/composables/useSortable.js new file mode 100644 index 0000000..5533076 --- /dev/null +++ b/resources/vue/composables/useSortable.js @@ -0,0 +1,72 @@ +import {ref, watch} from "vue"; +import {$gettext} from "@/assets/javascripts/lib/gettext"; + +const getNestedValue = (object, path) => path.split('.').reduce((acc, key) => acc && acc[key], object); + +export function useSortable(data) { + const sortKey = ref(null); + const sortOrder = ref('asc'); + const sortedData = ref([...data.value]); + + const sortData = () => { + if (!sortKey.value) { + sortedData.value = [...data.value]; + return; + } + + sortedData.value = [...data.value].sort((a, b) => { + const aValue = getNestedValue(a, sortKey.value); + const bValue = getNestedValue(b, sortKey.value); + if (aValue < bValue) return sortOrder.value === 'asc' ? -1 : 1; + if (aValue > bValue) return sortOrder.value === 'asc' ? 1 : -1; + return 0; + }); + }; + + watch([data, sortKey, sortOrder], sortData, { immediate: true }); + + function sortBy(key){ + if (sortKey.value === key) { + sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'; + } else { + sortKey.value = key; + sortOrder.value = 'asc'; + } + } + + function getSortClass(key) { + if (sortKey.value === key) { + return sortOrder.value === 'asc' ? ['sortasc'] : ['sortdesc']; + } + + return []; + } + + function getAriaSortString(key) { + return key === sortKey.value + ? (sortOrder.value === 'asc' ? 'ascending' : 'descending') + : null; + } + + function getAriaSortLabel(key, label) { + if (sortKey.value !== key) { + return null; + } + + if (sortOrder.value === 'asc') { + return $gettext('Es wird aufsteigend nach der Spalte %{ label } sortiert.', { label }); + } + + return $gettext('Es wird absteigend nach der Spalte %{ label } sortiert.', { label }); + } + + return { + sortKey, + sortedData, + sortOrder, + sortBy, + getSortClass, + getAriaSortString, + getAriaSortLabel + }; +} diff --git a/resources/vue/store/pinia/forum/ForumConfig.js b/resources/vue/store/pinia/forum/ForumConfig.js new file mode 100644 index 0000000..d2de6b6 --- /dev/null +++ b/resources/vue/store/pinia/forum/ForumConfig.js @@ -0,0 +1,34 @@ +import {defineStore} from "pinia"; +import {ref} from "vue"; + +export const useForumConfig = defineStore( + 'forum_config', + () => { + const isAdmin = ref(false); + const isModerator = ref(false); + const anonymousPost = ref(false); + const tileLayout = ref(true); + + function toggleForumLayout() { + tileLayout.value = !tileLayout.value; + + const configId = `${STUDIP.USER_ID}_FORUM_TILE_LAYOUT`; + + const data = { + id: configId, + type: 'config-values', + attributes: { value: tileLayout.value } + }; + + STUDIP.jsonapi.PATCH(`config-values/${configId}`, { data: { data } }); + } + + return { + isAdmin, + isModerator, + anonymousPost, + tileLayout, + toggleForumLayout + } + } +) diff --git a/resources/vue/store/pinia/forum/ForumPost.js b/resources/vue/store/pinia/forum/ForumPost.js new file mode 100644 index 0000000..9534559 --- /dev/null +++ b/resources/vue/store/pinia/forum/ForumPost.js @@ -0,0 +1,53 @@ +import {defineStore} from "pinia"; +import {ref} from "vue"; +export const useForumPost = defineStore( + 'forum_discussion_post', + () => { + + const posts = ref([]); + + function initPosts(newPosts) { + posts.value = newPosts; + } + + function addPost(post) { + posts.value.push(post); + } + + function updatePost(post) { + const postIndex = posts.value.findIndex(({ id }) => id === post.id); + + posts.value[postIndex] = post; + } + + function removePost(postId) { + posts.value = posts.value.filter(({ id }) => id !== postId); + } + + function addPostReaction(reaction, postId) { + const postIndex = posts.value.findIndex(({ id }) => id === postId); + + posts.value[postIndex].reactions.push(reaction); + } + + function removePostReaction(reactionId, postId) { + const postIndex = posts.value.findIndex(({ id }) => id === postId); + + const postReactions = posts.value[postIndex].reactions; + + if (postReactions) { + posts.value[postIndex].reactions = postReactions.filter(({ id }) => id !== reactionId); + } + } + + return { + posts, + initPosts, + addPost, + updatePost, + removePost, + addPostReaction, + removePostReaction + } + } +) diff --git a/templates/forms/color_input.php b/templates/forms/color_input.php new file mode 100644 index 0000000..4cbb7f8 --- /dev/null +++ b/templates/forms/color_input.php @@ -0,0 +1,17 @@ +
+ required ? ' class="studiprequired"' : '') ?> for=""> + + title) ?> + + required) : ?> + + + required ? 'required aria-required="true"' : '') ?> + > + + +
diff --git a/templates/personal_notifications/notification.php b/templates/personal_notifications/notification.php index 2f86a50..ad93758 100644 --- a/templates/personal_notifications/notification.php +++ b/templates/personal_notifications/notification.php @@ -2,8 +2,15 @@
> -
+ +
+ +
+ +
+ +