From 7293abbad9c1a149cfffd99c9ab5060fe945b773 Mon Sep 17 00:00:00 2001 From: Ron Lucke Date: Wed, 10 Jan 2024 14:20:49 +0000 Subject: StEP #2472 Merge request studip/studip!2296 --- app/controllers/course/courseware.php | 6 + app/controllers/course/feedback.php | 10 +- app/views/course/courseware/courseware.php | 1 + app/views/course/courseware/index.php | 1 + app/views/course/feedback/_add_edit_entry_form.php | 6 + app/views/course/feedback/_entry.php | 5 + .../course/feedback/_new_edit_feedback_form.php | 12 +- app/views/course/feedback/index.php | 4 +- .../5.5.22_add_feedback_anonymous_entries.php | 27 +++ lib/classes/FeedbackRange.interface.php | 7 + lib/classes/JsonApi/RouteMap.php | 10 +- .../Routes/Courseware/CoursesUnitsIndex.php | 1 + .../Courseware/CoursewareInstancesUpdate.php | 28 ++- lib/classes/JsonApi/Routes/Feedback/Authority.php | 67 ++++-- .../Routes/Feedback/FeedbackElementsCreate.php | 114 ++++++++++ .../Routes/Feedback/FeedbackElementsDelete.php | 39 ++++ .../Routes/Feedback/FeedbackElementsShow.php | 17 +- .../Routes/Feedback/FeedbackElementsUpdate.php | 94 +++++++++ .../Routes/Feedback/FeedbackEntriesCreate.php | 115 +++++++++++ .../Routes/Feedback/FeedbackEntriesDelete.php | 40 ++++ .../Routes/Feedback/FeedbackEntriesShow.php | 12 +- .../Routes/Feedback/FeedbackEntriesUpdate.php | 95 +++++++++ .../JsonApi/Routes/Feedback/RangeTypeAware.php | 20 ++ .../JsonApi/Routes/Feedback/RatingHelper.php | 31 +++ .../JsonApi/Schemas/Courseware/Instance.php | 2 + .../Schemas/Courseware/StructuralElement.php | 23 +++ lib/classes/JsonApi/Schemas/Courseware/Unit.php | 11 + lib/classes/JsonApi/Schemas/FeedbackElement.php | 49 ++--- lib/classes/JsonApi/Schemas/FeedbackEntry.php | 1 + lib/models/Courseware/Instance.php | 45 ++++ lib/models/Courseware/StructuralElement.php | 47 ++++- lib/models/Courseware/Unit.php | 50 ++++- lib/models/FeedbackElement.php | 18 ++ lib/models/FeedbackEntry.php | 2 + lib/modules/CoursewareModule.class.php | 2 +- public/assets/images/icons/black/feedback.svg | 1 + public/assets/images/icons/blue/feedback.svg | 1 + public/assets/images/icons/green/feedback.svg | 1 + public/assets/images/icons/grey/feedback.svg | 1 + public/assets/images/icons/red/feedback.svg | 1 + public/assets/images/icons/white/feedback.svg | 1 + public/assets/images/icons/yellow/feedback.svg | 1 + resources/assets/stylesheets/scss/courseware.scss | 2 +- .../scss/courseware/layouts/ribbon.scss | 4 + .../stylesheets/scss/courseware/layouts/tile.scss | 11 +- resources/assets/stylesheets/scss/feedback.scss | 151 +++++++++++++- .../courseware/CoursewareBlockCommentsOverview.vue | 8 +- .../CoursewareCommentsOverviewDialog.vue | 2 +- .../courseware/CoursewareDashboardStudents.vue | 26 +-- .../courseware/CoursewareDashboardTasks.vue | 6 +- ...CoursewareStructuralElementCommentsOverview.vue | 10 +- resources/vue/components/courseware/ShelfApp.vue | 5 + .../courseware/blocks/CoursewareBlockFeedback.vue | 4 +- .../structural-element/CoursewareFeedbackPopup.vue | 109 ++++++++++ .../CoursewareStructuralElement.vue | 229 +++++++++++++++++++-- .../CoursewareStructuralElementDiscussion.vue | 2 +- .../CoursewareStructuralElementFeedback.vue | 17 +- .../unit/CoursewareShelfDialogTopics.vue | 2 +- .../courseware/unit/CoursewareUnitItem.vue | 178 +++++++++++++--- .../unit/CoursewareUnitItemDialogSettings.vue | 23 +++ .../CoursewareActivitiesWidgetFilterType.vue | 2 +- .../components/feedback/FeedbackCreateDialog.vue | 117 +++++++++++ .../vue/components/feedback/FeedbackDialog.vue | 223 ++++++++++++++++++++ .../components/feedback/FeedbackElementUpdate.vue | 69 +++++++ .../vue/components/feedback/FeedbackEntryBox.vue | 106 ++++++++++ .../components/feedback/FeedbackEntryCreate.vue | 114 ++++++++++ .../feedback/FeedbackFiveStarsHistogram.vue | 91 ++++++++ .../vue/components/feedback/StudipFiveStars.vue | 46 +++++ .../components/feedback/StudipFiveStarsInput.vue | 51 +++++ resources/vue/courseware-index-app.js | 8 + resources/vue/courseware-shelf-app.js | 7 + .../store/courseware/courseware-shelf.module.js | 29 ++- .../vue/store/courseware/courseware.module.js | 52 ++++- 73 files changed, 2570 insertions(+), 153 deletions(-) create mode 100644 db/migrations/5.5.22_add_feedback_anonymous_entries.php create mode 100644 lib/classes/JsonApi/Routes/Feedback/FeedbackElementsCreate.php create mode 100644 lib/classes/JsonApi/Routes/Feedback/FeedbackElementsDelete.php create mode 100644 lib/classes/JsonApi/Routes/Feedback/FeedbackElementsUpdate.php create mode 100644 lib/classes/JsonApi/Routes/Feedback/FeedbackEntriesCreate.php create mode 100644 lib/classes/JsonApi/Routes/Feedback/FeedbackEntriesDelete.php create mode 100644 lib/classes/JsonApi/Routes/Feedback/FeedbackEntriesUpdate.php create mode 100644 lib/classes/JsonApi/Routes/Feedback/RangeTypeAware.php create mode 100644 lib/classes/JsonApi/Routes/Feedback/RatingHelper.php create mode 100644 public/assets/images/icons/black/feedback.svg create mode 100644 public/assets/images/icons/blue/feedback.svg create mode 100644 public/assets/images/icons/green/feedback.svg create mode 100644 public/assets/images/icons/grey/feedback.svg create mode 100644 public/assets/images/icons/red/feedback.svg create mode 100644 public/assets/images/icons/white/feedback.svg create mode 100644 public/assets/images/icons/yellow/feedback.svg create mode 100644 resources/vue/components/courseware/structural-element/CoursewareFeedbackPopup.vue create mode 100644 resources/vue/components/feedback/FeedbackCreateDialog.vue create mode 100644 resources/vue/components/feedback/FeedbackDialog.vue create mode 100644 resources/vue/components/feedback/FeedbackElementUpdate.vue create mode 100644 resources/vue/components/feedback/FeedbackEntryBox.vue create mode 100644 resources/vue/components/feedback/FeedbackEntryCreate.vue create mode 100644 resources/vue/components/feedback/FeedbackFiveStarsHistogram.vue create mode 100644 resources/vue/components/feedback/StudipFiveStars.vue create mode 100644 resources/vue/components/feedback/StudipFiveStarsInput.vue diff --git a/app/controllers/course/courseware.php b/app/controllers/course/courseware.php index f8e721e..af7d0e9 100644 --- a/app/controllers/course/courseware.php +++ b/app/controllers/course/courseware.php @@ -36,6 +36,12 @@ class Course_CoursewareController extends CoursewareController $this->licenses = $this->getLicenses(); $this->oer_enabled = Config::get()->OERCAMPUS_ENABLED && $GLOBALS['perm']->have_perm(Config::get()->OER_PUBLIC_STATUS); $this->unitsNotFound = Unit::countBySql('range_id = ?', [Context::getId()]) === 0; + + $this->feedback_settings = json_encode([ + 'activated' => \Feedback::isActivated(), + 'adminPerm' => \Feedback::hasAdminPerm(Context::getId()), + 'createPerm' => \Feedback::hasCreatePerm(Context::getId()), + ]); } public function index_action(): void diff --git a/app/controllers/course/feedback.php b/app/controllers/course/feedback.php index dec2000..c6eb977 100644 --- a/app/controllers/course/feedback.php +++ b/app/controllers/course/feedback.php @@ -49,7 +49,7 @@ class Course_FeedbackController extends AuthenticatedController $widget->addLink( _('Neues Feedback-Element'), $this->url_for('course/feedback/create_form'), - Icon::create('star') + Icon::create('add') )->asDialog(); } } @@ -72,6 +72,8 @@ class Course_FeedbackController extends AuthenticatedController 'results_visible' => 1, 'commentable' => 1, 'mode' => FeedbackElement::MODE_5STAR_RATING, + 'mode' => 1, + 'anonymous_entries' => 1, ]); } @@ -99,6 +101,7 @@ class Course_FeedbackController extends AuthenticatedController 'description' => Studip\Markup::purifyHtml(Request::get('description')), 'results_visible' => intval(Request::get('results_visible')), 'commentable' => $commentable, + 'anonymous_entries' => intval(Request::get('anonymous_entries')), 'mode' => $mode ]); $feedback->store(); @@ -232,11 +235,13 @@ class Course_FeedbackController extends AuthenticatedController if ($rating == 0) { $rating = 1; } + $anonymous = intval(Request::get('anonymous')); $entry = FeedbackEntry::build([ 'feedback_id' => $this->feedback->id, 'user_id' => $GLOBALS['user']->id, 'rating' => $rating, - 'comment' => trim(Request::get('comment')) + 'comment' => trim(Request::get('comment')), + 'anonymous' => $anonymous, ]); $entry->store(); PageLayout::postSuccess(_('Feedback gespeichert')); @@ -268,6 +273,7 @@ class Course_FeedbackController extends AuthenticatedController } $entry->comment = trim(Request::get('comment')); $entry->rating = $rating; + $entry->anonymous = Request::int('anonymous', 0); $entry->store(); PageLayout::postSuccess(_('Änderungen gespeichert')); $this->redirect($entry->feedback->getRange()->getRangeUrl()); diff --git a/app/views/course/courseware/courseware.php b/app/views/course/courseware/courseware.php index 2dadc86..9de924d 100644 --- a/app/views/course/courseware/courseware.php +++ b/app/views/course/courseware/courseware.php @@ -6,6 +6,7 @@ entry-id="" unit-id="" licenses='' + feedback-settings='' > diff --git a/app/views/course/courseware/index.php b/app/views/course/courseware/index.php index 81296cb..eea4063 100644 --- a/app/views/course/courseware/index.php +++ b/app/views/course/courseware/index.php @@ -3,4 +3,5 @@ entry-type="courses" entry-id="" licenses='' + feedback-settings='' > diff --git a/app/views/course/feedback/_add_edit_entry_form.php b/app/views/course/feedback/_add_edit_entry_form.php index b117b78..1481f57 100644 --- a/app/views/course/feedback/_add_edit_entry_form.php +++ b/app/views/course/feedback/_add_edit_entry_form.php @@ -29,6 +29,12 @@ +anonymous_entries) : ?> + +
'feedback-entry-submit']) ?> 'feedback-entry-cancel']) ?> diff --git a/app/views/course/feedback/_entry.php b/app/views/course/feedback/_entry.php index 29f35fb..b7473d1 100644 --- a/app/views/course/feedback/_entry.php +++ b/app/views/course/feedback/_entry.php @@ -1,10 +1,15 @@
+ + + + + diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue index 143a03a..34d3145 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue @@ -52,6 +52,20 @@ ({{ elementProgress }} %) + @@ -593,6 +608,27 @@ + + +
0; + }, + menuItems() { let menu = [ { id: 4, label: this.$gettext('Informationen anzeigen'), icon: 'info', emit: 'showInfo' }, { id: 5, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' }, ]; + if (this.isFeedbackActivated) { + if (this.canCreateFeedbackElement && !this.hasFeedbackElement) { + menu.push({ + id: 6, + label: this.$gettext('Feedback aktivieren'), + icon: 'feedback', + emit: 'showFeedbackCreate', + }); + } + if (this.hasFeedbackElement) { + menu.push({ + id: 6, + label: this.$gettext('Feedback anzeigen'), + icon: 'feedback', + emit: 'showFeedback', + }); + } + } if (this.oerEnableSuggestions && this.inCourse && this.userId !== this.structuralElement.relationships.owner.data.id) { menu.push( - { id: 6, label: this.$gettext('Seite für OER Campus vorschlagen'), icon: 'oer-campus', + { id: 7, label: this.$gettext('Seite für OER Campus vorschlagen'), icon: 'oer-campus', emit: 'showSuggest' } ); } if (!document.documentElement.classList.contains('responsive-display')) { menu.push( - { id: 7, label: this.$gettext('Als Vollbild anzeigen'), icon: 'screen-full', + { id: 8, label: this.$gettext('Als Vollbild anzeigen'), icon: 'screen-full', emit: 'activateFullscreen'}, ); } @@ -1100,11 +1197,11 @@ export default { menu.push({ id: 3, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' }); } if (this.context.type === 'users') { - menu.push({ id: 8, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' }); + menu.push({ id: 9, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' }); } if (this.deletable && this.canEdit && !this.isTask && !this.blocked) { menu.push({ - id: 8, + id: 10, label: this.$gettext('Seite löschen'), icon: 'trash', emit: 'deleteCurrentElement', @@ -1319,9 +1416,9 @@ export default { companionInfo: 'companionInfo', companionWarning: 'companionWarning', companionError: 'companionError', + companionSuccess: 'companionSuccess', uploadImageForStructuralElement: 'uploadImageForStructuralElement', deleteImageForStructuralElement: 'deleteImageForStructuralElement', - companionSuccess: 'companionSuccess', setStockImageForStructuralElement: 'setStockImageForStructuralElement', showElementEditDialog: 'showElementEditDialog', showElementAddDialog: 'showElementAddDialog', @@ -1334,6 +1431,8 @@ export default { showElementPublicLinkDialog: 'showElementPublicLinkDialog', showElementRemoveLockDialog: 'showElementRemoveLockDialog', updateShowSuggestOerDialog: 'updateShowSuggestOerDialog', + showStructuralElementFeedbackDialog: 'showStructuralElementFeedbackDialog', + showStructuralElementFeedbackCreateDialog: 'showStructuralElementFeedbackCreateDialog', updateContainer: 'updateContainer', createContainer: 'createContainer', sortContainersInStructualElements: 'sortContainersInStructualElements', @@ -1345,6 +1444,8 @@ export default { activateStructuralElementComments: 'activateStructuralElementComments', deactivateStructuralElementComments: 'deactivateStructuralElementComments', loadRelatedFeedback: 'courseware-structural-element-feedback/loadRelated', + createFeedback: 'feedback-elements/create', + loadFeedbackElement: 'feedback-elements/loadById', }), initCurrent() { @@ -1421,7 +1522,10 @@ export default { this.deactivateStructuralElementComments({ element: this.currentElement }); break; case 'showFeedback': - this.displayFeedback = true; + this.showStructuralElementFeedbackDialog(true); + break; + case 'showFeedbackCreate': + this.showStructuralElementFeedbackCreateDialog(true); break; } }, @@ -1771,28 +1875,110 @@ export default { this.showStockImageSelector = false; this.deletingPreviewImage = false; }, + activateFeedback() { + const data = { + attributes: { + question: this.$gettext('Bewerten Sie das Lernmaterial'), + description: '', + mode: 1, + 'results-visible': true, + 'is-commentable': true, + 'anonymous-entries': true, + }, + relationships: { + range: { + data: { + type: 'courseware-structural-elements', + id: this.currentElement.id, + }, + }, + }, + }; + this.createFeedback(data).then(() => { + this.loadStructuralElement(this.currentElement.id); + }); + }, + async showFeedbackPopup(to, from) { + let showRatingPopup = false; + let ratingPopupFeedbackElement = null; + const toId = to.params.id; + const toElem = this.structuralElementById({id: toId}); + if (toId === this.nextElement?.id && toElem.relationships.parent.data.id === this.rootId) { + const firstLevelElement = await this.findFirstLevelParent(this.currentElement); + const feedbackElementId = firstLevelElement?.relationships?.['feedback-element']?.data?.id; + if (feedbackElementId) { + await this.loadFeedbackElement({ id: feedbackElementId, options: { include: 'entries' }}); + ratingPopupFeedbackElement = this.getFeedbackElementById({ id: feedbackElementId }); + const hasUserEntry = this.feedbackEntries.filter( + (entry) => + parseInt(entry.relationships?.['feedback-element']?.data?.id) == feedbackElementId && + this.currentUser.id === entry.relationships?.author?.data?.id + ).length > 0; + + if (this.currentUser.id !== ratingPopupFeedbackElement?.relationships?.author?.data?.id && !hasUserEntry) { + showRatingPopup = true; + } else { + ratingPopupFeedbackElement = null; + } + } + } + this.showRatingPopup = showRatingPopup; + this.ratingPopupFeedbackElement = ratingPopupFeedbackElement; + }, + async findFirstLevelParent(elem) { + const parentId = elem.relationships.parent.data.id; + if (!parentId) { + return null; + } + if (parentId == this.rootId) { + await this.loadStructuralElement(elem.id); + return this.structuralElementById({ id: elem.id }); + } + const parent = this.structuralElementById({ id: parentId }); + + return this.findFirstLevelParent(parent); + }, + submitFeedback() { + this.showRatingPopup = false; + this.companionSuccess({ info: this.$gettext('Feedback wurde abgegeben.') }); + } }, created() { this.pluginManager.registerComponentsLocally(this); }, watch: { - async structuralElement() { - this.setCurrentElementId(this.structuralElement.id); - this.initCurrent(); - if (this.isTask) { - this.loadTask({ - taskId: this.structuralElement.relationships.task.data.id, - }); - } + $route: { + handler(to, from) { + if (this.courseware.attributes['show-feedback-popup']) { + this.showFeedbackPopup(to, from); + } + }, + deep: true + }, + structuralElement: { + async handler() { + this.setCurrentElementId(this.structuralElement.id); + this.initCurrent(); + if (this.isTask) { + this.loadTask({ + taskId: this.structuralElement.relationships.task.data.id, + }); + } - if (this.isLink) { - this.loadStructuralElement(this.structuralElement.attributes['target-id']); - } + if (this.isLink) { + this.loadStructuralElement(this.structuralElement.attributes['target-id']); + } - if (this.inCourse && this.courseware.attributes['sequential-progression'] && !this.userIsTeacher) { - this.loadProgresses(); - } + if (this.inCourse && this.courseware.attributes['sequential-progression'] && !this.userIsTeacher) { + this.loadProgresses(); + } + + if (this.inCourse) { + this.loadFeedbackElement({ id: this.feedbackElementId }); + } + }, + deep: true }, containers() { this.containerList = this.containers; @@ -1818,3 +2004,4 @@ export default { }), }; + diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDiscussion.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDiscussion.vue index 3428d83..419b8b0 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDiscussion.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDiscussion.vue @@ -47,7 +47,7 @@ export default { hasFeedback: false, text: { comments: this.$gettext('Kommentare zur Seite'), - feedback: this.$gettext('Feedback zur Seite') + feedback: this.$gettext('Anmerkungen zur Seite') } } }, diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue index acecf63..8d9f958 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue @@ -14,10 +14,10 @@ />
+ v-if="!userIsTeacher && feedback.length === 0" + :msgCompanion="$gettext('Es wurde noch keine Anmerkung hinzugefügt.')" + mood="pointing" + />