Repository: ls1intum/Artemis
Pull request #10850: `Communication`: Add notification preset picker to course title bar
Author: login: PaRangger
Labels: name: feature
name: client
description: Pull requests that update TypeScript code. (Added Automatically!)
name: ready to merge
name: communication
description: Pull requests that affect the corresponding module
name: core
description: Pull requests that affect the corresponding module
Created: 2025-05-13T12:55:22Z | Merged: 2025-05-25T20:32:55Z

<!-- Thanks for contributing to Artemis! Before you submit your pull request, please make sure to check all tasks by putting an x in the [ ] (don't: [x ], [ x], do: [x]). Remove not applicable tasks and do not leave them unchecked -->
<!-- If your pull request is not ready for review yet, create a draft pull request! -->

### Checklist
#### General
<!-- Remove tasks that are not applicable for your PR. Please only put the PR into ready for review, if all relevant tasks are checked! -->
<!-- You only need to choose one of the first two check items: Generally, test on the test servers. -->
<!-- If it's only a small change, testing it locally is acceptable, and you may remove the first checkmark. If you are unsure, please test on the test servers. -->
- [x] I tested **all** changes and their related features with **all** corresponding user types on a test server.
- [x] Language: I followed the [guidelines for inclusive, diversity-sensitive, and appreciative language](https://docs.artemis.cit.tum.de/dev/guidelines/language-guidelines/).
- [x] I chose a title conforming to the [naming conventions for pull requests](https://docs.artemis.cit.tum.de/dev/development-process/development-process.html#naming-conventions-for-github-pull-requests).

#### Client
- [x] **Important**: I implemented the changes with a very good performance, prevented too many (unnecessary) REST calls and made sure the UI is responsive, even with large data (e.g. using paging).
- [x] I **strictly** followed the principle of **data economy** for all client-server REST calls.
- [x] I **strictly** followed the [client coding and design guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/client/).
- [x] Following the [theming guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/client-design/), I specified colors only in the theming variable files and checked that the changes look consistent in both the light and the dark theme.
- [x] I added multiple integration tests (Jest) related to the features (with a high test coverage), while following the [test guidelines](https://docs.artemis.cit.tum.de/dev/guidelines/client-tests/).
- [x] I documented the TypeScript code using JSDoc style.
- [x] I added multiple screenshots/screencasts of my UI changes.


### Motivation and Context
<!-- Why is this change required? What problem does it solve? -->
<!-- If it fixes an open issue, please link to the issue here. -->
Currently, users need to go to the course settings to select a notification preset.

### Description
<!-- Describe your changes in detail -->
I added the notification preset picker to the course title bar. This way users can change to presets quickly without having to navigate to the settings.

### Steps for Testing
<!-- Please describe in detail how reviewers can test your changes. Make sure to take all related features and views into account! Below is an example that you can refine. -->
Prerequisites:
- 1 Instructor/Tutor/Student

1. Log in to Artemis.
2. Navigate to a course.
3. Verify that on the top right there is a new "eye" icon.
4. When clicking the button you should be able to select a notification preset.
5. When navigating to the course settings, the preset should be applied accordingly.

### Testserver States
You can manage test servers using [Helios](https://helios.aet.cit.tum.de/). Check environment statuses in the [environment list](https://helios.aet.cit.tum.de/repo/69562331/environment/list). To deploy to a test server, go to the [CI/CD](https://helios.aet.cit.tum.de/repo/69562331/ci-cd) page, find your PR or branch, and trigger the deployment.

### Review Progress
<!-- Each PR should be reviewed by at least two other developers. The code, the functionality (= manual test) and the exam mode need to be reviewed. -->
<!-- The reviewer or author check the following boxes depending on what was reviewed or tested. All boxes should be checked before merge. -->
<!-- You can add additional checkboxes if it makes sense to only review parts of the code or functionality. -->
<!-- When changes are pushed, uncheck the affected boxes. (Not all changes require full re-reviews.) -->
<!-- All PRs that might affect the exam mode (e.g. change a client component that is also used in the exam mode) need an additional verification that the exam mode still works. -->

#### Code Review
- [x] Code Review 1
- [x] Code Review 2
#### Manual Tests
- [x] Test 1
- [x] Test 2

### Test Coverage
<!-- Please add the test coverages for all changed files modified in this PR here. You can use `supporting_script/code-coverage/generate_code_cov_table/generate_code_cov_table.py` to automatically generate the coverage table from the corresponding artefacts of your branch (follow the ReadMe for setup details). -->
<!-- Alternatively you can execute the tests locally (see build.gradle and package.json) or look into the corresponding artefacts. -->
<!-- The line coverage must be above 90% for changes files, and you must use extensive and useful assertions for server tests and expect statements for client tests. -->
<!-- Note: Confirm in the last column that you have implemented extensive assertions for server tests and expect statements for client tests. -->
<!--       Remove rows with only trivial changes from the table. -->
<!--
| Class/File | Line Coverage | Confirmation (assert/expect) |
|------------|--------------:|-----------------------------:|
| ExerciseService.java | 85% | ✅                           |
| programming-exercise.component.ts | 95% | ✅              |
-->
#### Client

| Class/File | Line Coverage | Confirmation (assert/expect) |
|------------|--------------:|---------------------:|
| course-notification-preset-picker.component.ts | 92% | ✅ |
| course-notification-setting.service.ts | 97.61% | ✅ |
| course-notification.service.ts | 95.74% | ✅ |
| course-overview.component.ts | 92.23% | ✅ |
| notification-settings.component.ts | 95.45% | ✅ |

### Screenshots
<!-- Add screenshots to demonstrate the changes in the UI. Remove the section if you did not change the UI. -->
<!-- Create a GIF file from a screen recording in a docker container https://toub.es/2017/09/11/high-quality-gif-with-ffmpeg-and-docker/ -->
![presets](https://github.com/user-attachments/assets/4de6364a-9536-4307-95a7-f871ce09ae95)



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added a notification preset picker to the course overview, allowing users to quickly select notification settings.
  - Introduced a compact button style for the preset picker in the course overview for a streamlined appearance.
  - Enhanced visual feedback with dynamic icons and smooth transitions when selecting notification presets.

- **Improvements**
  - Notification setting changes and preset selections are now applied immediately, removing previous delays and batching.
  - Notification settings and information are now cached, resulting in faster and more responsive updates.

- **Bug Fixes**
  - Improved handling and display logic for custom notification presets.

- **Tests**
  - Expanded and improved test coverage for notification preset management and notification settings components.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.html b/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.html
index 28ca3134c71e..5e5144c0cb2f 100644
--- a/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.html
+++ b/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.html
@@ -1,8 +1,10 @@
-<div class="course-notification-preset-picker" ngbDropdown>
-    <button class="btn btn-outline-secondary" type="button" ngbDropdownToggle>
-        <fa-icon [icon]="farEye" class="me-1" />
-        <span jhiTranslate="artemisApp.courseNotification.preset.dropdown"></span>:
-        <span [jhiTranslate]="selectedPresetLangKey"></span>
+<div [ngClass]="{ 'is-small-button': isSmallButton(), 'is-recently-selected': isRecentlySelected }" class="course-notification-preset-picker" ngbDropdown>
+    <button [ngClass]="{ 'btn-sm': isSmallButton() }" class="btn btn-outline-secondary d-flex align-items-center" type="button" ngbDropdownToggle>
+        <fa-icon [icon]="selectedCourseSettingPreset() && selectedCourseSettingPreset()!.typeId === 3 ? farEyeSlash : farEye" class="course-notification-preset-picker-eye-icon" />
+        <span class="course-notification-preset-picker-text">
+            <span jhiTranslate="artemisApp.courseNotification.preset.dropdown"></span>:
+            <span [jhiTranslate]="selectedPresetLangKey"></span>
+        </span>
     </button>
     <div ngbDropdownMenu>
         @for (preset of availableCourseSettingPresets(); track preset) {
@@ -24,7 +26,7 @@
 
         <div class="course-notification-preset-picker-item" ngbDropdownItem (click)="presetSelected(0)">
             <div>
-                @if (selectedCourseSettingPreset() === null) {
+                @if (selectedCourseSettingPreset() === undefined) {
                     <fa-icon [icon]="faCheck" class="me-1" />
                 }
                 <strong [jhiTranslate]="'artemisApp.courseNotification.preset.customUserCourseNotificationSettingPreset.title'"></strong>
diff --git a/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss b/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss
index 24383695dc15..48759586f05e 100644
--- a/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss
+++ b/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss
@@ -53,3 +53,42 @@
 .course-notification-preset-picker-item:hover::after {
     opacity: 0.1;
 }
+
+.course-notification-preset-picker-eye-icon,
+.is-small-button:hover .course-notification-preset-picker-eye-icon,
+.is-small-button.is-recently-selected .course-notification-preset-picker-eye-icon {
+    margin-right: 0.25rem;
+
+    transition: margin-right 0.2s 0.1s ease-in-out;
+}
+
+.is-small-button .course-notification-preset-picker-eye-icon {
+    margin-right: 0;
+
+    transition: margin-right 0.2s 0.4s ease-in-out;
+}
+
+.is-small-button .course-notification-preset-picker-text {
+    max-width: 0;
+    opacity: 0;
+    overflow: hidden;
+    display: inline-block;
+
+    transition:
+        opacity 0.2s 0.1s ease-in-out,
+        max-width 0.2s 0.4s ease-in-out;
+}
+
+.is-small-button:hover .course-notification-preset-picker-text,
+.is-small-button.is-recently-selected .course-notification-preset-picker-text {
+    max-width: 10rem;
+    opacity: 1;
+
+    transition:
+        opacity 0.2s 0.4s ease-in-out,
+        max-width 0.2s 0.1s ease-in-out;
+}
+
+.is-small-button .dropdown-item {
+    min-width: 25rem;
+}
diff --git a/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.ts b/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.ts
index 4de51647daaa..8d6a24c53d3a 100644
--- a/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.ts
+++ b/src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.ts
@@ -1,7 +1,8 @@
 import { Component, effect, input, output } from '@angular/core';
+import { NgClass } from '@angular/common';
 import { TranslateDirective } from 'app/shared/language/translate.directive';
 import { FaIconComponent } from '@fortawesome/angular-fontawesome';
-import { faEye } from '@fortawesome/free-regular-svg-icons';
+import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
 import { faCheck } from '@fortawesome/free-solid-svg-icons';
 import { CourseNotificationSettingPreset } from 'app/communication/shared/entities/course-notification/course-notification-setting-preset';
 import { NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap';
@@ -12,20 +13,24 @@ import { NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle } from
  */
 @Component({
     selector: 'jhi-course-notification-preset-picker',
-    imports: [TranslateDirective, FaIconComponent, NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle],
+    imports: [TranslateDirective, FaIconComponent, NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle, NgClass],
     templateUrl: './course-notification-preset-picker.component.html',
     styleUrls: ['./course-notification-preset-picker.component.scss'],
 })
 export class CourseNotificationPresetPickerComponent {
     readonly availableCourseSettingPresets = input.required<CourseNotificationSettingPreset[]>();
     readonly selectedCourseSettingPreset = input.required<CourseNotificationSettingPreset | undefined>();
+    readonly isSmallButton = input<boolean>(false);
 
     readonly onPresetSelected = output<number>();
 
     // Icons
     protected readonly farEye = faEye;
+    protected readonly farEyeSlash = faEyeSlash;
     protected readonly faCheck = faCheck;
 
+    private recentlySelectedTimeout: NodeJS.Timeout;
+    protected isRecentlySelected: boolean = false;
     protected selectedPresetLangKey: string;
 
     constructor() {
@@ -43,6 +48,15 @@ export class CourseNotificationPresetPickerComponent {
      * @param presetTypeId - The type ID of the selected preset
      */
     protected presetSelected(presetTypeId: number) {
+        this.isRecentlySelected = true;
+        if (this.recentlySelectedTimeout) {
+            clearTimeout(this.recentlySelectedTimeout);
+        }
+
+        this.recentlySelectedTimeout = setTimeout(() => {
+            this.isRecentlySelected = false;
+        }, 5000);
+
         this.onPresetSelected.emit(presetTypeId);
     }
 }
diff --git a/src/main/webapp/app/communication/course-notification/course-notification-setting.service.spec.ts b/src/main/webapp/app/communication/course-notification/course-notification-setting.service.spec.ts
index 4cd2467a77a7..383999712851 100644
--- a/src/main/webapp/app/communication/course-notification/course-notification-setting.service.spec.ts
+++ b/src/main/webapp/app/communication/course-notification/course-notification-setting.service.spec.ts
@@ -7,6 +7,7 @@ import { CourseNotificationSettingInfo } from 'app/communication/shared/entities
 import { CourseNotificationSettingSpecification } from 'app/communication/shared/entities/course-notification/course-notification-setting-specification';
 import { CourseNotificationChannel } from 'app/communication/shared/entities/course-notification/course-notification-channel';
 import { CourseNotificationChannelSetting } from 'app/communication/shared/entities/course-notification/course-notification-channel-setting';
+import { CourseNotificationSettingPreset } from 'app/communication/shared/entities/course-notification/course-notification-setting-preset';
 
 describe('CourseNotificationSettingService', () => {
     let service: CourseNotificationSettingService;
@@ -46,7 +47,7 @@ describe('CourseNotificationSettingService', () => {
 
             let result: CourseNotificationSettingInfo | undefined;
             service.getSettingInfo(courseId).subscribe((response) => {
-                result = response.body || undefined;
+                result = response;
             });
 
             const req = httpMock.expectOne(`/api/communication/notification/${courseId}/settings`);
@@ -56,6 +57,69 @@ describe('CourseNotificationSettingService', () => {
 
             expect(result).toEqual(mockSettingInfo);
         }));
+
+        it('should return cached value if available and forceFetch is false', fakeAsync(() => {
+            const courseId = 123;
+            const mockSettingInfo: CourseNotificationSettingInfo = {
+                selectedPreset: 1,
+                notificationTypeChannels: {
+                    newPostNotification: {
+                        [CourseNotificationChannel.PUSH]: true,
+                        [CourseNotificationChannel.EMAIL]: false,
+                        [CourseNotificationChannel.WEBAPP]: true,
+                    },
+                },
+            };
+
+            service.getSettingInfo(courseId).subscribe();
+            const firstReq = httpMock.expectOne(`/api/communication/notification/${courseId}/settings`);
+            firstReq.flush(mockSettingInfo);
+            tick();
+
+            let result: CourseNotificationSettingInfo | undefined;
+            service.getSettingInfo(courseId).subscribe((response) => {
+                result = response;
+            });
+            tick();
+
+            httpMock.expectNone(`/api/communication/notification/${courseId}/settings`);
+            expect(result).toEqual(mockSettingInfo);
+        }));
+
+        it('should make a new request if forceFetch is true, even with cached value', fakeAsync(() => {
+            const courseId = 123;
+            const mockSettingInfo: CourseNotificationSettingInfo = {
+                selectedPreset: 1,
+                notificationTypeChannels: {
+                    newPostNotification: {
+                        [CourseNotificationChannel.PUSH]: true,
+                        [CourseNotificationChannel.EMAIL]: false,
+                        [CourseNotificationChannel.WEBAPP]: true,
+                    },
+                },
+            };
+
+            const updatedMockSettingInfo: CourseNotificationSettingInfo = {
+                ...mockSettingInfo,
+                selectedPreset: 2,
+            };
+
+            service.getSettingInfo(courseId).subscribe();
+            const firstReq = httpMock.expectOne(`/api/communication/notification/${courseId}/settings`);
+            firstReq.flush(mockSettingInfo);
+            tick();
+
+            let result: CourseNotificationSettingInfo | undefined;
+            service.getSettingInfo(courseId, true).subscribe((response) => {
+                result = response;
+            });
+
+            const secondReq = httpMock.expectOne(`/api/communication/notification/${courseId}/settings`);
+            secondReq.flush(updatedMockSettingInfo);
+            tick();
+
+            expect(result).toEqual(updatedMockSettingInfo);
+        }));
     });
 
     describe('setSettingPreset', () => {
@@ -63,7 +127,7 @@ describe('CourseNotificationSettingService', () => {
             const courseId = 123;
             const presetTypeId = 2;
 
-            service.setSettingPreset(courseId, presetTypeId);
+            service.setSettingPreset(courseId, presetTypeId, undefined);
 
             const req = httpMock.expectOne(`/api/communication/notification/${courseId}/setting-preset`);
             expect(req.request.method).toBe('PUT');
@@ -71,40 +135,209 @@ describe('CourseNotificationSettingService', () => {
             req.flush({});
             tick();
         }));
+
+        it('should update the subject with new preset when current value exists', fakeAsync(() => {
+            const courseId = 123;
+            const presetTypeId = 2;
+            const mockSettingInfo: CourseNotificationSettingInfo = {
+                selectedPreset: 1,
+                notificationTypeChannels: {
+                    newPostNotification: {
+                        [CourseNotificationChannel.PUSH]: true,
+                        [CourseNotificationChannel.EMAIL]: false,
+                        [CourseNotificationChannel.WEBAPP]: true,
+                    },
+                },
+            };
+
+            service.getSettingInfo(courseId).subscribe();
+            const firstReq = httpMock.expectOne(`/api/communication/notification/${courseId}/settings`);
+            firstReq.flush(mockSettingInfo);
+            tick();
+
+            service.setSettingPreset(courseId, presetTypeId, undefined);
+            const updateReq = httpMock.expectOne(`/api/communication/notification/${courseId}/setting-preset`);
+            updateReq.flush({});
+            tick();
+
+            let result: CourseNotificationSettingInfo | undefined;
+            service.getSettingInfo(courseId).subscribe((response) => {
+                result = response;
+            });
+            tick();
+
+            expect(result).toBeDefined();
+            expect(result?.selectedPreset).toBe(presetTypeId);
+        }));
+
+        it('should copy preset map when copyPreset is provided', fakeAsync(() => {
+            const courseId = 123;
+            const presetTypeId = 2;
+            const mockSettingInfo: CourseNotificationSettingInfo = {
+                selectedPreset: 1,
+                notificationTypeChannels: {
+                    oldNotification: {
+                        [CourseNotificationChannel.PUSH]: false,
+                        [CourseNotificationChannel.EMAIL]: false,
+                        [CourseNotificationChannel.WEBAPP]: false,
+                    },
+                },
+            };
+
+            const mockPreset: CourseNotificationSettingPreset = {
+                identifier: 'preset1',
+                typeId: 1,
+                presetMap: {
+                    newNotification: {
+                        [CourseNotificationChannel.PUSH]: true,
+                        [CourseNotificationChannel.EMAIL]: true,
+                        [CourseNotificationChannel.WEBAPP]: true,
+                    },
+                },
+            };
+
+            service.getSettingInfo(courseId).subscribe();
+            const firstReq = httpMock.expectOne(`/api/communication/notification/${courseId}/settings`);
+            firstReq.flush(mockSettingInfo);
+            tick();
+
+            service.setSettingPreset(courseId, presetTypeId, mockPreset);
+            const updateReq = httpMock.expectOne(`/api/communication/notification/${courseId}/setting-preset`);
+            updateReq.flush({});
+            tick();
+
+            let result: CourseNotificationSettingInfo | undefined;
+            service.getSettingInfo(courseId).subscribe((response) => {
+                result = response;
+            });
+            tick();
+
+            expect(result).toBeDefined();
+            expect(result?.selectedPreset).toBe(presetTypeId);
+            expect(result?.notificationTypeChannels).toEqual(mockPreset.presetMap);
+        }));
     });
 
     describe('setSettingSpecification', () => {
         it('should make PUT request with transformed specifications', fakeAsync(() => {
             const courseId = 123;
-            const channelSetting1: CourseNotificationChannelSetting = {
+            const channelSetting: CourseNotificationChannelSetting = {
                 [CourseNotificationChannel.PUSH]: true,
                 [CourseNotificationChannel.EMAIL]: false,
                 [CourseNotificationChannel.WEBAPP]: true,
             };
-            const channelSetting2: CourseNotificationChannelSetting = {
-                [CourseNotificationChannel.PUSH]: false,
-                [CourseNotificationChannel.EMAIL]: true,
-                [CourseNotificationChannel.WEBAPP]: true,
-            };
 
-            const specifications: CourseNotificationSettingSpecification[] = [
-                new CourseNotificationSettingSpecification('notification1', 1, channelSetting1),
-                new CourseNotificationSettingSpecification('notification2', 2, channelSetting2),
-            ];
+            const specification = new CourseNotificationSettingSpecification('notification1', 1, channelSetting);
 
-            service.setSettingSpecification(courseId, specifications);
+            service.setSettingSpecification(courseId, specification, undefined);
 
             const req = httpMock.expectOne(`/api/communication/notification/${courseId}/setting-specification`);
             expect(req.request.method).toBe('PUT');
             expect(req.request.body).toEqual({
                 notificationTypeChannels: {
-                    1: channelSetting1,
-                    2: channelSetting2,
+                    1: channelSetting,
                 },
             });
             req.flush({});
             tick();
         }));
+
+        it('should update the subject with new specification when current value exists', fakeAsync(() => {
+            const courseId = 123;
+            const mockSettingInfo: CourseNotificationSettingInfo = {
+                selectedPreset: 1,
+                notificationTypeChannels: {
+                    oldNotification: {
+                        [CourseNotificationChannel.PUSH]: false,
+                        [CourseNotificationChannel.EMAIL]: false,
+                        [CourseNotificationChannel.WEBAPP]: false,
+                    },
+                },
+            };
+
+            const channelSetting: CourseNotificationChannelSetting = {
+                [CourseNotificationChannel.PUSH]: true,
+                [CourseNotificationChannel.EMAIL]: false,
+                [CourseNotificationChannel.WEBAPP]: true,
+            };
+
+            const specification = new CourseNotificationSettingSpecification('notification1', 1, channelSetting);
+
+            service.getSettingInfo(courseId).subscribe();
+            const firstReq = httpMock.expectOne(`/api/communication/notification/${courseId}/settings`);
+            firstReq.flush(mockSettingInfo);
+            tick();
+
+            service.setSettingSpecification(courseId, specification, undefined);
+            const updateReq = httpMock.expectOne(`/api/communication/notification/${courseId}/setting-specification`);
+            updateReq.flush({});
+            tick();
+
+            let result: CourseNotificationSettingInfo | undefined;
+            service.getSettingInfo(courseId).subscribe((response) => {
+                result = response;
+            });
+            tick();
+
+            expect(result).toBeDefined();
+            expect(result?.selectedPreset).toBe(0);
+            expect(result?.notificationTypeChannels['notification1']).toEqual(channelSetting);
+        }));
+
+        it('should copy preset map when copyPreset is provided', fakeAsync(() => {
+            const courseId = 123;
+            const mockSettingInfo: CourseNotificationSettingInfo = {
+                selectedPreset: 1,
+                notificationTypeChannels: {
+                    oldNotification: {
+                        [CourseNotificationChannel.PUSH]: false,
+                        [CourseNotificationChannel.EMAIL]: false,
+                        [CourseNotificationChannel.WEBAPP]: false,
+                    },
+                },
+            };
+
+            const mockPreset: CourseNotificationSettingPreset = {
+                identifier: 'preset1',
+                typeId: 1,
+                presetMap: {
+                    newNotification: {
+                        [CourseNotificationChannel.PUSH]: true,
+                        [CourseNotificationChannel.EMAIL]: true,
+                        [CourseNotificationChannel.WEBAPP]: true,
+                    },
+                },
+            };
+
+            const channelSetting: CourseNotificationChannelSetting = {
+                [CourseNotificationChannel.PUSH]: true,
+                [CourseNotificationChannel.EMAIL]: false,
+                [CourseNotificationChannel.WEBAPP]: true,
+            };
+
+            const specification = new CourseNotificationSettingSpecification('notification1', 1, channelSetting);
+
+            service.getSettingInfo(courseId).subscribe();
+            const firstReq = httpMock.expectOne(`/api/communication/notification/${courseId}/settings`);
+            firstReq.flush(mockSettingInfo);
+            tick();
+
+            service.setSettingSpecification(courseId, specification, mockPreset);
+            const updateReq = httpMock.expectOne(`/api/communication/notification/${courseId}/setting-specification`);
+            updateReq.flush({});
+            tick();
+
+            let result: CourseNotificationSettingInfo | undefined;
+            service.getSettingInfo(courseId).subscribe((response) => {
+                result = response;
+            });
+            tick();
+
+            expect(result).toBeDefined();
+            expect(result?.selectedPreset).toBe(0);
+            expect(result?.notificationTypeChannels).toHaveProperty('newNotification');
+            expect(result?.notificationTypeChannels['notification1']).toEqual(channelSetting);
+        }));
     });
 
     describe('transformNotificationSettingSpecificationToRequestBody', () => {
diff --git a/src/main/webapp/app/communication/course-notification/course-notification-setting.service.ts b/src/main/webapp/app/communication/course-notification/course-notification-setting.service.ts
index 1d5e1156ad97..5d5c1d4aa9f8 100644
--- a/src/main/webapp/app/communication/course-notification/course-notification-setting.service.ts
+++ b/src/main/webapp/app/communication/course-notification/course-notification-setting.service.ts
@@ -1,9 +1,11 @@
 import { Injectable, inject } from '@angular/core';
-import { HttpClient, HttpResponse } from '@angular/common/http';
+import { HttpClient } from '@angular/common/http';
 import { CourseNotificationSettingSpecification } from 'app/communication/shared/entities/course-notification/course-notification-setting-specification';
 import { CourseNotificationChannelSetting } from 'app/communication/shared/entities/course-notification/course-notification-channel-setting';
 import { CourseNotificationSettingInfo } from 'app/communication/shared/entities/course-notification/course-notification-setting-info';
-import { Observable } from 'rxjs';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { map, tap } from 'rxjs/operators';
+import { CourseNotificationSettingPreset } from 'app/communication/shared/entities/course-notification/course-notification-setting-preset';
 
 /**
  * Service for managing course notification settings.
@@ -17,33 +19,104 @@ export class CourseNotificationSettingService {
 
     private http = inject(HttpClient);
 
+    private settingInfoSubjects: Record<number, BehaviorSubject<CourseNotificationSettingInfo | undefined>> = {};
+
+    /**
+     * Gets or creates a BehaviorSubject for a specific course's setting info
+     *
+     * @param courseId - The ID of the course
+     * @returns BehaviorSubject for the course's setting info
+     */
+    private getSettingInfoSubject(courseId: number): BehaviorSubject<CourseNotificationSettingInfo | undefined> {
+        if (!this.settingInfoSubjects[courseId]) {
+            this.settingInfoSubjects[courseId] = new BehaviorSubject<CourseNotificationSettingInfo | undefined>(undefined);
+        }
+        return this.settingInfoSubjects[courseId];
+    }
+
     /**
      * Retrieves notification settings information for a specific course.
+     * Returns an Observable that will emit updates when settings change.
      *
      * @param courseId - The ID of the course
+     * @param forceFetch - Skips the cache and forces an http request
      * @returns An Observable with the course notification setting information
      */
-    getSettingInfo(courseId: number): Observable<HttpResponse<CourseNotificationSettingInfo>> {
-        return this.http.get<CourseNotificationSettingInfo>(this.apiEndpoint + courseId + '/settings', { observe: 'response' });
+    getSettingInfo(courseId: number, forceFetch = false): Observable<CourseNotificationSettingInfo | undefined> {
+        const subject = this.getSettingInfoSubject(courseId);
+
+        if (!forceFetch && subject.getValue()) {
+            return subject.asObservable().pipe(map((value) => value as CourseNotificationSettingInfo));
+        }
+
+        this.http
+            .get<CourseNotificationSettingInfo>(this.apiEndpoint + courseId + '/settings', { observe: 'response' })
+            .pipe(
+                tap((response) => {
+                    if (response.body) {
+                        subject.next(response.body);
+                    }
+                }),
+            )
+            .subscribe();
+
+        return subject.asObservable().pipe(map((value) => value as CourseNotificationSettingInfo | undefined));
     }
 
     /**
      * Sets a notification setting preset for a course.
+     * Updates the cached setting info.
      *
      * @param courseId - The ID of the course
      * @param presetTypeId - The ID of the preset to apply
+     * @param copyPreset - The preset to copy the settings from
      */
-    setSettingPreset(courseId: number, presetTypeId: number): void {
+    setSettingPreset(courseId: number, presetTypeId: number, copyPreset: CourseNotificationSettingPreset | undefined): void {
+        const subject = this.getSettingInfoSubject(courseId);
+        const currentValue = subject.getValue();
+
+        if (currentValue) {
+            if (copyPreset) {
+                currentValue.notificationTypeChannels = copyPreset.presetMap;
+            }
+            const updatedValue = { ...currentValue, selectedPreset: presetTypeId };
+            subject.next(updatedValue);
+        }
+
         this.http.put(this.apiEndpoint + courseId + '/setting-preset', presetTypeId).subscribe();
     }
 
     /**
      * Updates individual notification settings for a course.
+     * Forces a refresh of the setting info.
      *
      * @param courseId - The ID of the course
-     * @param notificationSettingSpecifications - Array of notification setting specifications to update
+     * @param notificationSettingSpecification - Array of notification setting specifications to update
+     * @param copyPreset - The preset to copy the settings from
      */
-    setSettingSpecification(courseId: number, notificationSettingSpecifications: CourseNotificationSettingSpecification[]): void {
+    setSettingSpecification(
+        courseId: number,
+        notificationSettingSpecification: CourseNotificationSettingSpecification,
+        copyPreset: CourseNotificationSettingPreset | undefined,
+    ): void {
+        const subject = this.getSettingInfoSubject(courseId);
+        const currentValue = subject.getValue();
+
+        if (currentValue) {
+            if (copyPreset) {
+                currentValue.notificationTypeChannels = copyPreset.presetMap;
+            }
+            if (currentValue.notificationTypeChannels[notificationSettingSpecification.typeId]) {
+                currentValue.notificationTypeChannels[notificationSettingSpecification.typeId] = notificationSettingSpecification.channelSetting;
+            } else {
+                currentValue.notificationTypeChannels[notificationSettingSpecification.identifier] = notificationSettingSpecification.channelSetting;
+            }
+            const updatedValue = { ...currentValue, selectedPreset: 0 };
+            subject.next(updatedValue);
+        }
+
+        const notificationSettingSpecifications = [notificationSettingSpecification];
+
         this.http
             .put(this.apiEndpoint + courseId + '/setting-specification', {
                 notificationTypeChannels: this.transformNotificationSettingSpecificationToRequestBody(notificationSettingSpecifications),
diff --git a/src/main/webapp/app/communication/course-notification/course-notification.service.ts b/src/main/webapp/app/communication/course-notification/course-notification.service.ts
index 43c4d5b789d2..927dfc0c8bc5 100644
--- a/src/main/webapp/app/communication/course-notification/course-notification.service.ts
+++ b/src/main/webapp/app/communication/course-notification/course-notification.service.ts
@@ -3,7 +3,7 @@ import { faComments, faPersonChalkboard, faRectangleList, faTriangleExclamation
 import dayjs from 'dayjs/esm';
 import { CourseNotification } from 'app/communication/shared/entities/course-notification/course-notification';
 import { HttpClient, HttpResponse } from '@angular/common/http';
-import { BehaviorSubject, Observable } from 'rxjs';
+import { BehaviorSubject, Observable, of, tap } from 'rxjs';
 import { CourseNotificationInfo } from 'app/communication/shared/entities/course-notification/course-notification-info';
 import { CourseNotificationPage } from 'app/communication/shared/entities/course-notification/course-notification-page';
 import { CourseNotificationCategory } from 'app/communication/shared/entities/course-notification/course-notification-category';
@@ -89,6 +89,7 @@ export class CourseNotificationService {
     private notificationSubject = new BehaviorSubject<Record<number, CourseNotification[]>>({});
     private courseNotificationCountMap: Record<number, number> = {};
     private notificationCountSubject = new BehaviorSubject<Record<number, number>>({});
+    private cachedNotificationInfo: HttpResponse<CourseNotificationInfo> | null = null;
 
     public notificationCount$: Observable<Record<number, number>> = this.notificationCountSubject.asObservable();
     public notifications$: Observable<Record<number, CourseNotification[]>> = this.notificationSubject.asObservable();
@@ -104,7 +105,18 @@ export class CourseNotificationService {
      * @returns Observable with notification information
      */
     public getInfo(): Observable<HttpResponse<CourseNotificationInfo>> {
-        return this.http.get<CourseNotificationInfo>(this.apiEndpoint + 'info', { observe: 'response' });
+        if (this.cachedNotificationInfo) {
+            return of(this.cachedNotificationInfo);
+        }
+
+        // Otherwise, fetch from server and cache the result
+        return this.http.get<CourseNotificationInfo>(this.apiEndpoint + 'info', { observe: 'response' }).pipe(
+            tap((response) => {
+                if (response.body) {
+                    this.cachedNotificationInfo = response;
+                }
+            }),
+        );
     }
 
     /**
diff --git a/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.html b/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.html
index 235010632db3..0c06aea1d6f3 100644
--- a/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.html
+++ b/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.html
@@ -66,6 +66,14 @@
                                 <span class="d-none d-sm-inline" jhiTranslate="artemisApp.exercise.refresh">Refresh</span>
                             </button>
                         }
+                        @if (selectableSettingPresets) {
+                            <jhi-course-notification-preset-picker
+                                [availableCourseSettingPresets]="selectableSettingPresets"
+                                [selectedCourseSettingPreset]="selectedSettingPreset"
+                                [isSmallButton]="true"
+                                (onPresetSelected)="presetSelected($event)"
+                            />
+                        }
                         @if (course()!.id) {
                             <jhi-course-notification-overview [courseId]="course()!.id!" />
                         }
diff --git a/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.spec.ts b/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.spec.ts
index 32d3c782953b..d873caa1df82 100644
--- a/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.spec.ts
+++ b/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.spec.ts
@@ -59,6 +59,11 @@ import { MockAccountService } from 'test/helpers/mocks/service/mock-account.serv
 import { MockProfileService } from 'test/helpers/mocks/service/mock-profile.service';
 import { generateExampleTutorialGroupsConfiguration } from 'test/helpers/sample/tutorialgroup/tutorialGroupsConfigurationExampleModels';
 import { MockMetisConversationService } from 'test/helpers/mocks/service/mock-metis-conversation.service';
+import { CourseNotificationSettingService } from 'app/communication/course-notification/course-notification-setting.service';
+import { CourseNotificationService } from 'app/communication/course-notification/course-notification.service';
+import { CourseNotificationSettingPreset } from 'app/communication/shared/entities/course-notification/course-notification-setting-preset';
+import { CourseNotificationSettingInfo } from 'app/communication/shared/entities/course-notification/course-notification-setting-info';
+import { CourseNotificationInfo } from 'app/communication/shared/entities/course-notification/course-notification-info';
 
 const endDate1 = dayjs().add(1, 'days');
 const visibleDate1 = dayjs().subtract(1, 'days');
@@ -146,6 +151,8 @@ describe('CourseOverviewComponent', () => {
     let courseSidebarService: CourseSidebarService;
     let profileService: ProfileService;
     let modalService: NgbModal;
+    let courseNotificationSettingService: CourseNotificationSettingService;
+    let courseNotificationService: CourseNotificationService;
 
     let metisConversationService: MetisConversationService;
 
@@ -154,6 +161,24 @@ describe('CourseOverviewComponent', () => {
         courseInformationSharingConfiguration: CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING,
     } as Course;
 
+    const mockCourseId = 123;
+
+    const mockNotificationSettingPresets: CourseNotificationSettingPreset[] = [
+        { typeId: 1, identifier: 'All Notifications', presetMap: { test: { PUSH: true, EMAIL: true, WEBAPP: true } } },
+        { typeId: 2, identifier: 'Important Only', presetMap: { test: { PUSH: true, EMAIL: false, WEBAPP: true } } },
+        { typeId: 3, identifier: 'Minimal', presetMap: { test: { PUSH: false, EMAIL: false, WEBAPP: true } } },
+    ];
+
+    const mockSettingInfo: CourseNotificationSettingInfo = {
+        selectedPreset: 1,
+        notificationTypeChannels: { test: { PUSH: true, EMAIL: true, WEBAPP: true } },
+    };
+
+    const mockNotificationInfo: CourseNotificationInfo = {
+        presets: mockNotificationSettingPresets,
+        notificationTypes: {},
+    };
+
     beforeEach(fakeAsync(() => {
         route = {
             params: of({ courseId: course1.id }) as Params,
@@ -192,6 +217,7 @@ describe('CourseOverviewComponent', () => {
                 MockProvider(MetisConversationService),
                 MockProvider(CourseAccessStorageService),
                 MockProvider(NgbModal),
+                MockProvider(CourseNotificationSettingService),
                 { provide: Router, useValue: router },
                 { provide: ActivatedRoute, useValue: route },
                 { provide: MetisConversationService, useClass: MockMetisConversationService },
@@ -225,6 +251,8 @@ describe('CourseOverviewComponent', () => {
                 metisConversationService = fixture.debugElement.injector.get(MetisConversationService);
                 jhiWebsocketServiceSubscribeSpy = jest.spyOn(jhiWebsocketService, 'subscribe');
                 jest.spyOn(teamService, 'teamAssignmentUpdates', 'get').mockResolvedValue(of(new TeamAssignmentPayload()));
+                courseNotificationSettingService = TestBed.inject(CourseNotificationSettingService);
+                courseNotificationService = TestBed.inject(CourseNotificationService);
                 // default for findOneForDashboardStub is to return the course
                 findOneForDashboardStub = jest.spyOn(courseService, 'findOneForDashboard').mockReturnValue(
                     of(
@@ -247,6 +275,9 @@ describe('CourseOverviewComponent', () => {
                     activeProfiles: [PROFILE_IRIS, PROFILE_LTI, PROFILE_PROD],
                     testServer: false,
                 } as unknown as ProfileInfo);
+                jest.spyOn(courseNotificationSettingService, 'getSettingInfo').mockReturnValue(of(mockSettingInfo));
+                jest.spyOn(courseNotificationService, 'getInfo').mockReturnValue(of(new HttpResponse({ body: mockNotificationInfo })));
+                jest.spyOn(courseNotificationSettingService, 'setSettingPreset').mockImplementation();
             });
     }));
 
@@ -764,4 +795,65 @@ describe('CourseOverviewComponent', () => {
             expect(component.manageViewLink()).toEqual(['/course-management', '123', 'course-statistics']);
         });
     });
+
+    it('should initialize course notification values when both settingInfo and info are available', fakeAsync(() => {
+        component.courseId.set(mockCourseId);
+        component.ngOnInit();
+        tick();
+
+        const selectableSettingPresets = (component as any).selectableSettingPresets;
+        const selectedSettingPreset = (component as any).selectedSettingPreset;
+
+        expect(selectableSettingPresets).toBeDefined();
+        expect(selectableSettingPresets).toEqual(mockNotificationSettingPresets);
+        expect(selectedSettingPreset).toBeDefined();
+        expect(selectedSettingPreset).toEqual(mockNotificationSettingPresets[0]);
+    }));
+
+    it('should select a new notification preset when presetSelected is called', fakeAsync(() => {
+        const setSettingPresetSpy = jest.spyOn(courseNotificationSettingService, 'setSettingPreset');
+
+        component.ngOnInit();
+        tick();
+
+        component.presetSelected(2);
+
+        const selectedSettingPreset = (component as any).selectedSettingPreset;
+
+        expect(selectedSettingPreset).toBeDefined();
+        expect(selectedSettingPreset).toEqual(mockNotificationSettingPresets[1]);
+        expect(setSettingPresetSpy).toHaveBeenCalledWith(1, 2, mockNotificationSettingPresets[0]);
+    }));
+
+    it('should set selectedSettingPreset to undefined when custom settings are selected', fakeAsync(() => {
+        component.ngOnInit();
+        tick();
+
+        component.presetSelected(0);
+
+        const selectedSettingPreset = (component as any).selectedSettingPreset;
+
+        expect(selectedSettingPreset).toBeUndefined();
+        expect(courseNotificationSettingService.setSettingPreset).toHaveBeenCalledWith(1, 0, mockNotificationSettingPresets[0]);
+    }));
+
+    it('should update notification settings when both services return data', fakeAsync(() => {
+        component.courseId.set(mockCourseId);
+        const getSettingInfoSpy = jest.spyOn(courseNotificationSettingService, 'getSettingInfo').mockImplementation(() => {
+            return of(undefined);
+        });
+
+        component.ngOnInit();
+        tick();
+
+        expect((component as any).selectableSettingPresets).toBeUndefined();
+
+        getSettingInfoSpy.mockReturnValue(of(mockSettingInfo));
+
+        (component as any).settingInfo = mockSettingInfo;
+        (component as any).initializeCourseNotificationValues();
+
+        expect((component as any).selectableSettingPresets).toBeDefined();
+        expect((component as any).selectedSettingPreset).toBeDefined();
+    }));
 });
diff --git a/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts b/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts
index fcfa762c8671..3e7f459fb366 100644
--- a/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts
+++ b/src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts
@@ -31,6 +31,12 @@ import { CourseTutorialGroupsComponent } from 'app/tutorialgroup/shared/course-t
 import { CourseConversationsComponent } from 'app/communication/shared/course-conversations/course-conversations.component';
 import { Course, isCommunicationEnabled } from 'app/core/course/shared/entities/course.model';
 import { CourseUnenrollmentModalComponent } from 'app/core/course/overview/course-unenrollment-modal/course-unenrollment-modal.component';
+import { CourseNotificationSettingPreset } from 'app/communication/shared/entities/course-notification/course-notification-setting-preset';
+import { CourseNotificationInfo } from 'app/communication/shared/entities/course-notification/course-notification-info';
+import { CourseNotificationSettingInfo } from 'app/communication/shared/entities/course-notification/course-notification-setting-info';
+import { CourseNotificationSettingService } from 'app/communication/course-notification/course-notification-setting.service';
+import { CourseNotificationService } from 'app/communication/course-notification/course-notification.service';
+import { CourseNotificationPresetPickerComponent } from 'app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component';
 
 @Component({
     selector: 'jhi-course-overview',
@@ -49,6 +55,7 @@ import { CourseUnenrollmentModalComponent } from 'app/core/course/overview/cours
         CourseNotificationOverviewComponent,
         CourseTitleBarComponent,
         CourseSidebarComponent,
+        CourseNotificationPresetPickerComponent,
     ],
     providers: [MetisConversationService],
 })
@@ -61,6 +68,8 @@ export class CourseOverviewComponent extends BaseCourseContainerComponent implem
     private modalService = inject(NgbModal);
     private examParticipationService = inject(ExamParticipationService);
     private sidebarItemService = inject(CourseSidebarItemService);
+    protected readonly courseNotificationSettingService: CourseNotificationSettingService = inject(CourseNotificationSettingService);
+    protected readonly courseNotificationService: CourseNotificationService = inject(CourseNotificationService);
 
     private toggleSidebarEventSubscription: Subscription;
     private teamAssignmentUpdateListener: Subscription;
@@ -68,6 +77,11 @@ export class CourseOverviewComponent extends BaseCourseContainerComponent implem
     private examStartedSubscription: Subscription;
     manageViewLink = signal<string[]>(['']);
 
+    protected selectableSettingPresets: CourseNotificationSettingPreset[];
+    protected selectedSettingPreset?: CourseNotificationSettingPreset;
+    private info?: CourseNotificationInfo;
+    private settingInfo?: CourseNotificationSettingInfo;
+
     courseActionItems = signal<CourseActionItem[]>([]);
     canUnenroll = signal<boolean>(false);
     showRefreshButton = signal<boolean>(false);
@@ -95,6 +109,26 @@ export class CourseOverviewComponent extends BaseCourseContainerComponent implem
         this.subscription = this.route?.params.subscribe(async (params: { courseId: string }) => {
             const id = Number(params.courseId);
             this.courseId.set(id);
+
+            this.courseNotificationSettingService.getSettingInfo(this.courseId(), false).subscribe((settingInfo) => {
+                if (settingInfo) {
+                    this.settingInfo = settingInfo;
+
+                    if (this.info) {
+                        this.initializeCourseNotificationValues();
+                    }
+                }
+            });
+
+            this.courseNotificationService.getInfo().subscribe((info) => {
+                if (info.body) {
+                    this.info = info.body;
+
+                    if (this.settingInfo) {
+                        this.initializeCourseNotificationValues();
+                    }
+                }
+            });
         });
         await super.ngOnInit();
 
@@ -108,6 +142,28 @@ export class CourseOverviewComponent extends BaseCourseContainerComponent implem
         this.sidebarItems.set(this.getSidebarItems());
     }
 
+    /**
+     * Initializes component values once both settingInfo and info are available.
+     * Sets up selectable presets, and the currently selected preset.
+     */
+    private initializeCourseNotificationValues() {
+        this.selectableSettingPresets = this.info!.presets;
+
+        this.selectedSettingPreset =
+            this.settingInfo!.selectedPreset === 0 ? undefined : this.selectableSettingPresets.find((preset) => preset.typeId === this.settingInfo!.selectedPreset)!;
+    }
+
+    /**
+     * Handles selection of a notification preset.
+     *
+     * @param presetTypeId - The ID of the selected preset (0 for custom settings)
+     */
+    presetSelected(presetTypeId: number) {
+        this.courseNotificationSettingService.setSettingPreset(this.courseId(), presetTypeId, this.selectedSettingPreset);
+
+        this.selectedSettingPreset = presetTypeId === 0 ? undefined : this.selectableSettingPresets.find((preset) => preset.typeId === presetTypeId)!;
+    }
+
     protected handleNavigationEndActions() {
         this.determineManageViewLink();
     }
diff --git a/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.html b/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.html
index 4f41b72de21b..8fa2f2899a25 100644
--- a/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.html
+++ b/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.html
@@ -12,6 +12,7 @@ <h5>
         <jhi-course-notification-preset-picker
             [availableCourseSettingPresets]="selectableSettingPresets"
             [selectedCourseSettingPreset]="selectedSettingPreset"
+            [isSmallButton]="false"
             (onPresetSelected)="presetSelected($event)"
         />
 
diff --git a/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.spec.ts b/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.spec.ts
index b8bc77a17377..a344b22c60f1 100644
--- a/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.spec.ts
+++ b/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.spec.ts
@@ -29,20 +29,28 @@ describe('NotificationSettingsComponent', () => {
     const courseId = 123;
 
     const mockPresets: CourseNotificationSettingPreset[] = [
-        new CourseNotificationSettingPreset('preset1', 1, {
-            newPostNotification: {
-                [CourseNotificationChannel.PUSH]: true,
-                [CourseNotificationChannel.EMAIL]: false,
-                [CourseNotificationChannel.WEBAPP]: true,
+        {
+            identifier: 'preset1',
+            typeId: 1,
+            presetMap: {
+                newPostNotification: {
+                    [CourseNotificationChannel.PUSH]: true,
+                    [CourseNotificationChannel.EMAIL]: false,
+                    [CourseNotificationChannel.WEBAPP]: true,
+                },
             },
-        }),
-        new CourseNotificationSettingPreset('preset2', 2, {
-            newPostNotification: {
-                [CourseNotificationChannel.PUSH]: false,
-                [CourseNotificationChannel.EMAIL]: true,
-                [CourseNotificationChannel.WEBAPP]: true,
+        },
+        {
+            identifier: 'preset2',
+            typeId: 2,
+            presetMap: {
+                newPostNotification: {
+                    [CourseNotificationChannel.PUSH]: false,
+                    [CourseNotificationChannel.EMAIL]: true,
+                    [CourseNotificationChannel.WEBAPP]: true,
+                },
             },
-        }),
+        },
     ];
 
     const mockNotificationTypes: Record<number, string> = {
@@ -71,7 +79,7 @@ describe('NotificationSettingsComponent', () => {
         } as unknown as jest.Mocked<CourseNotificationService>;
 
         courseNotificationSettingServiceMock = {
-            getSettingInfo: jest.fn().mockReturnValue(of(new HttpResponse({ body: mockSettingInfo }))),
+            getSettingInfo: jest.fn().mockReturnValue(of(mockSettingInfo)),
             setSettingPreset: jest.fn(),
             setSettingSpecification: jest.fn(),
         } as unknown as jest.Mocked<CourseNotificationSettingService>;
@@ -81,25 +89,19 @@ describe('NotificationSettingsComponent', () => {
         };
 
         await TestBed.configureTestingModule({
-            imports: [NotificationSettingsComponent],
+            imports: [
+                NotificationSettingsComponent,
+                MockDirective(TranslateDirective),
+                MockComponent(FaIconComponent),
+                MockComponent(CourseNotificationPresetPickerComponent),
+                MockComponent(CourseNotificationSettingSpecificationCardComponent),
+            ],
             providers: [
                 { provide: CourseNotificationService, useValue: courseNotificationServiceMock },
                 { provide: CourseNotificationSettingService, useValue: courseNotificationSettingServiceMock },
                 { provide: ActivatedRoute, useValue: activatedRouteMock },
             ],
-        })
-            .overrideComponent(NotificationSettingsComponent, {
-                remove: { imports: [TranslateDirective, FaIconComponent, CourseNotificationPresetPickerComponent, CourseNotificationSettingSpecificationCardComponent] },
-                add: {
-                    imports: [
-                        MockDirective(TranslateDirective),
-                        MockComponent(FaIconComponent),
-                        MockComponent(CourseNotificationPresetPickerComponent),
-                        MockComponent(CourseNotificationSettingSpecificationCardComponent),
-                    ],
-                },
-            })
-            .compileComponents();
+        }).compileComponents();
 
         fixture = TestBed.createComponent(NotificationSettingsComponent);
         component = fixture.componentInstance;
@@ -116,29 +118,38 @@ describe('NotificationSettingsComponent', () => {
     });
 
     it('should fetch setting info and notification info on initialization', () => {
-        expect(courseNotificationSettingServiceMock.getSettingInfo).toHaveBeenCalledWith(courseId);
+        expect(courseNotificationSettingServiceMock.getSettingInfo).toHaveBeenCalledWith(courseId, true);
         expect(courseNotificationServiceMock.getInfo).toHaveBeenCalled();
     });
 
-    it('should initialize values after receiving both info responses', () => {
+    it('should initialize values after receiving both info responses', fakeAsync(() => {
+        component['settingInfo'] = mockSettingInfo;
+        component['info'] = mockInfo;
+        component.initializeValues();
+        tick();
+
         expect(component['isLoading']).toBeFalse();
         expect(component['selectableSettingPresets']).toEqual(mockPresets);
         expect(component['selectedSettingPreset']).toEqual(mockPresets[0]);
         expect(component['notificationSpecifications']).toHaveLength(1);
-    });
+    }));
+
+    it('should update selected preset when presetSelected is called', () => {
+        component['settingInfo'] = mockSettingInfo;
+        component['info'] = mockInfo;
+        component.initializeValues();
 
-    it('should update selected preset when presetSelected is called', fakeAsync(() => {
         component.presetSelected(2);
 
         expect(component['selectedSettingPreset']).toEqual(mockPresets[1]);
+        expect(courseNotificationSettingServiceMock.setSettingPreset).toHaveBeenCalledWith(courseId, 2, mockPresets[1]);
+    });
 
-        expect(component['notificationSpecifications'][0].channelSetting).toEqual(mockPresets[1].presetMap['newPostNotification']);
+    it('should handle option changes correctly', () => {
+        component['settingInfo'] = mockSettingInfo;
+        component['info'] = mockInfo;
+        component.initializeValues();
 
-        tick(2000);
-        expect(courseNotificationSettingServiceMock.setSettingPreset).toHaveBeenCalledWith(courseId, 2);
-    }));
-
-    it('should handle option changes and queue them for upload', fakeAsync(() => {
         const mockSpecification = new CourseNotificationSettingSpecification('newPostNotification', 1, {
             [CourseNotificationChannel.PUSH]: false,
             [CourseNotificationChannel.EMAIL]: true,
@@ -148,14 +159,13 @@ describe('NotificationSettingsComponent', () => {
         component.optionChanged(mockSpecification);
 
         expect(component['selectedSettingPreset']).toBeUndefined();
-
-        expect(component['settingSpecificationsToUpload']).toContain(mockSpecification);
-
-        tick(5000);
-        expect(courseNotificationSettingServiceMock.setSettingSpecification).toHaveBeenCalledWith(courseId, [mockSpecification]);
-    }));
+        expect(courseNotificationSettingServiceMock.setSettingSpecification).toHaveBeenCalledWith(courseId, mockSpecification, mockPresets[0]);
+    });
 
     it('should render preset picker when not loading', () => {
+        component['settingInfo'] = mockSettingInfo;
+        component['info'] = mockInfo;
+        component.initializeValues();
         component['isLoading'] = false;
         fixture.detectChanges();
 
@@ -167,6 +177,9 @@ describe('NotificationSettingsComponent', () => {
     });
 
     it('should render specification cards when not loading', () => {
+        component['settingInfo'] = mockSettingInfo;
+        component['info'] = mockInfo;
+        component.initializeValues();
         component['isLoading'] = false;
         fixture.detectChanges();
 
@@ -177,7 +190,10 @@ describe('NotificationSettingsComponent', () => {
     });
 
     it('should correctly update specifications from notification map', () => {
-        const updateMethod = component['updateSpecificationArrayByNotificationMap'].bind(component);
+        component['info'] = {
+            notificationTypes: { 1: 'newPostNotification' },
+            presets: [],
+        };
 
         const testMap: CourseNotificationSettingsMap = {
             newPostNotification: {
@@ -187,12 +203,8 @@ describe('NotificationSettingsComponent', () => {
             },
         };
 
-        component['info'] = {
-            notificationTypes: { 1: 'newPostNotification' },
-            presets: [],
-        };
+        component['updateSpecificationArrayByNotificationMap'](testMap, true);
 
-        updateMethod(testMap, true);
         expect(component['notificationSpecifications']).toHaveLength(1);
         expect(component['notificationSpecifications'][0].identifier).toBe('newPostNotification');
         expect(component['notificationSpecifications'][0].typeId).toBe(1);
@@ -206,7 +218,8 @@ describe('NotificationSettingsComponent', () => {
             },
         };
 
-        updateMethod(testMapWithIds, false);
+        component['updateSpecificationArrayByNotificationMap'](testMapWithIds, false);
+
         expect(component['notificationSpecifications']).toHaveLength(1);
         expect(component['notificationSpecifications'][0].identifier).toBe('newPostNotification');
         expect(component['notificationSpecifications'][0].typeId).toBe(1);
diff --git a/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.ts b/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.ts
index 04ff85e3f9f7..db7ebff41f9a 100644
--- a/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.ts
+++ b/src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.ts
@@ -37,9 +37,6 @@ export class NotificationSettingsComponent extends CourseSettingCategoryDirectiv
     protected settingInfo?: CourseNotificationSettingInfo;
     protected info?: CourseNotificationInfo;
     protected notificationSpecifications: CourseNotificationSettingSpecification[] = [];
-    protected settingSpecificationsToUpload: CourseNotificationSettingSpecification[] = [];
-    protected putPresetTimeout: NodeJS.Timeout;
-    protected putSpecificationsTimeout: any;
 
     constructor() {
         super();
@@ -55,7 +52,11 @@ export class NotificationSettingsComponent extends CourseSettingCategoryDirectiv
         this.selectedSettingPreset =
             this.settingInfo!.selectedPreset === 0 ? undefined : this.selectableSettingPresets.find((preset) => preset.typeId === this.settingInfo!.selectedPreset)!;
 
-        this.updateSpecificationArrayByNotificationMap(this.settingInfo!.notificationTypeChannels!, false);
+        if (this.settingInfo!.selectedPreset === 0) {
+            this.updateSpecificationArrayByNotificationMap(this.settingInfo!.notificationTypeChannels!, this.settingInfo!.notificationTypeChannels![1] === undefined);
+        } else if (this.settingInfo!.selectedPreset !== 0) {
+            this.updateSpecificationArrayByNotificationMap(this.selectedSettingPreset!.presetMap!, true);
+        }
 
         this.isLoading = false;
     }
@@ -67,19 +68,9 @@ export class NotificationSettingsComponent extends CourseSettingCategoryDirectiv
      * @param presetTypeId - The ID of the selected preset (0 for custom settings)
      */
     presetSelected(presetTypeId: number) {
-        if (this.putPresetTimeout) {
-            clearTimeout(this.putPresetTimeout);
-        }
-
         this.selectedSettingPreset = presetTypeId === 0 ? undefined : this.selectableSettingPresets.find((preset) => preset.typeId === presetTypeId)!;
 
-        if (presetTypeId !== 0) {
-            this.updateSpecificationArrayByNotificationMap(this.selectedSettingPreset!.presetMap, true);
-        }
-
-        this.putPresetTimeout = setTimeout(() => {
-            this.courseNotificationSettingService.setSettingPreset(this.courseId, presetTypeId);
-        }, 2000);
+        this.courseNotificationSettingService.setSettingPreset(this.courseId, presetTypeId, this.selectedSettingPreset);
     }
 
     /**
@@ -89,16 +80,9 @@ export class NotificationSettingsComponent extends CourseSettingCategoryDirectiv
      * @param specification - The notification specification that was changed
      */
     optionChanged(specification: CourseNotificationSettingSpecification) {
+        this.courseNotificationSettingService.setSettingSpecification(this.courseId, specification, this.selectedSettingPreset);
+
         this.selectedSettingPreset = undefined;
-        this.settingSpecificationsToUpload.push(specification);
-
-        if (!this.putSpecificationsTimeout) {
-            // To avoid making many server calls we collect the specifications over a timeframe and then upload them.
-            this.putSpecificationsTimeout = setTimeout(() => {
-                this.courseNotificationSettingService.setSettingSpecification(this.courseId, this.settingSpecificationsToUpload);
-                this.putSpecificationsTimeout = undefined;
-            }, 3000);
-        }
     }
 
     /**
@@ -114,9 +98,9 @@ export class NotificationSettingsComponent extends CourseSettingCategoryDirectiv
      * Fetches notification settings and info from the server.
      */
     onCourseIdAvailable(): void {
-        this.courseNotificationSettingService.getSettingInfo(this.courseId).subscribe((settingInfo) => {
-            if (settingInfo.body) {
-                this.settingInfo = settingInfo.body;
+        this.courseNotificationSettingService.getSettingInfo(this.courseId, true).subscribe((settingInfo) => {
+            if (settingInfo) {
+                this.settingInfo = settingInfo;
 
                 if (this.info) {
                     this.initializeValues();
@@ -137,14 +121,6 @@ export class NotificationSettingsComponent extends CourseSettingCategoryDirectiv
 
     ngOnDestroy() {
         super.ngOnDestroy();
-        if (this.putPresetTimeout) {
-            clearTimeout(this.putPresetTimeout);
-            this.courseNotificationSettingService.setSettingPreset(this.courseId, this.selectedSettingPreset?.typeId ?? 0);
-        }
-        if (this.putSpecificationsTimeout) {
-            clearTimeout(this.putSpecificationsTimeout);
-            this.courseNotificationSettingService.setSettingSpecification(this.courseId, this.settingSpecificationsToUpload);
-        }
     }
 
     /**


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.ts (1)</summary><blockquote>

`100-119`: _🛠️ Refactor suggestion_

**Missing unsubscribe leads to dangling listeners**

`getSettingInfo()` and `getInfo()` return long-lived observables, yet the component keeps no references to unsubscribe them in `ngOnDestroy`.  
Even though the component usually lives for the whole settings page, holding on to BehaviorSubjects of destroyed courses wastes memory.

Add a `Subscription` composite or use the `takeUntil(destroy$)` pattern.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss (1)</summary><blockquote>

`57-94`: **Fix CSS specificity and formatting issues.**

The new CSS transitions for the notification preset picker are well-designed, but there are a few minor issues:

1. The selector `.is-small-button .course-notification-preset-picker-eye-icon` on line 65 should come before the more specific selector `.is-small-button:hover .course-notification-preset-picker-eye-icon` to follow the CSS specificity rule.

2. There are unnecessary empty lines before some declarations (lines 62, 68, 77).


```diff
.course-notification-preset-picker-eye-icon,
+.is-small-button .course-notification-preset-picker-eye-icon,
.is-small-button:hover .course-notification-preset-picker-eye-icon,
.is-small-button.is-recently-selected .course-notification-preset-picker-eye-icon {
    margin-right: 0.25rem;
-
    transition: margin-right 0.2s 0.1s ease-in-out;
}

-.is-small-button .course-notification-preset-picker-eye-icon {
-    margin-right: 0;
-
-    transition: margin-right 0.2s 0.4s ease-in-out;
-}

.is-small-button .course-notification-preset-picker-text {
    max-width: 0;
    opacity: 0;
    overflow: hidden;
    display: inline-block;
-
    transition:
        opacity 0.2s 0.1s ease-in-out,
        max-width 0.2s 0.4s ease-in-out;
}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Codacy Static Code Analysis</summary>

[notice] 62-62: src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss#L62
Unexpected empty line before declaration (declaration-empty-line-before)

---

[warning] 65-65: src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss#L65
Expected selector ".is-small-button .course-notification-preset-picker-eye-icon" to come before selector ".is-small-button:hover .course-notification-preset-picker-eye-icon" (no-descending-specificity)

---

[notice] 68-68: src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss#L68
Unexpected empty line before declaration (declaration-empty-line-before)

---

[notice] 77-77: src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss#L77
Unexpected empty line before declaration (declaration-empty-line-before)

</details>

</details>

</blockquote></details>
<details>
<summary>src/main/webapp/app/communication/course-notification/course-notification.service.ts (1)</summary><blockquote>

`108-119`: **Implemented caching mechanism for notification info.**

The caching mechanism appropriately returns cached data when available, reducing unnecessary network requests. The implementation follows reactive programming principles with RxJS.

However, consider implementing cache invalidation or a time-to-live mechanism for the cached data to prevent stale information in long-running sessions.



```typescript
private cachedNotificationInfo: HttpResponse<CourseNotificationInfo> | null = null;
private cacheTimestamp: number | null = null;
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds

public getInfo(): Observable<HttpResponse<CourseNotificationInfo>> {
    const now = Date.now();
    if (this.cachedNotificationInfo && this.cacheTimestamp && (now - this.cacheTimestamp < this.CACHE_TTL)) {
        return of(this.cachedNotificationInfo);
    }

    // Otherwise, fetch from server and cache the result
    return this.http.get<CourseNotificationInfo>(this.apiEndpoint + 'info', { observe: 'response' }).pipe(
        tap((response) => {
            if (response.body) {
                this.cachedNotificationInfo = response;
                this.cacheTimestamp = Date.now();
            }
        }),
    );
}
```

</blockquote></details>
<details>
<summary>src/main/webapp/app/core/course/overview/course-overview/course-overview.component.spec.ts (1)</summary><blockquote>

`799-858`: **Comprehensive tests for notification preset functionality.**

The tests verify:
1. Initialization of notification presets
2. Selecting a preset and updating the state
3. Handling custom presets
4. Conditional initialization when data is available

These tests provide thorough coverage of the new functionality.

However, the tests access private component properties directly, which could make them brittle if the implementation changes. Consider testing through component outputs or DOM interactions when possible.



For example, instead of checking private properties directly:

```typescript
// Verify the behavior through observable outputs or DOM elements
const presetPicker = fixture.debugElement.query(By.css('jhi-course-notification-preset-picker'));
expect(presetPicker).toBeTruthy();
expect(presetPicker.componentInstance.selectedCourseSettingPreset()).toEqual(mockNotificationSettingPresets[0]);
```

</blockquote></details>
<details>
<summary>src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.spec.ts (1)</summary><blockquote>

`125-135`: **`fakeAsync/tick()` is unnecessary here – remove to simplify the test**

`initializeValues()` is a synchronous method; wrapping the call in `fakeAsync` and advancing the virtual clock with `tick()` adds noise without providing benefit.  
Removing both makes the test easier to read and slightly faster.

```diff
-    it('should initialize values after receiving both info responses', fakeAsync(() => {
+    it('should initialize values after receiving both info responses', () => {
         component['settingInfo'] = mockSettingInfo;
         component['info'] = mockInfo;
         component.initializeValues();
-        tick();
 
         expect(component['isLoading']).toBeFalse();
         …
-    }));
+    });
```

</blockquote></details>
<details>
<summary>src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts (1)</summary><blockquote>

`149-154`: **Guard against unknown preset IDs**

The non-null assertion (`!`) will throw if `find` returns `undefined`.  
Defensive fallback avoids runtime errors when the backend delivers an unknown `selectedPreset`.

```diff
-    this.selectedSettingPreset =
-        this.settingInfo!.selectedPreset === 0 ? undefined : this.selectableSettingPresets.find((p) => p.typeId === this.settingInfo!.selectedPreset)!;
+    this.selectedSettingPreset =
+        this.settingInfo!.selectedPreset === 0
+            ? undefined
+            : this.selectableSettingPresets.find((p) => p.typeId === this.settingInfo!.selectedPreset);
```

</blockquote></details>
<details>
<summary>src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.ts (1)</summary><blockquote>

`70-75`: **Debounce comment outdated – update or remove**

The JSDoc still mentions “schedules settings update to server with debouncing”, but the debouncing logic was removed.  
Update the comment to avoid confusion for future maintainers.

</blockquote></details>
<details>
<summary>src/main/webapp/app/communication/course-notification/course-notification-setting.service.ts (2)</summary><blockquote>

`74-85`: **Avoid in-place mutation of cached objects**

`currentValue.notificationTypeChannels = …` mutates the already-stored reference.  
Consumers holding the old reference (e.g., OnPush components) will not detect the change.

Clone the object before mutation:

```diff
-const currentValue = subject.getValue();
+const currentValue = structuredClone(subject.getValue());
```

(or `JSON.parse(JSON.stringify())` or a deep-clone utility).

Then continue with the immutable update.

---

`118-124`: **Inefficient request body construction**

`transformNotificationSettingSpecificationToRequestBody()` allocates a new object per call even when only one specification changes.  
For single-spec updates you can avoid the helper and build the payload inline:

```typescript
this.http.put(`${this.apiEndpoint}${courseId}/setting-specification`, {
    notificationTypeChannels: { [spec.typeId]: spec.channelSetting },
}).subscribe();
```

Keeps allocations low and improves readability.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between df74130cd4193c3bf5a5a118be03f70b0440ef36 and c9559195374b3f248f9d9711f52df260a6b9baa6.

</details>

<details>
<summary>📒 Files selected for processing (12)</summary>

* `src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.html` (2 hunks)
* `src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss` (1 hunks)
* `src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.ts` (3 hunks)
* `src/main/webapp/app/communication/course-notification/course-notification-setting.service.spec.ts` (3 hunks)
* `src/main/webapp/app/communication/course-notification/course-notification-setting.service.ts` (2 hunks)
* `src/main/webapp/app/communication/course-notification/course-notification.service.ts` (3 hunks)
* `src/main/webapp/app/core/course/overview/course-overview/course-overview.component.html` (1 hunks)
* `src/main/webapp/app/core/course/overview/course-overview/course-overview.component.spec.ts` (7 hunks)
* `src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts` (5 hunks)
* `src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.html` (1 hunks)
* `src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.spec.ts` (9 hunks)
* `src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.ts` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (2)</summary>

<details>
<summary>`src/main/webapp/**/*.html`: @if and @for are new and valid Angular syntax replacing *ngIf and *ngFor. They should always be used over the old style.</summary>

> `src/main/webapp/**/*.html`: @if and @for are new and valid Angular syntax replacing *ngIf and *ngFor. They should always be used over the old style.

- `src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.html`
- `src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.html`
- `src/main/webapp/app/core/course/overview/course-overview/course-overview.component.html`

</details>
<details>
<summary>`src/main/webapp/**/*.ts`: angular_style:https://angular.io/guide/styleguide;methods_in_html:false;lazy_loading:true;code_reuse:true;tests:meaningful;types:PascalCase;enums:PascalC...</summary>

> `src/main/webapp/**/*.ts`: angular_style:https://angular.io/guide/styleguide;methods_in_html:false;lazy_loading:true;code_reuse:true;tests:meaningful;types:PascalCase;enums:PascalCase;funcs:camelCase;props:camelCase;no_priv_prefix:true;strings:single_quotes;localize:true;btns:functionality;links:navigation;icons_text:newline;labels:associate;code_style:arrow_funcs,curly_braces,open_braces_same_line,indent_4;memory_leak_prevention:true;routes:naming_schema;chart_framework:ngx-charts;responsive_layout:true

- `src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.ts`
- `src/main/webapp/app/core/course/overview/course-overview/course-overview.component.spec.ts`
- `src/main/webapp/app/communication/course-notification/course-notification.service.ts`
- `src/main/webapp/app/communication/course-notification/course-notification-setting.service.spec.ts`
- `src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts`
- `src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.spec.ts`
- `src/main/webapp/app/communication/course-notification/course-notification-setting.service.ts`
- `src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.ts`

</details>

</details><details>
<summary>🪛 GitHub Check: Codacy Static Code Analysis</summary>

<details>
<summary>src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss</summary>

[notice] 62-62: src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss#L62
Unexpected empty line before declaration (declaration-empty-line-before)

---

[warning] 65-65: src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss#L65
Expected selector ".is-small-button .course-notification-preset-picker-eye-icon" to come before selector ".is-small-button:hover .course-notification-preset-picker-eye-icon" (no-descending-specificity)

---

[notice] 68-68: src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss#L68
Unexpected empty line before declaration (declaration-empty-line-before)

---

[notice] 77-77: src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.scss#L77
Unexpected empty line before declaration (declaration-empty-line-before)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Codacy Static Code Analysis

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.html (1)</summary>

`15-15`: **LGTM! Good addition of the button size control.**

The addition of `[isSmallButton]="false"` properly configures the notification preset picker to display in its full size in the settings view, which is appropriate for this context.

</details>
<details>
<summary>src/main/webapp/app/core/course/overview/course-overview/course-overview.component.html (1)</summary>

`69-76`: **Well-implemented notification preset picker integration!**

The notification preset picker is correctly integrated into the course title bar with proper conditional rendering and input/output bindings. Using `isSmallButton="true"` is appropriate for this compact UI context.

</details>
<details>
<summary>src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.ts (5)</summary>

`2-2`: **LGTM! Import additions are appropriate.**

The imports for `NgClass` and `faEyeSlash` are required for the new functionality and properly added to the component imports array.



Also applies to: 5-5, 16-16

---

`23-23`: **LGTM! Well-designed input property.**

The `isSmallButton` input is well-implemented using the modern Angular input API with a sensible default value of `false`.

---

`29-30`: **LGTM! Icon properly exposed.**

The `farEyeSlash` icon is correctly exposed as a protected property for use in the template.

---

`32-34`: **LGTM! Well-structured state management.**

The recently selected state and timeout management are well-designed to provide visual feedback to the user.

---

`51-59`: **LGTM! Clean implementation of the timeout handling.**

The timeout implementation for handling the "recently selected" state is clean and properly clears existing timeouts to prevent memory leaks.

</details>
<details>
<summary>src/main/webapp/app/communication/course-notification/course-notification-preset-picker/course-notification-preset-picker.component.html (4)</summary>

`1-1`: **Good conditional CSS implementation for responsive design.**

The implementation correctly uses NgClass to apply conditional styling based on the component's state, enhancing user experience with visual feedback when selections are made.

---

`3-3`: **Icon selection logic is properly implemented.**

The ternary operator correctly switches between `farEyeSlash` and `farEye` based on the preset's type ID. This provides visual distinction between notification presets.

---

`4-7`: **Well-structured text layout with proper translation support.**

The nested spans with appropriate CSS classes provide good structure for styling while maintaining proper translation support with jhiTranslate directive.

---

`29-29`: **Condition updated from null to undefined check.**

The change from checking against `null` to checking against `undefined` is correct, as Angular signals often return undefined rather than null for missing values.

</details>
<details>
<summary>src/main/webapp/app/communication/course-notification/course-notification.service.ts (2)</summary>

`6-6`: **Appropriate RxJS imports for caching implementation.**

The imports have been correctly expanded to include the required RxJS operators for implementing the caching mechanism.

---

`92-92`: **Added private field for caching notification info.**

The private field provides an appropriate place to store the cached response.

</details>
<details>
<summary>src/main/webapp/app/communication/course-notification/course-notification-setting.service.spec.ts (4)</summary>

`10-10`: **Added import for CourseNotificationSettingPreset.**

Correctly imported the required entity for the new test cases.

---

`61-122`: **Comprehensive tests for caching behavior in getSettingInfo.**

The tests thoroughly verify the caching functionality:
1. Returning cached values when available
2. Force-fetching new data when requested

This is a good practice that ensures the caching mechanism works as expected.

---

`139-218`: **Tests for subject updates and preset copying in setSettingPreset.**

These tests properly verify that:
1. The BehaviorSubject is updated with the new preset
2. The preset map is copied correctly when a copyPreset is provided

This ensures the reactive data flow works as expected.

---

`245-340`: **Tests for specification updates and preset copying in setSettingSpecification.**

Similar to the setSettingPreset tests, these verify the subject updates and preset copying for specifications. They provide good coverage for the service's behavior.

</details>
<details>
<summary>src/main/webapp/app/core/course/overview/course-overview/course-overview.component.spec.ts (4)</summary>

`62-66`: **Added necessary imports for notification functionality.**

The imports are correctly added for the notification-related services and entities used in the tests.

---

`154-155`: **Added service declarations for notification services.**

The service declarations are correctly added to access them in the tests.

---

`166-180`: **Well-structured mock data for notification settings.**

The mock data provides a complete set of test values for notification presets and settings, making the tests more readable and maintainable.

---

`278-280`: **Configured service spies for notification functionality.**

The spies are properly set up to mock the service methods, which is a good practice for unit testing.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:50:42Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T21:05:37Z
user: login: HawKhiem
body: Tested on TS3. Works as described

![Screenshot 2025-05-14 001309](https://github.com/user-attachments/assets/3b400985-797c-4fdc-8424-3e0e0a301dd2)

https://github.com/user-attachments/assets/aaf3f089-7069-405b-8dac-2c5f99284190


state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T22:14:42Z
user: login: denizozturk95
body: Works as described. Tested on TS3
state: APPROVED
author_association: NONE
submitted_at: 2025-05-14T12:54:37Z
user: login: Jan-Thurner
body: Tested on TS2, works as expected
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:51:31Z
user: login: Predixx
body: Tested on ts2. Working as expected
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T10:51:44Z
user: login: chuuuun
body: tested on ts6, user 5. Worked as described.
state: APPROVED
author_association: NONE
submitted_at: 2025-05-16T17:59:53Z
user: login: eylulnc
body: code
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T14:21:30Z
user: login: krusche
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-25T20:32:42Z

Review comments:
diff_hunk: @@ -95,6 +109,26 @@ export class CourseOverviewComponent extends BaseCourseContainerComponent implem
         this.subscription = this.route?.params.subscribe(async (params: { courseId: string }) => {
             const id = Number(params.courseId);
             this.courseId.set(id);
+
+            this.courseNotificationSettingService.getSettingInfo(this.courseId(), false).subscribe((settingInfo) => {
+                if (settingInfo) {
+                    this.settingInfo = settingInfo;
+
+                    if (this.info) {
+                        this.initializeCourseNotificationValues();
+                    }
+                }
+            });
+
+            this.courseNotificationService.getInfo().subscribe((info) => {
+                if (info.body) {
+                    this.info = info.body;
+
+                    if (this.settingInfo) {
+                        this.initializeCourseNotificationValues();
+                    }
+                }
+            });
         });
path: src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Unmanaged nested subscriptions can lead to memory leaks**

Each route change installs **two new subscriptions** (`getSettingInfo`, `getInfo`) without ever unsubscribing or switching.  
If the user navigates between courses within the same component instance, callbacks will pile up.

Consider one of:

1. Replace the manual `.subscribe` with `switchMap` and a single `combineLatest` inside the `params` stream.
2. Store the inner subscriptions (`this.notificationSubs`) and `unsubscribe()`/`takeUntil(this.destroy$)` on `ngOnDestroy`.

This will prevent redundant HTTP calls and unnecessary memory retention.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In
src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts
around lines 109 to 132, the nested subscriptions inside the route params
subscription cause memory leaks because they are never unsubscribed. Refactor by
replacing the inner subscriptions with a single combined observable using
switchMap and combineLatest on the params stream, or alternatively store the
inner subscriptions in a class property and ensure they are unsubscribed in
ngOnDestroy using takeUntil or manual unsubscribe. This will prevent
accumulating subscriptions and redundant HTTP calls on route changes.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T19:50:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,6 +142,28 @@
         this.sidebarItems.set(this.getSidebarItems());
     }
 
+    /**
+     * Initializes component values once both settingInfo and info are available.
+     * Sets up selectable presets, and the currently selected preset.
+     */
+    private initializeCourseNotificationValues() {
+        this.selectableSettingPresets = this.info!.presets;
+
+        this.selectedSettingPreset =
+            this.settingInfo!.selectedPreset === 0 ? undefined : this.selectableSettingPresets.find((preset) => preset.typeId === this.settingInfo!.selectedPreset)!;
+    }
+
+    /**
+     * Handles selection of a notification preset.
+     *
+     * @param presetTypeId - The ID of the selected preset (0 for custom settings)
+     */
+    presetSelected(presetTypeId: number) {
+        this.courseNotificationSettingService.setSettingPreset(this.courseId(), presetTypeId, this.selectedSettingPreset);
+
+        this.selectedSettingPreset = presetTypeId === 0 ? undefined : this.selectableSettingPresets.find((preset) => preset.typeId === presetTypeId)!;
+    }
path: src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`presetSelected()` sends the _previous_ preset to the service – swap the two statements**

`this.selectedSettingPreset` is updated **after** the service call, meaning the service receives the old (or `undefined`) preset instead of the newly-selected one.  
This breaks the “copy-preset” behaviour in `CourseNotificationSettingService`.

```diff
-    this.courseNotificationSettingService.setSettingPreset(this.courseId(), presetTypeId, this.selectedSettingPreset);
-
-    this.selectedSettingPreset = presetTypeId === 0 ? undefined : this.selectableSettingPresets.find((preset) => preset.typeId === presetTypeId)!;
+    this.selectedSettingPreset = presetTypeId === 0 ? undefined : this.selectableSettingPresets.find((preset) => preset.typeId === presetTypeId)!;
+
+    this.courseNotificationSettingService.setSettingPreset(this.courseId(), presetTypeId, this.selectedSettingPreset);
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    presetSelected(presetTypeId: number) {
        this.selectedSettingPreset = presetTypeId === 0
            ? undefined
            : this.selectableSettingPresets.find((preset) => preset.typeId === presetTypeId)!;

        this.courseNotificationSettingService.setSettingPreset(
            this.courseId(),
            presetTypeId,
            this.selectedSettingPreset
        );
    }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In
src/main/webapp/app/core/course/overview/course-overview/course-overview.component.ts
around lines 161 to 165, the method presetSelected calls setSettingPreset with
this.selectedSettingPreset before updating it, causing the service to receive
the previous preset instead of the new one. To fix this, first update
this.selectedSettingPreset based on the presetTypeId, then call setSettingPreset
with the updated this.selectedSettingPreset to ensure the service gets the
correct preset.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T19:50:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,7 +52,11 @@ export class NotificationSettingsComponent extends CourseSettingCategoryDirectiv
         this.selectedSettingPreset =
             this.settingInfo!.selectedPreset === 0 ? undefined : this.selectableSettingPresets.find((preset) => preset.typeId === this.settingInfo!.selectedPreset)!;
 
-        this.updateSpecificationArrayByNotificationMap(this.settingInfo!.notificationTypeChannels!, false);
+        if (this.settingInfo!.selectedPreset === 0) {
+            this.updateSpecificationArrayByNotificationMap(this.settingInfo!.notificationTypeChannels!, this.settingInfo!.notificationTypeChannels![1] === undefined);
+        } else if (this.settingInfo!.selectedPreset !== 0) {
+            this.updateSpecificationArrayByNotificationMap(this.selectedSettingPreset!.presetMap!, true);
+        }
path: src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fragile detection of key-type in `notificationTypeChannels`**

`this.settingInfo!.notificationTypeChannels![1] === undefined` assumes that **type ID `1` is always present** when numeric keys are used.  
This fails for courses whose first notification type has a different ID, producing a wrong `useValue` flag.

A safer approach is to inspect the key type dynamically:

```diff
-this.updateSpecificationArrayByNotificationMap(
-    this.settingInfo!.notificationTypeChannels!,
-    this.settingInfo!.notificationTypeChannels![1] === undefined,
-);
+const firstKey = Object.keys(this.settingInfo!.notificationTypeChannels!)[0];
+const useValue = isNaN(Number(firstKey)); // true => string keys, false => numeric keys
+this.updateSpecificationArrayByNotificationMap(this.settingInfo!.notificationTypeChannels!, useValue);
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        if (this.settingInfo!.selectedPreset === 0) {
            const firstKey = Object.keys(this.settingInfo!.notificationTypeChannels!)[0];
            const useValue = isNaN(Number(firstKey)); // true => string keys, false => numeric keys
            this.updateSpecificationArrayByNotificationMap(this.settingInfo!.notificationTypeChannels!, useValue);
        } else if (this.settingInfo!.selectedPreset !== 0) {
            this.updateSpecificationArrayByNotificationMap(this.selectedSettingPreset!.presetMap!, true);
        }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In
src/main/webapp/app/core/course/overview/course-settings/notification-settings/notification-settings.component.ts
around lines 55 to 59, the code assumes that the notificationTypeChannels object
always has a key '1', which is fragile and can cause incorrect behavior if the
first notification type has a different ID. To fix this, replace the hardcoded
check for key '1' with a dynamic inspection of the keys in
notificationTypeChannels to determine if any keys exist or to check the presence
of keys more robustly, ensuring the useValue flag is set correctly regardless of
the specific keys present.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T19:50:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,33 +19,104 @@ export class CourseNotificationSettingService {
 
     private http = inject(HttpClient);
 
+    private settingInfoSubjects: Record<number, BehaviorSubject<CourseNotificationSettingInfo | undefined>> = {};
+
+    /**
+     * Gets or creates a BehaviorSubject for a specific course's setting info
+     *
+     * @param courseId - The ID of the course
+     * @returns BehaviorSubject for the course's setting info
+     */
+    private getSettingInfoSubject(courseId: number): BehaviorSubject<CourseNotificationSettingInfo | undefined> {
+        if (!this.settingInfoSubjects[courseId]) {
+            this.settingInfoSubjects[courseId] = new BehaviorSubject<CourseNotificationSettingInfo | undefined>(undefined);
+        }
+        return this.settingInfoSubjects[courseId];
+    }
+
     /**
      * Retrieves notification settings information for a specific course.
+     * Returns an Observable that will emit updates when settings change.
      *
      * @param courseId - The ID of the course
+     * @param forceFetch - Skips the cache and forces an http request
      * @returns An Observable with the course notification setting information
      */
-    getSettingInfo(courseId: number): Observable<HttpResponse<CourseNotificationSettingInfo>> {
-        return this.http.get<CourseNotificationSettingInfo>(this.apiEndpoint + courseId + '/settings', { observe: 'response' });
+    getSettingInfo(courseId: number, forceFetch = false): Observable<CourseNotificationSettingInfo | undefined> {
+        const subject = this.getSettingInfoSubject(courseId);
+
+        if (!forceFetch && subject.getValue()) {
+            return subject.asObservable().pipe(map((value) => value as CourseNotificationSettingInfo));
+        }
+
+        this.http
+            .get<CourseNotificationSettingInfo>(this.apiEndpoint + courseId + '/settings', { observe: 'response' })
+            .pipe(
+                tap((response) => {
+                    if (response.body) {
+                        subject.next(response.body);
+                    }
+                }),
+            )
+            .subscribe();
+
+        return subject.asObservable().pipe(map((value) => value as CourseNotificationSettingInfo | undefined));
path: src/main/webapp/app/communication/course-notification/course-notification-setting.service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Service swallows HTTP errors – propagate or handle gracefully**

`getSettingInfo()` subscribes internally and never forwards errors.  
If the request fails (e.g., 403), components stay in a loading state without feedback.

Return the HTTP observable to the caller and let the component decide, or at least emit `undefined`/an error on the subject:

```diff
-this.http
-    .get<CourseNotificationSettingInfo>(…)
-    .pipe(tap((response) => subject.next(response.body)))
-    .subscribe();
-return subject.asObservable();
+return this.http
+    .get<CourseNotificationSettingInfo>(…)
+    .pipe(
+        tap((resp) => subject.next(resp.body)),
+        catchError((err) => {
+            subject.error(err);
+            return throwError(() => err);
+        }),
+        switchMap(() => subject.asObservable()),
+    );
```




> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In
src/main/webapp/app/communication/course-notification/course-notification-setting.service.ts
between lines 52 and 63, the getSettingInfo() method subscribes internally to
the HTTP observable and does not propagate errors, causing components to remain
in a loading state without feedback on failures. To fix this, remove the
internal subscription and instead return the HTTP observable directly to the
caller so that components can handle success and error cases appropriately.
Alternatively, if using the subject pattern, ensure that errors are caught and
emitted as undefined or an error notification on the subject to inform
subscribers of failure.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T19:50:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-13T14:25:01Z
body: ### End-to-End (E2E) Test Results Summary

<table><tr><th><th>Tests</th><th>Passed ☑️</th><th>Skipped ⚠️</th><th>Failed ❌️</th><th>Time ⏱</th></tr><tr><td>End-to-End (E2E) Test Report</td><td>201 ran</td><td>197 passed</td><td>3 skipped</td><td>1 failed</td><td>53m 40s 988ms</td></tr></table> <table><tr><th>Test</th><th>Result</th><th>Time ⏱</th></tr><tr><td colspan="3"><strong>End-to-End (E2E) Test Report</strong></td></tr><tr><td colspan="3"><em>e2e/exercise/programming/ProgrammingExerciseStaticCodeAnalysis.spec.ts</em></td></tr><tr><td>ts.Static code analysis tests › Configures SCA grading and makes a successful submission with SCA errors</td><td>❌ failure</td><td>1m 47s 531ms</td></tr></table>
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-13T19:50:38Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change introduces notification preset management to course overview and settings components, adding support for dynamic preset selection and immediate updates to notification settings. It refactors services to cache and update notification settings reactively, modifies component templates and styles for conditional rendering and transitions, and updates related tests for new logic and service signatures.

## Changes

| Files/Groups | Change Summary |
|--------------|---------------|
| `course-notification-preset-picker.component.html`, `course-notification-preset-picker.component.scss`, `course-notification-preset-picker.component.ts` | Enhanced the notification preset picker with conditional styling, dynamic icon logic, a new `isSmallButton` input, and state for recent selection. Added CSS transitions for button and text reveal. |
| `course-notification-setting.service.ts`, `course-notification.service.ts` | Refactored to add caching and reactive updates for notification settings and info. Updated method signatures to support local cache updates and preset copying. |
| `course-notification-setting.service.spec.ts`, `course-notification.service.ts` | Expanded and updated tests for caching, state updates, and new service method signatures. |
| `course-overview.component.html`, `course-overview.component.ts` | Integrated notification preset picker into the course overview. Added logic to fetch, initialize, and handle preset selection and state. |
| `course-overview.component.spec.ts` | Added tests for notification preset management, initialization, and user interaction in the course overview component. |
| `notification-settings.component.html` | Explicitly set `[isSmallButton]="false"` on the preset picker. |
| `notification-settings.component.ts` | Removed debouncing and batching for server updates. Updated to immediate setting changes for presets and specifications. Adjusted initialization logic for new service signatures. |
| `notification-settings.component.spec.ts` | Refactored tests to use plain objects for presets, updated for new service signatures, and improved async handling. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant CourseOverviewComponent
    participant CourseNotificationSettingService
    participant CourseNotificationService
    participant Backend

    User->>CourseOverviewComponent: Loads course overview
    CourseOverviewComponent->>CourseNotificationSettingService: getSettingInfo(courseId)
    CourseOverviewComponent->>CourseNotificationService: getInfo()
    CourseNotificationSettingService-->>CourseOverviewComponent: Setting info (from cache or backend)
    CourseNotificationService-->>CourseOverviewComponent: Notification info (from cache or backend)
    CourseOverviewComponent->>CourseOverviewComponent: Initialize presets and selected preset

    User->>CourseOverviewComponent: Selects notification preset
    CourseOverviewComponent->>CourseNotificationSettingService: setSettingPreset(courseId, presetTypeId, preset)
    CourseNotificationSettingService->>Backend: PUT preset selection
    CourseNotificationSettingService-->>CourseOverviewComponent: Update local cache immediately
```

```mermaid
sequenceDiagram
    participant User
    participant NotificationSettingsComponent
    participant CourseNotificationSettingService
    participant Backend

    User->>NotificationSettingsComponent: Changes notification specification
    NotificationSettingsComponent->>CourseNotificationSettingService: setSettingSpecification(courseId, specification, preset)
    CourseNotificationSettingService->>Backend: PUT updated specification
    CourseNotificationSettingService-->>NotificationSettingsComponent: Update local cache immediately
```

## Suggested labels

`tests`

## Suggested reviewers

- HawKhiem
- anian03
- krusche

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNwSPbABsvkCiQBHbGlcSHFcLzpIACIAAwBhFmZsDHgGanh8DFiuAEFaegx8cQAzNIysnn9EElDuNIBrSjD8BWwKGrD1SMgBZ2je+Wwavis0G0wiUj54DFwKRWwGaXRIcgB3SBKSanaPWbDYD1yKGmZUbi9qEvwKZkPqdB98deRhymQCexJIsTXi+BldLiSq8aS1ZC0eD+MReeQlBb3XBHNodDzhHp9Cgaaz+CSZYZwgA0kHeHUgsDQ9C+GDQ+KI1HRrWRHiY7U6NVw4gwRE+rQYlJ5HiKpXKIKwYM5iBJ6wQAsg6zQyEiiGQTAwUlS7A0bhRzG0WAFk32nxRVKh4v4JVWIsBYsyEuqtR4jWaaAYy1V8AEPXxaHQWGiJFkJAGaUqXnwwKijxZLW4fngRFgoXw1rjbLRPDQpBx8S8jVmREOqHDWHw3DIyH9bAw2AVR38pJGaswkCCjTh31+oRZ0KqJG2FH8hQBQIqjvBmhgKJqPaikudqBZWDQ3Eu8Ci8FYdHgjK7mHo/hK8/oBwz+HZHk53N5OoM7hQzEuJFruAn6FojZWX0rFBudyYMsAb0EoiBJlgRDYPASgFuQ0qQFW7RFpA5ypPqvg2AAogAytAjBoD41YYEe0jcFk4GatIyAkJqCrqLAkBXBQpCQLQ1D+lKOKPtsuxNg4663LgMjFIxBbJqEh5sc4DSHK+0gkjczyvIwBbsAoUI8vY74kc4tAIVJswMF42BgQoz7+EcGCUfscwkEQVCWjQiDCfRyIUkmjFMFIVC5jOHjQLIlY4QwFDwNwoRMEoKCQlGjjsFEwwoQAUjhAAiUbabIkQklJyReOIL72KFJBVrAxSQq+FHzIycmQAAqgAkowgqkIg97QCEKGzC5FBLOKyBGSZ0WRlM3VzK0JxnEgJK0vSGRaV8/qZjUJI+YCsgoXGi4YMBaZ1RsKDqiSc6iLeNpjvaoJOrguUkQoGBlHcW2Ur2KKLqEqBrhuUTquBLnsAeoX4KqdWzDQ/7uiQHUorYTyIK0vD4PiZlKO+8CRNSIQKD5ObXu+4guWkyAAXVzCKHav1qXMWwYwpTy+CQAAeyy7lpACcAAMACkOPKKQJLqk9aHFsityXsmYRdTy3EorxuB7Og258k+SNSM2zTM3+m67R4cjaf4aDMHBKHup6Ku2uOlo3kWyDrAxBwrdI976MY4BQGQ9D7WgeCEKQ5COb9STsFwvD8MIZ2SCs+tRcoqjqFoOiuyYUBwKWxE4AQxBkMoND0EwO5zFwVCbA4TguIMGlx2omjaLoYCGG7pgGIgFAMAA9Pqszt+sJB9Ou7ffe3BfJKkwIOsPl5omAltXRgk9XjPl3j1kYAfWvroUAv0+zyvGBrzdG8ME02IF+R5BzBoKbGxwBjRPfBgWJAuSNdnAeMvQZf6hX+1GkKiC6lZCwc+7AADkyAFjFDYpIf4mx1QWgdIRA8641LIHiDhHCqklQ1GQLEJAYBEAYS8GAAQeACDZBApAPBiAwAwkBrIAhPwzp0FiL0JUURKhnyyOpNgYt9KUJcoyEkZBBQMBQrQWQtJzgMCynBIgss9ZkMqEwt8KBrIwXRCiWgCxuC0BeFgAgUxIh8EIgjB6CCshIPkN9VBdVYgCFwPvQhrDjLYMoeaEmkQmYCHwEzJ4EFVGuNVCsUmVxZCXmnI+Ms0DECXDQCGM8BiUSkK5JUQxRBjH2HtrgAU0dai9zIFQkozgsIhhwlcRAsBWFSViMUigpSSCsKUJWEiKFKiylqI2Oqp0xALhuuAqhuAgokEarQVhgRsCmMgAAZhJP4OJYjFrvTxASZAgjxAyNqSUkMrCywKKlkzT66jopxhSeQmKCoqDri3KuVcBR1CIN8LEts2TGKcUrGIoEkB0GYKCSTW4siiw4kakkjw2iKx6PWFgWs2ASQZiOMfI6lRZhQnHks68TDen0A+hcyp+jK5n2cC9DFp4BycgGXgpQcxKYUFYa8xC7pGIfQGRo6lZQ3QyEHLcaG/laa0l8NEBgwwCDMAGDi9Qr5YEtVEA0Qa6ZZyYrzmSpcuCUhKDKOQMZaiAZUitFQusPhWFfDRpQNCHgOksj4F8PFmx4UyqRRgXKtAhDCuJeYh5lRSY9OtgTHlAB5XGPg4Uoj/m1NR8xFjAXgR6/lgKtKGXVExfARA0hqLquC3R+L0k9CkseWYmiSWR04Qi2S0bLSkwzMKlgyrNBGHMJYXIBVc4OhVnGJQrjHItr1VrISHC+DcGwD6VN7AHnSCMAAOW4fs0NKw0AlG2H8LIXYLzPm4XMAZAAJaAABZAAMlLZ8VwaBuXKngVCFMSibXjRgeQPbTh9s8EOmRI6hn2AgnxZ2d8H4GAgGAIwrcO5d3nr3fu3BB4DxHikOe28ahL1FHvGDJA4N2j3gfKcR8T6IeQ1bB0aHOQYcoBoLhF9NCIAYKqW+99oiPwba/f2ucohf2cPIX+rUx0PhDWx6sBQ1gkE2D8vw3gfzOFIOdFR7A7YMSwcEiBPwP4tCoURqesHd4Tjw7UAjFAwDBiQ2Wap91YgaHwYQpBJClHZAUZ0PqKpw0LFoEsDw38U13JdS5N8xFP7k1EmEKg6iBr8FBYhEMDqBlhQkqhETBxmmezaVgcqPkBGKodNpRk7VeWGadthue6ncCabADQQ5uzjkeEM8ZohZnUkUNLKkcQViPIFEKfrG2WlYj6iZmAe2tBkRGtaJzShsQKzunULIHrkA+v0uRpQE8Lx6tUrutSXz4F/PODBYOfN1JWiYG3LVFknQkZ/nECsSo8XmgAo6VgOhcwuzeroMIpm3BDxuoK7gAZbX6JdcYl8AAjJzfw9w8poAaN1UIJRvBdkG0EHl+QLH8uJGTWY25HDvfcvtQzGbIX7wlcwIr4FoqlZoSZnwFXyG7LWc6L4AAmAArH9/ZNU/NdveClqYlAoiwXiR5hl4EtKkyc7MEkg2xFDPmxF9rnX3IzpViVWqnEvPuTxDsXwUkEDRUHAukHAK4zPcodE55iz5F1po8/JtnaKIKbbaIZiE5kD7TvUqgFA6n2IWpYdgBBhJ3kGnVxgLB551nW+Rg2Rs77p6P+KEcmUJL0Blvfd3tXt+2DoLM+l3m5BqGkUNDL91Gf3NwA53A0Pc+5DyHpBseE4sOqfFBX5ean171GPpQav8Ha+H3ryfJTq6SMaGEpRh+T8X5vwY5/Rw38WPWkl4A8yICaZFE2NucipxOfvFNMcHk3hnBUPHUQfM2CmnQkjlIHEuReNz4wAOuoOjKCvuoThIhAAhczrD9pDMrL0fA+BIgvKk/6dVPsCqQAkEImCD1S2S8BqFJ0/CUFoH2QADEshcBche4EY2AHVvhPoaJDlPYoh9ZzQ3UtkGlyklQqlUDCIsheQNE6pmYkBzp8CdlgU7IKBYdZBcoT838P8dgsB1kSskAbBRB6EcJFUWF3Eug2AIkKRDwehYhLsIhZABDTxoBtwSAIlqkmxwY7MHMNsfN3RZJ/RpDuwi0sBaJtRIAAB1KyVYcVMnUlP0KhD6OQ5hWgAACgAEpYgSRqFeDlgrtZDBCtVUBOQFNYh5hgg3Do9EImZqCtpFCxDUBjIdgRwRd/RDoqdGF4ERClCz1/D3x716A50IYGw0hGIPC+DvD7CsUICbpAjikwDGl9kcVvVksjCaZ1RuRggVZXx1BulfCa0whhlIBGo0p0BOUAJM861+8TdrdzctFLdnBJjbdY8cj+AE8ncX1U8jAoBocblz8qgKwr9ZAuApCdg9EMAuwkBb8kEH9KtIAABeNRc/YAHxdgzAPQRw6omoVwtNP5KhRIK8SdZvcUKwG6KwTeRITvdgIrKhPPIDQvUDcDMDUvaDTLSvCeJEmvcUHLTTJvFDFvdDNvQjYjbUYSWILPdY3PNufPbuEDYvCDJIKDBDVE/4lE5TJDZE1eZreREYfEZYDQWJUQbvRAXvajfvOjHOQOYfcuMfFqY0N3R8ZyUIBwCVLYAFBIZkv47E8UAQrkIsAQigLkxpBUJUcImgEiKIelfKQqHoQ6OUgiHBShNCAFKkVzXAdzfZWIUTTU28EFG4I1EIZAWfAAygDaAiAUFCAQEgSkfEW4LgTo/wBWRg1sXJUCDiFAa0OkbQK4H0DwGpACZYGA2oAUCAt4kgEXHM2dVgrdaAKwPwCZbGQEIpW4XM/M4g5cPqVbWJRU9IRM6Sd8V0zkD0osQEqcH0lyO2VncIh7E0zQ3JRFZEWMFEcGSgWNBwCOP4bAXRVLBQYcM6Lsc7Cwyo/w50KSZadofwGmQAkyDwKgkckXWc0IXc/0WIJgbgWQQczkFQqCVRC4BYFGW7OqHFfUBMWIisTcRJL4OMdktRG4HEHCbcDGZwOHWIPs2oW8HCD5dUh0Yc1yXuJsERICKIL4daKPW87pQdEQVc9c5yE9Vg3kz5OeO2cw48rcs8oAy8iI68yhYitWGCU2KhJ8l8m6FxYClYdQZAf8tccNZkFEVkgxPov+cgMAnEbdWYAFcCIgWkOM1kb3bCxzKkJkAiIiEApCrUnkV82ofTegRC2ofsnkVC0QdCrIUbIaUyLMww+7JPTo5EaEXIlieKGmRw8iL0H0FjCKR5ZwpUvgR8isfiocpYt9c4ZiWaGbB7YJXI1VE0jVIQh0nowQMiyJFEa00mN0qy5CosL0/AOlQ0pgLcrFBTbBCCOqaoc+ToKEGEGQvwagLpWcrAQ42JCiaGHxCRQzXlK030jWZ+NfZiClYpJoXIRASRBgcywZRoUbcCfKGXOa3aWABYIoYYSACsqs9IAyqSQi+QcCygPUgZTshAa9T+X1UkCijwUaNIHESdKU/+BUMc/UE5SSjFXUtIfYQ9eSOYD8ESn4EoAXE406n6qWFyPmXyFy+ge0vgQ6p5d9DStPGG4Se8etY3CGSYsC6YjtOY60O3B9R3JPZ3cINYn9SAV6yXBTUm+PR9Cml9eQdtK3fzbSiLJQAAbgZjerDRWxPXPGxljkoVDxFHPUj3kE5P+titZrfTUo/XahJJzz/RbnJOhKpIHhL1pLLyrwZPsvnkNpwzZJKplllu5J7xJOFMHzFPsBH2Yz1Qnw41+r1KlSA3RmsgDHDUXMImDJRF/mZLDyNrQNvDeG52LAfLv3DLpEyAoBwlIrOlYT/FRE6AGOEVpCHS0kNjECjnuvYkoqkiuqBXS3dPNqIDKtYV4XKnoEVDVDY3oARGrU4nnx6H2sgAAHEsJ8JYz2hUhr1w5OSVAehn8UQO7Gq+rarUJOlFBqy4yB6o7ywBBh7MznbGUogVTfi0SHRrLK7HpyqHhQgOjXI1zC7g8vYQrLEfB4QGyyyERpBGIO7tgclYB9lLbbII17NPQ9zJBaoa656vhRNYrpdj04xYgY6Iz47E6VzcBWFSZlpmSYYSsjLbxTK4GZ6+EpUzYSAIoM4KxxR/bIrnz0GU7ZjeE3R7oz6NyMwN7P4K7ILWh9Z5krh9cDod7i1MB5LJN3IxZOhm6kRlkpwUydifzaARdqHKKzrSUcUBi01aHcleguV+IYt0U9roBKzrB6pe6ayXJ7wYL4r4LmDITirjKiBbLaK94KqG7jRNCcGIpVhI7JCfi0Q1TTaMA96LGjbFq2wCHHleKorSHgVQhJGfwQ06GQ73G3ruHK4SAGJmgWGht0V7h6Vtp+lkAALYrJG3UaKUNomfgw6UImgElK4X8PA5GAVWVRRKA5kpwV99CaqcUvhOYbzzCagSJ6mO6rBtHqzIdYbUn8rFsAI2BP40KonnAqBZB7wA1lAg06p6bzRqz3RxB1YS6bqk1DqUtj1QnW0FU/rgIAH9JM6HAwollHg/p4pyQ6F87QnRxGTKh2SeHT07ztBzpSYJ6yI+qEJZQ6ZtIMYnl5qUJ6aCa9ZtDPZsajdG08b/MQW2IZjTdvb5iF97dliWaU92MoBt1Z7P40bFZbmDjy6zGyrHCnZRkuA6xmAwyKBnCuA/UV7zqR6SBgAN0uRuBeDerrImWXGag3G5496yq9A9BWFAAkwioUJc9IPpJeZLJbWEcCpcUjvrzNfpuK2FMRIBpcgDpdXsiGAG5ZIF5b3n5YPsgAAB9SR0r1shXPiKlcE9WDWJxPHzr/qITELNaC9tawNdbWA6Ty8TbETmSstUN2SeSnWrbEBiTMXsXFb1K8WHraADjUGBybopWrwZWKX5Wa1ApKw025XKANWJB8AYIRWTHcA970GU20Qc3KWanM3hkq2M2+L0GuA7WOGPGK70HTXzX1V1t83C2/DDQbXvjVTW3HX9n9SDhXXAN3Wi8daaTvX9amTF5pKsT3HGEzGQ2x3+SI3IAsWsHVKY2mx8WS3PGxm54K2ah62a3pKT27L3GBTvlh37m22zGvH3GABtAAXV7aLcgFFcstLYrtfbPdJfjdlerYoESqfZvcsYnGbcfaNug6NsFkCZujg+3qg/bcqLNbVTW01W/f7ek1tfg/cdHb1JdahOndhK9dHn9aXdbZXey2DY/q3dVt/X/TdcpJnc9bnZo/pIDeXb9b3g3b1P5MFMhZFPfiVSYx/nHy40nyKtwGJY+MObTS3tcZHdDf1Prqlspk0LbqBsikZTaXlWFFbcYcAktEns5ZxEHMjMJGMZ2HlBRsCNEyU6NTC2mBjHLI0arO7p0b6d7FaBfvlDjGksYa2ERC6N1MI2fhKHyPhWNGDUcyjaelhunNlREeWgias86EcMfLoftfFCrrCqvOEj5rrIRhJBjNqH7vwfpd1MZb1QUaOFIg5c6CShazTBcKGsamtCKADqiBy5NGVTmCq7qCm1uGYE6Z867p7t6baMknuiurCY8CG/usqGuYcY69FhRCCLXCfz/GoABTDJGPnv7uB1li+gLZglk0rHk3IFwHWFuFkn8AC/+StVnCNlM6ffC8LrQHvGhxjSsSS8gBsCZlSlViElHMPbjYUycrxzTEWr2+4BWrXORYOj4wDpQieoYCP1YN4D/uPXZS8AsuW9oEK4dCrqa76NiBZdwDZc+c5d1eI7ngFc7YNS8GscgPwtaBcm5Xmey8Z5qHvFetEmaBx+EWHABUFFoDkQF2RvgIWF8GmzgS0s+t0ohfGOha7VhfZtmP8yRbj1ivJuHXRbdw2Ia2xTCkAKJ83BJ94oK9bbKq4Dp4Z7a65ZZ73jZ7NY54hK+LU55Y07HfI44+Ay47hMnnndo53no8E4nGE/+pY8xYvRAswdrujY/RANc8lY+MdkHYD/1aD7I9U4o846o5459YNv49j+r6fYT7DfgcRFvkgCoViCCOGTIzCgigMBb5N5kWz5uBcNpfq8AMzOZdZfZaao9/Q6NoFcMBb7b+JJb9hcQC+4z40t6DPXpXNF+iM60hx+1QoLjBU4GqmdY7JKnbL+pPhO5UQ3bkmz+r4ywwf/xCf8yxf83HWA72n00Gvi8DE9tr0Z7a0nSUi7WPxlp/GPoKMLJG0479EkuOAtGnXRDdBQWVqVoKeSUAmJWCBJGmLEGABCAEAYAOPuiTrybwrWu5Syj2EZZlsbo4bC5MEWRBTMZwQFMEjTGuZSBTQ/gWyOfnvaxA0ycFTMnqxoF1NWEPiNVIEW9TUDMOIg9wjdloBCDpBb5N/OIK+CUCHCwgt8okTYKf5uqZxe/I/jQLm5WywTeGK0BPoZwmioQAbBgHQZlE84dKOUIxFQAy9MYpTXbnYV8KOEAAJJYOU7YtLu3GGHBcgWTYFlGiA0rkCyyAw5/aGAwMlpFRz4DCBxA3DB/z4zkDHBcLFpPwjSQhpg6AxTXg2gmIwtoaevBFjbhJoLEUWzNU3lTQxa00p05/dWqXzD7l8b+/gO/qkPWDP8fIr/Loe/x6Gf9v+a6UjB8lE421aMdteTCAOdpydXamNB2oqUKp6sZmj/dYKCR/5c9mYxpKAnD12jDQPAUGXsKNVJhOxImc8QpjyBJgpA8619YbMClXSL5wqQ7GfiR0A6adQiBfCnlkB1JkcRc/gI9FEFWIrAuacAxStAO7L+hSYYXCCh9AQgQVZgNwEXGFwRGtBtOahSNHQDpzYxOQa5A0m8Fh5fBZguVARM+QCxyROg0IzTghHJjHxiU/YQ5irD7qMEyY4Iv7rFSKBgAKwKtd3JjzlLIATqR9dblgARy1YCwAALw/B5ceQfqDACCnUCxBnCIPHAZFFuCtVTiNWPcOKJWDSVaEcmJVPtivyp5IAeXSQZmQ0EQgkecg80XAzCq7kfE7keEcaykjIinRTYfgRmUiBH57khDXwHyOlQzlXoDTc6P6BxQ7MuiMjSosXSQT1MP6CmAImk2Eb0oqqrVbMFQAoYdAquew0yChBcFup0gnQDpG6MYBVp7gjhMUZQHwBhVLCQY38j8yc6f5yQ0jBwjWiPwihOq2MQikaOIp1kzqZjcLtVgeR1YcOGVcRvM2ATDC2I+AbUVAhFGaj4A5YmtAhBSToA4uzQCCnGBRGUIUabqWcYRHnEfgVOWsdyjIRB6wjEsEY4RmGQLirYRxyDToAsx4zeR+YHgKEWZzEq0hSAqiA/ueFyGsDboaiYyNmK0i7jxRH4KSGSF9pUAbhWAelPUSSxZAZQDEMQoRAhgoQIhi0bGAfwBRi8Fef4pRlA1uAFDcazaM3Lr3hbE1xyRvB3InhqGjpze9QgWj+DMGVCyaNE5POEDZrkTOa6vXmj7lOqi0M8kBGNJzgjxG0nmP4krMsIGF8Z1hwwzCgsJoA6gqMpJJoaHxhLX9J47Qp2Pf2kl9CA2nQ7oU6z4xDCu81tZSYANFJTDHaMnRiTKV25SSjJaw8cSRk2GYFJyuwmgA5FqinDKRZjTnLCO4hPdqKVI9wi2ww4vt3h/WMKQh3eG5RVCGAXKjzy2BNk7SmAPGGcNQz/D5Mf3HEPVHPh+AIkHgB7GmNqDNB6a57cpmlEVFji8JSoealtSyCXhEAXYYLkcBEi8MQ0J5dSL5POibipIQA/2i6KgqatdYb+dyGyKlDoA3RgBAQTlBQBHJBxWotUM5PUhcFK464lEAWFhr7RTRlpN8bQPFqlTnoi9eZl1O8I1irel49hF7ECytTGMDDFEQD1YIqc8EGovceWOinuMAAaixToGTS1BYgKQWY3QZ0CBG7DH7n1Puj/SrRigsymwhqA3TwxzY2RmlDhSGxzo5YhYEMVWBCo+e9wCCo4X65wTTwzhVsThLT5z1YgHg08KwhcErAIJDRSoN7AykfgcU+sbJmowgo2EzqY7ShBzJ26PUowRDGGcDIEroAPQtwTSBkjP5zDlRLKB4cJBJClDrcIuLipgOh4HCCRrQeHogMOg6ispSqIEZznAafS546DYEg3goCySXJAQ32l5MtAeIlmeddWLmPiF9d9pwjNkeBJGCQTlmyWelGFzPGvIJJSA/gLpIejPYRezEyIvv2TSpoAUlABYHwFdnFhtOf2SbEzRQms5aAfNBLu9SFq4EVmCNJUrjI4RYBA5lRfUB+IM5ESoWJE72mRKJoG8KhyLViSsTN7rFn4lvHYjQEaaX5TgXY1oAXxWG9DrZ4JZvroBMZUCzRsMnvA+xeF8tZ5n7JfpPIBl5xrRAAfjQ7qdwpaDASj31Xkoit588nebPwPoryoA/7CVjcGPmmzDWFdKugfMvlOwvhz7FCpp23mB9d52pd4U/ICYLz75Y7T+YXww7B9O5mxK3oT2S5YNVBDk1YWPLmA5A/5r0xafOJIB3yJwP0i8ogCH4AE+2xJGmhAuqEyIVOsC5kiPM/4IK4GE8y+dTIcL+UboWbEZKB3TZ5suA13MZI0PY6X8WhGkqqiQA6G6SsMjzduDqJEViKK67UZUVfGdL/9xhz8CTkPgdoSkZh0pSfDOkgLVhWCswbYmoFaQtY32egi4uZg/bXFogRZOIFMRKyJD4ARA2vkbQxJ4kKAVrZUfIykpmdHm6jPdAejiSKTmBGBDcELmuwQg7ERinwJcRJw7EDsUNVVjUQbCFJYhpzAWT0ScUvVWg5MrCQrzmBK8tgkYOfNWD9xYpa5RQnXiUK4ldpDeixaie3NqFu4PcJALhRrR4XqTZ2bQgRdpIMmZZxFrbNduHVEU9LHmJk7UDRTGHmSJhA0qTtZNAGzDZS2I6CET2VKvy96iAKhVz2PDLNuUmhRJsBB6g6Qf6qODBRqVnkuJB2uyzALVktCwSrgBwHKv7gLB40wCTwmkbJACm8pnl3wXmS9PFalVz5eIxiVspq7Mjj+4Ix0TcHDjEiWq25eQLsqOJ6p1gVyeoMBKOQ+1aeE/IXrUV5TWkI8QmNAriO06rUNw7KegPrEhWwhoVCsvMStJph5RoBwOSqFkLIBiIVgjhUlUXM5whl7eyoxAGFRsKxBt00AtKPvmdn6kak/K4+Ksu1Q0BdU+0bblQgf5hQlAqy6dEghc6mNr5h9EOSBP3FORsY/pLWGdAziFzIB7+HQVNI/LqQgirZIagIUDSY0NZBdeTF8CZxbIZqG1Bav1g2QNBuuCmOmUMXqnbUmpaiVBRKMtAGiKAcILEbDUKp0Lyify0SUSoUwCjiKPM92i9MTYmUxZsRJBKaSkxNi+5wjW5X8ENJtgRMvlPKuiGxiFU/GWQeII3S54EqCwCa5hlVHxBqMFyjBf2pDmAJSR2c8gHHuV1CB6r7sBqn2tuBGZ7hj0znVQemvMansrGVFPOaQFGa3tzhUkOMKlwvxThuIo1YigKMSXGcUlm8EkSur3gEQKA/CIdYEu6DQq3pWoqfBOLWknd+e+6mWJisrXKlQmQHPeCcEmZ35ZAr87dPtz+VxFMAiUBMM2vJitql65rcgObCdp6KpZW4pBG6hU6srfc+RCCq9O9I4gVhSCJUd7huC4yyRDajaChGeUIR5835U2G6oak7VPg2MFOSLg7SkatI+qwGQb04Kad9Kjy4ulSvlJ3UtVIah0MUu16kSylTcipS3KomotaJruTuXTW9xfBGaxvNiZTWGxwsJNZuLml9Vsglh6NUaumEpO/RscmlFJXha0s0ntLmSOkxycIskX9Kn2vS22A5ocWDLpFZkvvOMssmTKVFrGNRbLL43VlINs6HwHCzEG7RseccmRFJD6Cv0D1nJZoGGINlJSepKEM8YZFaQoxJkvgaEXOutzQU8tN9EHhMw5wcVoiZ6OmRHSSlmxHA6+Sdd7h7VyYZa51MqdGONFXztSeWgaNAHwB5TIwVID4efnQYKFRCeAQbXgC/XW4RtGRG0VNNWyRA84rpGNfYIpmjgk0QoZGg2Jtx8BJp/ocQKNtCDLix1u4fcPIBRq4IZ1pDMkSmrloDNC0+agIoZFchox0yaWAxnBXDXGMBsV9DALWtsasJ/Sx2qEKdu40Xa1VnWk9ROHgaa5OMtjewF1uSy5o6mSMh7eTlaDDj1stTSDW6jBB2cZA1AEMlpDYB/wkAzAfZIJo/DfjdBt6tBVgraIQFHSwqJKRAOvpdhQm4M0Orkzvbwyy58Sy1Cjv1H7k1QJY40ZzDCoO4RwuHX8rKsjxDgeEa4asBLPPVFgI1ZdcHTyCp7OcJ2Na6VrQFyAzSPR+pf0slVtIPlgi+pE8DmEoRsbXIvY3qcazQ3yBHInVQUIf2igPlT+KdfuUMhxAwFZgwPOxNKIwBpQQgCwEbExEBCiBZAcRCkO/lkj9dIwG21SPEWIinVytrkTXB5w+BhEshKEBLXwB2a5RTQIMZLiTsITIAIyesUqBdhbWYjDcWveubs1WyabEWUmqpTJvYl0TO5u7dPvu0z7XDG6m/Q7bHUjLI1G6BxZbXQAYVTgmFl7aln73z5LLJFEqids0JaXcc2lgi2zV0vs0SK/JLm1dm5r41J8d2UbPvRvwH1w7lxYZAiWPtsYHFq1v2xuo4S51zxgFS+l9gjqyC58B2biWIB/vDor7uqa+j1hH34Vb7Vhdm/fXvr6UwHbYQyy+ESRpo96565+vFptSv1nob9cdAFDOlA7WC9WoyfXemUZbeq8+f+gA7bCAOQk1JoB3WlpOs2dKA23SxzSwdc2SKEDmgJA5Gz3a4tD2GBoUMSqwMj746/yg4kHpD29R8AsgMg7/tBj/6NOfk6g5OzM3r6wDt/DpUIp33QGBlu+3Q35M4MsdlJOeRuCnGdxexUyvsSYUqhHghx2qpcKZQSgzxUB44tcJOA3AMBmGR46gAAPo3cfDKyPjHQB8OCJTg9cUw+7EYDswqcVOdmF9miPTIAA7AABYBA0yEoBTmSMAAOEoOzFoDsxEjX2L7CUCpwU5aAGRgAGycw0AFRgQOzD6A1HwjTcKAN4dwB+H9IAR2iJ/2COexwjQAA= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=ls1intum/Artemis&utm_content=10850):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` or `@coderabbitai title` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-19T08:39:46Z
body: ### End-to-End (E2E) Test Results Summary

<table><tr><th><th>Tests</th><th>Passed ✅</th><th>Skipped ⚠️</th><th>Failed</th><th>Time ⏱</th></tr><tr><td>End-to-End (E2E) Test Report</td><td>201 ran</td><td>198 passed</td><td>3 skipped</td><td>0 failed</td><td>51m 26s 214ms</td></tr></table> <table><tr><th>Test</th><th>Result</th><th>Time ⏱</th></tr><tr><td colspan="3">No test annotations available</td></tr></table>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-20T10:41:04Z
body: ### End-to-End (E2E) Test Results Summary

<table><tr><th><th>Tests</th><th>Passed ☑️</th><th>Skipped ⚠️</th><th>Failed ❌️</th><th>Time ⏱</th></tr><tr><td>End-to-End (E2E) Test Report</td><td>201 ran</td><td>197 passed</td><td>3 skipped</td><td>1 failed</td><td>50m 48s 113ms</td></tr></table> <table><tr><th>Test</th><th>Result</th><th>Time ⏱</th></tr><tr><td colspan="3"><strong>End-to-End (E2E) Test Report</strong></td></tr><tr><td colspan="3"><em>e2e/exercise/quiz-exercise/QuizExerciseParticipation.spec.ts</em></td></tr><tr><td>ts.Quiz Exercise Participation › DnD Quiz participation › Student can participate in DnD Quiz</td><td>❌ failure</td><td>2m 4s 46ms</td></tr></table>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-24T11:20:06Z
body: ### End-to-End (E2E) Test Results Summary

<table><tr><th><th>Tests</th><th>Passed ☑️</th><th>Skipped ⚠️</th><th>Failed ❌️</th><th>Time ⏱</th></tr><tr><td>End-to-End (E2E) Test Report</td><td>201 ran</td><td>197 passed</td><td>3 skipped</td><td>1 failed</td><td>58m 43s 224ms</td></tr></table> <table><tr><th>Test</th><th>Result</th><th>Time ⏱</th></tr><tr><td colspan="3"><strong>End-to-End (E2E) Test Report</strong></td></tr><tr><td colspan="3"><em>e2e/exercise/programming/ProgrammingExerciseStaticCodeAnalysis.spec.ts</em></td></tr><tr><td>ts.Static code analysis tests › Configures SCA grading and makes a successful submission with SCA errors</td><td>❌ failure</td><td>1m 58s 768ms</td></tr></table>
author_association: NONE