From 553e9c11807a117cc601b059843f4214334d5390 Mon Sep 17 00:00:00 2001 From: Tim Cremer <65229601+cremertim@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:31:01 +0100 Subject: [PATCH 01/13] Communication: Allow tutors to monitor channels as moderator (#9874) --- .../service/conversation/ConversationService.java | 4 +++- .../answer-post-reactions-bar.component.ts | 8 ++++---- .../post-reactions-bar/post-reactions-bar.component.ts | 9 ++++++--- .../answer-post-reactions-bar.component.spec.ts | 5 +++-- .../post-reactions-bar.component.spec.ts | 7 +++++-- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java index c2a1761b6b46..1a981ee84f99 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/conversation/ConversationService.java @@ -143,7 +143,9 @@ public Optional isMemberOrCreateForCourseWideElseThrow(Long conver if (conversation instanceof Channel channel && channel.getIsCourseWide()) { ConversationParticipant conversationParticipant = ConversationParticipant.createWithDefaultValues(user, channel); - conversationParticipant.setIsModerator(authorizationCheckService.isAtLeastInstructorInCourse(channel.getCourse(), user)); + boolean canBecomeModerator = (channel.getIsAnnouncementChannel() ? authorizationCheckService.isAtLeastInstructorInCourse(channel.getCourse(), user) + : authorizationCheckService.isAtLeastTeachingAssistantInCourse(channel.getCourse(), user)); + conversationParticipant.setIsModerator(canBecomeModerator); lastReadDate.ifPresent(conversationParticipant::setLastRead); conversationParticipantRepository.saveAndFlush(conversationParticipant); } diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts index eed3ef65d59e..8a843639365a 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/answer-post-reactions-bar/answer-post-reactions-bar.component.ts @@ -74,10 +74,10 @@ export class AnswerPostReactionsBarComponent extends PostingsReactionsBarDirecti this.isAuthorOfOriginalPost = this.metisService.metisUserIsAuthorOfPosting(this.posting.post!); this.isAnswerOfAnnouncement = getAsChannelDTO(this.posting.post?.conversation)?.isAnnouncementChannel ?? false; const isCourseWideChannel = getAsChannelDTO(this.posting.post?.conversation)?.isCourseWide ?? false; - const isAtLeastInstructorInCourse = this.metisService.metisUserIsAtLeastInstructorInCourse(); - const mayEditOrDeleteOtherUsersAnswer = - (isCourseWideChannel && isAtLeastInstructorInCourse) || (getAsChannelDTO(this.metisService.getCurrentConversation())?.hasChannelModerationRights ?? false); - this.mayDelete = !this.isReadOnlyMode && (this.isAuthorOfPosting || mayEditOrDeleteOtherUsersAnswer); + const canDeletePost = this.isAnswerOfAnnouncement ? this.metisService.metisUserIsAtLeastInstructorInCourse() : this.metisService.metisUserIsAtLeastTutorInCourse(); + const mayDeleteOtherUsersAnswer = + (isCourseWideChannel && canDeletePost) || (getAsChannelDTO(this.metisService.getCurrentConversation())?.hasChannelModerationRights ?? false); + this.mayDelete = !this.isReadOnlyMode && (this.isAuthorOfPosting || (mayDeleteOtherUsersAnswer && canDeletePost)); this.mayDeleteOutput.emit(this.mayDelete); } diff --git a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts index 569eec43b75f..9213fcaeae56 100644 --- a/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts +++ b/src/main/webapp/app/shared/metis/posting-reactions-bar/post-reactions-bar/post-reactions-bar.component.ts @@ -100,7 +100,7 @@ export class PostReactionsBarComponent extends PostingsReactionsBarDirective { it('should display the delete option to instructor if posting is in course-wide channel from a student', () => { metisServiceUserIsAtLeastInstructorMock.mockReturnValue(true); metisServiceUserPostingAuthorMock.mockReturnValue(false); + metisServiceUserIsAtLeastTutorMock.mockReturnValue(true); component.posting = { ...metisResolvingAnswerPostUser1, post: { ...metisPostInChannel } }; component.posting.authorRole = UserRole.USER; component.ngOnInit(); @@ -158,7 +159,7 @@ describe('AnswerPostReactionsBarComponent', () => { expect(getEditButton()).not.toBeNull(); }); - it('should not display edit and delete options to tutor if posting is in course-wide channel from a student', () => { + it('should display edit and delete options to tutor if posting is in course-wide channel from a student', () => { metisServiceUserIsAtLeastInstructorMock.mockReturnValue(false); metisServiceUserIsAtLeastTutorMock.mockReturnValue(true); metisServiceUserPostingAuthorMock.mockReturnValue(false); @@ -167,7 +168,7 @@ describe('AnswerPostReactionsBarComponent', () => { component.ngOnInit(); fixture.detectChanges(); expect(getEditButton()).toBeNull(); - expect(getDeleteButton()).toBeNull(); + expect(getDeleteButton()).not.toBeNull(); }); it('should not display edit and delete options to users that are neither author or tutor', () => { diff --git a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts index ce01de09f53b..c567c465fe56 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-reactions-bar/post-reactions-bar/post-reactions-bar.component.spec.ts @@ -223,7 +223,7 @@ describe('PostReactionsBarComponent', () => { expect(debugElement.query(By.directive(ConfirmIconComponent))).toBeNull(); }); - it('should not display edit and delete options to tutor if posting is in course-wide channel', () => { + it('should not display edit option but should display delete option to tutor if posting is in course-wide channel', () => { metisServiceUserIsAtLeastInstructorStub.mockReturnValue(false); metisServiceUserIsAtLeastTutorStub.mockReturnValue(true); metisServiceUserIsAuthorOfPostingStub.mockReturnValue(false); @@ -232,7 +232,7 @@ describe('PostReactionsBarComponent', () => { component.ngOnInit(); fixture.detectChanges(); expect(getEditButton()).toBeNull(); - expect(getDeleteButton()).toBeNull(); + expect(getDeleteButton()).not.toBeNull(); }); it('should not display edit and delete options to tutor if posting is announcement', () => { @@ -257,11 +257,14 @@ describe('PostReactionsBarComponent', () => { it('should display the delete option to instructor if posting is in course-wide channel from a student', () => { metisServiceUserIsAtLeastInstructorStub.mockReturnValue(true); + metisServiceUserIsAtLeastTutorStub.mockReturnValue(true); metisServiceUserIsAuthorOfPostingStub.mockReturnValue(false); component.posting = { ...metisPostInChannel }; component.posting.authorRole = UserRole.USER; + component.ngOnInit(); fixture.detectChanges(); + component.setMayDelete(); expect(getDeleteButton()).not.toBeNull(); }); From 505c609aff6276c52a58e14f8270fdb1794a34ca Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel <75392103+rabeatwork@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:32:28 +0100 Subject: [PATCH 02/13] Quiz exercises: Improve user interface of quiz header and footer (#9744) --- .../quiz-participation.component.html | 459 +++++++++--------- .../quiz-participation.component.scss | 163 ++----- .../quiz-participation.component.ts | 5 +- .../connection-status.component.html | 15 +- .../connection-status.component.scss | 6 +- .../shared/layouts/main/main.component.html | 2 +- .../shared/layouts/main/main.component.scss | 7 + .../notification-sidebar.scss | 6 +- src/main/webapp/content/scss/global.scss | 4 + src/main/webapp/i18n/de/quizExercise.json | 2 +- src/main/webapp/i18n/en/quizExercise.json | 2 +- 11 files changed, 276 insertions(+), 395 deletions(-) diff --git a/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html b/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html index 1908c0bae689..683200e66119 100644 --- a/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html +++ b/src/main/webapp/app/exercises/quiz/participate/quiz-participation.component.html @@ -1,89 +1,149 @@ -
+
@if (quizExercise) { -
-

- {{ quizExercise.course?.title ? quizExercise.course?.title : quizExercise.exerciseGroup?.exam?.course?.title }} - - {{ quizExercise.title }} - @switch (mode) { - @case ('practice') { - - } - @case ('preview') { - +
+
+
+ {{ quizExercise.course?.title ? quizExercise.course?.title : quizExercise.exerciseGroup?.exam?.course?.title }} + - {{ quizExercise.title }} + @switch (mode) { + @case ('practice') { + + } + @case ('preview') { + + } + @case ('solution') { + + } } - @case ('solution') { - + + + + + +
+
+ @if (!showingResult) { +
+ @if (!waitingForQuizStart) { +
+ + + {{ remainingTimeText }} + +
+ } + +
+ } @else if (mode !== 'solution') { +
+
+ + {{ userScore }}/{{ totalScore }} ({{ + roundScoreSpecifiedByCourseSettings(result.score, quizExercise.course || quizExercise.exerciseGroup?.exam?.course) + }} + %) +
+
} - } -

- @if (!waitingForQuizStart && !submission.submitted && !showingResult && remainingTimeSeconds >= 0) { -

- } - @if (!waitingForQuizStart && submission.submitted && !showingResult) { -

- } - @if (!waitingForQuizStart && showingResult && mode !== 'solution') { -

- } +
+
+
- } - -
- @if (quizExercise) { -
-
- @for (question of quizExercise.quizQuestions; track question; let i = $index) { + + +
+
+
+ @for (question of quizExercise.quizQuestions; track question; let index = $index) {
- @if (question.type === DRAG_AND_DROP) { - - DD - - } - @if (question.type === MULTIPLE_CHOICE) { - - MC - + @switch (question.type) { + @case (DRAG_AND_DROP) { + + } + @case (MULTIPLE_CHOICE) { + + } + @case (SHORT_ANSWER) { + + } } - @if (question.type === SHORT_ANSWER) { + {{ 'artemisApp.quizExercise.explanationAnswered' | artemisTranslate }} + {{ 'artemisApp.quizExercise.explanationNotAnswered' | artemisTranslate }} + - SA + {{ abbreviation }} - } - {{ 'artemisApp.quizExercise.explanationAnswered' | artemisTranslate }} - {{ 'artemisApp.quizExercise.explanationNotAnswered' | artemisTranslate }} +
}
+ +
- @for (question of quizExercise.quizQuestions; track question; let i = $index) { + @if (!waitingForQuizStart) { + @if (!submission.submitted && !showingResult && remainingTimeSeconds >= 0) { +

+ } + @if (submission.submitted && !showingResult) { +

+ } + @if (showingResult && mode !== 'solution') { +

+ } + } + @for (question of quizExercise.quizQuestions; track question; let index = $index) {
@if (question.type === MULTIPLE_CHOICE) { [submittedResult]="result" [quizQuestions]="quizExercise.quizQuestions" [forceSampleSolution]="mode === 'solution'" - [questionIndex]="i + 1" + [questionIndex]="index + 1" [score]="questionScores[question.id!]" /> } @if (question.type === DRAG_AND_DROP) { [clickDisabled]="submission.submitted || remainingTimeSeconds < 0" [showResult]="showingResult" [forceSampleSolution]="mode === 'solution'" - [questionIndex]="i + 1" + [questionIndex]="index + 1" [score]="questionScores[question.id!]" /> } @if (question.type === SHORT_ANSWER) { [clickDisabled]="submission.submitted || remainingTimeSeconds < 0" [showResult]="showingResult" [forceSampleSolution]="mode === 'solution'" - [questionIndex]="i + 1" + [questionIndex]="index + 1" [score]="questionScores[question.id!]" /> }
}
- } -
- @if (quizExercise) { -