From 6e5e558ce34528a14ff79a5e0be2dc29d1eba914 Mon Sep 17 00:00:00 2001 From: Patrik Zander <38403547+pzdr7@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:39:43 +0200 Subject: [PATCH 01/14] Development: Update monaco-editor to 0.52.0 (#9431) --- angular.json | 2 +- package-lock.json | 9 +++--- package.json | 2 +- prebuild.mjs | 30 ++++++++++++++++--- .../webapp/app/core/config/monaco.config.ts | 26 +++++++++------- .../adapter/monaco-text-editor.adapter.ts | 4 +-- .../actions/adapter/text-editor.interface.ts | 5 ++-- .../communication/channel-reference.action.ts | 5 ++-- .../exercise-reference.action.ts | 7 +++-- .../communication/user-mention.action.ts | 7 +++-- .../model/actions/text-editor-action.model.ts | 9 ------ 11 files changed, 62 insertions(+), 44 deletions(-) diff --git a/angular.json b/angular.json index e5543ff2ce60..008ac75d13bf 100644 --- a/angular.json +++ b/angular.json @@ -113,7 +113,7 @@ }, { "glob": "**/*", - "input": "./node_modules/monaco-editor/min/vs", + "input": "./node_modules/monaco-editor/bundles/vs", "output": "vs" } ], diff --git a/package-lock.json b/package-lock.json index c72840ee901d..4b7bc1437ba6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "jszip": "3.10.1", "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", - "monaco-editor": "0.51.0", + "monaco-editor": "0.52.0", "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", @@ -16958,10 +16958,9 @@ "license": "MIT" }, "node_modules/monaco-editor": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.51.0.tgz", - "integrity": "sha512-xaGwVV1fq343cM7aOYB6lVE4Ugf0UyimdD/x5PWcWBMKENwectaEu77FAN7c5sFiyumqeJdX1RPTh1ocioyDjw==", - "license": "MIT" + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==" }, "node_modules/moo-color": { "version": "1.0.3", diff --git a/package.json b/package.json index dfe192b8d21b..b1063f9a90e0 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "jszip": "3.10.1", "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", - "monaco-editor": "0.51.0", + "monaco-editor": "0.52.0", "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", diff --git a/prebuild.mjs b/prebuild.mjs index 7ef783be432c..5f362babe3fc 100644 --- a/prebuild.mjs +++ b/prebuild.mjs @@ -5,10 +5,11 @@ * - webpack.DefinePlugin and * - MergeJsonWebpackPlugin */ -import fs from "fs"; -import path from "path"; -import { hashElement } from "folder-hash"; -import { fileURLToPath } from "url"; +import fs from 'fs'; +import path from 'path'; +import { hashElement } from 'folder-hash'; +import { fileURLToPath } from 'url'; +import * as esbuild from 'esbuild'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -111,4 +112,25 @@ for (const group of groups) { } } +/* + * The workers of the monaco editor must be bundled separately. + * Specialized workers are available in the vs/esm/language/ directory. + * Be sure to modify the MonacoConfig if you choose to add a worker here. + * For more details, refer to https://github.com/microsoft/monaco-editor/blob/main/samples/browser-esm-esbuild/build.js + */ +const workerEntryPoints = [ + 'vs/language/json/json.worker.js', + 'vs/language/css/css.worker.js', + 'vs/language/html/html.worker.js', + 'vs/language/typescript/ts.worker.js', + 'vs/editor/editor.worker.js' +]; +await esbuild.build({ + entryPoints: workerEntryPoints.map((entry) => `node_modules/monaco-editor/esm/${entry}`), + bundle: true, + format: 'esm', + outbase: 'node_modules/monaco-editor/esm', + outdir: 'node_modules/monaco-editor/bundles' +}); + console.log("Pre-Build complete!"); diff --git a/src/main/webapp/app/core/config/monaco.config.ts b/src/main/webapp/app/core/config/monaco.config.ts index aa40e47c177c..f37dfe5a4069 100644 --- a/src/main/webapp/app/core/config/monaco.config.ts +++ b/src/main/webapp/app/core/config/monaco.config.ts @@ -1,19 +1,23 @@ /** * Sets up the MonacoEnvironment for the monaco editor's service worker. + * See https://github.com/microsoft/monaco-editor/blob/main/samples/browser-esm-esbuild/index.js */ export function MonacoConfig() { self.MonacoEnvironment = { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getWorkerUrl: function (workerId: string, label: string) { - /* - * This is the AMD-based service worker, which comes bundled with a few special workers for selected languages. - * (e.g.: javascript, typescript, html, css) - * - * It is also possible to use an ESM-based approach, which requires a little more setup and case distinctions in this method. - * At the moment, it seems that the ESM-based approaches are incompatible with the Artemis client, as they would require custom builders. - * Support for custom builders was removed in #6546. - */ - return 'vs/base/worker/workerMain.js'; + getWorkerUrl: (_moduleId: string, label: string): string => { + if (label === 'json') { + return './vs/language/json/json.worker.js'; + } + if (label === 'css' || label === 'scss' || label === 'less') { + return './vs/language/css/css.worker.js'; + } + if (label === 'html' || label === 'handlebars' || label === 'razor') { + return './vs/language/html/html.worker.js'; + } + if (label === 'typescript' || label === 'javascript') { + return './vs/language/typescript/ts.worker.js'; + } + return './vs/editor/editor.worker.js'; }, }; } diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/monaco-text-editor.adapter.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/monaco-text-editor.adapter.ts index 7bd630b7d479..f55bbfe3e3f1 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/monaco-text-editor.adapter.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/monaco-text-editor.adapter.ts @@ -152,8 +152,8 @@ export class MonacoTextEditorAdapter implements TextEditor { return this.editor.getDomNode() ?? undefined; } - typeText(text: string) { - this.editor.trigger('MonacoTextEditorAdapter::typeText', 'type', { text }); + triggerCompletion(): void { + this.editor.trigger('MonacoTextEditorAdapter::triggerCompletion', 'editor.action.triggerSuggest', {}); } getTextAtRange(range: TextEditorRange): string { diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/text-editor.interface.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/text-editor.interface.ts index b3403c274b64..b847645a2efd 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/text-editor.interface.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/adapter/text-editor.interface.ts @@ -42,10 +42,9 @@ export interface TextEditor { getDomNode(): HTMLElement | undefined; /** - * Types the given text into the editor as if the user had typed it, e.g. to trigger a completer registered in the editor. - * @param text The text to type into the editor. + * Triggers the completion in the editor, e.g. by showing a widget. */ - typeText(text: string): void; + triggerCompletion(): void; /** * Retrieves the text at the given range in the editor. diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/channel-reference.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/channel-reference.action.ts index f3bc053b43da..5afefaf5275d 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/channel-reference.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/channel-reference.action.ts @@ -44,11 +44,12 @@ export class ChannelReferenceAction extends TextEditorAction { } /** - * Types the text '#' into the editor and focuses it. This will trigger the completion provider to show the available channels. + * Inserts the text '#' into the editor and focuses it. This method will trigger the completion provider to show the available channels. * @param editor The editor to type the text into. */ run(editor: TextEditor) { - this.typeText(editor, ChannelReferenceAction.DEFAULT_INSERT_TEXT); + this.replaceTextAtCurrentSelection(editor, ChannelReferenceAction.DEFAULT_INSERT_TEXT); + editor.triggerCompletion(); editor.focus(); } diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/exercise-reference.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/exercise-reference.action.ts index 84fe668e955e..44b72e0f8724 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/exercise-reference.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/exercise-reference.action.ts @@ -52,11 +52,12 @@ export class ExerciseReferenceAction extends TextEditorDomainActionWithOptions { } /** - * Types the text '/exercise' into the editor and focuses it. This will trigger the completion provider to show the available exercises. - * @param editor The editor to type the text into. + * Inserts the text '/exercise' into the editor and focuses it. This method will trigger the completion provider to show the available exercises. + * @param editor The editor to insert the text into. */ run(editor: TextEditor): void { - this.typeText(editor, ExerciseReferenceAction.DEFAULT_INSERT_TEXT); + this.replaceTextAtCurrentSelection(editor, ExerciseReferenceAction.DEFAULT_INSERT_TEXT); + editor.triggerCompletion(); editor.focus(); } diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/user-mention.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/user-mention.action.ts index e6fa9b208397..240805b4adda 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/communication/user-mention.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/communication/user-mention.action.ts @@ -45,11 +45,12 @@ export class UserMentionAction extends TextEditorAction { } /** - * Types the text '@' into the editor and focuses it. This will trigger the completion provider to show the available users. - * @param editor The editor to type the text into. + * Inserts the text '@' into the editor and focuses it. This method will trigger the completion provider to show the available users. + * @param editor The editor to insert the text into. */ run(editor: TextEditor) { - this.typeText(editor, UserMentionAction.DEFAULT_INSERT_TEXT); + this.replaceTextAtCurrentSelection(editor, UserMentionAction.DEFAULT_INSERT_TEXT); + editor.triggerCompletion(); editor.focus(); } diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/text-editor-action.model.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/text-editor-action.model.ts index 96a8e6549b9c..62e037868ce0 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/text-editor-action.model.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/text-editor-action.model.ts @@ -148,15 +148,6 @@ export abstract class TextEditorAction implements Disposable { return text.startsWith(openDelimiter) && text.endsWith(closeDelimiter) && text.length >= openDelimiter.length + closeDelimiter.length; } - /** - * Types the given text in the editor at the current cursor position. You can use this e.g. to trigger a suggestion. - * @param editor The editor to type the text in. - * @param text The text to type. - */ - typeText(editor: TextEditor, text: string): void { - editor.typeText(text); - } - /** * Replaces the text at the current selection with the given text. If there is no selection, the text is inserted at the current cursor position. * @param editor The editor to replace the text in. From 51a4ff0b602b49151f458e652437d166096d7525 Mon Sep 17 00:00:00 2001 From: Paul Rangger <48455539+PaRangger@users.noreply.github.com> Date: Sat, 12 Oct 2024 00:02:28 +0200 Subject: [PATCH 02/14] Communication: Add profile pictures to channel member overview (#9450) --- .../artemis/core/dto/UserPublicInfoDTO.java | 14 +++++++++++++- src/main/webapp/app/core/user/user.model.ts | 1 + .../course-wide-search.component.html | 4 ++-- .../conversation-member-row.component.html | 14 +++++++++++++- .../conversation-member-row.component.scss | 19 +++++++++++++++++++ .../conversation-member-row.component.ts | 12 ++++++++++-- src/main/webapp/i18n/de/metis.json | 4 +++- src/main/webapp/i18n/en/metis.json | 4 +++- .../course-wide-search.component.spec.ts | 4 +++- .../conversation-member-row.component.spec.ts | 6 ++++++ 10 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/UserPublicInfoDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/UserPublicInfoDTO.java index a6da8966dfc5..f84bf9e0819a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/dto/UserPublicInfoDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/UserPublicInfoDTO.java @@ -25,6 +25,8 @@ public class UserPublicInfoDTO { private String lastName; + private String imageUrl; + private Boolean isInstructor; private Boolean isEditor; @@ -43,6 +45,7 @@ public UserPublicInfoDTO(User user) { this.name = user.getName(); this.firstName = user.getFirstName(); this.lastName = user.getLastName(); + this.imageUrl = user.getImageUrl(); } /** @@ -101,6 +104,14 @@ public void setLastName(String lastName) { this.lastName = lastName; } + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + public Boolean getIsInstructor() { return isInstructor; } @@ -152,6 +163,7 @@ public int hashCode() { @Override public String toString() { return "UserPublicInfoDTO{" + "id=" + id + ", login='" + login + '\'' + ", name='" + name + '\'' + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' - + ", isInstructor=" + isInstructor + ", isEditor=" + isEditor + ", isTeachingAssistant=" + isTeachingAssistant + ", isStudent=" + isStudent + '}'; + + ", imageUrl='" + imageUrl + '\'' + ", isInstructor=" + isInstructor + ", isEditor=" + isEditor + ", isTeachingAssistant=" + isTeachingAssistant + ", isStudent=" + + isStudent + '}'; } } diff --git a/src/main/webapp/app/core/user/user.model.ts b/src/main/webapp/app/core/user/user.model.ts index 816cf4fc9a9c..b55ff839fd54 100644 --- a/src/main/webapp/app/core/user/user.model.ts +++ b/src/main/webapp/app/core/user/user.model.ts @@ -66,6 +66,7 @@ export class UserPublicInfoDTO { public firstName?: string; public lastName?: string; public email?: string; + public imageUrl?: string; public isInstructor?: boolean; public isEditor?: boolean; public isTeachingAssistant?: boolean; diff --git a/src/main/webapp/app/overview/course-conversations/course-wide-search/course-wide-search.component.html b/src/main/webapp/app/overview/course-conversations/course-wide-search/course-wide-search.component.html index f4a255b49422..aa3b35159b88 100644 --- a/src/main/webapp/app/overview/course-conversations/course-wide-search/course-wide-search.component.html +++ b/src/main/webapp/app/overview/course-conversations/course-wide-search/course-wide-search.component.html @@ -3,9 +3,9 @@

@if (!courseWideSearchConfig.searchTerm) { - All Messages + } @else { - Search Results for "{{ courseWideSearchConfig.searchTerm }}" + }

diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.html b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.html index a481df162d51..47eff1257e65 100644 --- a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.html +++ b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.html @@ -1,11 +1,23 @@ @if (activeConversation && course) {
- + @if (userImageUrl) { + + } @else { + {{ userInitials }} + } @if (isChannel(activeConversation) && conversationMember?.isChannelModerator) { } {{ userLabel }} + @if (!conversationMember.isStudent) { + + }
@if (canBeRemovedFromConversation || canBeGrantedChannelModeratorRole || canBeRevokedChannelModeratorRole) { diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.scss b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.scss index 28814b8391f5..a2f66745bed6 100644 --- a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.scss +++ b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.scss @@ -1,3 +1,5 @@ +$profile-picture-height: 2rem; + .conversation-member-row { min-height: 3rem; @@ -14,4 +16,21 @@ .dropdown-toggle::after { content: none; } + + .conversation-member-row-default-profile-picture { + font-size: 0.8rem; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .conversation-member-row-profile-picture, + .conversation-member-row-default-profile-picture { + width: $profile-picture-height; + height: $profile-picture-height; + max-width: $profile-picture-height; + max-height: $profile-picture-height; + background-color: var(--gray-400); + color: var(--white); + } } diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts index 5044543d4878..39a712c64424 100644 --- a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts +++ b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, HostBinding, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { faChalkboardTeacher, faEllipsis, faUser, faUserCheck, faUserGear } from '@fortawesome/free-solid-svg-icons'; +import { faEllipsis, faUser, faUserCheck, faUserGear, faUserGraduate } from '@fortawesome/free-solid-svg-icons'; import { User } from 'app/core/user/user.model'; import { ConversationDTO } from 'app/entities/metis/conversation/conversation.model'; import { AccountService } from 'app/core/auth/account.service'; @@ -20,6 +20,8 @@ import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { getAsGroupChatDTO, isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model'; import { GroupChatService } from 'app/shared/metis/conversations/group-chat.service'; import { catchError } from 'rxjs/operators'; +import { getBackgroundColorHue } from 'app/utils/color.utils'; +import { getInitialsFromString } from 'app/utils/text.utils'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector @@ -56,6 +58,9 @@ export class ConversationMemberRowComponent implements OnInit, OnDestroy { canBeRevokedChannelModeratorRole = false; userLabel: string; + userImageUrl: string | undefined; + userDefaultPictureHue: string; + userInitials: string; // icons userIcon: IconProp = faUser; userTooltip = ''; @@ -88,7 +93,10 @@ export class ConversationMemberRowComponent implements OnInit, OnDestroy { this.isCreator = true; } + this.userImageUrl = this.conversationMember.imageUrl; this.userLabel = getUserLabel(this.conversationMember); + this.userInitials = getInitialsFromString(this.conversationMember.name ?? 'NA'); + this.userDefaultPictureHue = getBackgroundColorHue(this.conversationMember.id ? this.conversationMember.id.toString() : 'default'); this.setUserAuthorityIconAndTooltip(); // the creator of a channel can not be removed from the channel this.canBeRemovedFromConversation = !this.isCurrentUser && this.canRemoveUsersFromConversation(this.activeConversation); @@ -242,7 +250,7 @@ export class ConversationMemberRowComponent implements OnInit, OnDestroy { const toolTipTranslationPath = 'artemisApp.metis.userAuthorityTooltips.'; // highest authority is displayed if (this.conversationMember.isInstructor) { - this.userIcon = faChalkboardTeacher; + this.userIcon = faUserGraduate; this.userTooltip = this.translateService.instant(toolTipTranslationPath + 'instructor'); } else if (this.conversationMember.isEditor || this.conversationMember.isTeachingAssistant) { this.userIcon = faUserCheck; diff --git a/src/main/webapp/i18n/de/metis.json b/src/main/webapp/i18n/de/metis.json index 5100a48cd2e4..8a669a9c8c8c 100644 --- a/src/main/webapp/i18n/de/metis.json +++ b/src/main/webapp/i18n/de/metis.json @@ -90,7 +90,9 @@ "TECH_SUPPORT": "Technische Hilfe", "ORGANIZATION": "Organisation", "RANDOM": "Sonstiges", - "ANNOUNCEMENT": "Ankündigung" + "ANNOUNCEMENT": "Ankündigung", + "allPublicMessages": "Alle öffentlichen Nachrichten", + "searchResults": "Suchergebnisse für {{ search }}" }, "post": { "context": "Kontext", diff --git a/src/main/webapp/i18n/en/metis.json b/src/main/webapp/i18n/en/metis.json index bd0cc952c805..9a3ff0977f2b 100644 --- a/src/main/webapp/i18n/en/metis.json +++ b/src/main/webapp/i18n/en/metis.json @@ -90,7 +90,9 @@ "TECH_SUPPORT": "Tech Support", "ORGANIZATION": "Organization", "RANDOM": "Random", - "ANNOUNCEMENT": "Announcement" + "ANNOUNCEMENT": "Announcement", + "allPublicMessages": "All Public Messages", + "searchResults": "Search Results for '{{ search }}'" }, "post": { "context": "Context", diff --git a/src/test/javascript/spec/component/overview/course-conversations/course-wide-search.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/course-wide-search.component.spec.ts index 969c54184708..288ed74a8c86 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/course-wide-search.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/course-wide-search.component.spec.ts @@ -14,11 +14,12 @@ import { BehaviorSubject } from 'rxjs'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { MessageInlineInputComponent } from 'app/shared/metis/message/message-inline-input/message-inline-input.component'; import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; import { PostSortCriterion, SortDirection } from 'app/shared/metis/metis.util'; import { metisExamChannelDTO, metisExerciseChannelDTO, metisGeneralChannelDTO, metisLectureChannelDTO } from '../../../helpers/sample/metis-sample-data'; import { getElement } from '../../../helpers/utils/general.utils'; import { NgbTooltipMocksModule } from '../../../helpers/mocks/directive/ngbTooltipMocks.module'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector @@ -68,6 +69,7 @@ describe('CourseWideSearchComponent', () => { MockComponent(PostingThreadComponent), MockComponent(MessageInlineInputComponent), MockComponent(PostCreateEditModalComponent), + MockDirective(TranslateDirective), ], providers: [MockProvider(MetisConversationService), MockProvider(MetisService), MockProvider(NgbModal)], }).compileComponents(); diff --git a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.spec.ts index e58af8239cc1..133fc64978b9 100644 --- a/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-conversations/dialogs/conversation-detail-dialog/tabs/conversation-members/conversation-member-row/conversation-member-row.component.spec.ts @@ -22,6 +22,7 @@ import { of } from 'rxjs'; import { isGroupChatDTO } from 'app/entities/metis/conversation/group-chat.model'; import { By } from '@angular/platform-browser'; import { NgbDropdownMocksModule } from '../../../../../../../../helpers/mocks/directive/ngbDropdownMocks.module'; +import { getElement } from '../../../../../../../../helpers/utils/general.utils'; const memberTemplate = { id: 1, @@ -167,6 +168,11 @@ examples.forEach((activeConversation) => { } })); + it('should display default profile picture', () => { + fixture.detectChanges(); + expect(getElement(fixture.debugElement, '.conversation-member-row-default-profile-picture')).not.toBeNull(); + }); + function checkGrantModeratorButton(shouldExist: boolean) { const grantModeratorRoleButton = fixture.debugElement.query(By.css('.grant-moderator')); if (shouldExist) { From 61eda321df24f9b5f6360c69c46a2a63cac7cf51 Mon Sep 17 00:00:00 2001 From: Dmytro Polityka <33299157+undernagruzez@users.noreply.github.com> Date: Sat, 12 Oct 2024 09:56:31 +0200 Subject: [PATCH 03/14] Programming exercises: Improve preliminary AI feedback (#9324) --- .../aet/artemis/assessment/domain/Result.java | 2 +- .../aet/artemis/exercise/domain/Exercise.java | 5 +- .../artemis/exercise/domain/Submission.java | 9 +- .../exercise/web/ParticipationResource.java | 14 +- .../domain/ProgrammingExercise.java | 3 +- ...mingExerciseCodeReviewFeedbackService.java | 27 +-- src/main/webapp/app/entities/result.model.ts | 18 -- ...code-editor-student-container.component.ts | 3 +- ...exercise-trigger-build-button.component.ts | 5 +- .../code-editor-actions.component.html | 3 + .../actions/code-editor-actions.component.ts | 4 +- .../shared/code-editor/code-editor.module.ts | 2 + .../code-editor-container.component.html | 1 + .../utils/programming-exercise.utils.ts | 6 +- .../assessment-progress-label.component.ts | 4 +- ...exercise-assessment-dashboard.component.ts | 4 +- .../exercise-scores.component.ts | 4 +- .../shared/feedback/feedback.component.html | 14 +- .../participation/participation.utils.ts | 12 +- .../shared/result/result.component.html | 20 +- .../shared/result/result.component.ts | 13 +- .../exercises/shared/result/result.service.ts | 13 +- .../exercises/shared/result/result.utils.ts | 47 ++-- .../result/updating-result.component.ts | 15 +- .../course-exercise-details.component.ts | 5 +- .../exercise-buttons.module.ts | 3 +- ...ise-details-student-actions.component.html | 26 +- ...rcise-details-student-actions.component.ts | 32 +-- .../request-feedback-button.component.html | 39 +++ .../request-feedback-button.component.ts | 117 +++++++++ src/main/webapp/i18n/de/exercise.json | 4 +- src/main/webapp/i18n/de/result.json | 3 +- src/main/webapp/i18n/en/exercise.json | 2 +- src/main/webapp/i18n/en/result.json | 1 + .../ParticipationIntegrationTest.java | 109 ++++----- .../exam-navigation-sidebar.component.spec.ts | 3 +- .../component/exercises/shared/result.spec.ts | 26 +- .../request-feedback-button.component.spec.ts | 228 ++++++++++++++++++ .../component/shared/result.component.spec.ts | 1 - .../spec/component/utils/result.utils.spec.ts | 16 +- .../code-editor-container.integration.spec.ts | 2 + .../spec/service/result.service.spec.ts | 6 +- 42 files changed, 644 insertions(+), 227 deletions(-) create mode 100644 src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html create mode 100644 src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts create mode 100644 src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java b/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java index cc14d7a35e34..77c01c6fae19 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/domain/Result.java @@ -629,7 +629,7 @@ public boolean isAutomatic() { * @return true if the result is an automatic AI Athena result */ @JsonIgnore - public boolean isAthenaAutomatic() { + public boolean isAthenaBased() { return AssessmentType.AUTOMATIC_ATHENA == assessmentType; } diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java index 7503427a81fc..b25eb7ab154d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Exercise.java @@ -562,8 +562,9 @@ public Submission findLatestSubmissionWithRatedResultWithCompletionDate(Particip boolean ratedOrPractice = Boolean.TRUE.equals(result.isRated()) || participation.isPracticeMode(); boolean noProgrammingAndAssessmentOver = !isProgrammingExercise && isAssessmentOver; // For programming exercises we check that the assessment due date has passed (if set) for manual results otherwise we always show the automatic result - boolean programmingAfterAssessmentOrAutomatic = isProgrammingExercise && ((result.isManual() && isAssessmentOver) || result.isAutomatic()); - if (ratedOrPractice && (noProgrammingAndAssessmentOver || programmingAfterAssessmentOrAutomatic)) { + boolean programmingAfterAssessmentOrAutomaticOrAthena = isProgrammingExercise + && ((result.isManual() && isAssessmentOver) || result.isAutomatic() || result.isAthenaBased()); + if (ratedOrPractice && (noProgrammingAndAssessmentOver || programmingAfterAssessmentOrAutomaticOrAthena)) { // take the first found result that fulfills the above requirements // or // take newer results and thus disregard older ones diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java index 326507d47dd4..304c206938b8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/domain/Submission.java @@ -162,7 +162,7 @@ public Result getResultForCorrectionRound(int correctionRound) { */ @NotNull private List filterNonAutomaticResults() { - return results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaAutomatic())).toList(); + return results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaBased())).toList(); } /** @@ -188,8 +188,7 @@ public boolean hasResultForCorrectionRound(int correctionRound) { */ @JsonIgnore public void removeAutomaticResults() { - this.results = this.results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaAutomatic())) - .collect(Collectors.toCollection(ArrayList::new)); + this.results = this.results.stream().filter(result -> result == null || !(result.isAutomatic() || result.isAthenaBased())).collect(Collectors.toCollection(ArrayList::new)); } /** @@ -214,7 +213,7 @@ public List getResults() { @JsonIgnore public List getManualResults() { - return results.stream().filter(result -> result != null && !result.isAutomatic() && !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new)); + return results.stream().filter(result -> result != null && !result.isAutomatic() && !result.isAthenaBased()).collect(Collectors.toCollection(ArrayList::new)); } /** @@ -224,7 +223,7 @@ public List getManualResults() { */ @JsonIgnore public List getNonAthenaResults() { - return results.stream().filter(result -> result != null && !result.isAthenaAutomatic()).collect(Collectors.toCollection(ArrayList::new)); + return results.stream().filter(result -> result != null && !result.isAthenaBased()).collect(Collectors.toCollection(ArrayList::new)); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index c6cdc6ee1730..898b4456de07 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java @@ -20,7 +20,6 @@ import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; -import org.apache.velocity.exception.ResourceNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -382,7 +381,7 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc throw new BadRequestAlertException("Not intended for the use in exams", "participation", "preconditions not met"); } if (exercise.getDueDate() != null && now().isAfter(exercise.getDueDate())) { - throw new BadRequestAlertException("The due date is over", "participation", "preconditions not met"); + throw new BadRequestAlertException("The due date is over", "participation", "feedbackRequestAfterDueDate", true); } if (exercise instanceof ProgrammingExercise) { ((ProgrammingExercise) exercise).validateSettingsForFeedbackRequest(); @@ -393,7 +392,7 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc StudentParticipation participation = (exercise instanceof ProgrammingExercise) ? programmingExerciseParticipationService.findStudentParticipationByExerciseAndStudentId(exercise, principal.getName()) : studentParticipationRepository.findByExerciseIdAndStudentLogin(exercise.getId(), principal.getName()) - .orElseThrow(() -> new ResourceNotFoundException("Participation not found")); + .orElseThrow(() -> new BadRequestAlertException("Submission not found", "participation", "noSubmissionExists", true)); checkAccessPermissionOwner(participation, user); participation = studentParticipationRepository.findByIdWithResultsElseThrow(participation.getId()); @@ -406,15 +405,14 @@ private ResponseEntity handleExerciseFeedbackRequest(Exerc } else if (exercise instanceof ProgrammingExercise) { if (participation.findLatestLegalResult() == null) { - throw new BadRequestAlertException("User has not reached the conditions to submit a feedback request", "participation", "preconditions not met"); + throw new BadRequestAlertException("You need to submit at least once and have the build results", "participation", "noSubmissionExists", true); } } // Check if feedback has already been requested - var currentDate = now(); - var participationIndividualDueDate = participation.getIndividualDueDate(); - if (participationIndividualDueDate != null && currentDate.isAfter(participationIndividualDueDate)) { - throw new BadRequestAlertException("Request has already been sent", "participation", "already sent"); + var latestResult = participation.findLatestResult(); + if (latestResult != null && latestResult.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA && latestResult.getCompletionDate().isAfter(now())) { + throw new BadRequestAlertException("Request has already been sent", "participation", "feedbackRequestAlreadySent", true); } // Process feedback request diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java index df7911670a22..c2a4666c7c1b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingExercise.java @@ -712,8 +712,7 @@ private boolean checkForRatedAndAssessedResult(Result result) { * @return true if the result is manual and the assessment is over, or it is an automatic result, false otherwise */ private boolean checkForAssessedResult(Result result) { - return result.getCompletionDate() != null - && ((result.isManual() && ExerciseDateService.isAfterAssessmentDueDate(this)) || result.isAutomatic() || result.isAthenaAutomatic()); + return result.getCompletionDate() != null && ((result.isManual() && ExerciseDateService.isAfterAssessmentDueDate(this)) || result.isAutomatic() || result.isAthenaBased()); } @Override diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java index 8c92446d22d0..935a3412b10e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ProgrammingExerciseCodeReviewFeedbackService.java @@ -4,6 +4,7 @@ import static java.time.ZonedDateTime.now; import java.time.ZonedDateTime; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -11,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -59,6 +61,9 @@ public class ProgrammingExerciseCodeReviewFeedbackService { private final ProgrammingMessagingService programmingMessagingService; + @Value("${artemis.athena.allowed-feedback-attempts:20}") + private int allowedFeedbackAttempts; + public ProgrammingExerciseCodeReviewFeedbackService(GroupNotificationService groupNotificationService, Optional athenaFeedbackSuggestionsService, SubmissionService submissionService, ResultService resultService, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ResultRepository resultRepository, @@ -111,14 +116,14 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici var submissionOptional = programmingExerciseParticipationService.findProgrammingExerciseParticipationWithLatestSubmissionAndResult(participation.getId()) .findLatestSubmission(); if (submissionOptional.isEmpty()) { - throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmission"); + throw new BadRequestAlertException("No legal submissions found", "submission", "noSubmissionExists"); } var submission = submissionOptional.get(); // save result and transmit it over websockets to notify the client about the status var automaticResult = this.submissionService.saveNewEmptyResult(submission); automaticResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); - automaticResult.setRated(false); + automaticResult.setRated(true); // we want to use this feedback to give the grade in the future automaticResult.setScore(100.0); automaticResult.setSuccessful(null); automaticResult.setCompletionDate(ZonedDateTime.now().plusMinutes(5)); // we do not want to show dates without a completion date, but we want the students to know their @@ -127,7 +132,6 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici try { - setIndividualDueDateAndLockRepository(participation, programmingExercise, false); this.programmingMessagingService.notifyUserAboutNewResult(automaticResult, participation); // now the client should be able to see new result @@ -158,9 +162,10 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici feedback.setDetailText(individualFeedbackItem.description()); feedback.setHasLongFeedbackText(false); feedback.setType(FeedbackType.AUTOMATIC); - feedback.setCredits(0.0); + feedback.setCredits(individualFeedbackItem.credits()); return feedback; - }).toList(); + }).sorted(Comparator.comparing(Feedback::getCredits, Comparator.nullsLast(Comparator.naturalOrder()))).toList(); + ; automaticResult.setSuccessful(true); automaticResult.setCompletionDate(ZonedDateTime.now()); @@ -176,9 +181,6 @@ public void generateAutomaticNonGradedFeedback(ProgrammingExerciseStudentPartici this.resultRepository.save(automaticResult); this.programmingMessagingService.notifyUserAboutNewResult(automaticResult, participation); } - finally { - unlockRepository(participation, programmingExercise); - } } /** @@ -225,15 +227,10 @@ private void checkRateLimitOrThrow(ProgrammingExerciseStudentParticipation parti List athenaResults = participation.getResults().stream().filter(result -> result.getAssessmentType() == AssessmentType.AUTOMATIC_ATHENA).toList(); - long countOfAthenaResultsInProcessOrSuccessful = athenaResults.stream().filter(result -> result.isSuccessful() == null || result.isSuccessful() == Boolean.TRUE).count(); - long countOfSuccessfulRequests = athenaResults.stream().filter(result -> result.isSuccessful() == Boolean.TRUE).count(); - if (countOfAthenaResultsInProcessOrSuccessful >= 3) { - throw new BadRequestAlertException("Cannot send additional AI feedback requests now. Try again later!", "participation", "preconditions not met"); - } - if (countOfSuccessfulRequests >= 20) { - throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "preconditions not met"); + if (countOfSuccessfulRequests >= this.allowedFeedbackAttempts) { + throw new BadRequestAlertException("Maximum number of AI feedback requests reached.", "participation", "maxAthenaResultsReached", true); } } } diff --git a/src/main/webapp/app/entities/result.model.ts b/src/main/webapp/app/entities/result.model.ts index d6c2f96adaaa..47fff80fda31 100644 --- a/src/main/webapp/app/entities/result.model.ts +++ b/src/main/webapp/app/entities/result.model.ts @@ -39,24 +39,6 @@ export class Result implements BaseEntity { this.successful = false; // default value } - /** - * Checks whether the result is a manual result. A manual result can be from type MANUAL or SEMI_AUTOMATIC - * - * @return true if the result is a manual result - */ - public static isManualResult(that: Result): boolean { - return that.assessmentType === AssessmentType.MANUAL || that.assessmentType === AssessmentType.SEMI_AUTOMATIC; - } - - /** - * Checks whether the result is generated by Athena AI. - * - * @return true if the result is an automatic Athena AI result - */ - public static isAthenaAIResult(that: Result): boolean { - return that.assessmentType === AssessmentType.AUTOMATIC_ATHENA; - } - /** * Checks whether the given result has an assessment note that is not empty. * @param that the result of which the presence of an assessment note is being checked diff --git a/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.ts b/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.ts index 34aa3e0b0d5a..ad6ed37728d2 100644 --- a/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.ts +++ b/src/main/webapp/app/exercises/programming/participate/code-editor-student-container.component.ts @@ -26,6 +26,7 @@ import { ExerciseHint } from 'app/entities/hestia/exercise-hint.model'; import { ExerciseHintService } from 'app/exercises/shared/exercise-hint/shared/exercise-hint.service'; import { HttpResponse } from '@angular/common/http'; import { AlertService } from 'app/core/util/alert.service'; +import { isManualResult as isManualResultFunction } from 'app/exercises/shared/result/result.utils'; @Component({ selector: 'jhi-code-editor-student', @@ -148,7 +149,7 @@ export class CodeEditorStudentContainerComponent implements OnInit, OnDestroy { let hasTutorFeedback = false; if (this.latestResult) { // latest result is the first element of results, see loadParticipationWithLatestResult - isManualResult = Result.isManualResult(this.latestResult); + isManualResult = isManualResultFunction(this.latestResult); if (isManualResult) { hasTutorFeedback = this.latestResult.feedbacks!.some((feedback) => feedback.type === FeedbackType.MANUAL); } diff --git a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-trigger-build-button.component.ts b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-trigger-build-button.component.ts index 52089b28c991..1431ad0bfe38 100644 --- a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-trigger-build-button.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-trigger-build-button.component.ts @@ -13,6 +13,7 @@ import { SubmissionType } from 'app/entities/submission.model'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import { AlertService } from 'app/core/util/alert.service'; import { hasParticipationChanged } from 'app/exercises/shared/participation/participation.utils'; +import { isManualResult } from 'app/exercises/shared/result/result.utils'; /** * Component for triggering a build for the CURRENT submission of the student (does not create a new commit!). @@ -60,7 +61,7 @@ export abstract class ProgrammingExerciseTriggerBuildButtonComponent implements if (hasDueDatePassed(this.exercise)) { // If the last result was manual, the instructor might not want to override it with a new automatic result. const newestResult = !!this.participation.results && head(orderBy(this.participation.results, ['id'], ['desc'])); - this.lastResultIsManual = !!newestResult && Result.isManualResult(newestResult); + this.lastResultIsManual = !!newestResult && isManualResult(newestResult); } // We can trigger the build only if the participation is active (has build plan), if the build plan was archived (new build plan will be created) // or the due date is over. @@ -126,7 +127,7 @@ export abstract class ProgrammingExerciseTriggerBuildButtonComponent implements .pipe( filter((result) => !!result), tap((result: Result) => { - this.lastResultIsManual = !!result && Result.isManualResult(result); + this.lastResultIsManual = !!result && isManualResult(result); }), ) .subscribe(); diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html b/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html index 29e3ded8363c..d0186a25665e 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/actions/code-editor-actions.component.html @@ -1,3 +1,6 @@ +@if (!!participation()?.exercise) { + +} @if (commitState === CommitState.CONFLICT) {
diff --git a/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts b/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts index 5e95bb66dbac..262fcb9e30a7 100644 --- a/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts +++ b/src/main/webapp/app/exercises/programming/shared/utils/programming-exercise.utils.ts @@ -7,6 +7,7 @@ import { SubmissionType } from 'app/entities/submission.model'; import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { isPracticeMode } from 'app/entities/participation/student-participation.model'; +import { isAIResultAndFailed, isAIResultAndIsBeingProcessed, isAIResultAndProcessed, isAIResultAndTimedOut } from 'app/exercises/shared/result/result.utils'; export const createBuildPlanUrl = (template: string, projectKey: string, buildPlanId: string): string | undefined => { if (template && projectKey && buildPlanId) { @@ -59,7 +60,10 @@ export const isResultPreliminary = (latestResult: Result, programmingExercise?: if (!programmingExercise) { return false; } - if (latestResult.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { + if (isAIResultAndProcessed(latestResult)) { + return true; + } + if (isAIResultAndIsBeingProcessed(latestResult) || isAIResultAndTimedOut(latestResult) || isAIResultAndFailed(latestResult)) { return false; } if (latestResult.participation?.type === ParticipationType.PROGRAMMING && isPracticeMode(latestResult.participation)) { diff --git a/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts b/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts index 75f9243432c3..254a3b2a5f82 100644 --- a/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts +++ b/src/main/webapp/app/exercises/shared/assessment-progress-label/assessment-progress-label.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnChanges } from '@angular/core'; import { Submission, getLatestSubmissionResult } from 'app/entities/submission.model'; -import { Result } from 'app/entities/result.model'; +import { isManualResult } from 'app/exercises/shared/result/result.utils'; @Component({ selector: 'jhi-assessment-progress-label', @@ -14,7 +14,7 @@ export class AssessmentProgressLabelComponent implements OnChanges { ngOnChanges() { this.numberAssessedSubmissions = this.submissions.filter((submission) => { const result = getLatestSubmissionResult(submission); - return result?.rated && Result.isManualResult(result) && result?.completionDate; + return result?.rated && isManualResult(result) && result?.completionDate; }).length; } } diff --git a/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts b/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts index a5bffcbe5575..63a23747c1f9 100644 --- a/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts +++ b/src/main/webapp/app/exercises/shared/dashboards/tutor/exercise-assessment-dashboard.component.ts @@ -42,12 +42,12 @@ import { ArtemisNavigationUtilService, getLinkToSubmissionAssessment } from 'app import { AssessmentType } from 'app/entities/assessment-type.model'; import { LegendPosition } from '@swimlane/ngx-charts'; import { AssessmentDashboardInformationEntry } from 'app/course/dashboards/assessment-dashboard/assessment-dashboard-information.component'; -import { Result } from 'app/entities/result.model'; import dayjs from 'dayjs/esm'; import { faCheckCircle, faExclamationTriangle, faFolderOpen, faListAlt, faQuestionCircle, faSort, faSpinner } from '@fortawesome/free-solid-svg-icons'; import { GraphColors } from 'app/entities/statistics.model'; import { PROFILE_LOCALVC } from 'app/app.constants'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { isManualResult } from 'app/exercises/shared/result/result.utils'; export interface ExampleSubmissionQueryParams { readOnly?: boolean; @@ -640,7 +640,7 @@ export class ExerciseAssessmentDashboardComponent implements OnInit { */ calculateSubmissionStatusIsDraft(submission: Submission, correctionRound = 0): boolean { const tmpResult = submission.results?.[correctionRound]; - return !(tmpResult?.completionDate && Result.isManualResult(tmpResult)); + return !(tmpResult?.completionDate && isManualResult(tmpResult)); } /** diff --git a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts index c2642528bc00..dbc3b28d2e83 100644 --- a/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-scores/exercise-scores.component.ts @@ -13,7 +13,6 @@ import { areManualResultsAllowed } from 'app/exercises/shared/exercise/exercise. import { ResultService } from 'app/exercises/shared/result/result.service'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; -import { Result } from 'app/entities/result.model'; import { ProgrammingSubmission } from 'app/entities/programming/programming-submission.model'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AssessmentType } from 'app/entities/assessment-type.model'; @@ -27,6 +26,7 @@ import dayjs from 'dayjs/esm'; import { ExerciseCacheService } from 'app/exercises/shared/exercise/exercise-cache.service'; import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; import { PROFILE_LOCALVC } from 'app/app.constants'; +import { isManualResult } from 'app/exercises/shared/result/result.utils'; /** * Filter properties for a result @@ -229,7 +229,7 @@ export class ExerciseScoresComponent implements OnInit, OnDestroy { case FilterProp.BUILD_FAILED: return !!(participation.submissions?.[0] && (participation.submissions?.[0] as ProgrammingSubmission).buildFailed); case FilterProp.MANUAL: - return !!latestResult && Result.isManualResult(latestResult); + return !!latestResult && isManualResult(latestResult); case FilterProp.AUTOMATIC: return latestResult?.assessmentType === AssessmentType.AUTOMATIC; case FilterProp.LOCKED: diff --git a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html index a0de0676f7dc..23ecac9676b2 100644 --- a/src/main/webapp/app/exercises/shared/feedback/feedback.component.html +++ b/src/main/webapp/app/exercises/shared/feedback/feedback.component.html @@ -119,11 +119,15 @@

{{ 'artemisApp.result.preliminary' | artemisTranslate | uppercase }}
- @if (exercise?.assessmentType !== AssessmentType.AUTOMATIC) { -

- } - @if (exercise?.assessmentType === AssessmentType.AUTOMATIC) { -

+ @if (result?.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { +

+ } @else { + @if (exercise?.assessmentType !== AssessmentType.AUTOMATIC) { +

+ } + @if (exercise?.assessmentType === AssessmentType.AUTOMATIC) { +

+ } }

} diff --git a/src/main/webapp/app/exercises/shared/participation/participation.utils.ts b/src/main/webapp/app/exercises/shared/participation/participation.utils.ts index 5fc349f22b27..d931734c6407 100644 --- a/src/main/webapp/app/exercises/shared/participation/participation.utils.ts +++ b/src/main/webapp/app/exercises/shared/participation/participation.utils.ts @@ -6,6 +6,7 @@ import dayjs from 'dayjs/esm'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { Result } from 'app/entities/result.model'; import { orderBy as _orderBy } from 'lodash-es'; +import { isAIResultAndIsBeingProcessed } from 'app/exercises/shared/result/result.utils'; /** * Check if the participation has changed. @@ -102,7 +103,11 @@ export const isParticipationInDueTime = (participation: Participation, exercise: * @param participation * @param showUngradedResults */ -export function getLatestResultOfStudentParticipation(participation: StudentParticipation | undefined, showUngradedResults: boolean): Result | undefined { +export function getLatestResultOfStudentParticipation( + participation: StudentParticipation | undefined, + showUngradedResults: boolean, + showAthenaPreliminaryFeedback: boolean = false, +): Result | undefined { if (!participation) { return undefined; } @@ -111,8 +116,11 @@ export function getLatestResultOfStudentParticipation(participation: StudentPart if (participation.results) { participation.results = _orderBy(participation.results, 'completionDate', 'desc'); } + // The latest result is the first rated result in the sorted array (=newest) or any result if the option is active to show ungraded results. - const latestResult = participation.results?.find(({ rated }) => showUngradedResults || rated === true); + const latestResult = participation.results?.find( + (result) => showUngradedResults || result.rated === true || (showAthenaPreliminaryFeedback && isAIResultAndIsBeingProcessed(result)), + ); // Make sure that the participation result is connected to the newest result. return latestResult ? { ...latestResult, participation: participation } : undefined; } diff --git a/src/main/webapp/app/exercises/shared/result/result.component.html b/src/main/webapp/app/exercises/shared/result/result.component.html index 2dfd17685054..911320c183df 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.html +++ b/src/main/webapp/app/exercises/shared/result/result.component.html @@ -12,6 +12,9 @@ } @case (ResultTemplateStatus.FEEDBACK_GENERATION_FAILED) { @if (result) { + @if (showIcon) { + + } {{ resultString }} @@ -20,17 +23,16 @@ } } @case (ResultTemplateStatus.IS_GENERATING_FEEDBACK) { - @if (result) { - - - - {{ resultString }} - - - } + + + + } @case (ResultTemplateStatus.FEEDBACK_GENERATION_TIMED_OUT) { @if (result) { + @if (showIcon) { + + } {{ resultString }} @@ -59,7 +61,7 @@ } @if (!isInSidebarCard) { - ({{ result!.completionDate | artemisTimeAgo }}) + ({{ result!.completionDate | artemisTimeAgo }} ) } @if (hasBuildArtifact() && participation.type === ParticipationType.PROGRAMMING) { diff --git a/src/main/webapp/app/exercises/shared/result/result.component.ts b/src/main/webapp/app/exercises/shared/result/result.component.ts index 3415021e11c7..f9edf80994d9 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/result.component.ts @@ -1,6 +1,13 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, Optional, SimpleChanges } from '@angular/core'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; -import { MissingResultInformation, ResultTemplateStatus, evaluateTemplateStatus, getResultIconClass, getTextColorClass } from 'app/exercises/shared/result/result.utils'; +import { + MissingResultInformation, + ResultTemplateStatus, + evaluateTemplateStatus, + getResultIconClass, + getTextColorClass, + isAthenaAIResult, +} from 'app/exercises/shared/result/result.utils'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; @@ -190,7 +197,7 @@ export class ResultComponent implements OnInit, OnChanges, OnDestroy { this.resultString = this.resultService.getResultString(this.result, this.exercise, this.short); } else if ( this.result && - ((this.result.score !== undefined && (this.result.rated || this.result.rated == undefined || this.showUngradedResults)) || Result.isAthenaAIResult(this.result)) + ((this.result.score !== undefined && (this.result.rated || this.result.rated == undefined || this.showUngradedResults)) || isAthenaAIResult(this.result)) ) { this.textColorClass = getTextColorClass(this.result, this.templateStatus); this.resultIconClass = getResultIconClass(this.result, this.templateStatus); @@ -230,7 +237,7 @@ export class ResultComponent implements OnInit, OnChanges, OnDestroy { return 'artemisApp.result.resultString.automaticAIFeedbackTimedOutTooltip'; } else if (this.templateStatus === ResultTemplateStatus.IS_GENERATING_FEEDBACK) { return 'artemisApp.result.resultString.automaticAIFeedbackInProgressTooltip'; - } else if (this.templateStatus === ResultTemplateStatus.HAS_RESULT && Result.isAthenaAIResult(this.result)) { + } else if (this.templateStatus === ResultTemplateStatus.HAS_RESULT && isAthenaAIResult(this.result)) { return 'artemisApp.result.resultString.automaticAIFeedbackSuccessfulTooltip'; } } diff --git a/src/main/webapp/app/exercises/shared/result/result.service.ts b/src/main/webapp/app/exercises/shared/result/result.service.ts index 2fd62aa574b7..ea4c9eaaca4c 100644 --- a/src/main/webapp/app/exercises/shared/result/result.service.ts +++ b/src/main/webapp/app/exercises/shared/result/result.service.ts @@ -24,6 +24,7 @@ import { isAIResultAndIsBeingProcessed, isAIResultAndProcessed, isAIResultAndTimedOut, + isAthenaAIResult, isStudentParticipation, } from 'app/exercises/shared/result/result.utils'; import { CsvDownloadService } from 'app/shared/util/CsvDownloadService'; @@ -94,7 +95,7 @@ export class ResultService implements IResultService { const relativeScore = roundValueSpecifiedByCourseSettings(result.score!, getCourseFromExercise(exercise)); const points = roundValueSpecifiedByCourseSettings((result.score! * exercise.maxPoints!) / 100, getCourseFromExercise(exercise)); if (exercise.type !== ExerciseType.PROGRAMMING) { - if (Result.isAthenaAIResult(result)) { + if (isAthenaAIResult(result)) { return this.getResultStringNonProgrammingExerciseWithAIFeedback(result, relativeScore, points, short); } return this.getResultStringNonProgrammingExercise(relativeScore, points, short); @@ -112,7 +113,7 @@ export class ResultService implements IResultService { */ private getResultStringNonProgrammingExerciseWithAIFeedback(result: Result, relativeScore: number, points: number, short: boolean | undefined): string { let aiFeedbackMessage: string = ''; - if (result && Result.isAthenaAIResult(result) && result.successful === undefined) { + if (result && isAthenaAIResult(result) && result.successful === undefined) { return this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackInProgress'); } aiFeedbackMessage = this.getResultStringNonProgrammingExercise(relativeScore, points, short); @@ -149,9 +150,7 @@ export class ResultService implements IResultService { */ private getResultStringProgrammingExercise(result: Result, exercise: ProgrammingExercise, relativeScore: number, points: number, short: boolean | undefined): string { let buildAndTestMessage: string; - if (result.submission && (result.submission as ProgrammingSubmission).buildFailed) { - buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.buildFailed'); - } else if (isAIResultAndFailed(result)) { + if (isAIResultAndFailed(result)) { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackFailed'); } else if (isAIResultAndIsBeingProcessed(result)) { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackInProgress'); @@ -159,6 +158,8 @@ export class ResultService implements IResultService { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackTimedOut'); } else if (isAIResultAndProcessed(result)) { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.automaticAIFeedbackSuccessful'); + } else if (result.submission && (result.submission as ProgrammingSubmission).buildFailed) { + buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.buildFailed'); } else if (!result.testCaseCount) { buildAndTestMessage = this.translateService.instant('artemisApp.result.resultString.buildSuccessfulNoTests'); } else { @@ -187,7 +188,7 @@ export class ResultService implements IResultService { * @param short flag that indicates if the resultString should use the short format */ private getBaseResultStringProgrammingExercise(result: Result, relativeScore: number, points: number, buildAndTestMessage: string, short: boolean | undefined): string { - if (Result.isAthenaAIResult(result)) { + if (isAthenaAIResult(result)) { return buildAndTestMessage; } if (short) { diff --git a/src/main/webapp/app/exercises/shared/result/result.utils.ts b/src/main/webapp/app/exercises/shared/result/result.utils.ts index be0126c54db9..cee9c6dbb07f 100644 --- a/src/main/webapp/app/exercises/shared/result/result.utils.ts +++ b/src/main/webapp/app/exercises/shared/result/result.utils.ts @@ -119,20 +119,29 @@ export const getUnreferencedFeedback = (feedbacks: Feedback[] | undefined): Feed return feedbacks ? feedbacks.filter((feedbackElement) => !feedbackElement.reference && feedbackElement.type === FeedbackType.MANUAL_UNREFERENCED) : undefined; }; -export function isAIResultAndFailed(result: Result | undefined) { - return result && Result.isAthenaAIResult(result) && result.successful === false; +export function isAIResultAndFailed(result: Result | undefined): boolean { + return (result && isAthenaAIResult(result) && result.successful === false) ?? false; } -export function isAIResultAndTimedOut(result: Result | undefined) { - return result && Result.isAthenaAIResult(result) && result.successful === undefined && result.completionDate && dayjs().isAfter(result.completionDate); +export function isAIResultAndTimedOut(result: Result | undefined): boolean { + return (result && isAthenaAIResult(result) && result.successful === undefined && result.completionDate && dayjs().isAfter(result.completionDate)) ?? false; } -export function isAIResultAndProcessed(result: Result | undefined) { - return result && Result.isAthenaAIResult(result) && result.successful === true; +export function isAIResultAndProcessed(result: Result | undefined): boolean { + return (result && isAthenaAIResult(result) && result.successful === true) ?? false; } -export function isAIResultAndIsBeingProcessed(result: Result | undefined) { - return result && Result.isAthenaAIResult(result) && result.successful === undefined && result.completionDate && dayjs().isSameOrBefore(result.completionDate); +export function isAIResultAndIsBeingProcessed(result: Result | undefined): boolean { + return (result && isAthenaAIResult(result) && result.successful === undefined && result.completionDate && dayjs().isSameOrBefore(result.completionDate)) ?? false; +} + +/** + * Checks whether the result is generated by Athena AI. + * + * @return true if the result is an automatic Athena AI result + */ +export function isAthenaAIResult(result: Result): boolean { + return result.assessmentType === AssessmentType.AUTOMATIC_ATHENA; } export const evaluateTemplateStatus = ( @@ -248,9 +257,12 @@ export const getTextColorClass = (result: Result | undefined, templateStatus: Re } if (result.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { - if (result.successful == undefined) { + if (isAIResultAndIsBeingProcessed(result)) { return 'text-primary'; } + if (isAIResultAndFailed(result)) { + return 'text-danger'; + } return 'text-secondary'; } @@ -258,11 +270,11 @@ export const getTextColorClass = (result: Result | undefined, templateStatus: Re return 'result-late'; } - if (isBuildFailedAndResultIsAutomatic(result) || isAIResultAndFailed(result)) { + if (isBuildFailedAndResultIsAutomatic(result)) { return 'text-danger'; } - if (resultIsPreliminary(result) || isAIResultAndIsBeingProcessed(result) || isAIResultAndTimedOut(result)) { + if (resultIsPreliminary(result)) { return 'text-secondary'; } @@ -294,18 +306,19 @@ export const getResultIconClass = (result: Result | undefined, templateStatus: R return faQuestionCircle; } - if (result.assessmentType === AssessmentType.AUTOMATIC_ATHENA) { - if (result.successful === undefined) { - return faCircleNotch; - } - return faQuestionCircle; + if (isAIResultAndProcessed(result)) { + return faCheckCircle; } if (isBuildFailedAndResultIsAutomatic(result) || isAIResultAndFailed(result)) { return faTimesCircle; } - if (resultIsPreliminary(result) || isAIResultAndTimedOut(result) || isAIResultAndIsBeingProcessed(result)) { + if (isAIResultAndIsBeingProcessed(result)) { + return faCircleNotch; + } + + if (resultIsPreliminary(result) || isAIResultAndTimedOut(result)) { return faQuestionCircle; } diff --git a/src/main/webapp/app/exercises/shared/result/updating-result.component.ts b/src/main/webapp/app/exercises/shared/result/updating-result.component.ts index 55cde780b0ec..640b02f38bff 100644 --- a/src/main/webapp/app/exercises/shared/result/updating-result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/updating-result.component.ts @@ -13,7 +13,7 @@ import { StudentParticipation } from 'app/entities/participation/student-partici import { Result } from 'app/entities/result.model'; import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { getLatestResultOfStudentParticipation, hasParticipationChanged } from 'app/exercises/shared/participation/participation.utils'; -import { MissingResultInformation } from 'app/exercises/shared/result/result.utils'; +import { MissingResultInformation, isAIResultAndIsBeingProcessed, isAthenaAIResult } from 'app/exercises/shared/result/result.utils'; import { convertDateFromServer } from 'app/utils/date.utils'; /** @@ -59,7 +59,7 @@ export class UpdatingResultComponent implements OnChanges, OnDestroy { */ ngOnChanges(changes: SimpleChanges) { if (hasParticipationChanged(changes)) { - this.result = getLatestResultOfStudentParticipation(this.participation, this.showUngradedResults); + this.result = getLatestResultOfStudentParticipation(this.participation, this.showUngradedResults, true); this.missingResultInfo = MissingResultInformation.NONE; this.subscribeForNewResults(); @@ -101,10 +101,17 @@ export class UpdatingResultComponent implements OnChanges, OnDestroy { // Ignore initial null result of subscription filter((result) => !!result), // Ignore ungraded results if ungraded results are supposed to be ignored. - filter((result: Result) => this.showUngradedResults || result.rated === true), + // If the result is a preliminary feedback(being generated), show it + filter((result: Result) => this.showUngradedResults || result.rated === true || isAthenaAIResult(result)), map((result) => ({ ...result, completionDate: convertDateFromServer(result.completionDate), participation: this.participation })), tap((result) => { - this.result = result; + if ((isAthenaAIResult(result) && isAIResultAndIsBeingProcessed(result)) || result.rated) { + this.result = result; + } else if (result.rated === false && this.showUngradedResults) { + this.result = result; + } else { + this.result = getLatestResultOfStudentParticipation(this.participation, this.showUngradedResults, false); + } this.onParticipationChange.emit(); if (result) { this.showResult.emit(); diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts index 62a1b6fa7429..4790d0b24bf9 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts @@ -198,7 +198,6 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.exerciseCategories = this.exercise.categories ?? []; this.allowComplaintsForAutomaticAssessments = false; this.plagiarismCaseInfo = newExerciseDetails.plagiarismCaseInfo; - if (this.exercise.type === ExerciseType.PROGRAMMING) { const programmingExercise = this.exercise as ProgrammingExercise; const isAfterDateForComplaint = @@ -243,7 +242,7 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp private filterUnfinishedResults(participations?: StudentParticipation[]) { participations?.forEach((participation: Participation) => { if (participation.results) { - participation.results = participation.results.filter((result: Result) => result.completionDate && result.successful !== undefined); + participation.results = participation.results.filter((result: Result) => result.completionDate); } }); } @@ -254,7 +253,7 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.sortedHistoryResults = this.studentParticipations .flatMap((participation) => participation.results ?? []) .sort(this.resultSortFunction) - .filter((result) => !(result.assessmentType === AssessmentType.AUTOMATIC_ATHENA && result.successful == undefined)); + .filter((result) => !(result.assessmentType === AssessmentType.AUTOMATIC_ATHENA && dayjs().isBefore(result.completionDate))); } } diff --git a/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts b/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts index 1b36ab7e6f5d..7b6aab3d5f7c 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-buttons.module.ts @@ -6,9 +6,10 @@ import { OrionExerciseDetailsStudentActionsComponent } from 'app/orion/participa import { ExerciseDetailsStudentActionsComponent } from 'app/overview/exercise-details/exercise-details-student-actions.component'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ArtemisSharedPipesModule } from 'app/shared/pipes/shared-pipes.module'; +import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component'; @NgModule({ - imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisSharedPipesModule, OrionModule, FeatureToggleModule], + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, ArtemisSharedPipesModule, OrionModule, FeatureToggleModule, RequestFeedbackButtonComponent], declarations: [ExerciseDetailsStudentActionsComponent, OrionExerciseDetailsStudentActionsComponent], exports: [ExerciseDetailsStudentActionsComponent, OrionExerciseDetailsStudentActionsComponent], }) diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html index 53a99d1f440a..6e2df76cbef9 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html @@ -135,30 +135,8 @@ } - @if (exercise.allowFeedbackRequests) { - @if (athenaEnabled) { - - - Send automatic feedback request - - } @else { - - - Send manual feedback request - - } + @if (exercise.allowFeedbackRequests && gradedParticipation && exercise.type === ExerciseType.PROGRAMMING) { + } } diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts index fcdf131a87c4..2991bac3355f 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts @@ -110,6 +110,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges this.profileService.getProfileInfo().subscribe((profileInfo) => { this.localVCEnabled = profileInfo.activeProfiles?.includes(PROFILE_LOCALVC); this.athenaEnabled = profileInfo.activeProfiles?.includes(PROFILE_ATHENA); + // The online IDE is only available with correct SpringProfile and if it's enabled for this exercise if (profileInfo.activeProfiles?.includes(PROFILE_THEIA) && this.programmingExercise) { this.theiaEnabled = true; @@ -257,6 +258,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges }); } + // TODO remove this method once support of the button component is implemented for text and modeling exercises requestFeedback() { if (!this.assureConditionsSatisfied()) return; if (this.exercise.type === ExerciseType.PROGRAMMING) { @@ -341,6 +343,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges * 3. There is no already pending feedback request. * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. */ + // TODO remove this method once support of the button component is implemented for text and modeling exercises assureConditionsSatisfied(): boolean { this.updateParticipations(); if (this.exercise.type === ExerciseType.PROGRAMMING) { @@ -378,7 +381,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges } } - if (this.hasAthenaResultForlatestSubmission()) { + if (this.hasAthenaResultForLatestSubmission()) { const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); this.alertService.warning(submitFirstWarning); return false; @@ -386,29 +389,14 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges return true; } - hasAthenaResultForlatestSubmission(): boolean { + hasAthenaResultForLatestSubmission(): boolean { if (this.gradedParticipation?.submissions && this.gradedParticipation?.results) { - const sortedSubmissions = this.gradedParticipation.submissions.slice().sort((a, b) => { - const dateA = this.getDateValue(a.submissionDate) ?? -Infinity; - const dateB = this.getDateValue(b.submissionDate) ?? -Infinity; - return dateB - dateA; - }); - - return this.gradedParticipation.results.some((result) => result.submission?.id === sortedSubmissions[0]?.id); + // submissions.results is always undefined so this is necessary + return ( + this.gradedParticipation.submissions.last()?.id === + this.gradedParticipation?.results.filter((result) => result.assessmentType == AssessmentType.AUTOMATIC_ATHENA).first()?.submission?.id + ); } return false; } - - private getDateValue = (date: any): number => { - if (dayjs.isDayjs(date)) { - return date.valueOf(); - } - if (date instanceof Date) { - return date.valueOf(); - } - if (typeof date === 'string') { - return new Date(date).valueOf(); - } - return -Infinity; // fallback for null, undefined, or invalid dates - }; } diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html new file mode 100644 index 000000000000..6d6addcc2b84 --- /dev/null +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.html @@ -0,0 +1,39 @@ +@if (!isExamExercise) { + @if (athenaEnabled) { + @if (exercise().type === ExerciseType.TEXT) { + + } @else { + + + + + } + } @else { + + + + + } +} diff --git a/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts new file mode 100644 index 000000000000..b9aecaec56d5 --- /dev/null +++ b/src/main/webapp/app/overview/exercise-details/request-feedback-button/request-feedback-button.component.ts @@ -0,0 +1,117 @@ +import { Component, OnInit, inject, input, output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faPenSquare } from '@fortawesome/free-solid-svg-icons'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { PROFILE_ATHENA } from 'app/app.constants'; +import { StudentParticipation } from 'app/entities/participation/student-participation.model'; +import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { AlertService } from 'app/core/util/alert.service'; +import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; +import { AssessmentType } from 'app/entities/assessment-type.model'; +import { TranslateService } from '@ngx-translate/core'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { isExamExercise } from 'app/shared/util/utils'; +import { ExerciseDetailsType, ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; + +@Component({ + selector: 'jhi-request-feedback-button', + standalone: true, + imports: [CommonModule, ArtemisSharedCommonModule, NgbTooltipModule, FontAwesomeModule], + templateUrl: './request-feedback-button.component.html', +}) +export class RequestFeedbackButtonComponent implements OnInit { + faPenSquare = faPenSquare; + athenaEnabled = false; + isExamExercise: boolean; + participation?: StudentParticipation; + + isGeneratingFeedback = input(); + smallButtons = input(false); + exercise = input.required(); + generatingFeedback = output(); + + private feedbackSent = false; + private profileService = inject(ProfileService); + private alertService = inject(AlertService); + private courseExerciseService = inject(CourseExerciseService); + private translateService = inject(TranslateService); + private exerciseService = inject(ExerciseService); + private participationService = inject(ParticipationService); + + protected readonly ExerciseType = ExerciseType; + + ngOnInit() { + this.profileService.getProfileInfo().subscribe((profileInfo) => { + this.athenaEnabled = profileInfo.activeProfiles?.includes(PROFILE_ATHENA); + }); + this.isExamExercise = isExamExercise(this.exercise()); + if (this.isExamExercise || !this.exercise().id) { + return; + } + this.updateParticipation(); + } + + private updateParticipation() { + if (this.exercise().id) { + this.exerciseService.getExerciseDetails(this.exercise().id!).subscribe({ + next: (exerciseResponse: HttpResponse) => { + this.participation = this.participationService.getSpecificStudentParticipation(exerciseResponse.body!.exercise.studentParticipations ?? [], false); + }, + error: (error: HttpErrorResponse) => { + this.alertService.error(`artemisApp.${error.error.entityName}.errors.${error.error.errorKey}`); + }, + }); + } + } + + requestFeedback() { + if (!this.assureConditionsSatisfied()) { + return; + } + + this.courseExerciseService.requestFeedback(this.exercise().id!).subscribe({ + next: (participation: StudentParticipation) => { + if (participation) { + this.generatingFeedback.emit(); + this.feedbackSent = true; + this.alertService.success('artemisApp.exercise.feedbackRequestSent'); + } + }, + error: (error: HttpErrorResponse) => { + this.alertService.error(`artemisApp.exercise.${error.error.errorKey}`); + }, + }); + } + + /** + * Checks if the conditions for requesting automatic non-graded feedback are satisfied. + * The student can request automatic non-graded feedback under the following conditions: + * 1. They have a graded submission. + * 2. The deadline for the exercise has not been exceeded. + * 3. There is no already pending feedback request. + * @returns {boolean} `true` if all conditions are satisfied, otherwise `false`. + */ + assureConditionsSatisfied(): boolean { + if (this.exercise().type === ExerciseType.PROGRAMMING || !this.hasAthenaResultForLatestSubmission()) { + return true; + } + const submitFirstWarning = this.translateService.instant('artemisApp.exercise.submissionAlreadyHasAthenaResult'); + this.alertService.warning(submitFirstWarning); + return false; + } + + hasAthenaResultForLatestSubmission(): boolean { + if (this.participation?.submissions && this.participation?.results) { + // submissions.results is always undefined so this is neccessary + return ( + this.participation.submissions?.last()?.id === + this.participation.results?.filter((result) => result.assessmentType == AssessmentType.AUTOMATIC_ATHENA).first()?.submission?.id + ); + } + return false; + } +} diff --git a/src/main/webapp/i18n/de/exercise.json b/src/main/webapp/i18n/de/exercise.json index fb698572ee34..3aee88e253c5 100644 --- a/src/main/webapp/i18n/de/exercise.json +++ b/src/main/webapp/i18n/de/exercise.json @@ -168,8 +168,8 @@ "resumeProgrammingExercise": "Die Aufgabe wurde wieder aufgenommen. Du kannst nun weiterarbeiten!", "feedbackRequestSent": "Deine Feedbackanfrage wurde gesendet.", "feedbackRequestAlreadySent": "Deine Feedbackanfrage wurde bereits gesendet.", - "notEnoughPoints": "Um eine Feedbackanfrage zu senden, brauchst du mindestens eine Abgabe.", - "lockRepositoryWarning": "Dein Repository wird gesperrt. Du kannst erst weiterarbeiten, wenn deine Feedbackanfrage beantwortet wird.", + "noSubmissionExists": "Um eine Feedbackanfrage zu senden, brauchst du mindestens eine Abgabe.", + "lockRepositoryWarning": "Dein Repository wird gesperrt. Du kannst erst weiterarbeiten wenn deine Feedbackanfrage beantwortet wird.", "feedbackRequestAfterDueDate": "Du kannst nach der Abgabefrist keine weiteren Anfragen einreichen.", "maxAthenaResultsReached": "Du hast die maximale Anzahl an KI-Feedbackanfragen erreicht.", "athenaFeedbackSuccessful": "AI-Feedback erfolgreich generiert. Klicke auf das Ergebnis, um Details zu sehen.", diff --git a/src/main/webapp/i18n/de/result.json b/src/main/webapp/i18n/de/result.json index c97ee41d3d33..933db7e69593 100644 --- a/src/main/webapp/i18n/de/result.json +++ b/src/main/webapp/i18n/de/result.json @@ -92,7 +92,8 @@ "preliminary": "vorläufig", "preliminaryTooltip": "Dein Ergebnis ist noch nicht endgültig, weil weitere Tests nach der Einreichungsfrist ausgeführt werden.", "preliminaryTooltipSemiAutomatic": "Dein Ergebnis ist noch nicht endgültig, weil weitere Tests nach der Einreichungsfrist ausgeführt werden oder eine manuelle Bewertung aussteht.", - "codeIssuesTooltip": "Die automatische Codeanalyse hat Code-Issues gefunden.", + "preliminaryTooltipAthena": "Dies ist eine KI-Bewertung. Das tatsächliche Ergebnis kann abweichen.", + "codeIssuesTooltip": "Die automatische Codeanalyse hat Codeissues gefunden.", "noResultDetails": "Keine weiteren Informationen verfügbar für dieses Ergebnis.", "onlyCompilationTested": "Dein Code kompiliert erfolgreich. Derzeit sind keine Testfälle sichtbar.", "chart": { diff --git a/src/main/webapp/i18n/en/exercise.json b/src/main/webapp/i18n/en/exercise.json index 6a06631fc343..f50e61efcbb8 100644 --- a/src/main/webapp/i18n/en/exercise.json +++ b/src/main/webapp/i18n/en/exercise.json @@ -168,7 +168,7 @@ "resumeProgrammingExercise": "The exercise has been resumed. You can now continue working on the exercise!", "feedbackRequestSent": "Your feedback request has been sent.", "feedbackRequestAlreadySent": "Your feedback request has already been sent.", - "notEnoughPoints": "You have to submit your work at least once.", + "noSubmissionExists": "You have to submit your work at least once.", "lockRepositoryWarning": "Your repository will be locked. You can only continue working after you receive an answer.", "feedbackRequestAfterDueDate": "You cannot submit feedback requests after the due date.", "maxAthenaResultsReached": "You have reached the maximum number of AI feedback requests.", diff --git a/src/main/webapp/i18n/en/result.json b/src/main/webapp/i18n/en/result.json index 67bdc0711adb..f8178c0cde1e 100644 --- a/src/main/webapp/i18n/en/result.json +++ b/src/main/webapp/i18n/en/result.json @@ -92,6 +92,7 @@ "preliminary": "preliminary", "preliminaryTooltip": "Your result is not final yet, because more tests will be executed after the due date", "preliminaryTooltipSemiAutomatic": "Your result is not final yet, because more tests will be executed after the due date or a manual assessment will be done.", + "preliminaryTooltipAthena": "This is an AI grading. The actual result may differ", "codeIssuesTooltip": "The automatic code analysis generated some warnings for your code.", "noResultDetails": "No result details available.", "onlyCompilationTested": "Your code compiled successfully. There are currently no tests visible.", diff --git a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java index 1162e7b3477d..04df504a5dbb 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exercise/participation/ParticipationIntegrationTest.java @@ -3,7 +3,6 @@ import static de.tum.cit.aet.artemis.core.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; import static de.tum.cit.aet.artemis.core.util.TestResourceUtils.HalfSecond; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.doNothing; @@ -538,7 +537,51 @@ void requestFeedbackAlreadySent() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void requestFeedbackSuccess_withAthenaSuccess() throws Exception { + void requestProgrammingFeedbackIfARequestAlreadySent_withAthenaSuccess() throws Exception { + + var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); + course.setRestrictedAthenaModulesAccess(true); + this.courseRepository.save(course); + + this.programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); + this.exerciseRepository.save(programmingExercise); + + athenaRequestMockProvider.mockGetFeedbackSuggestionsAndExpect("programming"); + + var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, + userUtilService.getUserByLogin(TEST_PREFIX + "student1")); + + var localRepo = new LocalRepository(defaultBranch); + localRepo.configureRepos("testLocalRepo", "testOriginRepo"); + + participation.setRepositoryUri(ParticipationFactory.getMockFileRepositoryUri(localRepo).getURI().toString()); + participationRepo.save(participation); + + gitService.getDefaultLocalPathOfRepo(participation.getVcsRepositoryUri()); + + Result result1 = participationUtilService.createSubmissionAndResult(participation, 100, false); + Result result2 = participationUtilService.addResultToParticipation(participation, result1.getSubmission()); + result2.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); + result2.setSuccessful(null); + resultRepository.save(result2); + + request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, ProgrammingExerciseStudentParticipation.class, HttpStatus.OK); + + verify(programmingMessagingService, timeout(2000).times(2)).notifyUserAboutNewResult(resultCaptor.capture(), any()); + + Result invokedResult = resultCaptor.getAllValues().getFirst(); + assertThat(invokedResult).isNotNull(); + assertThat(invokedResult.getId()).isNotNull(); + assertThat(invokedResult.isSuccessful()).isTrue(); + assertThat(invokedResult.isAthenaBased()).isTrue(); + assertThat(invokedResult.getFeedbacks()).hasSize(1); + + localRepo.resetLocalRepo(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void requestProgrammingFeedbackSuccess_withAthenaSuccess() throws Exception { var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); course.setRestrictedAthenaModulesAccess(true); @@ -566,9 +609,6 @@ void requestFeedbackSuccess_withAthenaSuccess() throws Exception { result2.setCompletionDate(ZonedDateTime.now()); resultRepository.save(result2); - doNothing().when(programmingExerciseParticipationService).lockStudentRepositoryAndParticipation(eq(programmingExercise), any()); - doNothing().when(programmingExerciseParticipationService).unlockStudentRepositoryAndParticipation(any()); - request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, ProgrammingExerciseStudentParticipation.class, HttpStatus.OK); verify(programmingMessagingService, timeout(2000).times(2)).notifyUserAboutNewResult(resultCaptor.capture(), any()); @@ -577,7 +617,7 @@ void requestFeedbackSuccess_withAthenaSuccess() throws Exception { assertThat(invokedResult).isNotNull(); assertThat(invokedResult.getId()).isNotNull(); assertThat(invokedResult.isSuccessful()).isTrue(); - assertThat(invokedResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedResult.isAthenaBased()).isTrue(); assertThat(invokedResult.getFeedbacks()).hasSize(1); localRepo.resetLocalRepo(); @@ -614,7 +654,7 @@ void requestTextFeedbackSuccess_withAthenaSuccess() throws Exception { Result invokedTextResult = resultCaptor.getAllValues().get(1); assertThat(invokedTextResult).isNotNull(); assertThat(invokedTextResult.getId()).isNotNull(); - assertThat(invokedTextResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedTextResult.isAthenaBased()).isTrue(); assertThat(invokedTextResult.getFeedbacks()).hasSize(1); } @@ -649,13 +689,13 @@ void requestModelingFeedbackSuccess_withAthenaSuccess() throws Exception { Result invokedModelingResult = resultCaptor.getAllValues().get(1); assertThat(invokedModelingResult).isNotNull(); assertThat(invokedModelingResult.getId()).isNotNull(); - assertThat(invokedModelingResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedModelingResult.isAthenaBased()).isTrue(); assertThat(invokedModelingResult.getFeedbacks()).hasSize(1); } @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void requestFeedbackSuccess_withAthenaFailure() throws Exception { + void requestProgrammingFeedbackSuccess_withAthenaFailure() throws Exception { var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); course.setRestrictedAthenaModulesAccess(true); @@ -682,9 +722,6 @@ void requestFeedbackSuccess_withAthenaFailure() throws Exception { result2.setCompletionDate(ZonedDateTime.now()); resultRepository.save(result2); - doNothing().when(programmingExerciseParticipationService).lockStudentRepositoryAndParticipation(any(), any()); - doNothing().when(programmingExerciseParticipationService).unlockStudentRepositoryAndParticipation(any()); - request.putWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, ProgrammingExerciseStudentParticipation.class, HttpStatus.OK); verify(programmingMessagingService, timeout(2000).times(2)).notifyUserAboutNewResult(resultCaptor.capture(), any()); @@ -693,7 +730,7 @@ void requestFeedbackSuccess_withAthenaFailure() throws Exception { assertThat(invokedResult).isNotNull(); assertThat(invokedResult.getId()).isNotNull(); assertThat(invokedResult.isSuccessful()).isFalse(); - assertThat(invokedResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedResult.isAthenaBased()).isTrue(); assertThat(invokedResult.getFeedbacks()).hasSize(0); localRepo.resetLocalRepo(); @@ -729,7 +766,7 @@ void requestTextFeedbackSuccess_withAthenaFailure() throws Exception { Result invokedTextResult = resultCaptor.getAllValues().getFirst(); assertThat(invokedTextResult).isNotNull(); - assertThat(invokedTextResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedTextResult.isAthenaBased()).isTrue(); assertThat(invokedTextResult.getFeedbacks()).hasSize(0); } @@ -763,7 +800,7 @@ void requestModelingFeedbackSuccess_withAthenaFailure() throws Exception { Result invokedModelingResult = resultCaptor.getAllValues().getFirst(); assertThat(invokedModelingResult).isNotNull(); - assertThat(invokedModelingResult.isAthenaAutomatic()).isTrue(); + assertThat(invokedModelingResult.isAthenaBased()).isTrue(); assertThat(invokedModelingResult.getFeedbacks()).hasSize(0); } @@ -1615,7 +1652,7 @@ void whenFeedbackRequestedAndDeadlinePassed_thenFail() throws Exception { result.setCompletionDate(ZonedDateTime.now()); resultRepository.save(result); - request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "preconditions not met"); + request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "feedbackRequestAfterDueDate"); localRepo.resetLocalRepo(); } @@ -1643,50 +1680,14 @@ void whenFeedbackRequestedAndRateLimitExceeded_thenFail() throws Exception { resultRepository.save(result); // generate 5 athena results - for (int i = 0; i < 5; i++) { - var athenaResult = ParticipationFactory.generateResult(false, 100).participation(participation); - athenaResult.setCompletionDate(ZonedDateTime.now()); - athenaResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); - resultRepository.save(athenaResult); - } - - request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "preconditions not met"); - - localRepo.resetLocalRepo(); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void whenFeedbackRequestedAndRateLimitStillUnknownDueRequestsInProgress_thenFail() throws Exception { - - programmingExercise.setDueDate(ZonedDateTime.now().plusDays(100)); - programmingExercise = exerciseRepository.save(programmingExercise); - - var participation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INACTIVE, programmingExercise, - userUtilService.getUserByLogin(TEST_PREFIX + "student1")); - - var localRepo = new LocalRepository(defaultBranch); - localRepo.configureRepos("testLocalRepo", "testOriginRepo"); - - participation.setRepositoryUri(ParticipationFactory.getMockFileRepositoryUri(localRepo).getURI().toString()); - participationRepo.save(participation); - - gitService.getDefaultLocalPathOfRepo(participation.getVcsRepositoryUri()); - - var result = ParticipationFactory.generateResult(false, 100).participation(participation); - result.setCompletionDate(ZonedDateTime.now()); - resultRepository.save(result); - - // generate 5 athena results - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 20; i++) { var athenaResult = ParticipationFactory.generateResult(false, 100).participation(participation); athenaResult.setCompletionDate(ZonedDateTime.now()); athenaResult.setAssessmentType(AssessmentType.AUTOMATIC_ATHENA); - athenaResult.setSuccessful(null); resultRepository.save(athenaResult); } - request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "preconditions not met"); + request.putAndExpectError("/api/exercises/" + programmingExercise.getId() + "/request-feedback", null, HttpStatus.BAD_REQUEST, "maxAthenaResultsReached"); localRepo.resetLocalRepo(); } diff --git a/src/test/javascript/spec/component/exam/participate/exam-navigation-sidebar.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exam-navigation-sidebar.component.spec.ts index 45ce49fc2b4d..3a9102c72e8e 100644 --- a/src/test/javascript/spec/component/exam/participate/exam-navigation-sidebar.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exam-navigation-sidebar.component.spec.ts @@ -20,6 +20,7 @@ import { TranslateService } from '@ngx-translate/core'; import { facSaveSuccess, facSaveWarning } from 'src/main/webapp/content/icons/icons'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { ExamLiveEventsButtonComponent } from 'app/exam/participate/events/exam-live-events-button.component'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; describe('ExamNavigationSidebarComponent', () => { let fixture: ComponentFixture; @@ -33,7 +34,7 @@ describe('ExamNavigationSidebarComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ArtemisTestModule, TranslateTestingModule, MockModule(NgbTooltipModule)], + imports: [ArtemisTestModule, TranslateTestingModule, MockModule(NgbTooltipModule), MockModule(ArtemisSharedCommonModule)], declarations: [ExamNavigationSidebarComponent, MockComponent(ExamTimerComponent), MockComponent(ExamLiveEventsButtonComponent)], providers: [ ExamParticipationService, diff --git a/src/test/javascript/spec/component/exercises/shared/result.spec.ts b/src/test/javascript/spec/component/exercises/shared/result.spec.ts index 72c2cd1aafca..ff076e80d8c8 100644 --- a/src/test/javascript/spec/component/exercises/shared/result.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/result.spec.ts @@ -12,7 +12,7 @@ import { TranslateService } from '@ngx-translate/core'; import { cloneDeep } from 'lodash-es'; import { Submission } from 'app/entities/submission.model'; import { ExerciseType } from 'app/entities/exercise.model'; -import { faQuestionCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; +import { faCheckCircle, faQuestionCircle, faTimesCircle } from '@fortawesome/free-regular-svg-icons'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import { ModelingExercise } from 'app/entities/modeling-exercise.model'; import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; @@ -141,11 +141,31 @@ describe('ResultComponent', () => { expect(component.result!.participation).toEqual(participation1); expect(component.submission).toEqual(submission1); expect(component.textColorClass).toBe('text-secondary'); - expect(component.resultIconClass).toEqual(faQuestionCircle); + expect(component.resultIconClass).toEqual(faCheckCircle); expect(component.resultString).toBe('artemisApp.result.resultString.short (artemisApp.result.preliminary)'); expect(component.templateStatus).toBe(ResultTemplateStatus.HAS_RESULT); }); + it('should set (automatic athena) results for programming exercise', () => { + const submission1: Submission = { id: 1 }; + const result1: Result = { id: 1, submission: submission1, score: 0.8, assessmentType: AssessmentType.AUTOMATIC_ATHENA, successful: true }; + const result2: Result = { id: 2 }; + const participation1 = cloneDeep(programmingParticipation); + participation1.results = [result1, result2]; + component.participation = participation1; + component.showUngradedResults = true; + + fixture.detectChanges(); + + expect(component.result).toEqual(result1); + expect(component.result!.participation).toEqual(participation1); + expect(component.submission).toEqual(submission1); + expect(component.textColorClass).toBe('text-secondary'); + expect(component.resultIconClass).toEqual(faCheckCircle); + expect(component.resultString).toBe('artemisApp.result.resultString.automaticAIFeedbackSuccessful (artemisApp.result.preliminary)'); + expect(component.templateStatus).toBe(ResultTemplateStatus.HAS_RESULT); + }); + it('should set (automatic athena) results for text exercise', () => { const submission1: Submission = { id: 1 }; const result1: Result = { id: 1, submission: submission1, score: 1, assessmentType: AssessmentType.AUTOMATIC_ATHENA, successful: true }; @@ -161,7 +181,7 @@ describe('ResultComponent', () => { expect(component.result!.participation).toEqual(participation1); expect(component.submission).toEqual(submission1); expect(component.textColorClass).toBe('text-secondary'); - expect(component.resultIconClass).toEqual(faQuestionCircle); + expect(component.resultIconClass).toEqual(faCheckCircle); expect(component.resultString).toBe('artemisApp.result.resultString.short (artemisApp.result.preliminary)'); }); diff --git a/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts b/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts new file mode 100644 index 000000000000..94219fdd2420 --- /dev/null +++ b/src/test/javascript/spec/component/overview/exercise-details/request-feedback-button/request-feedback-button.component.spec.ts @@ -0,0 +1,228 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { MockProvider } from 'ng-mocks'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { Observable, of } from 'rxjs'; +import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { StudentParticipation } from 'app/entities/participation/student-participation.model'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { AlertService } from 'app/core/util/alert.service'; +import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component'; +import { ArtemisTestModule } from '../../../../test.module'; +import { MockProfileService } from '../../../../helpers/mocks/service/mock-profile.service'; +import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; + +describe('RequestFeedbackButtonComponent', () => { + let component: RequestFeedbackButtonComponent; + let fixture: ComponentFixture; + let debugElement: DebugElement; + let profileService: ProfileService; + let alertService: AlertService; + let courseExerciseService: CourseExerciseService; + let exerciseService: ExerciseService; + + beforeEach(() => { + return TestBed.configureTestingModule({ + imports: [ArtemisTestModule, RequestFeedbackButtonComponent], + providers: [{ provide: ProfileService, useClass: MockProfileService }, MockProvider(HttpClient)], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(RequestFeedbackButtonComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + courseExerciseService = debugElement.injector.get(CourseExerciseService); + exerciseService = debugElement.injector.get(ExerciseService); + profileService = debugElement.injector.get(ProfileService); + alertService = debugElement.injector.get(AlertService); + }); + }); + + function setAthenaEnabled(enabled: boolean) { + jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of({ activeProfiles: enabled ? ['athena'] : [] } as ProfileInfo)); + } + + function mockExerciseDetails(exercise: Exercise) { + jest.spyOn(exerciseService, 'getExerciseDetails').mockReturnValue(of(new HttpResponse({ body: { exercise: exercise } }))); + } + + it('should handle errors when requestFeedback fails', fakeAsync(() => { + setAthenaEnabled(true); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: true }], + testRun: false, + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.TEXT, course: undefined, studentParticipations: [participation] } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + mockExerciseDetails(exercise); + + jest.spyOn(courseExerciseService, 'requestFeedback').mockReturnValue( + new Observable((subscriber) => { + subscriber.error({ error: { errorKey: 'someError' } }); + }), + ); + jest.spyOn(alertService, 'error'); + + component.requestFeedback(); + tick(); + + expect(alertService.error).toHaveBeenCalledWith('artemisApp.exercise.someError'); + })); + + it('should display the button when Athena is enabled and it is not an exam exercise', fakeAsync(() => { + setAthenaEnabled(true); + const exercise = { id: 1, type: ExerciseType.TEXT, course: {} } as Exercise; // course undefined means exam exercise + fixture.componentRef.setInput('exercise', exercise); + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + expect(button.nativeElement.disabled).toBeTrue(); + })); + + it('should not display the button when it is an exam exercise', fakeAsync(() => { + setAthenaEnabled(true); + fixture.componentRef.setInput('exercise', { id: 1, type: ExerciseType.TEXT, course: undefined } as Exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + const link = debugElement.query(By.css('a')); + expect(button).toBeNull(); + expect(link).toBeNull(); + })); + + it('should disable the button when participation is missing', fakeAsync(() => { + setAthenaEnabled(true); + const exercise = { id: 1, type: ExerciseType.TEXT, course: {}, studentParticipations: undefined } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + expect(button.nativeElement.disabled).toBeTrue(); + })); + + it('should display the correct button label and style when Athena is enabled', fakeAsync(() => { + setAthenaEnabled(true); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: true }], + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.TEXT, course: {}, studentParticipations: [participation] } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + component.isExamExercise = false; + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + + const span = button.query(By.css('span')); + expect(span.nativeElement.textContent).toContain('artemisApp.exerciseActions.requestAutomaticFeedback'); + })); + + it('should call requestFeedback() when button is clicked', fakeAsync(() => { + setAthenaEnabled(true); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: false }], + testRun: false, + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.PROGRAMMING, studentParticipations: [participation], course: {} } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + jest.spyOn(component, 'requestFeedback'); + jest.spyOn(courseExerciseService, 'requestFeedback').mockReturnValue( + new Observable((subscriber) => { + subscriber.next(); + subscriber.complete(); + }), + ); + + const button = debugElement.query(By.css('a')); + button.nativeElement.click(); + tick(); + + expect(component.requestFeedback).toHaveBeenCalled(); + })); + + it('should show an alert when requestFeedback() is called and conditions are not satisfied', fakeAsync(() => { + setAthenaEnabled(true); + + const exercise = { id: 1, type: ExerciseType.TEXT, course: {} } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + + jest.spyOn(component, 'hasAthenaResultForLatestSubmission').mockReturnValue(true); + jest.spyOn(alertService, 'warning'); + + component.requestFeedback(); + + expect(alertService.warning).toHaveBeenCalled(); + })); + + it('should disable the button if latest submission is not submitted or feedback is generating', fakeAsync(() => { + setAthenaEnabled(true); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: false }], + testRun: false, + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.TEXT, studentParticipations: [participation], course: {} } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + fixture.componentRef.setInput('isGeneratingFeedback', false); + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + expect(button.nativeElement.disabled).toBeTrue(); + })); + + it('should enable the button if latest submission is submitted and feedback is not generating', fakeAsync(() => { + setAthenaEnabled(true); + const participation = { + id: 1, + submissions: [{ id: 1, submitted: true }], + testRun: false, + } as StudentParticipation; + const exercise = { id: 1, type: ExerciseType.TEXT, course: {}, studentParticipations: [participation] } as Exercise; + fixture.componentRef.setInput('exercise', exercise); + fixture.componentRef.setInput('isGeneratingFeedback', false); + mockExerciseDetails(exercise); + + component.ngOnInit(); + tick(); + fixture.detectChanges(); + + const button = debugElement.query(By.css('button')); + expect(button).not.toBeNull(); + expect(button.nativeElement.disabled).toBeFalse(); + })); +}); diff --git a/src/test/javascript/spec/component/shared/result.component.spec.ts b/src/test/javascript/spec/component/shared/result.component.spec.ts index d72fdc5f3bd2..e013c1e03e58 100644 --- a/src/test/javascript/spec/component/shared/result.component.spec.ts +++ b/src/test/javascript/spec/component/shared/result.component.spec.ts @@ -370,7 +370,6 @@ describe('ResultComponent', () => { it('should use special handling if result is an automatic AI result', () => { comp.result = { ...mockResult, score: 90, assessmentType: AssessmentType.AUTOMATIC_ATHENA }; - jest.spyOn(Result, 'isAthenaAIResult').mockReturnValue(true); comp.evaluate(); diff --git a/src/test/javascript/spec/component/utils/result.utils.spec.ts b/src/test/javascript/spec/component/utils/result.utils.spec.ts index 4796766de234..24303c3e59af 100644 --- a/src/test/javascript/spec/component/utils/result.utils.spec.ts +++ b/src/test/javascript/spec/component/utils/result.utils.spec.ts @@ -15,6 +15,7 @@ import { faCheckCircle, faQuestionCircle, faTimesCircle } from '@fortawesome/fre import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'; import { ExerciseType } from 'app/entities/exercise.model'; import { Result } from 'app/entities/result.model'; +import dayjs from 'dayjs/esm'; describe('ResultUtils', () => { it('should filter out all non unreferenced feedbacks', () => { @@ -69,7 +70,7 @@ describe('ResultUtils', () => { { result: { score: 0, successful: undefined, assessmentType: AssessmentType.AUTOMATIC_ATHENA }, templateStatus: ResultTemplateStatus.IS_GENERATING_FEEDBACK, - expected: 'text-primary', + expected: 'text-secondary', }, { result: { score: 0, successful: true, assessmentType: AssessmentType.AUTOMATIC_ATHENA }, @@ -128,7 +129,12 @@ describe('ResultUtils', () => { expected: faTimesCircle, }, { - result: { feedbacks: [{ type: FeedbackType.AUTOMATIC, text: 'AI result being generated test case' }], assessmentType: AssessmentType.AUTOMATIC_ATHENA }, + result: { + feedbacks: [{ type: FeedbackType.AUTOMATIC, text: 'AI result being generated test case' }], + assessmentType: AssessmentType.AUTOMATIC_ATHENA, + successful: undefined, + completionDate: dayjs().add(5, 'minutes'), + }, templateStatus: ResultTemplateStatus.IS_GENERATING_FEEDBACK, expected: faCircleNotch, }, @@ -138,9 +144,10 @@ describe('ResultUtils', () => { participation: { type: ParticipationType.STUDENT, exercise: { type: ExerciseType.TEXT } }, successful: true, assessmentType: AssessmentType.AUTOMATIC_ATHENA, + completionDate: dayjs().subtract(5, 'minutes'), } as Result, templateStatus: ResultTemplateStatus.HAS_RESULT, - expected: faQuestionCircle, + expected: faCheckCircle, }, { result: { @@ -148,9 +155,10 @@ describe('ResultUtils', () => { participation: { type: ParticipationType.STUDENT, exercise: { type: ExerciseType.TEXT } }, successful: false, assessmentType: AssessmentType.AUTOMATIC_ATHENA, + completionDate: dayjs().subtract(5, 'minutes'), } as Result, templateStatus: ResultTemplateStatus.HAS_RESULT, - expected: faQuestionCircle, + expected: faTimesCircle, }, ])('should correctly determine result icon', ({ result, templateStatus, expected }) => { expect(getResultIconClass(result, templateStatus!)).toBe(expected); diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts index 406dce4f6a5c..d55cc49a6983 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-container.integration.spec.ts @@ -74,6 +74,7 @@ import { CodeEditorHeaderComponent } from 'app/exercises/programming/shared/code import { AlertService } from 'app/core/util/alert.service'; import { MockResizeObserver } from '../../helpers/mocks/service/mock-resize-observer'; import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; +import { RequestFeedbackButtonComponent } from 'app/overview/exercise-details/request-feedback-button/request-feedback-button.component'; import { MonacoEditorComponent } from '../../../../../main/webapp/app/shared/monaco-editor/monaco-editor.component'; describe('CodeEditorContainerIntegration', () => { @@ -123,6 +124,7 @@ describe('CodeEditorContainerIntegration', () => { TreeviewItemComponent, MockPipe(ArtemisDatePipe), MockComponent(CodeEditorTutorAssessmentInlineFeedbackComponent), + MockComponent(RequestFeedbackButtonComponent), ], providers: [ CodeEditorConflictStateService, diff --git a/src/test/javascript/spec/service/result.service.spec.ts b/src/test/javascript/spec/service/result.service.spec.ts index 7bd887f9cb9e..e6c24cf75850 100644 --- a/src/test/javascript/spec/service/result.service.spec.ts +++ b/src/test/javascript/spec/service/result.service.spec.ts @@ -307,8 +307,10 @@ describe('ResultService', () => { it('should return correct string for Athena non graded successful feedback', () => { programmingExercise.assessmentDueDate = dayjs().subtract(5, 'minutes'); - expect(resultService.getResultString(result6, programmingExercise)).toBe('artemisApp.result.resultString.automaticAIFeedbackSuccessful'); - expect(translateServiceSpy).toHaveBeenCalledOnce(); + expect(resultService.getResultString(result6, programmingExercise)).toBe( + 'artemisApp.result.resultString.automaticAIFeedbackSuccessful (artemisApp.result.preliminary)', + ); + expect(translateServiceSpy).toHaveBeenCalledTimes(2); }); it('should return correct string for Athena non graded unsuccessful feedback', () => { From ced37c15469d9eeba275c227f677206b6332d48f Mon Sep 17 00:00:00 2001 From: Lucas Welscher Date: Sat, 12 Oct 2024 10:26:32 +0200 Subject: [PATCH 04/14] Development: Fix wrong result subscription for exam exercises (#9453) --- .../artemis/core/config/websocket/WebsocketConfiguration.java | 4 +--- .../participate/summary/exam-result-summary.component.html | 1 + .../programming-exam-summary.component.html | 2 +- .../programming-exam-summary.component.ts | 2 ++ 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java index 9163cfb7d7f1..d0c6941cc698 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java @@ -64,7 +64,6 @@ import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.exam.repository.ExamRepository; -import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository; import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository; @@ -309,8 +308,7 @@ private boolean allowSubscription(@Nullable Principal principal, String destinat // TODO: Is it right that TAs are not allowed to subscribe to exam exercises? if (exerciseRepository.isExamExercise(exerciseId)) { - Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId); - return authorizationCheckService.isAtLeastInstructorInCourse(login, exercise.getCourseViaExerciseGroupOrCourseMember().getId()); + return authorizationCheckService.isAtLeastInstructorInExercise(login, exerciseId); } else { return authorizationCheckService.isAtLeastTeachingAssistantInExercise(login, exerciseId); diff --git a/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html b/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html index f1017919bfb7..44eea193c092 100644 --- a/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html +++ b/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html @@ -190,6 +190,7 @@

[resultsPublished]="resultsArePublished" [isPrinting]="isPrinting" [isAfterResultsArePublished]="resultsArePublished" + [instructorView]="instructorView" /> } } diff --git a/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html b/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html index c483a168cd8a..2a0df39f1353 100644 --- a/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html +++ b/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.html @@ -48,7 +48,7 @@
@if (exercise.problemStatement) { - + }
diff --git a/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.ts b/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.ts index 2243c413644a..5ffdffc38018 100644 --- a/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.ts +++ b/src/main/webapp/app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component.ts @@ -39,6 +39,8 @@ export class ProgrammingExamSummaryComponent implements OnInit { @Input() isAfterResultsArePublished?: boolean = false; + @Input() instructorView?: boolean = false; + readonly PROGRAMMING: ExerciseType = ExerciseType.PROGRAMMING; protected readonly AssessmentType = AssessmentType; From 6eaebc2f0f9e3214de22607ebf47b627fddcf928 Mon Sep 17 00:00:00 2001 From: Ole Vester <73833780+ole-ve@users.noreply.github.com> Date: Sat, 12 Oct 2024 10:29:31 +0200 Subject: [PATCH 05/14] Development: Fix issues with server test flakiness (#9417) --- .../PushNotificationDeviceConfiguration.java | 4 +- .../artemis/core/service/ZipFileService.java | 9 +++- .../ExerciseScoresChartIntegrationTest.java | 7 --- .../ParticipantScoreIntegrationTest.java | 7 --- .../ResultListenerIntegrationTest.java | 7 --- .../CourseCompetencyIntegrationTest.java | 2 - .../NotificationScheduleServiceTest.java | 2 - .../SingleUserNotificationServiceTest.java | 47 +++++++++++++------ .../TutorialGroupNotificationServiceTest.java | 5 -- .../service/EmailSummaryServiceTest.java | 3 -- ...DeviceConfigurationCleanupServiceTest.java | 6 +-- .../artemis/core/MetricsIntegrationTest.java | 5 -- .../core/StatisticsIntegrationTest.java | 1 - .../aet/artemis/exam/ExamIntegrationTest.java | 1 - .../ExamParticipationIntegrationTest.java | 1 - .../exam/ExamRegistrationIntegrationTest.java | 1 - .../cit/aet/artemis/exam/ExamStartTest.java | 1 - .../exam/ProgrammingExamIntegrationTest.java | 1 - .../artemis/exam/TestExamIntegrationTest.java | 1 - .../CourseGitlabJenkinsIntegrationTest.java | 1 - ...rogrammingExerciseTestCaseServiceTest.java | 3 +- ...gExerciseGitDiffReportIntegrationTest.java | 5 ++ ...mmingExerciseGitDiffReportServiceTest.java | 5 ++ .../hestia/StructuralTestCaseServiceTest.java | 5 ++ .../TestwiseCoverageReportServiceTest.java | 5 ++ .../BehavioralTestCaseServiceTest.java | 5 ++ ...AbstractLocalCILocalVCIntegrationTest.java | 19 ++++---- .../icl/LocalCIIntegrationTest.java | 12 ++++- .../icl/LocalCIResourceIntegrationTest.java | 10 +++- .../icl/LocalCIResultServiceTest.java | 7 +++ .../icl/LocalVCIntegrationTest.java | 20 +++++++- .../icl/LocalVCLocalCIIntegrationTest.java | 7 +++ .../icl/LocalVCLocalCITestService.java | 14 +++++- .../icl/LocalVCSshIntegrationTest.java | 31 ++++++++++-- .../icl/MultipleHostKeyProviderTest.java | 7 +++ .../quiz/QuizSubmissionIntegrationTest.java | 14 ++++-- .../base/AbstractArtemisIntegrationTest.java | 11 +++++ 37 files changed, 201 insertions(+), 91 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java index c6bfd3384110..b9a911ce6194 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/push_notification/PushNotificationDeviceConfiguration.java @@ -106,7 +106,9 @@ public boolean equals(Object object) { return false; } PushNotificationDeviceConfiguration that = (PushNotificationDeviceConfiguration) object; - return token.equals(that.token) && deviceType == that.deviceType && expirationDate.equals(that.expirationDate) && Arrays.equals(secretKey, that.secretKey) + // Use compareTo rather than equals for dates to ensure timestamps and dates with the same time are considered equal + // This is caused by Java internal design having different classes for Date (java.util) and Timestamp (java.sql) + return token.equals(that.token) && deviceType == that.deviceType && expirationDate.compareTo(that.expirationDate) == 0 && Arrays.equals(secretKey, that.secretKey) && owner.equals(that.owner); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/ZipFileService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/ZipFileService.java index 4d40473c4eb9..5871cd7ed7d4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/ZipFileService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/ZipFileService.java @@ -6,6 +6,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; import java.util.zip.ZipEntry; @@ -33,6 +34,12 @@ public class ZipFileService { private final FileService fileService; + /** + * Set of file names that should be ignored when zipping. + * This currently only includes the gc.log.lock (garbage collector) file created by JGit in programming repositories. + */ + private static final Set IGNORED_ZIP_FILE_NAMES = Set.of(Path.of("gc.log.lock")); + public ZipFileService(FileService fileService) { this.fileService = fileService; } @@ -113,7 +120,7 @@ private void createZipFileFromPathStream(Path zipFilePath, Stream paths, P if (extraFilter != null) { filteredPaths = filteredPaths.filter(extraFilter); } - filteredPaths.forEach(path -> { + filteredPaths.filter(path -> !IGNORED_ZIP_FILE_NAMES.contains(path)).forEach(path -> { ZipEntry zipEntry = new ZipEntry(pathsRoot.relativize(path).toString()); copyToZipFile(zipOutputStream, path, zipEntry); }); diff --git a/src/test/java/de/tum/cit/aet/artemis/assessment/ExerciseScoresChartIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/assessment/ExerciseScoresChartIntegrationTest.java index fd48d4a5ace5..8782f11f8593 100644 --- a/src/test/java/de/tum/cit/aet/artemis/assessment/ExerciseScoresChartIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/assessment/ExerciseScoresChartIntegrationTest.java @@ -3,10 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import java.time.Instant; import java.time.ZonedDateTime; import java.util.List; -import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.AfterEach; @@ -15,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.util.ReflectionTestUtils; import de.tum.cit.aet.artemis.assessment.repository.ParticipantScoreRepository; import de.tum.cit.aet.artemis.assessment.service.ParticipantScoreScheduleService; @@ -60,11 +57,7 @@ class ExerciseScoresChartIntegrationTest extends AbstractSpringIntegrationIndepe @BeforeEach void setupTestScenario() { - // Prevents the ParticipantScoreScheduleService from scheduling tasks related to prior results - ReflectionTestUtils.setField(participantScoreScheduleService, "lastScheduledRun", Optional.of(Instant.now())); - ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 50; - participantScoreScheduleService.activate(); ZonedDateTime pastTimestamp = ZonedDateTime.now().minusDays(5); userUtilService.addUsers(TEST_PREFIX, 3, 2, 0, 0); Course course = courseUtilService.createCourse(); diff --git a/src/test/java/de/tum/cit/aet/artemis/assessment/ParticipantScoreIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/assessment/ParticipantScoreIntegrationTest.java index 85f521f00be5..1313f5147150 100644 --- a/src/test/java/de/tum/cit/aet/artemis/assessment/ParticipantScoreIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/assessment/ParticipantScoreIntegrationTest.java @@ -3,10 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import java.time.Instant; import java.time.ZonedDateTime; import java.util.List; -import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.AfterEach; @@ -15,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.util.ReflectionTestUtils; import de.tum.cit.aet.artemis.assessment.domain.GradingScale; import de.tum.cit.aet.artemis.assessment.dto.score.ScoreDTO; @@ -98,11 +95,7 @@ class ParticipantScoreIntegrationTest extends AbstractSpringIntegrationLocalCILo @BeforeEach void setupTestScenario() { - // Prevents the ParticipantScoreScheduleService from scheduling tasks related to prior results - ReflectionTestUtils.setField(participantScoreScheduleService, "lastScheduledRun", Optional.of(Instant.now())); - ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; - participantScoreScheduleService.activate(); ZonedDateTime pastTimestamp = ZonedDateTime.now().minusDays(5); // creating the users student1, tutor1 and instructors1 userUtilService.addUsers(TEST_PREFIX, 1, 1, 0, 1); diff --git a/src/test/java/de/tum/cit/aet/artemis/assessment/ResultListenerIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/assessment/ResultListenerIntegrationTest.java index 4877e18e8c99..dffe1451edf7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/assessment/ResultListenerIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/assessment/ResultListenerIntegrationTest.java @@ -4,10 +4,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import java.time.Instant; import java.time.ZonedDateTime; import java.util.List; -import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.AfterEach; @@ -17,7 +15,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.util.ReflectionTestUtils; import de.tum.cit.aet.artemis.assessment.domain.ParticipantScore; import de.tum.cit.aet.artemis.assessment.domain.Result; @@ -84,11 +81,7 @@ void cleanup() { @BeforeEach void setupTestScenario() { - // Prevents the ParticipantScoreScheduleService from scheduling tasks related to prior results - ReflectionTestUtils.setField(participantScoreScheduleService, "lastScheduledRun", Optional.of(Instant.now())); - ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 100; - participantScoreScheduleService.activate(); ZonedDateTime pastReleaseDate = ZonedDateTime.now().minusDays(5); ZonedDateTime pastDueDate = ZonedDateTime.now().minusDays(3); ZonedDateTime pastAssessmentDueDate = ZonedDateTime.now().minusDays(2); diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java index 748a1f4d5b0f..df2c561bda7e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/CourseCompetencyIntegrationTest.java @@ -50,8 +50,6 @@ class CourseCompetencyIntegrationTest extends AbstractCompetencyPrerequisiteInte @BeforeEach void setupTestScenario() { super.setupTestScenario(TEST_PREFIX, course -> competencyUtilService.createCompetency(course, "penguin")); - - participantScoreScheduleService.activate(); } private Result createExerciseParticipationSubmissionAndResult(Exercise exercise, StudentParticipation studentParticipation, double pointsOfExercise, diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationScheduleServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationScheduleServiceTest.java index 21b6a8abdaed..c49163742ba4 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationScheduleServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/NotificationScheduleServiceTest.java @@ -6,7 +6,6 @@ import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anySet; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; @@ -81,7 +80,6 @@ void init() { exercise.setMaxPoints(5.0); exerciseRepository.saveAndFlush(exercise); - doNothing().when(javaMailSender).send(any(MimeMessage.class)); sizeBefore = notificationRepository.count(); } diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java index 9a3f7db09aff..347438c89a97 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java @@ -22,12 +22,14 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.MESSAGE_REPLY_IN_CONVERSATION_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_PLAGIARISM_CASE_STUDENT_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_ASSIGNED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_ASSIGNED_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_STUDENT_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_TUTOR_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_MULTIPLE_TUTOR_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_STUDENT_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_TUTOR_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_UNASSIGNED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_UNASSIGNED_TITLE; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsService.NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsService.NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_FAILED; @@ -40,7 +42,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anySet; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; @@ -63,6 +64,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.springframework.beans.factory.annotation.Autowired; import de.tum.cit.aet.artemis.assessment.domain.AssessmentType; @@ -132,6 +134,12 @@ class SingleUserNotificationServiceTest extends AbstractSpringIntegrationIndepen @Autowired private ParticipationUtilService participationUtilService; + @Captor + private ArgumentCaptor appleNotificationCaptor; + + @Captor + private ArgumentCaptor firebaseNotificationCaptor; + private User user; private User userTwo; @@ -263,8 +271,6 @@ void setUp() { dataExport = new DataExport(); dataExport.setUser(user); - - doNothing().when(javaMailSender).send(any(MimeMessage.class)); } /** @@ -273,8 +279,10 @@ void setUp() { * @param expectedNotificationTitle is the title (NotificationTitleTypeConstants) of the expected notification */ private void verifyRepositoryCallWithCorrectNotification(String expectedNotificationTitle) { - Notification capturedNotification = notificationRepository.findAll().getFirst(); - assertThat(capturedNotification.getTitle()).as("Title of the captured notification should be equal to the expected one").isEqualTo(expectedNotificationTitle); + List capturedNotifications = notificationRepository.findAll(); + assertThat(capturedNotifications).isNotEmpty(); + List relevantNotifications = capturedNotifications.stream().filter(e -> e.getTitle().equals(expectedNotificationTitle)).toList(); + assertThat(relevantNotifications).as("Title of the captured notification should be equal to the expected one").hasSize(1); } /// General notify Tests @@ -531,24 +539,24 @@ void testTutorialGroupNotifications_tutorDeregistration() { @Test void testTutorialGroupNotifications_groupAssigned() { notificationSettingRepository.deleteAll(); - notificationSettingRepository - .save(new NotificationSetting(tutorialGroup.getTeachingAssistant(), true, true, true, NOTIFICATION__TUTOR_NOTIFICATION__TUTORIAL_GROUP_ASSIGN_UNASSIGN)); + User teachingAssistant = tutorialGroup.getTeachingAssistant(); + notificationSettingRepository.save(new NotificationSetting(teachingAssistant, true, true, true, NOTIFICATION__TUTOR_NOTIFICATION__TUTORIAL_GROUP_ASSIGN_UNASSIGN)); singleUserNotificationService.notifyTutorAboutAssignmentToTutorialGroup(tutorialGroup, tutorialGroup.getTeachingAssistant(), userThree); verifyRepositoryCallWithCorrectNotification(TUTORIAL_GROUP_ASSIGNED_TITLE); verifyEmail(); - verifyPush(1); + verifyPush(1, TUTORIAL_GROUP_ASSIGNED_TEXT, teachingAssistant); } @Test void testTutorialGroupNotifications_groupUnassigned() { notificationSettingRepository.deleteAll(); - notificationSettingRepository - .save(new NotificationSetting(tutorialGroup.getTeachingAssistant(), true, true, true, NOTIFICATION__TUTOR_NOTIFICATION__TUTORIAL_GROUP_ASSIGN_UNASSIGN)); + User teachingAssistant = tutorialGroup.getTeachingAssistant(); + notificationSettingRepository.save(new NotificationSetting(teachingAssistant, true, true, true, NOTIFICATION__TUTOR_NOTIFICATION__TUTORIAL_GROUP_ASSIGN_UNASSIGN)); singleUserNotificationService.notifyTutorAboutUnassignmentFromTutorialGroup(tutorialGroup, tutorialGroup.getTeachingAssistant(), userThree); verifyRepositoryCallWithCorrectNotification(TUTORIAL_GROUP_UNASSIGNED_TITLE); verifyEmail(); - verifyPush(1); + verifyPush(1, TUTORIAL_GROUP_UNASSIGNED_TEXT, teachingAssistant); } @Test @@ -579,9 +587,20 @@ private void verifyEmail() { * * @param times how often the email should have been sent */ - private void verifyPush(int times) { - verify(applePushNotificationService, timeout(1500).times(times)).sendNotification(any(Notification.class), anySet(), any(Object.class)); - verify(firebasePushNotificationService, timeout(1500).times(times)).sendNotification(any(Notification.class), anySet(), any(Object.class)); + private void verifyPush(int times, String text, User recipient) { + verify(applePushNotificationService, timeout(1500).atLeast(times)).sendNotification(appleNotificationCaptor.capture(), anySet(), any(Object.class)); + verify(firebasePushNotificationService, timeout(1500).atLeast(times)).sendNotification(firebaseNotificationCaptor.capture(), anySet(), any(Object.class)); + + List appleNotifications = filterRelevantNotifications(appleNotificationCaptor.getAllValues(), text, recipient); + assertThat(appleNotifications).as(times + " Apple notifications should have been sent").hasSize(times); + + List firebaseNotifications = filterRelevantNotifications(firebaseNotificationCaptor.getAllValues(), text, recipient); + assertThat(firebaseNotifications).as(times + " Firebase notifications should have been sent").hasSize(times); + } + + private List filterRelevantNotifications(List notifications, String title, User recipient) { + return notifications.stream().filter(notification -> notification instanceof SingleUserNotification).map(notification -> (SingleUserNotification) notification) + .filter(notification -> title.equals(notification.getText()) && recipient.getId().equals(notification.getRecipient().getId())).toList(); } private static Stream getNotificationTypesAndTitlesParametersForGroupChat() { diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/TutorialGroupNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/TutorialGroupNotificationServiceTest.java index 23ad94f5a00f..26816a87c239 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/TutorialGroupNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notifications/service/TutorialGroupNotificationServiceTest.java @@ -4,8 +4,6 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_UPDATED_TITLE; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsService.NOTIFICATION__TUTORIAL_GROUP_NOTIFICATION__TUTORIAL_GROUP_DELETE_UPDATE; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; @@ -15,8 +13,6 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import jakarta.mail.internet.MimeMessage; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -86,7 +82,6 @@ void setUp() { userRepository.findOneByLogin(TEST_PREFIX + "tutor1").orElseThrow(), IntStream.range(1, STUDENT_COUNT + 1) .mapToObj((studentId) -> userRepository.findOneByLogin(TEST_PREFIX + "student" + studentId).orElseThrow()).collect(Collectors.toSet())); - doNothing().when(javaMailSender).send(any(MimeMessage.class)); tutorialGroupNotificationRepository.deleteAll(); notificationSettingRepository.deleteAll(); } diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/service/EmailSummaryServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/service/EmailSummaryServiceTest.java index 4a42a5a826a5..a54be369017e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/service/EmailSummaryServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/service/EmailSummaryServiceTest.java @@ -3,7 +3,6 @@ import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsService.NOTIFICATION__WEEKLY_SUMMARY__BASIC_WEEKLY_SUMMARY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; @@ -118,8 +117,6 @@ void setUp() { exerciseRepository.saveAll(allTestExercises); weeklyEmailSummaryService.setScheduleInterval(Duration.ofDays(7)); - - doNothing().when(javaMailSender).send(any(MimeMessage.class)); } /** diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/service/PushNotificationDeviceConfigurationCleanupServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/service/PushNotificationDeviceConfigurationCleanupServiceTest.java index dc59407ff76b..c8b142c4dc59 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/service/PushNotificationDeviceConfigurationCleanupServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/service/PushNotificationDeviceConfigurationCleanupServiceTest.java @@ -1,10 +1,9 @@ package de.tum.cit.aet.artemis.communication.service; -import static org.springframework.test.util.AssertionErrors.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Set; @@ -54,6 +53,7 @@ void cleanupTest() { List result = deviceConfigurationRepository.findByUserIn(Set.of(user), PushNotificationDeviceType.FIREBASE); - assertEquals("The result is not correct", Collections.singletonList(valid), result); + assertThat(result).contains(valid); + assertThat(result).doesNotContain(expired); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/MetricsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/MetricsIntegrationTest.java index 2b1b73492764..4aefa856aef2 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/MetricsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/MetricsIntegrationTest.java @@ -4,7 +4,6 @@ import static de.tum.cit.aet.artemis.core.util.TimeUtil.toRelativeTime; import static org.assertj.core.api.Assertions.assertThat; -import java.time.Instant; import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -20,7 +19,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.util.ReflectionTestUtils; import de.tum.cit.aet.artemis.assessment.domain.ParticipantScore; import de.tum.cit.aet.artemis.assessment.repository.StudentScoreRepository; @@ -84,10 +82,7 @@ class MetricsIntegrationTest extends AbstractSpringIntegrationIndependentTest { @BeforeEach void setupTestScenario() throws Exception { - // Prevents the ParticipantScoreScheduleService from scheduling tasks related to prior results - ReflectionTestUtils.setField(participantScoreScheduleService, "lastScheduledRun", Optional.of(Instant.now())); ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 100; - participantScoreScheduleService.activate(); userUtilService.addUsers(TEST_PREFIX, 3, 1, 1, 1); diff --git a/src/test/java/de/tum/cit/aet/artemis/core/StatisticsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/StatisticsIntegrationTest.java index bcd5a8d5c8ae..3a483ba5b736 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/StatisticsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/StatisticsIntegrationTest.java @@ -95,7 +95,6 @@ class StatisticsIntegrationTest extends AbstractSpringIntegrationIndependentTest @BeforeEach void initTestCase() { - participantScoreScheduleService.activate(); userUtilService.addUsers(TEST_PREFIX, NUMBER_OF_STUDENTS, 1, 0, 1); course = modelingExerciseUtilService.addCourseWithOneModelingExercise(); diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/ExamIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/ExamIntegrationTest.java index 99e6b153def6..52f93e2d6741 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exam/ExamIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exam/ExamIntegrationTest.java @@ -203,7 +203,6 @@ void setup() { userTestRepository.save(instructor10); ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; - participantScoreScheduleService.activate(); } @BeforeEach diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/ExamParticipationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/ExamParticipationIntegrationTest.java index 0483663734ad..ec49d8ec78a7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exam/ExamParticipationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exam/ExamParticipationIntegrationTest.java @@ -185,7 +185,6 @@ void initTestCase() { gitlabRequestMockProvider.enableMockingOfRequests(); ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; - participantScoreScheduleService.activate(); } @AfterEach diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/ExamRegistrationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/ExamRegistrationIntegrationTest.java index 016803accf6b..8402a7e431f3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exam/ExamRegistrationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exam/ExamRegistrationIntegrationTest.java @@ -88,7 +88,6 @@ void initTestCase() { examUtilService.addStudentExamForTestExam(testExam1, student1); ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; - participantScoreScheduleService.activate(); } @AfterEach diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/ExamStartTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/ExamStartTest.java index 399cb4306f6d..ea6ca670ad01 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exam/ExamStartTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exam/ExamStartTest.java @@ -112,7 +112,6 @@ void initTestCase() throws GitAPIException { exam = examUtilService.addExamWithExerciseGroup(course1, true); ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; - participantScoreScheduleService.activate(); doNothing().when(gitService).combineAllCommitsOfRepositoryIntoOne(any()); diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/ProgrammingExamIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/ProgrammingExamIntegrationTest.java index ef813745a412..a598f6ddbb79 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exam/ProgrammingExamIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exam/ProgrammingExamIntegrationTest.java @@ -100,7 +100,6 @@ void initTestCase() { gitlabRequestMockProvider.enableMockingOfRequests(); ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; - participantScoreScheduleService.activate(); } @AfterEach diff --git a/src/test/java/de/tum/cit/aet/artemis/exam/TestExamIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/exam/TestExamIntegrationTest.java index 159866350e68..0853bed96f6c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/exam/TestExamIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/exam/TestExamIntegrationTest.java @@ -74,7 +74,6 @@ void initTestCase() { examUtilService.addStudentExamForTestExam(testExam1, student1); ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 200; - participantScoreScheduleService.activate(); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java index 50e46b7e57db..f23887c80d43 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/CourseGitlabJenkinsIntegrationTest.java @@ -46,7 +46,6 @@ class CourseGitlabJenkinsIntegrationTest extends AbstractSpringIntegrationJenkin @BeforeEach void setup() { - participantScoreScheduleService.activate(); courseTestService.setup(TEST_PREFIX, this); gitlabRequestMockProvider.enableMockingOfRequests(); jenkinsRequestMockProvider.enableMockingOfRequests(jenkinsServer); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTestCaseServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTestCaseServiceTest.java index 2d985741a637..cf293bf0740c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTestCaseServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseTestCaseServiceTest.java @@ -4,7 +4,6 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.Collections; @@ -95,7 +94,7 @@ void shouldResetExamExerciseTestCases() { private void testResetTestCases(ProgrammingExercise programmingExercise, Visibility expectedVisibility) { String dummyHash = "9b3a9bd71a0d80e5bbc42204c319ed3d1d4f0d6d"; - when(gitService.getLastCommitHash(any())).thenReturn(ObjectId.fromString(dummyHash)); + doReturn(ObjectId.fromString(dummyHash)).when(gitService).getLastCommitHash(any()); participationUtilService.addProgrammingParticipationWithResultForExercise(programmingExercise, TEST_PREFIX + "student1"); new ArrayList<>(testCaseRepository.findByExerciseId(programmingExercise.getId())).getFirst().weight(50.0); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java index d0144595c37b..6d5bf267139a 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportIntegrationTest.java @@ -53,6 +53,11 @@ void initTestCase() throws Exception { exercise = ProgrammingExerciseFactory.generateProgrammingExercise(ZonedDateTime.now().minusDays(1), ZonedDateTime.now().plusDays(7), course); } + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + @AfterEach void cleanup() throws Exception { solutionRepo.resetLocalRepo(); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportServiceTest.java index fefa75a3cf8a..70e3c6fb8301 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/ProgrammingExerciseGitDiffReportServiceTest.java @@ -58,6 +58,11 @@ class ProgrammingExerciseGitDiffReportServiceTest extends AbstractLocalCILocalVC @Autowired private ProgrammingExerciseGitDiffReportRepository reportRepository; + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 1, 1, 1, 1); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/StructuralTestCaseServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/StructuralTestCaseServiceTest.java index 21e976498ece..08d6f5994ac7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/StructuralTestCaseServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/StructuralTestCaseServiceTest.java @@ -56,6 +56,11 @@ class StructuralTestCaseServiceTest extends AbstractLocalCILocalVCIntegrationTes private ProgrammingExercise exercise; + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + @BeforeEach void initTestCase() { Course course = courseUtilService.addEmptyCourse(); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageReportServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageReportServiceTest.java index fc7b8542022a..ba89b52dc804 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageReportServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/TestwiseCoverageReportServiceTest.java @@ -69,6 +69,11 @@ class TestwiseCoverageReportServiceTest extends AbstractLocalCILocalVCIntegratio private final LocalRepository solutionRepo = new LocalRepository("main"); + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + @BeforeEach void setup() throws Exception { userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 1); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/BehavioralTestCaseServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/BehavioralTestCaseServiceTest.java index 802284ffd99f..f646dd652ded 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/BehavioralTestCaseServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/hestia/behavioral/BehavioralTestCaseServiceTest.java @@ -78,6 +78,11 @@ class BehavioralTestCaseServiceTest extends AbstractLocalCILocalVCIntegrationTes private ProgrammingExercise exercise; + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + @BeforeEach void initTestCase() { userUtilService.addUsers(TEST_PREFIX, 0, 0, 0, 1); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/AbstractLocalCILocalVCIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/AbstractLocalCILocalVCIntegrationTest.java index 08fe3a292db2..0f559f0c3a50 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/AbstractLocalCILocalVCIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/AbstractLocalCILocalVCIntegrationTest.java @@ -30,9 +30,7 @@ import de.tum.cit.aet.artemis.programming.util.ProgrammingExerciseUtilService; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; -public class AbstractLocalCILocalVCIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { - - protected static final String TEST_PREFIX = "localvclocalciintegration"; +public abstract class AbstractLocalCILocalVCIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { @Autowired protected TeamRepository teamRepository; @@ -112,20 +110,23 @@ public class AbstractLocalCILocalVCIntegrationTest extends AbstractSpringIntegra protected String auxiliaryRepositorySlug; + protected abstract String getTestPrefix(); + @BeforeEach void initUsersAndExercise() throws JsonProcessingException { // The port cannot be injected into the LocalVCLocalCITestService because {local.server.port} is not available when the class is instantiated. // Thus, "inject" the port from here. localVCLocalCITestService.setPort(port); - List users = userUtilService.addUsers(TEST_PREFIX, 2, 1, 0, 2); - student1Login = TEST_PREFIX + "student1"; + String testPrefix = getTestPrefix(); + List users = userUtilService.addUsers(testPrefix, 2, 1, 0, 2); + student1Login = testPrefix + "student1"; student1 = users.stream().filter(user -> student1Login.equals(user.getLogin())).findFirst().orElseThrow(); - student2Login = TEST_PREFIX + "student2"; - tutor1Login = TEST_PREFIX + "tutor1"; - instructor1Login = TEST_PREFIX + "instructor1"; + student2Login = testPrefix + "student2"; + tutor1Login = testPrefix + "tutor1"; + instructor1Login = testPrefix + "instructor1"; instructor1 = users.stream().filter(user -> instructor1Login.equals(user.getLogin())).findFirst().orElseThrow(); - instructor2Login = TEST_PREFIX + "instructor2"; + instructor2Login = testPrefix + "instructor2"; instructor2 = users.stream().filter(user -> instructor2Login.equals(user.getLogin())).findFirst().orElseThrow(); // Remove instructor2 from the instructor group of the course. instructor2.setGroups(Set.of()); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java index b48bd518766e..46fcf73bdfb3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIIntegrationTest.java @@ -81,6 +81,8 @@ @Execution(ExecutionMode.SAME_THREAD) class LocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { + private static final String TEST_PREFIX = "localciint"; + @Autowired private LocalVCServletService localVCServletService; @@ -99,6 +101,11 @@ class LocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { @Value("${artemis.user-management.internal-admin.password}") private String localVCPassword; + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + private LocalRepository studentAssignmentRepository; private LocalRepository testsRepository; @@ -262,7 +269,8 @@ void testCommitHashNull() { // Should still work because in that case the latest commit should be retrieved from the repository. localVCServletService.processNewPush(null, studentAssignmentRepository.originGit.getRepository()); - localVCLocalCITestService.testLatestSubmission(studentParticipation.getId(), commitHash, 1, false); + // ToDo: Investigate why specifically this test requires so much time (all other << 5s) + localVCLocalCITestService.testLatestSubmission(studentParticipation.getId(), commitHash, 1, false, 120); } @Test @@ -291,7 +299,7 @@ void testProjectTypeIsNull() { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testCannotFindResults() { + void testResultsNotFound() { ProgrammingExerciseStudentParticipation studentParticipation = localVCLocalCITestService.createParticipation(programmingExercise, student1Login); // Should return a build result that indicates that the build failed. diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java index b57491c34db5..83f9d9019f27 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResourceIntegrationTest.java @@ -42,6 +42,8 @@ class LocalCIResourceIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { + private static final String TEST_PREFIX = "localciresourceint"; + @Autowired @Qualifier("hazelcastInstance") private HazelcastInstance hazelcastInstance; @@ -75,6 +77,11 @@ class LocalCIResourceIntegrationTest extends AbstractLocalCILocalVCIntegrationTe protected IMap buildAgentInformation; + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + @BeforeEach void createJobs() { // temporarily remove listener to avoid triggering build job processing @@ -136,11 +143,10 @@ void clearDataStructures() { @WithMockUser(username = TEST_PREFIX + "admin", roles = "ADMIN") void testGetQueuedBuildJobs_returnsJobs() throws Exception { var retrievedJobs = request.get("/api/admin/queued-jobs", HttpStatus.OK, List.class); - assertThat(retrievedJobs).isEmpty(); // Adding a lot of jobs as they get processed very quickly due to mocking queuedJobs.addAll(List.of(job1, job2)); var retrievedJobs1 = request.get("/api/admin/queued-jobs", HttpStatus.OK, List.class); - assertThat(retrievedJobs1).hasSize(2); + assertThat(retrievedJobs1).hasSize(retrievedJobs.size() + 2); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResultServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResultServiceTest.java index e0947713cf57..a951d065e0cc 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResultServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalCIResultServiceTest.java @@ -14,9 +14,16 @@ class LocalCIResultServiceTest extends AbstractLocalCILocalVCIntegrationTest { + private static final String TEST_PREFIX = "localciresultservice"; + @Autowired private LocalCIResultService localCIResultService; + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + @Test void testThrowsExceptionWhenResultIsNotLocalCIBuildResult() { var wrongBuildResult = ProgrammingExerciseFactory.generateTestResultDTO("some-name", "some-repository", ZonedDateTime.now().minusSeconds(10), diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java index e57b850240a2..1531a4e2649a 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCIntegrationTest.java @@ -37,6 +37,8 @@ */ class LocalVCIntegrationTest extends AbstractLocalCILocalVCIntegrationTest { + private static final String TEST_PREFIX = "localvcint"; + private LocalRepository assignmentRepository; private LocalRepository templateRepository; @@ -60,6 +62,11 @@ void initRepositories() throws GitAPIException, IOException, URISyntaxException testsRepository = localVCLocalCITestService.createAndConfigureLocalRepository(projectKey1, projectKey1.toLowerCase() + "-tests"); } + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + @AfterEach void removeRepositories() throws IOException { assignmentRepository.resetLocalRepo(); @@ -77,7 +84,16 @@ void testFetchPush_repositoryDoesNotExist() throws IOException, GitAPIException, // Delete the remote repository. someRepository.originGit.close(); - FileUtils.deleteDirectory(someRepository.originRepoFile); + try { + FileUtils.deleteDirectory(someRepository.originRepoFile); + } + catch (IOException exception) { + // JGit creates a lock file in each repository that could cause deletion problems. + if (exception.getMessage().contains("gc.log.lock")) { + return; + } + throw exception; + } // Try to fetch from the remote repository. localVCLocalCITestService.testFetchThrowsException(someRepository.localGit, student1Login, USER_PASSWORD, projectKey, repositorySlug, InvalidRemoteException.class, ""); @@ -122,7 +138,7 @@ void testFetchPush_usingVcsAccessToken() { @Test void testFetchPush_wrongCredentials() throws InvalidNameException { - var student1 = new LdapUserDto().login(TEST_PREFIX + "student1"); + var student1 = new LdapUserDto().login(getTestPrefix() + "student1"); student1.setUid(new LdapName("cn=student1,ou=test,o=lab")); var fakeUser = new LdapUserDto().login(localVCBaseUsername); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIIntegrationTest.java index 526d5b8a522f..877ccd6493e0 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCIIntegrationTest.java @@ -87,6 +87,8 @@ class LocalVCLocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTes private static final Logger log = LoggerFactory.getLogger(LocalVCLocalCIIntegrationTest.class); + private static final String TEST_PREFIX = "localvcciint"; + @Autowired private ExamUtilService examUtilService; @@ -124,6 +126,11 @@ class LocalVCLocalCIIntegrationTest extends AbstractLocalCILocalVCIntegrationTes protected IQueue queuedJobs; + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + @BeforeAll void setupAll() { CredentialsProvider.setDefault(new UsernamePasswordCredentialsProvider(localVCUsername, localVCPassword)); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCITestService.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCITestService.java index 08c02eedceed..c6d7556e3766 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCITestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCLocalCITestService.java @@ -54,6 +54,7 @@ import de.tum.cit.aet.artemis.assessment.domain.Result; import de.tum.cit.aet.artemis.assessment.domain.Visibility; +import de.tum.cit.aet.artemis.assessment.service.ParticipantScoreScheduleService; import de.tum.cit.aet.artemis.assessment.test_repository.ResultTestRepository; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationUtilService; @@ -91,6 +92,9 @@ public class LocalVCLocalCITestService { @Autowired private ParticipationVcsAccessTokenService participationVcsAccessTokenService; + @Autowired + private ParticipantScoreScheduleService participantScoreScheduleService; + @Autowired private ResultTestRepository resultRepository; @@ -573,7 +577,11 @@ public void testLatestSubmission(Long participationId, String expectedCommitHash int expectedCodeIssueCount, Integer timeoutInSeconds) { // wait for result to be persisted Duration timeoutDuration = timeoutInSeconds != null ? Duration.ofSeconds(timeoutInSeconds) : Duration.ofSeconds(DEFAULT_AWAITILITY_TIMEOUT_IN_SECONDS); - await().atMost(timeoutDuration).until(() -> resultRepository.findFirstWithSubmissionsByParticipationIdOrderByCompletionDateDesc(participationId).isPresent()); + await().atMost(timeoutDuration).until(() -> { + participantScoreScheduleService.executeScheduledTasks(); + await().until(participantScoreScheduleService::isIdle); + return resultRepository.findFirstWithSubmissionsByParticipationIdOrderByCompletionDateDesc(participationId).isPresent(); + }); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); List submissions = programmingSubmissionRepository.findAllByParticipationIdWithResults(participationId); @@ -609,6 +617,10 @@ public void testLatestSubmission(Long participationId, String expectedCommitHash testLatestSubmission(participationId, expectedCommitHash, expectedSuccessfulTestCaseCount, buildFailed, false, 0, null); } + public void testLatestSubmission(Long participationId, String expectedCommitHash, int expectedSuccessfulTestCaseCount, boolean buildFailed, int timeoutInSeconds) { + testLatestSubmission(participationId, expectedCommitHash, expectedSuccessfulTestCaseCount, buildFailed, false, 0, timeoutInSeconds); + } + /** * Perform a push operation and fail if there was no exception. * diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java index ac20db1c209c..bca0ed60beb9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java @@ -12,6 +12,7 @@ import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; +import java.util.Objects; import java.util.concurrent.TimeUnit; import org.apache.sshd.client.SshClient; @@ -20,6 +21,7 @@ import org.apache.sshd.common.SshException; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter; +import org.apache.sshd.common.session.helpers.AbstractSession; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.session.ServerSession; import org.junit.jupiter.api.Test; @@ -35,9 +37,16 @@ @Profile(PROFILE_LOCALVC) class LocalVCSshIntegrationTest extends LocalVCIntegrationTest { + private static final String TEST_PREFIX = "localvcsshint"; + @Autowired private SshServer sshServer; + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + private final String hostname = "localhost"; private final int port = 7921; @@ -111,8 +120,8 @@ void testAuthenticationFailure() { void testConnectOverSshAndReceivePack() throws IOException, GeneralSecurityException { try (var client = clientConnectToArtemisSshServer()) { assertThat(client).isNotNull(); - var serverSessions = sshServer.getActiveSessions(); - var serverSession = serverSessions.getFirst(); + var user = userTestRepository.getUser(); + var serverSession = getCurrentServerSession(user); final var uploadCommandString = "git-upload-pack '/git/" + projectKey1 + "/" + templateRepositorySlug + "'"; @@ -144,9 +153,13 @@ private SshGitCommand setupCommand(String commandString, ServerSession serverSes return command; } + /** + * Note: Don't count unattached sessions as a potential result from previous tests. + * See {@link org.apache.sshd.server.SshServer#getActiveSessions} + * and {@link org.apache.sshd.common.session.helpers.AbstractSession#getSession}. + */ private SshClient clientConnectToArtemisSshServer() throws GeneralSecurityException, IOException { var serverSessions = sshServer.getActiveSessions(); - var numberOfSessions = serverSessions.size(); localVCLocalCITestService.createParticipation(programmingExercise, student1Login); KeyPair keyPair = setupKeyPairAndAddToUser(); User user = userTestRepository.getUser(); @@ -155,6 +168,7 @@ private SshClient clientConnectToArtemisSshServer() throws GeneralSecurityExcept client.start(); ClientSession clientSession; + int numberOfSessions = serverSessions.size(); try { ConnectFuture connectFuture = client.connect(user.getName(), hostname, port); connectFuture.await(10, TimeUnit.SECONDS); @@ -169,11 +183,20 @@ private SshClient clientConnectToArtemisSshServer() throws GeneralSecurityExcept } serverSessions = sshServer.getActiveSessions(); + var attachedServerSessions = serverSessions.stream().filter(Objects::nonNull).count(); assertThat(clientSession.isAuthenticated()).isTrue(); - assertThat(serverSessions.size()).isEqualTo(numberOfSessions + 1); + assertThat(attachedServerSessions).as("There are more server sessions activated than expected.").isEqualTo(numberOfSessions + 1); return client; } + private AbstractSession getCurrentServerSession(User user) { + var serverSessions = sshServer.getActiveSessions(); + // parallel tests might create additional sessions, we need to be specific + var serverSession = serverSessions.stream().filter(session -> user.getName().equals(session.getUsername())).findFirst(); + + return serverSession.orElseThrow(() -> new IllegalStateException("No server session found for user " + user.getName())); + } + private KeyPair setupKeyPairAndAddToUser() throws GeneralSecurityException, IOException { User user = userTestRepository.getUser(); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/MultipleHostKeyProviderTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/MultipleHostKeyProviderTest.java index bf9c4cde79fc..a386ae6ccc2d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/MultipleHostKeyProviderTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/MultipleHostKeyProviderTest.java @@ -13,6 +13,13 @@ @Profile(PROFILE_LOCALVC) class MultipleHostKeyProviderTest extends AbstractLocalCILocalVCIntegrationTest { + private static final String TEST_PREFIX = "multiplehostkeyprovider"; + + @Override + protected String getTestPrefix() { + return TEST_PREFIX; + } + @Test void testMultipleHostKeyProvider() { MultipleHostKeyProvider multipleHostKeyProvider = new MultipleHostKeyProvider(Path.of("./")); diff --git a/src/test/java/de/tum/cit/aet/artemis/quiz/QuizSubmissionIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/quiz/QuizSubmissionIntegrationTest.java index 5a56c62d5273..41736fa7c6a9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/quiz/QuizSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/quiz/QuizSubmissionIntegrationTest.java @@ -1,12 +1,12 @@ package de.tum.cit.aet.artemis.quiz; +import static de.tum.cit.aet.artemis.core.config.Constants.EXERCISE_TOPIC_ROOT; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import java.io.IOException; import java.time.Duration; @@ -414,7 +414,7 @@ void testQuizSubmitPractice_badRequest() throws Exception { Result result = request.postWithResponseBody("/api/exercises/" + quizExerciseServer.getId() + "/submissions/practice", quizSubmission, Result.class, HttpStatus.BAD_REQUEST); assertThat(result).isNull(); - verifyNoInteractions(websocketMessagingService); + verifyNoWebsocketMessageForExercise(quizExerciseServer); } @Test @@ -431,7 +431,7 @@ void testQuizSubmitPractice_badRequest_exam() throws Exception { Result result = request.postWithResponseBody("/api/exercises/" + quizExerciseServer.getId() + "/submissions/practice", quizSubmission, Result.class, HttpStatus.BAD_REQUEST); assertThat(result).isNull(); - verifyNoInteractions(websocketMessagingService); + verifyNoWebsocketMessageForExercise(quizExerciseServer); } @Test @@ -451,7 +451,7 @@ void testQuizSubmitPractice_forbidden() throws Exception { QuizExercise quizExercise = QuizExerciseFactory.createQuiz(course, ZonedDateTime.now().minusSeconds(4), null, QuizMode.SYNCHRONIZED); quizExerciseService.save(quizExercise); request.postWithResponseBody("/api/exercises/" + quizExercise.getId() + "/submissions/practice", new QuizSubmission(), Result.class, HttpStatus.FORBIDDEN); - verifyNoInteractions(websocketMessagingService); + verifyNoWebsocketMessageForExercise(quizExercise); } @Test @@ -757,6 +757,12 @@ private QuizExercise setupQuizExerciseParameters() { return quizExercise; } + private void verifyNoWebsocketMessageForExercise(QuizExercise exercise) { + String topic = EXERCISE_TOPIC_ROOT + exercise.getId() + "/newResults"; + verify(websocketMessagingService, never()).sendMessage(eq(topic), any()); + verify(websocketMessagingService, never()).sendMessageToUser(any(), eq(topic), any()); + } + @Nested @Isolated class QuizSubmitLiveModeIsolatedTest { diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractArtemisIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractArtemisIntegrationTest.java index 4157341a3deb..469e4f117837 100644 --- a/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractArtemisIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractArtemisIntegrationTest.java @@ -4,7 +4,9 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import java.time.Instant; import java.util.List; +import java.util.Optional; import jakarta.mail.internet.MimeMessage; @@ -23,6 +25,7 @@ import org.springframework.context.annotation.Import; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.util.ReflectionTestUtils; import de.tum.cit.aet.artemis.assessment.service.ParticipantScoreScheduleService; import de.tum.cit.aet.artemis.assessment.test_repository.ResultTestRepository; @@ -199,6 +202,14 @@ void mockMailService() { doNothing().when(javaMailSender).send(any(MimeMessage.class)); } + @BeforeEach + void resetParticipantScoreScheduler() { + // Prevents the ParticipantScoreScheduleService from scheduling tasks related to prior results + ReflectionTestUtils.setField(participantScoreScheduleService, "lastScheduledRun", Optional.of(Instant.now())); + ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 100; + participantScoreScheduleService.activate(); + } + @AfterEach void stopQuizScheduler() { scheduleService.clearAllTasks(); From f35832913d3a866c3c448593daba8ef022754615 Mon Sep 17 00:00:00 2001 From: Marcel Gaupp Date: Sat, 12 Oct 2024 10:43:10 +0200 Subject: [PATCH 06/14] Programming exercises: Add R programming language template (#9256) --- build.gradle | 9 +-- .../programming-exercise-features.inc | 4 ++ ...ProgrammingPlagiarismDetectionService.java | 12 ++-- .../domain/ProgrammingLanguage.java | 15 ++--- .../service/TemplateUpgradePolicyService.java | 4 +- .../ci/ContinuousIntegrationService.java | 8 +-- ...abCIProgrammingLanguageFeatureService.java | 2 +- ...kinsProgrammingLanguageFeatureService.java | 10 ++-- .../build_plan/JenkinsBuildPlanService.java | 4 +- ...alCIProgrammingLanguageFeatureService.java | 16 ++--- src/main/resources/config/application.yml | 2 + .../resources/templates/aeolus/r/default.sh | 26 ++++++++ .../resources/templates/aeolus/r/default.yaml | 14 +++++ .../jenkins/r/regularRuns/pipeline.groovy | 59 +++++++++++++++++++ .../templates/r/exercise/DESCRIPTION | 7 +++ .../resources/templates/r/exercise/NAMESPACE | 1 + .../templates/r/exercise/R/convert.R | 3 + src/main/resources/templates/r/readme | 6 ++ .../templates/r/solution/DESCRIPTION | 7 +++ .../resources/templates/r/solution/NAMESPACE | 1 + .../templates/r/solution/R/convert.R | 17 ++++++ .../resources/templates/r/test/DESCRIPTION | 14 +++++ .../templates/r/test/tests/testthat.R | 12 ++++ .../r/test/tests/testthat/test-convert.R | 47 +++++++++++++++ .../programming/programming-exercise.model.ts | 15 ++--- .../file-browser/supported-file-extensions.ts | 1 + src/test/resources/config/application.yml | 2 + 27 files changed, 275 insertions(+), 43 deletions(-) create mode 100644 src/main/resources/templates/aeolus/r/default.sh create mode 100644 src/main/resources/templates/aeolus/r/default.yaml create mode 100644 src/main/resources/templates/jenkins/r/regularRuns/pipeline.groovy create mode 100644 src/main/resources/templates/r/exercise/DESCRIPTION create mode 100644 src/main/resources/templates/r/exercise/NAMESPACE create mode 100644 src/main/resources/templates/r/exercise/R/convert.R create mode 100644 src/main/resources/templates/r/readme create mode 100644 src/main/resources/templates/r/solution/DESCRIPTION create mode 100644 src/main/resources/templates/r/solution/NAMESPACE create mode 100644 src/main/resources/templates/r/solution/R/convert.R create mode 100644 src/main/resources/templates/r/test/DESCRIPTION create mode 100644 src/main/resources/templates/r/test/tests/testthat.R create mode 100644 src/main/resources/templates/r/test/tests/testthat/test-convert.R diff --git a/build.gradle b/build.gradle index 29da90bf3674..cdad264bac58 100644 --- a/build.gradle +++ b/build.gradle @@ -246,14 +246,15 @@ dependencies { implementation "org.gitlab4j:gitlab4j-api:6.0.0-rc.5" implementation "de.jplag:jplag:${jplag_version}" - implementation "de.jplag:java:${jplag_version}" - implementation "de.jplag:kotlin:${jplag_version}" + implementation "de.jplag:c:${jplag_version}" - implementation "de.jplag:swift:${jplag_version}" implementation "de.jplag:java:${jplag_version}" + implementation "de.jplag:javascript:${jplag_version}" + implementation "de.jplag:kotlin:${jplag_version}" implementation "de.jplag:python-3:${jplag_version}" + implementation "de.jplag:rlang:${jplag_version}" implementation "de.jplag:rust:${jplag_version}" - implementation "de.jplag:javascript:${jplag_version}" + implementation "de.jplag:swift:${jplag_version}" implementation "de.jplag:text:${jplag_version}" // those are transitive dependencies of JPlag Text --> Stanford NLP diff --git a/docs/user/exercises/programming-exercise-features.inc b/docs/user/exercises/programming-exercise-features.inc index 7bccf1596315..660e2bd4bf02 100644 --- a/docs/user/exercises/programming-exercise-features.inc +++ b/docs/user/exercises/programming-exercise-features.inc @@ -37,6 +37,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------+---------+ | JavaScript | yes | yes | +----------------------+----------+---------+ + | R | yes | yes | + +----------------------+----------+---------+ - Not all ``templates`` support the same feature set and supported features can also change depending on the continuous integration system setup. Depending on the feature set, some options might not be available during the creation of the programming exercise. @@ -71,6 +73,8 @@ Instructors can still use those templates to generate programming exercises and +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ | JavaScript | no | no | yes | no | n/a | no | no | L: yes, J: no | +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ + | R | no | no | yes | no | n/a | no | no | L: yes, J: no | + +----------------------+----------------------+----------------------+---------------------+--------------+------------------------------------------+------------------------------+----------------------------+------------------------+ - *Sequential Test Runs*: ``Artemis`` can generate a build plan which first executes structural and then behavioral tests. This feature can help students to better concentrate on the immediate challenge at hand. - *Static Code Analysis*: ``Artemis`` can generate a build plan which additionally executes static code analysis tools. diff --git a/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java index 9fe9b1cc0f8c..ea6ebfdf3b81 100644 --- a/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/plagiarism/service/ProgrammingPlagiarismDetectionService.java @@ -37,6 +37,7 @@ import de.jplag.options.JPlagOptions; import de.jplag.python3.PythonLanguage; import de.jplag.reporting.reportobject.ReportObjectFactory; +import de.jplag.rlang.RLanguage; import de.jplag.rust.RustLanguage; import de.jplag.swift.SwiftLanguage; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; @@ -310,14 +311,15 @@ public void deleteTempLocalRepository(Repository repository) { private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExercise) { return switch (programmingExercise.getProgrammingLanguage()) { - case JAVA -> new JavaLanguage(); case C -> new CLanguage(); - case PYTHON -> new PythonLanguage(); - case SWIFT -> new SwiftLanguage(); + case JAVA -> new JavaLanguage(); + case JAVASCRIPT -> new JavaScriptLanguage(); case KOTLIN -> new KotlinLanguage(); + case PYTHON -> new PythonLanguage(); + case R -> new RLanguage(); case RUST -> new RustLanguage(); - case JAVASCRIPT -> new JavaScriptLanguage(); - case EMPTY, PHP, DART, HASKELL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, VHDL, RUBY, POWERSHELL, ADA -> + case SWIFT -> new SwiftLanguage(); + case EMPTY, PHP, DART, HASKELL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, VHDL, RUBY, POWERSHELL, ADA -> throw new BadRequestAlertException("Programming language " + programmingExercise.getProgrammingLanguage() + " not supported for plagiarism check.", "ProgrammingExercise", "notSupported"); }; diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java index 4206bfe15dbc..781ad04f98c6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/ProgrammingLanguage.java @@ -38,18 +38,19 @@ public enum ProgrammingLanguage { PHP("php"); private static final Set ENABLED_LANGUAGES = Set.of( - EMPTY, - JAVA, - PYTHON, + ASSEMBLER, C, HASKELL, + JAVA, + JAVASCRIPT, KOTLIN, - VHDL, - ASSEMBLER, - SWIFT, OCAML, + PYTHON, + R, RUST, - JAVASCRIPT + SWIFT, + VHDL, + EMPTY ); // @formatter:on diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java index 16285e6a0695..4c73046b1ab0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/TemplateUpgradePolicyService.java @@ -32,8 +32,8 @@ public TemplateUpgradePolicyService(JavaTemplateUpgradeService javaRepositoryUpg public TemplateUpgradeService getUpgradeService(ProgrammingLanguage programmingLanguage) { return switch (programmingLanguage) { case JAVA -> javaRepositoryUpgradeService; - case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT -> defaultRepositoryUpgradeService; - case C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case KOTLIN, PYTHON, C, HASKELL, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R -> defaultRepositoryUpgradeService; + case C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + programmingLanguage); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java index b4f67794c073..b9050501c67a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/ci/ContinuousIntegrationService.java @@ -219,8 +219,8 @@ enum RepositoryCheckoutPath implements CustomizableCheckoutPath { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT -> "assignment"; - case C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, PYTHON, C, HASKELL, KOTLIN, VHDL, ASSEMBLER, SWIFT, OCAML, EMPTY, RUST, JAVASCRIPT, R -> "assignment"; + case C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } @@ -230,9 +230,9 @@ public String forProgrammingLanguage(ProgrammingLanguage language) { @Override public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { - case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT -> ""; + case JAVA, PYTHON, HASKELL, KOTLIN, SWIFT, EMPTY, RUST, JAVASCRIPT, R -> ""; case C, VHDL, ASSEMBLER, OCAML -> "tests"; - case C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException("Unsupported programming language: " + language); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java index a92bcdd26cb5..0c71114e13bb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/gitlabci/GitLabCIProgrammingLanguageFeatureService.java @@ -25,7 +25,7 @@ public class GitLabCIProgrammingLanguageFeatureService extends ProgrammingLangua public GitLabCIProgrammingLanguageFeatureService() { programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false, false)); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, false, false, false, true, false, List.of(PLAIN_MAVEN, MAVEN_MAVEN), false, false)); - programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, false)); programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, false)); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java index 38893ea41093..45a473da9148 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/JenkinsProgrammingLanguageFeatureService.java @@ -7,6 +7,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.JAVASCRIPT; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.KOTLIN; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.PYTHON; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.R; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.RUST; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.SWIFT; import static de.tum.cit.aet.artemis.programming.domain.ProjectType.FACT; @@ -33,15 +34,16 @@ public class JenkinsProgrammingLanguageFeatureService extends ProgrammingLanguag public JenkinsProgrammingLanguageFeatureService() { // Must be extended once a new programming language is added programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, false, true, false, false, List.of(FACT, GCC), false, false)); + programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, false, false, false, false, true, List.of(), false, false)); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, true, true, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE, PLAIN_MAVEN, MAVEN_MAVEN, MAVEN_BLACKBOX), true, false)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, false)); programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, true, false, true, true, false, List.of(), true, false)); programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(R, new ProgrammingLanguageFeature(R, false, false, true, false, false, List.of(), false, false)); + programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, false)); // Jenkins is not supporting XCODE at the moment programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, true, true, true, false, List.of(PLAIN), false, false)); - programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, false, true, false, false, List.of(FACT, GCC), false, false)); - programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, false, false, false, false, true, List.of(), false, false)); - programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, false)); - programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, false)); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java index 6e904910ca57..f900cc0f6dd1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/jenkins/build_plan/JenkinsBuildPlanService.java @@ -184,8 +184,8 @@ private JenkinsXmlConfigBuilder builderFor(ProgrammingLanguage programmingLangua throw new UnsupportedOperationException("Xcode templates are not available for Jenkins."); } return switch (programmingLanguage) { - case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT -> jenkinsBuildPlanCreator; - case VHDL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, R, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> + case JAVA, KOTLIN, PYTHON, C, HASKELL, SWIFT, EMPTY, RUST, JAVASCRIPT, R -> jenkinsBuildPlanCreator; + case VHDL, ASSEMBLER, OCAML, C_SHARP, C_PLUS_PLUS, SQL, TYPESCRIPT, GO, MATLAB, BASH, RUBY, POWERSHELL, ADA, DART, PHP -> throw new UnsupportedOperationException(programmingLanguage + " templates are not available for Jenkins."); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java index 525170cca334..bc8292d407bb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/LocalCIProgrammingLanguageFeatureService.java @@ -10,6 +10,7 @@ import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.KOTLIN; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.OCAML; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.PYTHON; +import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.R; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.RUST; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.SWIFT; import static de.tum.cit.aet.artemis.programming.domain.ProgrammingLanguage.VHDL; @@ -39,17 +40,18 @@ public class LocalCIProgrammingLanguageFeatureService extends ProgrammingLanguag public LocalCIProgrammingLanguageFeatureService() { // Must be extended once a new programming language is added programmingLanguageFeatures.put(EMPTY, new ProgrammingLanguageFeature(EMPTY, false, false, false, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(ASSEMBLER, new ProgrammingLanguageFeature(ASSEMBLER, false, false, false, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, true, true, false, false, List.of(FACT, GCC), false, true)); + programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, true, false, false, false, true, List.of(), false, true)); programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, true, true, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE, PLAIN_MAVEN, MAVEN_MAVEN), false, true)); - programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of(), false, true)); - programmingLanguageFeatures.put(C, new ProgrammingLanguageFeature(C, false, true, true, false, false, List.of(FACT, GCC), false, true)); - programmingLanguageFeatures.put(ASSEMBLER, new ProgrammingLanguageFeature(ASSEMBLER, false, false, false, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, true)); programmingLanguageFeatures.put(KOTLIN, new ProgrammingLanguageFeature(KOTLIN, false, false, true, true, false, List.of(), false, true)); - programmingLanguageFeatures.put(VHDL, new ProgrammingLanguageFeature(VHDL, false, false, false, false, false, List.of(), false, true)); - programmingLanguageFeatures.put(HASKELL, new ProgrammingLanguageFeature(HASKELL, true, false, false, false, true, List.of(), false, true)); programmingLanguageFeatures.put(OCAML, new ProgrammingLanguageFeature(OCAML, false, false, false, false, true, List.of(), false, true)); - programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, false, true, true, false, List.of(PLAIN), false, true)); + programmingLanguageFeatures.put(PYTHON, new ProgrammingLanguageFeature(PYTHON, false, false, true, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(R, new ProgrammingLanguageFeature(R, false, false, true, false, false, List.of(), false, true)); programmingLanguageFeatures.put(RUST, new ProgrammingLanguageFeature(RUST, false, false, true, false, false, List.of(), false, true)); - programmingLanguageFeatures.put(JAVASCRIPT, new ProgrammingLanguageFeature(JAVASCRIPT, false, false, true, false, false, List.of(), false, true)); + programmingLanguageFeatures.put(SWIFT, new ProgrammingLanguageFeature(SWIFT, false, false, true, true, false, List.of(PLAIN), false, true)); + programmingLanguageFeatures.put(VHDL, new ProgrammingLanguageFeature(VHDL, false, false, false, false, false, List.of(), false, true)); } } diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index 924d087ec8f2..3924e2d804f9 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -91,6 +91,8 @@ artemis: default: "ghcr.io/ls1intum/artemis-rust-docker:v0.9.70" javascript: default: "ghcr.io/ls1intum/artemis-javascript-docker:v1.0.0" + r: + default: "ghcr.io/ls1intum/artemis-r-docker:v1.0.0" management: endpoints: diff --git a/src/main/resources/templates/aeolus/r/default.sh b/src/main/resources/templates/aeolus/r/default.sh new file mode 100644 index 000000000000..1d0b32e87105 --- /dev/null +++ b/src/main/resources/templates/aeolus/r/default.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e +export AEOLUS_INITIAL_DIRECTORY=${PWD} +install () { + echo '⚙️ executing install' + R CMD INSTALL assignment +} + +run_all_tests () { + echo '⚙️ executing run_all_tests' + Rscript -e 'library("testthat"); options(testthat.output_file = "junit.xml"); test_local(".", reporter = "junit")' +} + +main () { + if [[ "${1}" == "aeolus_sourcing" ]]; then + return 0 # just source to use the methods in the subshell, no execution + fi + local _script_name + _script_name=${BASH_SOURCE[0]:-$0} + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; install" + cd "${AEOLUS_INITIAL_DIRECTORY}" + bash -c "source ${_script_name} aeolus_sourcing; run_all_tests" +} + +main "${@}" diff --git a/src/main/resources/templates/aeolus/r/default.yaml b/src/main/resources/templates/aeolus/r/default.yaml new file mode 100644 index 000000000000..a41d23c6f012 --- /dev/null +++ b/src/main/resources/templates/aeolus/r/default.yaml @@ -0,0 +1,14 @@ +api: v0.0.1 +metadata: + name: R + id: r + description: Test package using testthat +actions: + - name: install + script: R CMD INSTALL assignment + - name: run_all_tests + script: Rscript -e 'library("testthat"); options(testthat.output_file = "junit.xml"); test_local(".", reporter = "junit")' + results: + - name: junit + path: tests/testthat/junit.xml + type: junit diff --git a/src/main/resources/templates/jenkins/r/regularRuns/pipeline.groovy b/src/main/resources/templates/jenkins/r/regularRuns/pipeline.groovy new file mode 100644 index 000000000000..9a2ec97b5843 --- /dev/null +++ b/src/main/resources/templates/jenkins/r/regularRuns/pipeline.groovy @@ -0,0 +1,59 @@ +/* + * This file configures the actual build steps for the automatic grading. + * + * !!! + * For regular exercises, there is no need to make changes to this file. + * Only this base configuration is actively supported by the Artemis maintainers + * and/or your Artemis instance administrators. + * !!! + */ + +dockerImage = '#dockerImage' +dockerFlags = '#dockerArgs' + +/** + * Main function called by Jenkins. + */ +void testRunner() { + docker.image(dockerImage).inside(dockerFlags) { c -> + runTestSteps() + } +} + +private void runTestSteps() { + test() +} + +/** + * Run unit tests + */ +private void test() { + stage('Test') { + sh ''' + R CMD INSTALL assignment + Rscript -e 'library("testthat"); options(testthat.output_file = "junit.xml"); test_local(".", reporter = "junit")' + ''' + } +} + +/** + * Script of the post build tasks aggregating all JUnit files in $WORKSPACE/results. + * + * Called by Jenkins. + */ +void postBuildTasks() { + sh ''' + rm -rf results + mkdir results + if [ -e tests/testthat/junit.xml ] + then + sed -i 's/]*>//g ; s/<\\/testsuites>/<\\/testsuite>/g' tests/testthat/junit.xml + fi + cp tests/testthat/junit.xml $WORKSPACE/results/ || true + sed -i 's/[^[:print:]\t]/�/g' $WORKSPACE/results/*.xml || true + ''' +} + +// very important, do not remove +// required so that Jenkins finds the methods defined in this script +return this diff --git a/src/main/resources/templates/r/exercise/DESCRIPTION b/src/main/resources/templates/r/exercise/DESCRIPTION new file mode 100644 index 000000000000..2933cb767621 --- /dev/null +++ b/src/main/resources/templates/r/exercise/DESCRIPTION @@ -0,0 +1,7 @@ +Package: assignment +Title: Artemis R Student Assignment +Version: 0.0.0.9000 +Author: Artemis +Description: This is an assignment to be solved by students. +License: MIT +Encoding: UTF-8 diff --git a/src/main/resources/templates/r/exercise/NAMESPACE b/src/main/resources/templates/r/exercise/NAMESPACE new file mode 100644 index 000000000000..9c9f9ac2d917 --- /dev/null +++ b/src/main/resources/templates/r/exercise/NAMESPACE @@ -0,0 +1 @@ +exportPattern("^[^\\.]") diff --git a/src/main/resources/templates/r/exercise/R/convert.R b/src/main/resources/templates/r/exercise/R/convert.R new file mode 100644 index 000000000000..28e787cf2967 --- /dev/null +++ b/src/main/resources/templates/r/exercise/R/convert.R @@ -0,0 +1,3 @@ +matrix_to_column_list <- function(mat) { + # TODO: implement +} diff --git a/src/main/resources/templates/r/readme b/src/main/resources/templates/r/readme new file mode 100644 index 000000000000..73377139d293 --- /dev/null +++ b/src/main/resources/templates/r/readme @@ -0,0 +1,6 @@ +# Matrix Columns + +Write a function `matrix_to_column_list` in R that takes a matrix of any shape and converts it into a list of +column-vectors. Each element of the list should represent a column of the matrix. + +1. [task][Convert to column-vectors](converts_3x3_matrix_to_vectors,converts_4x2_matrix_to_vectors,converts_1x5_matrix_to_scalars,converts_5x1_matrix_to_vector) diff --git a/src/main/resources/templates/r/solution/DESCRIPTION b/src/main/resources/templates/r/solution/DESCRIPTION new file mode 100644 index 000000000000..2933cb767621 --- /dev/null +++ b/src/main/resources/templates/r/solution/DESCRIPTION @@ -0,0 +1,7 @@ +Package: assignment +Title: Artemis R Student Assignment +Version: 0.0.0.9000 +Author: Artemis +Description: This is an assignment to be solved by students. +License: MIT +Encoding: UTF-8 diff --git a/src/main/resources/templates/r/solution/NAMESPACE b/src/main/resources/templates/r/solution/NAMESPACE new file mode 100644 index 000000000000..9c9f9ac2d917 --- /dev/null +++ b/src/main/resources/templates/r/solution/NAMESPACE @@ -0,0 +1 @@ +exportPattern("^[^\\.]") diff --git a/src/main/resources/templates/r/solution/R/convert.R b/src/main/resources/templates/r/solution/R/convert.R new file mode 100644 index 000000000000..7d701772ab7b --- /dev/null +++ b/src/main/resources/templates/r/solution/R/convert.R @@ -0,0 +1,17 @@ +matrix_to_column_list <- function(mat) { + if (!is.matrix(mat)) { + stop("Input must be a matrix") + } + + n_cols <- ncol(mat) + + # Initialize an empty list to store column-vectors + column_list <- vector("list", length = n_cols) + + # Loop through each column and store it in the list + for (i in 1:n_cols) { + column_list[[i]] <- mat[, i] + } + + return(column_list) +} diff --git a/src/main/resources/templates/r/test/DESCRIPTION b/src/main/resources/templates/r/test/DESCRIPTION new file mode 100644 index 000000000000..e19a2b735419 --- /dev/null +++ b/src/main/resources/templates/r/test/DESCRIPTION @@ -0,0 +1,14 @@ +Package: test +Title: Artemis R Tests +Version: 0.0.0.9000 +Author: Artemis +Description: This package tests the student assignment. +License: MIT +Encoding: UTF-8 +Imports: + assignment +Remotes: + local::./assignment +Suggests: + testthat (>= 3.0.0) +Config/testthat/edition: 3 diff --git a/src/main/resources/templates/r/test/tests/testthat.R b/src/main/resources/templates/r/test/tests/testthat.R new file mode 100644 index 000000000000..388438828173 --- /dev/null +++ b/src/main/resources/templates/r/test/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(tests) + +test_check("tests") diff --git a/src/main/resources/templates/r/test/tests/testthat/test-convert.R b/src/main/resources/templates/r/test/tests/testthat/test-convert.R new file mode 100644 index 000000000000..a84a0e879711 --- /dev/null +++ b/src/main/resources/templates/r/test/tests/testthat/test-convert.R @@ -0,0 +1,47 @@ +test_that("converts_3x3_matrix_to_vectors", { + mat <- matrix(c(5, 8, 11, 6, 9, 12, 7, 10, 13), nrow = 3, ncol = 3) + + result <- assignment::matrix_to_column_list(mat) + + # Make sure to only use exactly one "expect_" function per test + expect_equal(result, list( + c(5, 8, 11), + c(6, 9, 12), + c(7, 10, 13) + )) +}) + +test_that("converts_4x2_matrix_to_vectors", { + mat <- matrix(c(13, 13, 5, 18, 11, 4, 7, 10), nrow = 4, ncol = 2) + + result <- assignment::matrix_to_column_list(mat) + + expect_equal(result, list( + c(13, 13, 5, 18), + c(11, 4, 7, 10) + )) +}) + +test_that("converts_1x5_matrix_to_scalars", { + mat <- matrix(c(16, 10, 15, 8, 7), nrow = 1, ncol = 5) + + result <- assignment::matrix_to_column_list(mat) + + expect_equal(result, list( + 16, + 10, + 15, + 8, + 7 + )) +}) + +test_that("converts_5x1_matrix_to_vector", { + mat <- matrix(c(14, 9, 1, 3, 4), nrow = 5, ncol = 1) + + result <- assignment::matrix_to_column_list(mat) + + expect_equal(result, list( + c(14, 9, 1, 3, 4) + )) +}) diff --git a/src/main/webapp/app/entities/programming/programming-exercise.model.ts b/src/main/webapp/app/entities/programming/programming-exercise.model.ts index ef2d95985068..17d04e971160 100644 --- a/src/main/webapp/app/entities/programming/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming/programming-exercise.model.ts @@ -13,18 +13,19 @@ import { SubmissionPolicy } from 'app/entities/submission-policy.model'; import dayjs from 'dayjs/esm'; export enum ProgrammingLanguage { - JAVA = 'JAVA', - PYTHON = 'PYTHON', + EMPTY = 'EMPTY', + ASSEMBLER = 'ASSEMBLER', C = 'C', HASKELL = 'HASKELL', + JAVA = 'JAVA', + JAVASCRIPT = 'JAVASCRIPT', KOTLIN = 'KOTLIN', - VHDL = 'VHDL', - ASSEMBLER = 'ASSEMBLER', - SWIFT = 'SWIFT', OCAML = 'OCAML', - EMPTY = 'EMPTY', + PYTHON = 'PYTHON', + R = 'R', RUST = 'RUST', - JAVASCRIPT = 'JAVASCRIPT', + SWIFT = 'SWIFT', + VHDL = 'VHDL', } export enum ProjectType { diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/supported-file-extensions.ts b/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/supported-file-extensions.ts index 73ec34bebb00..89664e7f5963 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/supported-file-extensions.ts +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/file-browser/supported-file-extensions.ts @@ -1,5 +1,6 @@ export const supportedTextFileExtensions = [ 'Makefile', + 'R', 'Rakefile', 'ada', 'adb', diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index f48155253d32..ee94c5be7573 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -70,6 +70,8 @@ artemis: default: "~~invalid~~" javascript: default: "~~invalid~~" + r: + default: "~~invalid~~" spring: application: From 4c98c063fc4543cd9d85530b9c5b10f2eb647dc6 Mon Sep 17 00:00:00 2001 From: Timor Morrien Date: Sat, 12 Oct 2024 12:38:13 +0200 Subject: [PATCH 07/14] Iris: Upgrade settings system for Pyris V2 (#9247) --- docs/dev/guidelines/database.rst | 2 +- .../aet/artemis/iris/domain/IrisTemplate.java | 65 -------- .../domain/session/IrisHestiaSession.java | 37 ----- .../iris/domain/session/IrisSession.java | 3 +- .../domain/settings/IrisChatSubSettings.java | 19 --- .../IrisCompetencyGenerationSubSettings.java | 20 --- .../domain/settings/IrisCourseSettings.java | 19 --- .../domain/settings/IrisExerciseSettings.java | 15 -- .../domain/settings/IrisGlobalSettings.java | 84 ---------- .../settings/IrisHestiaSubSettings.java | 35 ---- ...tConverter.java => IrisListConverter.java} | 2 +- .../iris/domain/settings/IrisSettings.java | 6 - .../iris/domain/settings/IrisSubSettings.java | 29 ++-- .../domain/settings/IrisSubSettingsType.java | 2 +- .../dto/IrisCombinedChatSubSettingsDTO.java | 6 +- ...nedCompetencyGenerationSubSettingsDTO.java | 5 +- .../dto/IrisCombinedHestiaSubSettingsDTO.java | 13 -- .../iris/dto/IrisCombinedSettingsDTO.java | 2 +- .../IrisHestiaSessionRepository.java | 28 ---- .../repository/IrisSettingsRepository.java | 2 - .../repository/IrisTemplateRepository.java | 11 -- .../service/IrisDefaultTemplateService.java | 76 --------- .../iris/service/IrisSessionService.java | 8 +- .../service/pyris/PyrisConnectorService.java | 12 +- ...yrisModelDTO.java => PyrisVariantDTO.java} | 2 +- .../session/IrisCourseChatSessionService.java | 3 +- .../IrisExerciseChatSessionService.java | 5 +- .../session/IrisHestiaSessionService.java | 113 ------------- .../service/settings/IrisSettingsService.java | 96 +---------- .../settings/IrisSubSettingsService.java | 149 +++++------------- .../artemis/iris/web/IrisModelsResource.java | 49 ------ .../iris/web/IrisSettingsResource.java | 26 ++- .../iris/web/IrisVariantsResource.java | 56 +++++++ .../service/hestia/CodeHintService.java | 20 +-- .../web/hestia/CodeHintResource.java | 44 +----- .../changelog/20240825191919_changelog.xml | 81 ++++++++++ .../resources/config/liquibase/master.xml | 1 + .../manage/detail/course-detail.component.ts | 2 - .../iris/settings/iris-settings.model.ts | 15 +- .../iris/settings/iris-sub-settings.model.ts | 13 +- .../entities/iris/settings/iris-template.ts | 6 - .../{iris-model.ts => iris-variant.ts} | 2 +- .../exercise-hint-update.component.html | 15 -- src/main/webapp/app/iris/iris.module.ts | 2 - ...-common-sub-settings-update.component.html | 53 +++++-- ...is-common-sub-settings-update.component.ts | 73 +++++---- ...-autoupdate-settings-update.component.html | 20 --- ...al-autoupdate-settings-update.component.ts | 15 -- .../iris-settings-update.component.html | 44 ++---- .../iris-settings-update.component.ts | 19 +-- .../settings/shared/iris-enabled.component.ts | 3 - .../settings/shared/iris-settings.service.ts | 11 +- src/main/webapp/i18n/de/iris.json | 24 +-- src/main/webapp/i18n/en/iris.json | 24 +-- .../connector/IrisRequestMockProvider.java | 19 +-- .../iris/AbstractIrisIntegrationTest.java | 16 +- .../iris/PyrisConnectorServiceTest.java | 21 +-- .../settings/IrisSettingsIntegrationTest.java | 43 ++--- ...mmon-sub-settings-update.component.spec.ts | 91 ++++++----- ...s-course-settings-update.component.spec.ts | 9 +- .../settings/iris-enabled.component.spec.ts | 29 ++-- ...exercise-settings-update.component.spec.ts | 6 - ...s-global-settings-update.component.spec.ts | 8 +- .../iris-settings-update-component.spec.ts | 17 +- .../component/iris/settings/mock-settings.ts | 26 +-- 65 files changed, 462 insertions(+), 1310 deletions(-) delete mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/domain/IrisTemplate.java delete mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisHestiaSession.java delete mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisHestiaSubSettings.java rename src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/{IrisModelListConverter.java => IrisListConverter.java} (88%) delete mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedHestiaSubSettingsDTO.java delete mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisHestiaSessionRepository.java delete mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTemplateRepository.java delete mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/service/IrisDefaultTemplateService.java rename src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/{PyrisModelDTO.java => PyrisVariantDTO.java} (67%) delete mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisHestiaSessionService.java delete mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/web/IrisModelsResource.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/web/IrisVariantsResource.java create mode 100644 src/main/resources/config/liquibase/changelog/20240825191919_changelog.xml delete mode 100644 src/main/webapp/app/entities/iris/settings/iris-template.ts rename src/main/webapp/app/entities/iris/settings/{iris-model.ts => iris-variant.ts} (69%) delete mode 100644 src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.html delete mode 100644 src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.ts diff --git a/docs/dev/guidelines/database.rst b/docs/dev/guidelines/database.rst index 65365549f10a..ea4e436b53d4 100644 --- a/docs/dev/guidelines/database.rst +++ b/docs/dev/guidelines/database.rst @@ -295,7 +295,7 @@ Best Practices // IrisSubSettings.java @Column(name = "allowed_models") @Convert(converter = IrisModelListConverter.class) - private TreeSet allowedModels = new TreeSet<>(); + private TreeSet allowedVariants = new TreeSet<>(); * **Ordered Collection with duplicates**: When you want to order the collection of (potentially duplicated) objects of the relationship, then always use a ``List``. It is important to note here that there is no inherent order in a database table. One could argue that you can use the ``id`` field for the ordering, but there are edge cases where this can lead to problems. Therefore, for an ordered collection with duplicates, **always** annotate it with ``@OrderColumn``. An order column indicates to Hibernate that we want to order our collection based on a specific column of our data table. By default, the column name it expects is *tablenameS\_order*. For ordered collections, we also recommend that you annotate them with ``cascade = CascadeType.ALL`` and ``orphanRemoval = true``. E.g.: diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/IrisTemplate.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/IrisTemplate.java deleted file mode 100644 index e1b486a34cbf..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/IrisTemplate.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.tum.cit.aet.artemis.iris.domain; - -import java.util.Objects; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; - -import org.hibernate.annotations.Cache; -import org.hibernate.annotations.CacheConcurrencyStrategy; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.cit.aet.artemis.core.domain.DomainObject; - -/** - * An IrisTemplate represents a handlebars template for Iris. - * It is sent to the Iris Python server to generate a response. - */ -@Entity -@Table(name = "iris_template") -@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisTemplate extends DomainObject { - - @Column(name = "content", columnDefinition = "LONGTEXT") - private String content; - - /** - * Empty constructor required for Hibernate and Jackson. - */ - public IrisTemplate() { - } - - /** - * Create a new IrisTemplate with content. - * - * @param content the content of the template - */ - public IrisTemplate(String content) { - this.content = content; - } - - public String getContent() { - return content; - } - - public void setContent(String template) { - this.content = template; - } - - @Override - public boolean equals(Object other) { - if (!super.equals(other)) { - return false; - } - IrisTemplate template = (IrisTemplate) other; - return Objects.equals(content, template.content); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), content); - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisHestiaSession.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisHestiaSession.java deleted file mode 100644 index f23603711cf5..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisHestiaSession.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.tum.cit.aet.artemis.iris.domain.session; - -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.ManyToOne; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; - -/** - * An Iris session for a hestia code hint. - * Currently used to generate descriptions for code hints. - */ -@Entity -@DiscriminatorValue("HESTIA") -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisHestiaSession extends IrisSession { - - @ManyToOne - @JsonIgnore - private CodeHint codeHint; - - public CodeHint getCodeHint() { - return codeHint; - } - - public void setCodeHint(CodeHint codeHint) { - this.codeHint = codeHint; - } - - @Override - public String toString() { - return "IrisHestiaSession{" + "id=" + getId() + ", codeHint=" + (codeHint == null ? "null" : codeHint.getId()) + '}'; - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisSession.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisSession.java index 3e6240a383de..13a4bd6f8b2b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisSession.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/session/IrisSession.java @@ -28,7 +28,7 @@ /** * An IrisSession represents a list of messages of Artemis, a user, and an LLM. - * See {@link IrisExerciseChatSession} and {@link IrisHestiaSession} for concrete implementations. + * See {@link IrisExerciseChatSession} and {@link IrisCourseChatSession} for concrete implementations. */ @Entity @Table(name = "iris_session") @@ -40,7 +40,6 @@ @JsonSubTypes({ @JsonSubTypes.Type(value = IrisExerciseChatSession.class, name = "chat"), // TODO: Legacy. Should ideally be "exercise_chat" @JsonSubTypes.Type(value = IrisCourseChatSession.class, name = "course_chat"), - @JsonSubTypes.Type(value = IrisHestiaSession.class, name = "hestia"), }) // @formatter:on @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisChatSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisChatSubSettings.java index 4305461b71cf..bf2851ae7979 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisChatSubSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisChatSubSettings.java @@ -1,31 +1,21 @@ package de.tum.cit.aet.artemis.iris.domain.settings; import jakarta.annotation.Nullable; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.OneToOne; import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - /** * An {@link IrisSubSettings} implementation for chat settings. * Chat settings notably provide settings for the rate limit. - * Chat settings provide a single {@link IrisTemplate} for the chat messages. */ @Entity @DiscriminatorValue("CHAT") @JsonInclude(JsonInclude.Include.NON_EMPTY) public class IrisChatSubSettings extends IrisSubSettings { - @Nullable - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - private IrisTemplate template; - @Nullable @Column(name = "rate_limit") private Integer rateLimit; @@ -34,15 +24,6 @@ public class IrisChatSubSettings extends IrisSubSettings { @Column(name = "rate_limit_timeframe_hours") private Integer rateLimitTimeframeHours; - @Nullable - public IrisTemplate getTemplate() { - return template; - } - - public void setTemplate(@Nullable IrisTemplate template) { - this.template = template; - } - @Nullable public Integer getRateLimit() { return rateLimit; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCompetencyGenerationSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCompetencyGenerationSubSettings.java index f68ae30d4b53..b8447b1bb378 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCompetencyGenerationSubSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCompetencyGenerationSubSettings.java @@ -1,36 +1,16 @@ package de.tum.cit.aet.artemis.iris.domain.settings; -import jakarta.annotation.Nullable; -import jakarta.persistence.CascadeType; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.OneToOne; import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - /** * An {@link IrisSubSettings} implementation for the settings for competency generation. - * CompetencyGeneration settings provide a single {@link IrisTemplate} */ @Entity @DiscriminatorValue("COMPETENCY_GENERATION") @JsonInclude(JsonInclude.Include.NON_EMPTY) public class IrisCompetencyGenerationSubSettings extends IrisSubSettings { - @Nullable - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - private IrisTemplate template; - - @Nullable - public IrisTemplate getTemplate() { - return template; - } - - public void setTemplate(@Nullable IrisTemplate template) { - this.template = template; - } - } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java index 2354ffd3c142..ca9d8723781f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisCourseSettings.java @@ -32,19 +32,10 @@ public class IrisCourseSettings extends IrisSettings { @JoinColumn(name = "iris_lecture_ingestion_settings_id") private IrisLectureIngestionSubSettings irisLectureIngestionSettings; - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - @JoinColumn(name = "iris_hestia_settings_id") - private IrisHestiaSubSettings irisHestiaSettings; - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "iris_competency_generation_settings_id") private IrisCompetencyGenerationSubSettings irisCompetencyGenerationSettings; - @Override - public boolean isValid() { - return course != null; - } - public Course getCourse() { return course; } @@ -73,16 +64,6 @@ public void setIrisChatSettings(IrisChatSubSettings irisChatSettings) { this.irisChatSettings = irisChatSettings; } - @Override - public IrisHestiaSubSettings getIrisHestiaSettings() { - return irisHestiaSettings; - } - - @Override - public void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings) { - this.irisHestiaSettings = irisHestiaSettings; - } - @Override public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings() { return irisCompetencyGenerationSettings; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java index 410bbde1954c..fbfede75714c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisExerciseSettings.java @@ -28,11 +28,6 @@ public class IrisExerciseSettings extends IrisSettings { @JoinColumn(name = "iris_chat_settings_id") private IrisChatSubSettings irisChatSettings; - @Override - public boolean isValid() { - return exercise != null; - } - public Exercise getExercise() { return exercise; } @@ -60,16 +55,6 @@ public void setIrisChatSettings(IrisChatSubSettings irisChatSettings) { this.irisChatSettings = irisChatSettings; } - @Override - public IrisHestiaSubSettings getIrisHestiaSettings() { - return null; - } - - @Override - public void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings) { - - } - @Override public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings() { return null; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java index 0ae60c36edd8..587a85b37b22 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisGlobalSettings.java @@ -1,15 +1,12 @@ package de.tum.cit.aet.artemis.iris.domain.settings; import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; -import org.hibernate.Hibernate; - import com.fasterxml.jackson.annotation.JsonInclude; /** @@ -22,21 +19,6 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class IrisGlobalSettings extends IrisSettings { - @Column(name = "current_version") - private int currentVersion; - - @Column(name = "enable_auto_update_chat") - private boolean enableAutoUpdateChat; - - @Column(name = "enable_auto_update_hestia") - private boolean enableAutoUpdateHestia; - - @Column(name = "enable_auto_update_lecture_ingestion") - private boolean enableAutoUpdateLectureIngestion; - - @Column(name = "enable_auto_update_competency_generation") - private boolean enableAutoUpdateCompetencyGeneration; - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "iris_chat_settings_id") private IrisChatSubSettings irisChatSettings; @@ -45,66 +27,10 @@ public class IrisGlobalSettings extends IrisSettings { @JoinColumn(name = "iris_lecture_ingestion_settings_id") private IrisLectureIngestionSubSettings irisLectureIngestionSettings; - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) - @JoinColumn(name = "iris_hestia_settings_id") - private IrisHestiaSubSettings irisHestiaSettings; - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "iris_competency_generation_settings_id") private IrisCompetencyGenerationSubSettings irisCompetencyGenerationSettings; - @Override - public boolean isValid() { - var chatSettingsValid = !Hibernate.isInitialized(irisChatSettings) || irisChatSettings == null - || (irisChatSettings.getTemplate() != null && irisChatSettings.getTemplate().getContent() != null && !irisChatSettings.getTemplate().getContent().isEmpty()); - var hestiaSettingsValid = !Hibernate.isInitialized(irisHestiaSettings) || irisHestiaSettings == null - || (irisHestiaSettings.getTemplate() != null && irisHestiaSettings.getTemplate().getContent() != null && !irisHestiaSettings.getTemplate().getContent().isEmpty()); - var competencyGenerationSettingsValid = !Hibernate.isInitialized(irisCompetencyGenerationSettings) || irisCompetencyGenerationSettings == null - || (irisCompetencyGenerationSettings.getTemplate() != null && irisCompetencyGenerationSettings.getTemplate().getContent() != null - && !irisCompetencyGenerationSettings.getTemplate().getContent().isEmpty()); - return chatSettingsValid && hestiaSettingsValid && competencyGenerationSettingsValid; - } - - public int getCurrentVersion() { - return currentVersion; - } - - public void setCurrentVersion(int currentVersion) { - this.currentVersion = currentVersion; - } - - public boolean isEnableAutoUpdateChat() { - return enableAutoUpdateChat; - } - - public void setEnableAutoUpdateChat(boolean enableAutoUpdateChat) { - this.enableAutoUpdateChat = enableAutoUpdateChat; - } - - public boolean isEnableAutoUpdateLectureIngestion() { - return enableAutoUpdateLectureIngestion; - } - - public void setEnableAutoUpdateLectureIngestion(boolean enableAutoUpdateLectureIngestion) { - this.enableAutoUpdateLectureIngestion = enableAutoUpdateLectureIngestion; - } - - public boolean isEnableAutoUpdateHestia() { - return enableAutoUpdateHestia; - } - - public void setEnableAutoUpdateHestia(boolean enableAutoUpdateHestia) { - this.enableAutoUpdateHestia = enableAutoUpdateHestia; - } - - public boolean isEnableAutoUpdateCompetencyGeneration() { - return enableAutoUpdateCompetencyGeneration; - } - - public void setEnableAutoUpdateCompetencyGeneration(boolean enableAutoUpdateCompetencyGeneration) { - this.enableAutoUpdateCompetencyGeneration = enableAutoUpdateCompetencyGeneration; - } - @Override public IrisLectureIngestionSubSettings getIrisLectureIngestionSettings() { return irisLectureIngestionSettings; @@ -125,16 +51,6 @@ public void setIrisChatSettings(IrisChatSubSettings irisChatSettings) { this.irisChatSettings = irisChatSettings; } - @Override - public IrisHestiaSubSettings getIrisHestiaSettings() { - return irisHestiaSettings; - } - - @Override - public void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings) { - this.irisHestiaSettings = irisHestiaSettings; - } - @Override public IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings() { return irisCompetencyGenerationSettings; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisHestiaSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisHestiaSubSettings.java deleted file mode 100644 index 1c478a8ccfbe..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisHestiaSubSettings.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.tum.cit.aet.artemis.iris.domain.settings; - -import jakarta.annotation.Nullable; -import jakarta.persistence.CascadeType; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.OneToOne; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - -/** - * An {@link IrisSubSettings} implementation for the Hestia integration settings. - * Hestia settings provide a single {@link IrisTemplate} for the hestia code hint generation requests. - */ -@Entity -@DiscriminatorValue("HESTIA") -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisHestiaSubSettings extends IrisSubSettings { - - @Nullable - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - private IrisTemplate template; - - @Nullable - public IrisTemplate getTemplate() { - return template; - } - - public void setTemplate(@Nullable IrisTemplate template) { - this.template = template; - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisModelListConverter.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisListConverter.java similarity index 88% rename from src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisModelListConverter.java rename to src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisListConverter.java index 938ce5dae0c7..be4cf5199a0b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisModelListConverter.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisListConverter.java @@ -9,7 +9,7 @@ import jakarta.persistence.Converter; @Converter -public class IrisModelListConverter implements AttributeConverter, String> { +public class IrisListConverter implements AttributeConverter, String> { @Override public String convertToDatabaseColumn(SortedSet type) { diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java index 5ca715a2f688..5efb75e76ea0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSettings.java @@ -49,13 +49,7 @@ public abstract class IrisSettings extends DomainObject { public abstract void setIrisLectureIngestionSettings(IrisLectureIngestionSubSettings irisLectureIngestionSettings); - public abstract IrisHestiaSubSettings getIrisHestiaSettings(); - - public abstract void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings); - public abstract IrisCompetencyGenerationSubSettings getIrisCompetencyGenerationSettings(); public abstract void setIrisCompetencyGenerationSettings(IrisCompetencyGenerationSubSettings irisCompetencyGenerationSubSettings); - - public abstract boolean isValid(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java index 16588cf448a5..5288c247e891 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettings.java @@ -26,7 +26,6 @@ * IrisSubSettings is an abstract super class for the specific sub settings types. * Sub Settings are settings for a specific feature of Iris. * {@link IrisChatSubSettings} are used to specify settings for the chat feature. - * {@link IrisHestiaSubSettings} are used to specify settings for the Hestia integration. * {@link IrisCompetencyGenerationSubSettings} are used to specify settings for the competency generation feature. *

* Also see {@link de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService} for more information. @@ -41,7 +40,6 @@ @JsonSubTypes({ @JsonSubTypes.Type(value = IrisChatSubSettings.class, name = "chat"), @JsonSubTypes.Type(value = IrisLectureIngestionSubSettings.class, name = "lecture-ingestion"), - @JsonSubTypes.Type(value = IrisHestiaSubSettings.class, name = "hestia"), @JsonSubTypes.Type(value = IrisCompetencyGenerationSubSettings.class, name = "competency-generation") }) // @formatter:on @@ -51,13 +49,12 @@ public abstract class IrisSubSettings extends DomainObject { @Column(name = "enabled") private boolean enabled = false; - @Column(name = "allowed_models") - @Convert(converter = IrisModelListConverter.class) - private SortedSet allowedModels = new TreeSet<>(); + @Column(name = "allowed_variants", nullable = false) + @Convert(converter = IrisListConverter.class) + private SortedSet allowedVariants = new TreeSet<>(); - @Nullable - @Column(name = "preferred_model") - private String preferredModel; + @Column(name = "selected_variant", nullable = false) + private String selectedVariant; public boolean isEnabled() { return enabled; @@ -67,20 +64,20 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - public SortedSet getAllowedModels() { - return allowedModels; + public SortedSet getAllowedVariants() { + return allowedVariants; } - public void setAllowedModels(SortedSet allowedModels) { - this.allowedModels = allowedModels; + public void setAllowedVariants(SortedSet allowedVariants) { + this.allowedVariants = allowedVariants; } @Nullable - public String getPreferredModel() { - return preferredModel; + public String getSelectedVariant() { + return selectedVariant; } - public void setPreferredModel(@Nullable String preferredModel) { - this.preferredModel = preferredModel; + public void setSelectedVariant(@Nullable String selectedVariant) { + this.selectedVariant = selectedVariant; } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java index d938134f4555..2a8270d420bc 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/domain/settings/IrisSubSettingsType.java @@ -1,5 +1,5 @@ package de.tum.cit.aet.artemis.iris.domain.settings; public enum IrisSubSettingsType { - CHAT, HESTIA, COMPETENCY_GENERATION, LECTURE_INGESTION + CHAT, COMPETENCY_GENERATION, LECTURE_INGESTION } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedChatSubSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedChatSubSettingsDTO.java index 72d8e599ed70..c5589e824507 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedChatSubSettingsDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedChatSubSettingsDTO.java @@ -6,10 +6,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record IrisCombinedChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable Set allowedModels, - @Nullable String preferredModel, @Nullable IrisTemplate template) { +public record IrisCombinedChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable Set allowedVariants, + @Nullable String selectedVariant) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java index 18ffcbc17b50..414b422e0f64 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedCompetencyGenerationSubSettingsDTO.java @@ -6,9 +6,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record IrisCombinedCompetencyGenerationSubSettingsDTO(boolean enabled, @Nullable Set allowedModels, @Nullable String preferredModel, - @Nullable IrisTemplate template) { +public record IrisCombinedCompetencyGenerationSubSettingsDTO(boolean enabled, @Nullable Set allowedVariants, @Nullable String selectedVariant) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedHestiaSubSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedHestiaSubSettingsDTO.java deleted file mode 100644 index c70ce4825a92..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedHestiaSubSettingsDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.tum.cit.aet.artemis.iris.dto; - -import java.util.Set; - -import jakarta.annotation.Nullable; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - -@JsonInclude(JsonInclude.Include.NON_EMPTY) -public record IrisCombinedHestiaSubSettingsDTO(boolean enabled, @Nullable Set allowedModels, @Nullable String preferredModel, @Nullable IrisTemplate template) { -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java index 9353757c782e..355b05ae551a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisCombinedSettingsDTO.java @@ -4,5 +4,5 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public record IrisCombinedSettingsDTO(IrisCombinedChatSubSettingsDTO irisChatSettings, IrisCombinedLectureIngestionSubSettingsDTO irisLectureIngestionSettings, - IrisCombinedHestiaSubSettingsDTO irisHestiaSettings, IrisCombinedCompetencyGenerationSubSettingsDTO irisCompetencyGenerationSettings) { + IrisCombinedCompetencyGenerationSubSettingsDTO irisCompetencyGenerationSettings) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisHestiaSessionRepository.java b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisHestiaSessionRepository.java deleted file mode 100644 index 22a14bd98bd7..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisHestiaSessionRepository.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.tum.cit.aet.artemis.iris.repository; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; - -import java.util.List; - -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Repository; - -import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; -import de.tum.cit.aet.artemis.iris.domain.session.IrisHestiaSession; - -/** - * Repository interface for managing {@link IrisHestiaSession} entities. - * Provides custom queries for finding hestia sessions based on different criteria. - */ -@Repository -@Profile(PROFILE_IRIS) -public interface IrisHestiaSessionRepository extends ArtemisJpaRepository { - - /** - * Finds a list of {@link IrisHestiaSession} based on the exercise and user IDs. - * - * @param codeHintId The ID of the code hint. - * @return A list of hestia sessions sorted by creation date in descending order. - */ - List findByCodeHintIdOrderByCreationDateDesc(Long codeHintId); -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisSettingsRepository.java b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisSettingsRepository.java index 8c4ffd56068e..4dd037d54c77 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisSettingsRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisSettingsRepository.java @@ -29,7 +29,6 @@ public interface IrisSettingsRepository extends ArtemisJpaRepository findAllGlobalSettings(); @@ -43,7 +42,6 @@ default IrisGlobalSettings findGlobalSettingsElseThrow() { FROM IrisCourseSettings irisSettings LEFT JOIN FETCH irisSettings.irisChatSettings LEFT JOIN FETCH irisSettings.irisLectureIngestionSettings - LEFT JOIN FETCH irisSettings.irisHestiaSettings LEFT JOIN FETCH irisSettings.irisCompetencyGenerationSettings WHERE irisSettings.course.id = :courseId """) diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTemplateRepository.java b/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTemplateRepository.java deleted file mode 100644 index 2b1a930d7aef..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/repository/IrisTemplateRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.tum.cit.aet.artemis.iris.repository; - -import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - -/** - * Spring Data repository for the IrisTemplate entity. - */ -public interface IrisTemplateRepository extends ArtemisJpaRepository { - -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisDefaultTemplateService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisDefaultTemplateService.java deleted file mode 100644 index 2a4757596bf1..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisDefaultTemplateService.java +++ /dev/null @@ -1,76 +0,0 @@ -package de.tum.cit.aet.artemis.iris.service; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.Optional; - -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Profile; -import org.springframework.core.io.Resource; -import org.springframework.stereotype.Service; - -import de.tum.cit.aet.artemis.core.service.ResourceLoaderService; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; - -/** - * Service that loads default Iris templates from the resources/templates/iris folder. - */ -@Profile(PROFILE_IRIS) -@Service -public class IrisDefaultTemplateService { - - private static final Logger log = LoggerFactory.getLogger(IrisDefaultTemplateService.class); - - private final ResourceLoaderService resourceLoaderService; - - public IrisDefaultTemplateService(ResourceLoaderService resourceLoaderService) { - this.resourceLoaderService = resourceLoaderService; - } - - /** - * Loads the default Iris template with the given file name. - * For example, "chat.hbs" will load the template from "resources/templates/iris/chat.hbs". - * - * @param templateFileName The file name of the template to load. - * @return The loaded Iris template, or an empty template if an IO error occurred. - */ - public IrisTemplate load(String templateFileName) { - Path filePath = Path.of("templates", "iris", templateFileName); - Resource resource = resourceLoaderService.getResource(filePath); - try { - String fileContent = IOUtils.toString(resource.getInputStream(), StandardCharsets.UTF_8); - return new IrisTemplate(fileContent); - } - catch (IOException e) { - log.error("Error while loading Iris template from file: {}", filePath, e); - return new IrisTemplate(""); - } - } - - /** - * Loads the global template version from the "resources/templates/iris/template-version.txt" file. - * - * @return an Optional containing the version loaded from the file, or an empty Optional if there was an error. - */ - public Optional loadGlobalTemplateVersion() { - Path filePath = Path.of("templates", "iris", "template-version.txt"); - Resource resource = resourceLoaderService.getResource(filePath); - try { - String fileContent = IOUtils.toString(resource.getInputStream(), StandardCharsets.UTF_8); - int version = Integer.parseInt(fileContent.trim()); - return Optional.of(version); - } - catch (IOException e) { - log.error("Error while loading global template version from file: {}", filePath, e); - } - catch (NumberFormatException e) { - log.error("Content of {} was not a parseable int!", filePath, e); - } - return Optional.empty(); - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisSessionService.java index 41fa1247b739..c37844d9740a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/IrisSessionService.java @@ -14,12 +14,10 @@ import de.tum.cit.aet.artemis.iris.domain.message.IrisMessage; import de.tum.cit.aet.artemis.iris.domain.session.IrisCourseChatSession; import de.tum.cit.aet.artemis.iris.domain.session.IrisExerciseChatSession; -import de.tum.cit.aet.artemis.iris.domain.session.IrisHestiaSession; import de.tum.cit.aet.artemis.iris.domain.session.IrisSession; import de.tum.cit.aet.artemis.iris.service.session.IrisChatBasedFeatureInterface; import de.tum.cit.aet.artemis.iris.service.session.IrisCourseChatSessionService; import de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService; -import de.tum.cit.aet.artemis.iris.service.session.IrisHestiaSessionService; import de.tum.cit.aet.artemis.iris.service.session.IrisRateLimitedFeatureInterface; import de.tum.cit.aet.artemis.iris.service.session.IrisSubFeatureInterface; @@ -36,14 +34,11 @@ public class IrisSessionService { private final IrisCourseChatSessionService irisCourseChatSessionService; - private final IrisHestiaSessionService irisHestiaSessionService; - public IrisSessionService(UserRepository userRepository, IrisExerciseChatSessionService irisExerciseChatSessionService, - IrisCourseChatSessionService irisCourseChatSessionService, IrisHestiaSessionService irisHestiaSessionService) { + IrisCourseChatSessionService irisCourseChatSessionService) { this.userRepository = userRepository; this.irisExerciseChatSessionService = irisExerciseChatSessionService; this.irisCourseChatSessionService = irisCourseChatSessionService; - this.irisHestiaSessionService = irisHestiaSessionService; } /** @@ -138,7 +133,6 @@ private IrisSubFeatureWrapper getIrisSessionSubServic return switch (session) { case IrisExerciseChatSession chatSession -> (IrisSubFeatureWrapper) new IrisSubFeatureWrapper<>(irisExerciseChatSessionService, chatSession); case IrisCourseChatSession courseChatSession -> (IrisSubFeatureWrapper) new IrisSubFeatureWrapper<>(irisCourseChatSessionService, courseChatSession); - case IrisHestiaSession hestiaSession -> (IrisSubFeatureWrapper) new IrisSubFeatureWrapper<>(irisHestiaSessionService, hestiaSession); case null, default -> throw new BadRequestException("Unknown Iris session type " + session.getClass().getSimpleName()); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java index 785fc59b9ed7..f41de6b6c97d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/PyrisConnectorService.java @@ -19,10 +19,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; import de.tum.cit.aet.artemis.iris.exception.IrisException; import de.tum.cit.aet.artemis.iris.exception.IrisForbiddenException; import de.tum.cit.aet.artemis.iris.exception.IrisInternalPyrisErrorException; -import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisModelDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisVariantDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.lectureingestionwebhook.PyrisWebhookLectureIngestionExecutionDTO; import de.tum.cit.aet.artemis.iris.web.open.PublicPyrisStatusUpdateResource; @@ -50,13 +51,14 @@ public PyrisConnectorService(@Qualifier("pyrisRestTemplate") RestTemplate restTe } /** - * Requests all available models from Pyris + * Requests all available variants from Pyris for a feature * - * @return A list of available Models as IrisModelDTO + * @param feature The feature to get the variants for + * @return A list of available Models as IrisVariantDTO */ - public List getOfferedModels() throws PyrisConnectorException { + public List getOfferedVariants(IrisSubSettingsType feature) throws PyrisConnectorException { try { - var response = restTemplate.getForEntity(pyrisUrl + "/api/v1/models", PyrisModelDTO[].class); + var response = restTemplate.getForEntity(pyrisUrl + "/api/v1/pipelines/" + feature.name() + "/variants", PyrisVariantDTO[].class); if (!response.getStatusCode().is2xxSuccessful() || !response.hasBody()) { throw new PyrisConnectorException("Could not fetch offered models"); } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisModelDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisVariantDTO.java similarity index 67% rename from src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisModelDTO.java rename to src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisVariantDTO.java index 705fada64870..ccfbecf7ee9a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisModelDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/pyris/dto/PyrisVariantDTO.java @@ -3,5 +3,5 @@ import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record PyrisModelDTO(String id, String name, String description) { +public record PyrisVariantDTO(String id, String name, String description) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java index a2c404b13103..6dea7a728ca6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisCourseChatSessionService.java @@ -116,7 +116,8 @@ public void checkRateLimit(User user) { */ @Override public void requestAndHandleResponse(IrisCourseChatSession session) { - requestAndHandleResponse(session, "default", null); + var variant = irisSettingsService.getCombinedIrisSettingsFor(session.getCourse(), false).irisChatSettings().selectedVariant(); + requestAndHandleResponse(session, variant, null); } private void requestAndHandleResponse(IrisCourseChatSession session, String variant, CompetencyJol competencyJol) { diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java index cec0a9322134..47e0da357ea9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java @@ -146,9 +146,8 @@ public void requestAndHandleResponse(IrisExerciseChatSession session) { var exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(chatSession.getExercise().getId()); var latestSubmission = getLatestSubmissionIfExists(exercise, chatSession.getUser()); - // TODO: Use settings to determine the variant - // var irisSettings = irisSettingsService.getCombinedIrisSettingsFor(chatSession.getExercise(), false); - pyrisPipelineService.executeExerciseChatPipeline("default", latestSubmission, exercise, chatSession); + var variant = irisSettingsService.getCombinedIrisSettingsFor(session.getExercise(), false).irisChatSettings().selectedVariant(); + pyrisPipelineService.executeExerciseChatPipeline(variant, latestSubmission, exercise, chatSession); } private Optional getLatestSubmissionIfExists(ProgrammingExercise exercise, User user) { diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisHestiaSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisHestiaSessionService.java deleted file mode 100644 index 6762b6d23d43..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisHestiaSessionService.java +++ /dev/null @@ -1,113 +0,0 @@ -package de.tum.cit.aet.artemis.iris.service.session; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; - -import java.time.ZonedDateTime; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.core.security.Role; -import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; -import de.tum.cit.aet.artemis.iris.domain.session.IrisHestiaSession; -import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; -import de.tum.cit.aet.artemis.iris.repository.IrisHestiaSessionRepository; -import de.tum.cit.aet.artemis.iris.repository.IrisSessionRepository; -import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorService; -import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; -import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; -import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; - -/** - * Service to handle the Hestia integration of Iris. - */ -@Service -@Profile(PROFILE_IRIS) -public class IrisHestiaSessionService implements IrisButtonBasedFeatureInterface { - - private static final Logger log = LoggerFactory.getLogger(IrisHestiaSessionService.class); - - private final IrisSettingsService irisSettingsService; - - private final AuthorizationCheckService authCheckService; - - private final IrisSessionRepository irisSessionRepository; - - private final IrisHestiaSessionRepository irisHestiaSessionRepository; - - public IrisHestiaSessionService(PyrisConnectorService pyrisConnectorService, IrisSettingsService irisSettingsService, AuthorizationCheckService authCheckService, - IrisSessionRepository irisSessionRepository, IrisHestiaSessionRepository irisHestiaSessionRepository) { - this.irisSettingsService = irisSettingsService; - this.authCheckService = authCheckService; - this.irisSessionRepository = irisSessionRepository; - this.irisHestiaSessionRepository = irisHestiaSessionRepository; - } - - /** - * Creates a new Iris session for the given code hint. - * If there is already an existing session for the code hint from the last hour, it will be returned instead. - * - * @param codeHint The code hint to create the session for - * @return The Iris session for the code hint - */ - public IrisHestiaSession getOrCreateSession(CodeHint codeHint) { - var existingSessions = irisHestiaSessionRepository.findByCodeHintIdOrderByCreationDateDesc(codeHint.getId()); - // Return the newest session if there is one and it is not older than 1 hour - if (!existingSessions.isEmpty() && existingSessions.getFirst().getCreationDate().plusHours(1).isAfter(ZonedDateTime.now())) { - checkHasAccessTo(null, existingSessions.getFirst()); - return existingSessions.getFirst(); - } - - // Otherwise create a new session - var irisSession = new IrisHestiaSession(); - irisSession.setCodeHint(codeHint); - checkHasAccessTo(null, irisSession); - irisSession = irisSessionRepository.save(irisSession); - return irisSession; - } - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - record HestiaDTO(CodeHint codeHint, IrisHestiaSession session, ProgrammingExercise exercise) { - } - - /** - * Generates the description and content for a code hint. - * It does not directly save the code hint, but instead returns it with the generated description and content. - * This way the instructor can still modify the code hint before saving it or discard the changes. - * - * @param session The Iris session to generate the description for - * @return The code hint with the generated description and content - */ - @Override - public CodeHint executeRequest(IrisHestiaSession session) { - // TODO: Re-add in a future PR. Remember to reenable the test cases! - return null; - } - - /** - * Checks if the user has at least the given role for the exercise of the code hint. - * - * @param user The user to check the access for - * @param session The Iris session to check the access for - */ - @Override - public void checkHasAccessTo(User user, IrisHestiaSession session) { - var exercise = session.getCodeHint().getExercise(); - authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, exercise, user); - } - - /** - * Not supported for Iris Hestia sessions. - * - * @param session The session to get a message for - */ - @Override - public void checkIsFeatureActivatedFor(IrisHestiaSession session) { - irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.HESTIA, session.getCodeHint().getExercise()); - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java index 39bb14ff31cd..bf9d4a8d35d3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSettingsService.java @@ -9,7 +9,8 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.Objects; -import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; import java.util.function.Supplier; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -24,20 +25,17 @@ import de.tum.cit.aet.artemis.core.exception.ConflictException; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.exercise.domain.Exercise; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; import de.tum.cit.aet.artemis.iris.domain.settings.IrisChatSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCompetencyGenerationSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCourseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisExerciseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisGlobalSettings; -import de.tum.cit.aet.artemis.iris.domain.settings.IrisHestiaSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisLectureIngestionSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedSettingsDTO; import de.tum.cit.aet.artemis.iris.repository.IrisSettingsRepository; -import de.tum.cit.aet.artemis.iris.service.IrisDefaultTemplateService; /** * Service for managing {@link IrisSettings}. @@ -54,34 +52,14 @@ public class IrisSettingsService { private final IrisSubSettingsService irisSubSettingsService; - private final IrisDefaultTemplateService irisDefaultTemplateService; - private final AuthorizationCheckService authCheckService; - public IrisSettingsService(IrisSettingsRepository irisSettingsRepository, IrisSubSettingsService irisSubSettingsService, IrisDefaultTemplateService irisDefaultTemplateService, - AuthorizationCheckService authCheckService) { + public IrisSettingsService(IrisSettingsRepository irisSettingsRepository, IrisSubSettingsService irisSubSettingsService, AuthorizationCheckService authCheckService) { this.irisSettingsRepository = irisSettingsRepository; this.irisSubSettingsService = irisSubSettingsService; - this.irisDefaultTemplateService = irisDefaultTemplateService; this.authCheckService = authCheckService; } - private Optional loadGlobalTemplateVersion() { - return irisDefaultTemplateService.loadGlobalTemplateVersion(); - } - - private IrisTemplate loadDefaultChatTemplate() { - return irisDefaultTemplateService.load("chat.hbs"); - } - - private IrisTemplate loadDefaultHestiaTemplate() { - return irisDefaultTemplateService.load("hestia.hbs"); - } - - private IrisTemplate loadDefaultCompetencyGenerationTemplate() { - return irisDefaultTemplateService.load("competency-generation.hbs"); - } - /** * Hooks into the {@link ApplicationReadyEvent} and creates or updates the global IrisSettings object on startup. * @@ -98,10 +76,6 @@ public void execute(ApplicationReadyEvent event) throws Exception { if (allGlobalSettings.size() > 1) { var maxIdSettings = allGlobalSettings.stream().max(Comparator.comparingLong(IrisSettings::getId)).orElseThrow(); allGlobalSettings.stream().filter(settings -> !Objects.equals(settings.getId(), maxIdSettings.getId())).forEach(irisSettingsRepository::delete); - autoUpdateGlobalSettings(maxIdSettings); - } - else { - autoUpdateGlobalSettings(allGlobalSettings.stream().findFirst().get()); } } @@ -110,46 +84,20 @@ public void execute(ApplicationReadyEvent event) throws Exception { */ private void createInitialGlobalSettings() { var settings = new IrisGlobalSettings(); - settings.setCurrentVersion(loadGlobalTemplateVersion().orElse(0)); initializeIrisChatSettings(settings); initializeIrisLectureIngestionSettings(settings); - initializeIrisHestiaSettings(settings); initializeIrisCompetencyGenerationSettings(settings); irisSettingsRepository.save(settings); } - /** - * Auto updates the global IrisSettings object if the current version is outdated. - * - * @param settings The global IrisSettings object to update - */ - private void autoUpdateGlobalSettings(IrisGlobalSettings settings) { - Optional globalVersion = loadGlobalTemplateVersion(); - if (globalVersion.isEmpty() || settings.getCurrentVersion() < globalVersion.get()) { - if (settings.isEnableAutoUpdateChat() || settings.getIrisChatSettings() == null) { - initializeIrisChatSettings(settings); - } - if (settings.isEnableAutoUpdateLectureIngestion() || settings.getIrisLectureIngestionSettings() == null) { - initializeIrisLectureIngestionSettings(settings); - } - if (settings.isEnableAutoUpdateHestia() || settings.getIrisHestiaSettings() == null) { - initializeIrisHestiaSettings(settings); - } - if (settings.isEnableAutoUpdateCompetencyGeneration() || settings.getIrisCompetencyGenerationSettings() == null) { - initializeIrisCompetencyGenerationSettings(settings); - } - - globalVersion.ifPresent(settings::setCurrentVersion); - saveIrisSettings(settings); - } - } - private static T initializeSettings(T settings, Supplier constructor) { if (settings == null) { settings = constructor.get(); settings.setEnabled(false); + settings.setAllowedVariants(new TreeSet<>(Set.of("default"))); + settings.setSelectedVariant("default"); } return settings; } @@ -157,7 +105,6 @@ private static T initializeSettings(T settings, Supp private void initializeIrisChatSettings(IrisGlobalSettings settings) { var irisChatSettings = settings.getIrisChatSettings(); irisChatSettings = initializeSettings(irisChatSettings, IrisChatSubSettings::new); - irisChatSettings.setTemplate(loadDefaultChatTemplate()); settings.setIrisChatSettings(irisChatSettings); } @@ -167,17 +114,9 @@ private void initializeIrisLectureIngestionSettings(IrisGlobalSettings settings) settings.setIrisLectureIngestionSettings(irisLectureIngestionSettings); } - private void initializeIrisHestiaSettings(IrisGlobalSettings settings) { - var irisHestiaSettings = settings.getIrisHestiaSettings(); - irisHestiaSettings = initializeSettings(irisHestiaSettings, IrisHestiaSubSettings::new); - irisHestiaSettings.setTemplate(loadDefaultHestiaTemplate()); - settings.setIrisHestiaSettings(irisHestiaSettings); - } - private void initializeIrisCompetencyGenerationSettings(IrisGlobalSettings settings) { var irisCompetencyGenerationSettings = settings.getIrisCompetencyGenerationSettings(); irisCompetencyGenerationSettings = initializeSettings(irisCompetencyGenerationSettings, IrisCompetencyGenerationSubSettings::new); - irisCompetencyGenerationSettings.setTemplate(loadDefaultCompetencyGenerationTemplate()); settings.setIrisCompetencyGenerationSettings(irisCompetencyGenerationSettings); } @@ -214,9 +153,6 @@ private T saveNewIrisSettings(T settings) { if (settings instanceof IrisGlobalSettings) { throw new BadRequestAlertException("You can not create new global settings", "IrisSettings", "notGlobal"); } - if (!settings.isValid()) { - throw new BadRequestAlertException("New Iris settings are not valid", "IrisSettings", "notValid"); - } if (settings instanceof IrisCourseSettings courseSettings && irisSettingsRepository.findCourseSettings(courseSettings.getCourse().getId()).isPresent()) { throw new ConflictException("Iris settings for this course already exist", "IrisSettings", "alreadyExists"); } @@ -241,9 +177,6 @@ private T updateIrisSettings(long existingSettingsId, T if (!Objects.equals(existingSettingsId, settingsUpdate.getId())) { throw new ConflictException("Existing Iris settings ID does not match update ID", "IrisSettings", "idMismatch"); } - if (!settingsUpdate.isValid()) { - throw new BadRequestAlertException("Updated Iris settings are not valid", "IrisSettings", "notValid"); - } var existingSettings = irisSettingsRepository.findByIdElseThrow(existingSettingsId); @@ -269,17 +202,9 @@ else if (existingSettings instanceof IrisExerciseSettings exerciseSettings && se * @return The updated global Iris settings */ private IrisGlobalSettings updateGlobalSettings(IrisGlobalSettings existingSettings, IrisGlobalSettings settingsUpdate) { - existingSettings.setCurrentVersion(settingsUpdate.getCurrentVersion()); - - existingSettings.setEnableAutoUpdateChat(settingsUpdate.isEnableAutoUpdateChat()); - existingSettings.setEnableAutoUpdateLectureIngestion(settingsUpdate.isEnableAutoUpdateLectureIngestion()); - existingSettings.setEnableAutoUpdateHestia(settingsUpdate.isEnableAutoUpdateHestia()); - existingSettings.setEnableAutoUpdateCompetencyGeneration(settingsUpdate.isEnableAutoUpdateCompetencyGeneration()); - existingSettings.setIrisLectureIngestionSettings( irisSubSettingsService.update(existingSettings.getIrisLectureIngestionSettings(), settingsUpdate.getIrisLectureIngestionSettings(), null, GLOBAL)); existingSettings.setIrisChatSettings(irisSubSettingsService.update(existingSettings.getIrisChatSettings(), settingsUpdate.getIrisChatSettings(), null, GLOBAL)); - existingSettings.setIrisHestiaSettings(irisSubSettingsService.update(existingSettings.getIrisHestiaSettings(), settingsUpdate.getIrisHestiaSettings(), null, GLOBAL)); existingSettings.setIrisCompetencyGenerationSettings( irisSubSettingsService.update(existingSettings.getIrisCompetencyGenerationSettings(), settingsUpdate.getIrisCompetencyGenerationSettings(), null, GLOBAL)); @@ -299,8 +224,6 @@ private IrisCourseSettings updateCourseSettings(IrisCourseSettings existingSetti irisSubSettingsService.update(existingSettings.getIrisChatSettings(), settingsUpdate.getIrisChatSettings(), parentSettings.irisChatSettings(), COURSE)); existingSettings.setIrisLectureIngestionSettings(irisSubSettingsService.update(existingSettings.getIrisLectureIngestionSettings(), settingsUpdate.getIrisLectureIngestionSettings(), parentSettings.irisLectureIngestionSettings(), COURSE)); - existingSettings.setIrisHestiaSettings( - irisSubSettingsService.update(existingSettings.getIrisHestiaSettings(), settingsUpdate.getIrisHestiaSettings(), parentSettings.irisHestiaSettings(), COURSE)); existingSettings.setIrisCompetencyGenerationSettings(irisSubSettingsService.update(existingSettings.getIrisCompetencyGenerationSettings(), settingsUpdate.getIrisCompetencyGenerationSettings(), parentSettings.irisCompetencyGenerationSettings(), COURSE)); @@ -382,8 +305,7 @@ public IrisCombinedSettingsDTO getCombinedIrisGlobalSettings() { settingsList.add(getGlobalSettings()); return new IrisCombinedSettingsDTO(irisSubSettingsService.combineChatSettings(settingsList, false), - irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, false), irisSubSettingsService.combineHestiaSettings(settingsList, false), - irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, false)); + irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, false), irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, false)); } /** @@ -402,7 +324,7 @@ public IrisCombinedSettingsDTO getCombinedIrisSettingsFor(Course course, boolean settingsList.add(irisSettingsRepository.findCourseSettings(course.getId()).orElse(null)); return new IrisCombinedSettingsDTO(irisSubSettingsService.combineChatSettings(settingsList, minimal), - irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), irisSubSettingsService.combineHestiaSettings(settingsList, minimal), + irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, minimal)); } @@ -423,7 +345,7 @@ public IrisCombinedSettingsDTO getCombinedIrisSettingsFor(Exercise exercise, boo settingsList.add(getRawIrisSettingsFor(exercise)); return new IrisCombinedSettingsDTO(irisSubSettingsService.combineChatSettings(settingsList, minimal), - irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), irisSubSettingsService.combineHestiaSettings(settingsList, minimal), + irisSubSettingsService.combineLectureIngestionSubSettings(settingsList, minimal), irisSubSettingsService.combineCompetencyGenerationSettings(settingsList, minimal)); } @@ -450,7 +372,6 @@ public IrisCourseSettings getDefaultSettingsFor(Course course) { settings.setCourse(course); settings.setIrisLectureIngestionSettings(new IrisLectureIngestionSubSettings()); settings.setIrisChatSettings(new IrisChatSubSettings()); - settings.setIrisHestiaSettings(new IrisHestiaSubSettings()); settings.setIrisCompetencyGenerationSettings(new IrisCompetencyGenerationSubSettings()); return settings; } @@ -523,7 +444,6 @@ public void deleteSettingsFor(Exercise exercise) { private boolean isFeatureEnabledInSettings(IrisCombinedSettingsDTO settings, IrisSubSettingsType type) { return switch (type) { case CHAT -> settings.irisChatSettings().enabled(); - case HESTIA -> settings.irisHestiaSettings().enabled(); case COMPETENCY_GENERATION -> settings.irisCompetencyGenerationSettings().enabled(); case LECTURE_INGESTION -> settings.irisLectureIngestionSettings().enabled(); }; diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java index dfd555bb59c5..5a3c20b4c810 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/settings/IrisSubSettingsService.java @@ -15,18 +15,15 @@ import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; import de.tum.cit.aet.artemis.iris.domain.settings.IrisChatSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCompetencyGenerationSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisExerciseSettings; -import de.tum.cit.aet.artemis.iris.domain.settings.IrisHestiaSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisLectureIngestionSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSettingsType; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettings; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedChatSubSettingsDTO; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedCompetencyGenerationSubSettingsDTO; -import de.tum.cit.aet.artemis.iris.dto.IrisCombinedHestiaSubSettingsDTO; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedLectureIngestionSubSettingsDTO; /** @@ -76,10 +73,9 @@ public IrisChatSubSettings update(IrisChatSubSettings currentSettings, IrisChatS currentSettings.setRateLimit(newSettings.getRateLimit()); currentSettings.setRateLimitTimeframeHours(newSettings.getRateLimitTimeframeHours()); } - currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); - currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), - parentSettings != null ? parentSettings.allowedModels() : null)); - currentSettings.setTemplate(newSettings.getTemplate()); + currentSettings.setAllowedVariants(selectAllowedVariants(currentSettings.getAllowedVariants(), newSettings.getAllowedVariants())); + currentSettings.setSelectedVariant(validateSelectedVariant(currentSettings.getSelectedVariant(), newSettings.getSelectedVariant(), currentSettings.getAllowedVariants(), + parentSettings != null ? parentSettings.allowedVariants() : null)); return currentSettings; } @@ -114,40 +110,6 @@ public IrisLectureIngestionSubSettings update(IrisLectureIngestionSubSettings cu return currentSettings; } - /** - * Updates a Hestia sub settings object. - * If the new settings are null, the current settings will be deleted (except if the parent settings are null == if the settings are global). - * Special notes: - * - If the user is not an admin the allowed models will not be updated. - * - If the user is not an admin the preferred model will only be updated if it is included in the allowed models. - * - * @param currentSettings Current Hestia sub settings. - * @param newSettings Updated Hestia sub settings. - * @param parentSettings Parent Hestia sub settings. - * @param settingsType Type of the settings the sub settings belong to. - * @return Updated Hestia sub settings. - */ - public IrisHestiaSubSettings update(IrisHestiaSubSettings currentSettings, IrisHestiaSubSettings newSettings, IrisCombinedHestiaSubSettingsDTO parentSettings, - IrisSettingsType settingsType) { - if (newSettings == null) { - if (parentSettings == null) { - throw new IllegalArgumentException("Cannot delete the Hestia settings"); - } - return null; - } - if (currentSettings == null) { - currentSettings = new IrisHestiaSubSettings(); - } - if (settingsType == IrisSettingsType.EXERCISE || authCheckService.isAdmin()) { - currentSettings.setEnabled(newSettings.isEnabled()); - } - currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); - currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), - parentSettings != null ? parentSettings.allowedModels() : null)); - currentSettings.setTemplate(newSettings.getTemplate()); - return currentSettings; - } - /** * Updates a Competency Generation sub settings object. * If the new settings are null, the current settings will be deleted (except if the parent settings are null == if the settings are global). @@ -174,11 +136,10 @@ public IrisCompetencyGenerationSubSettings update(IrisCompetencyGenerationSubSet } if (authCheckService.isAdmin()) { currentSettings.setEnabled(newSettings.isEnabled()); - currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); + currentSettings.setAllowedVariants(selectAllowedVariants(currentSettings.getAllowedVariants(), newSettings.getAllowedVariants())); } - currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), - parentSettings != null ? parentSettings.allowedModels() : null)); - currentSettings.setTemplate(newSettings.getTemplate()); + currentSettings.setSelectedVariant(validateSelectedVariant(currentSettings.getSelectedVariant(), newSettings.getSelectedVariant(), currentSettings.getAllowedVariants(), + parentSettings != null ? parentSettings.allowedVariants() : null)); return currentSettings; } @@ -187,12 +148,12 @@ public IrisCompetencyGenerationSubSettings update(IrisCompetencyGenerationSubSet * If the user is an admin, all models are allowed. * Otherwise, only models that are allowed by the parent settings or the current settings are allowed. * - * @param allowedModels The allowed models of the current settings. - * @param updatedAllowedModels The allowed models of the updated settings. + * @param allowedVariants The allowed models of the current settings. + * @param updatedAllowedVariants The allowed models of the updated settings. * @return The filtered allowed models. */ - private SortedSet selectAllowedModels(SortedSet allowedModels, SortedSet updatedAllowedModels) { - return authCheckService.isAdmin() ? updatedAllowedModels : allowedModels; + private SortedSet selectAllowedVariants(SortedSet allowedVariants, SortedSet updatedAllowedVariants) { + return authCheckService.isAdmin() ? updatedAllowedVariants : allowedVariants; } /** @@ -200,23 +161,23 @@ private SortedSet selectAllowedModels(SortedSet allowedModels, S * If the user is an admin, all models are allowed. * Otherwise, only models that are allowed by the current settings are allowed. * - * @param preferredModel The preferred model of the current settings. - * @param newPreferredModel The preferred model of the updated settings. - * @param allowedModels The allowed models of the current settings. - * @param parentAllowedModels The allowed models of the parent settings. + * @param selectedVariant The preferred model of the current settings. + * @param newSelectedVariant The preferred model of the updated settings. + * @param allowedVariants The allowed models of the current settings. + * @param parentAllowedVariants The allowed models of the parent settings. * @return The validated preferred model. */ - private String validatePreferredModel(String preferredModel, String newPreferredModel, Set allowedModels, Set parentAllowedModels) { - if (newPreferredModel == null || newPreferredModel.isBlank()) { + private String validateSelectedVariant(String selectedVariant, String newSelectedVariant, Set allowedVariants, Set parentAllowedVariants) { + if (newSelectedVariant == null || newSelectedVariant.isBlank()) { return null; } - var canChangePreferredModel = authCheckService.isAdmin() || (allowedModels != null && !allowedModels.isEmpty() && allowedModels.contains(newPreferredModel)) - || ((allowedModels == null || allowedModels.isEmpty()) && parentAllowedModels != null && parentAllowedModels.contains(newPreferredModel)); - if (canChangePreferredModel) { - return newPreferredModel; + var canChangeSelectedVariant = authCheckService.isAdmin() || (allowedVariants != null && !allowedVariants.isEmpty() && allowedVariants.contains(newSelectedVariant)) + || ((allowedVariants == null || allowedVariants.isEmpty()) && parentAllowedVariants != null && parentAllowedVariants.contains(newSelectedVariant)); + if (canChangeSelectedVariant) { + return newSelectedVariant; } - return preferredModel; + return selectedVariant; } /** @@ -231,10 +192,9 @@ private String validatePreferredModel(String preferredModel, String newPreferred public IrisCombinedChatSubSettingsDTO combineChatSettings(ArrayList settingsList, boolean minimal) { var enabled = getCombinedEnabled(settingsList, IrisSettings::getIrisChatSettings); var rateLimit = getCombinedRateLimit(settingsList); - var allowedModels = minimal ? getCombinedAllowedModels(settingsList, IrisSettings::getIrisChatSettings) : null; - var preferredModel = minimal ? getCombinedPreferredModel(settingsList, IrisSettings::getIrisChatSettings) : null; - var template = minimal ? getCombinedTemplate(settingsList, IrisSettings::getIrisChatSettings, IrisChatSubSettings::getTemplate) : null; - return new IrisCombinedChatSubSettingsDTO(enabled, rateLimit, null, allowedModels, preferredModel, template); + var allowedVariants = !minimal ? getCombinedAllowedVariants(settingsList, IrisSettings::getIrisChatSettings) : null; + var selectedVariant = !minimal ? getCombinedSelectedVariant(settingsList, IrisSettings::getIrisChatSettings) : null; + return new IrisCombinedChatSubSettingsDTO(enabled, rateLimit, null, allowedVariants, selectedVariant); } /** @@ -251,24 +211,6 @@ public IrisCombinedLectureIngestionSubSettingsDTO combineLectureIngestionSubSett return new IrisCombinedLectureIngestionSubSettingsDTO(enabled); } - /** - * Combines the Hestia settings of multiple {@link IrisSettings} objects. - * If minimal is true, the returned object will only contain the enabled field. - * The minimal version can safely be sent to students. - * - * @param settingsList List of {@link IrisSettings} objects to combine. - * @param minimal Whether to return a minimal version of the combined settings. - * @return Combined Hestia settings. - */ - public IrisCombinedHestiaSubSettingsDTO combineHestiaSettings(ArrayList settingsList, boolean minimal) { - var actualSettingsList = settingsList.stream().filter(settings -> !(settings instanceof IrisExerciseSettings)).toList(); - var enabled = getCombinedEnabled(actualSettingsList, IrisSettings::getIrisHestiaSettings); - var allowedModels = minimal ? getCombinedAllowedModels(actualSettingsList, IrisSettings::getIrisHestiaSettings) : null; - var preferredModel = minimal ? getCombinedPreferredModel(actualSettingsList, IrisSettings::getIrisHestiaSettings) : null; - var template = minimal ? getCombinedTemplate(actualSettingsList, IrisSettings::getIrisHestiaSettings, IrisHestiaSubSettings::getTemplate) : null; - return new IrisCombinedHestiaSubSettingsDTO(enabled, allowedModels, preferredModel, template); - } - /** * Combines the Competency Generation settings of multiple {@link IrisSettings} objects. * If minimal is true, the returned object will only contain the enabled field. @@ -281,11 +223,9 @@ public IrisCombinedHestiaSubSettingsDTO combineHestiaSettings(ArrayList settingsList, boolean minimal) { var actualSettingsList = settingsList.stream().filter(settings -> !(settings instanceof IrisExerciseSettings)).toList(); var enabled = getCombinedEnabled(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings); - var allowedModels = minimal ? getCombinedAllowedModels(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings) : null; - var preferredModel = minimal ? getCombinedPreferredModel(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings) : null; - var template = minimal ? getCombinedTemplate(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings, IrisCompetencyGenerationSubSettings::getTemplate) - : null; - return new IrisCombinedCompetencyGenerationSubSettingsDTO(enabled, allowedModels, preferredModel, template); + var allowedVariants = !minimal ? getCombinedAllowedVariants(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings) : null; + var selectedVariant = !minimal ? getCombinedSelectedVariant(actualSettingsList, IrisSettings::getIrisCompetencyGenerationSettings) : null; + return new IrisCombinedCompetencyGenerationSubSettingsDTO(enabled, allowedVariants, selectedVariant); } /** @@ -322,43 +262,28 @@ private Integer getCombinedRateLimit(List settingsList) { } /** - * Combines the allowedModels field of multiple {@link IrisSettings} objects. - * Simply takes the last allowedModels. + * Combines the allowedVariants field of multiple {@link IrisSettings} objects. + * Simply takes the last allowedVariants. * * @param settingsList List of {@link IrisSettings} objects to combine. * @param subSettingsFunction Function to get the sub settings from an IrisSettings object. - * @return Combined allowedModels field. + * @return Combined allowedVariants field. */ - private Set getCombinedAllowedModels(List settingsList, Function subSettingsFunction) { - return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(IrisSubSettings::getAllowedModels).filter(Objects::nonNull) + private Set getCombinedAllowedVariants(List settingsList, Function subSettingsFunction) { + return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(IrisSubSettings::getAllowedVariants).filter(Objects::nonNull) .filter(models -> !models.isEmpty()).reduce((first, second) -> second).orElse(new TreeSet<>()); } /** - * Combines the preferredModel field of multiple {@link IrisSettings} objects. - * Simply takes the last preferredModel. - * TODO + * Combines the selectedVariant field of multiple {@link IrisSettings} objects. + * Simply takes the last selectedVariant. * * @param settingsList List of {@link IrisSettings} objects to combine. * @param subSettingsFunction Function to get the sub settings from an IrisSettings object. - * @return Combined preferredModel field. + * @return Combined selectedVariant field. */ - private String getCombinedPreferredModel(List settingsList, Function subSettingsFunction) { - return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(IrisSubSettings::getPreferredModel) + private String getCombinedSelectedVariant(List settingsList, Function subSettingsFunction) { + return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(IrisSubSettings::getSelectedVariant) .filter(model -> model != null && !model.isBlank()).reduce((first, second) -> second).orElse(null); } - - /** - * Combines the template field of multiple {@link IrisSettings} objects. - * Simply takes the last template. - * - * @param settingsList List of {@link IrisSettings} objects to combine. - * @param templateFunction Function to get the template from the sub settings from an IrisSettings object. - * @return Combined template field. - */ - private IrisTemplate getCombinedTemplate(List settingsList, Function subSettingsFunction, - Function templateFunction) { - return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(templateFunction) - .filter(template -> template != null && template.getContent() != null && !template.getContent().isBlank()).reduce((first, second) -> second).orElse(null); - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisModelsResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisModelsResource.java deleted file mode 100644 index ae4bedb82493..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisModelsResource.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.tum.cit.aet.artemis.iris.web; - -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; - -import java.util.List; - -import org.springframework.context.annotation.Profile; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import de.tum.cit.aet.artemis.core.exception.InternalServerErrorException; -import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor; -import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorException; -import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorService; -import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisModelDTO; - -/** - * REST controller for managing the models Pyris provides. - */ -@Profile(PROFILE_IRIS) -@RestController -@RequestMapping("api/") -public class IrisModelsResource { - - private final PyrisConnectorService pyrisConnectorService; - - public IrisModelsResource(PyrisConnectorService pyrisConnectorService) { - this.pyrisConnectorService = pyrisConnectorService; - } - - /** - * GET iris/models: Retrieve all available models offered by Pyris - * - * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body a List of the models - */ - @GetMapping("iris/models") - @EnforceAtLeastEditor - public ResponseEntity> getAllModels() { - try { - var models = pyrisConnectorService.getOfferedModels(); - return ResponseEntity.ok(models); - } - catch (PyrisConnectorException e) { - throw new InternalServerErrorException("Could not fetch available Iris models"); - } - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisSettingsResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisSettingsResource.java index 32676da073bb..a4f51180b159 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisSettingsResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisSettingsResource.java @@ -14,9 +14,13 @@ import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; -import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; -import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastInstructorInCourse; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastStudentInCourse; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastEditorInExercise; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastInstructorInExercise; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastStudentInExercise; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCourseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisExerciseSettings; @@ -71,10 +75,9 @@ public ResponseEntity getGlobalSettings() { * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the settings, or with status {@code 404 (Not Found)} if the course could not be found. */ @GetMapping("courses/{courseId}/raw-iris-settings") - @EnforceAtLeastEditor + @EnforceAtLeastEditorInCourse public ResponseEntity getRawCourseSettings(@PathVariable Long courseId) { var course = courseRepository.findByIdElseThrow(courseId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); var irisSettings = irisSettingsService.getRawIrisSettingsFor(course); return ResponseEntity.ok(irisSettings); } @@ -86,7 +89,7 @@ public ResponseEntity getRawCourseSettings(@PathVariable Long cour * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the settings, or with status {@code 404 (Not Found)} if the exercise could not be found. */ @GetMapping("programming-exercises/{exerciseId}/raw-iris-settings") - @EnforceAtLeastEditor + @EnforceAtLeastEditorInExercise public ResponseEntity getRawProgrammingExerciseSettings(@PathVariable Long exerciseId) { var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); var user = userRepository.getUserWithGroupsAndAuthorities(); @@ -103,11 +106,10 @@ public ResponseEntity getRawProgrammingExerciseSettings(@PathVaria * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the settings, or with status {@code 404 (Not Found)} if the course could not be found. */ @GetMapping("courses/{courseId}/iris-settings") - @EnforceAtLeastStudent + @EnforceAtLeastStudentInCourse public ResponseEntity getCourseSettings(@PathVariable Long courseId) { var course = courseRepository.findByIdElseThrow(courseId); var user = userRepository.getUserWithGroupsAndAuthorities(); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, user); // Editors can see the full settings, students only the reduced settings var getReduced = !authCheckService.isAtLeastEditorInCourse(course, user); @@ -122,11 +124,10 @@ public ResponseEntity getCourseSettings(@PathVariable L * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the settings, or with status {@code 404 (Not Found)} if the exercise could not be found. */ @GetMapping("programming-exercises/{exerciseId}/iris-settings") - @EnforceAtLeastStudent + @EnforceAtLeastStudentInExercise public ResponseEntity getProgrammingExerciseSettings(@PathVariable Long exerciseId) { var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); var user = userRepository.getUserWithGroupsAndAuthorities(); - authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.STUDENT, exercise, user); var combinedIrisSettings = irisSettingsService.getCombinedIrisSettingsFor(exercise, irisSettingsService.shouldShowMinimalSettings(exercise, user)); return ResponseEntity.ok(combinedIrisSettings); @@ -140,10 +141,9 @@ public ResponseEntity getProgrammingExerciseSettings(@P * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the updated settings, or with status {@code 404 (Not Found)} if the course could not be found. */ @PutMapping("courses/{courseId}/raw-iris-settings") - @EnforceAtLeastEditor + @EnforceAtLeastInstructorInCourse public ResponseEntity updateCourseSettings(@PathVariable Long courseId, @RequestBody IrisCourseSettings settings) { var course = courseRepository.findByIdElseThrow(courseId); - authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); settings.setCourse(course); var updatedSettings = irisSettingsService.saveIrisSettings(settings); return ResponseEntity.ok(updatedSettings); @@ -158,11 +158,9 @@ public ResponseEntity updateCourseSettings(@PathVariable Lon * found. */ @PutMapping("programming-exercises/{exerciseId}/raw-iris-settings") - @EnforceAtLeastInstructor + @EnforceAtLeastInstructorInExercise public ResponseEntity updateProgrammingExerciseSettings(@PathVariable Long exerciseId, @RequestBody IrisExerciseSettings settings) { var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); - var user = userRepository.getUserWithGroupsAndAuthorities(); - authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, exercise, user); settings.setExercise(exercise); var updatedSettings = irisSettingsService.saveIrisSettings(settings); return ResponseEntity.ok(updatedSettings); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisVariantsResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisVariantsResource.java new file mode 100644 index 000000000000..9342d1522023 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisVariantsResource.java @@ -0,0 +1,56 @@ +package de.tum.cit.aet.artemis.iris.web; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.cit.aet.artemis.core.exception.InternalServerErrorException; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; +import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorException; +import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorService; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisVariantDTO; + +/** + * REST controller for managing the variants Pyris provides. + */ +@Profile("iris") +@RestController +@RequestMapping("api/") +public class IrisVariantsResource { + + private static final Logger log = LoggerFactory.getLogger(IrisVariantsResource.class); + + private final PyrisConnectorService pyrisConnectorService; + + public IrisVariantsResource(PyrisConnectorService pyrisConnectorService) { + this.pyrisConnectorService = pyrisConnectorService; + } + + /** + * GET iris/variants/{feature}: Retrieve all available variants offered by Pyris for a certain feature + * + * @param featureRaw the feature for which to retrieve the variants + * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body a List of the variants + */ + @GetMapping("iris/variants/{feature}") + @EnforceAtLeastEditor + public ResponseEntity> getAllVariants(@PathVariable("feature") String featureRaw) { + var feature = IrisSubSettingsType.valueOf(featureRaw.toUpperCase().replace("-", "_")); + try { + var variants = pyrisConnectorService.getOfferedVariants(feature); + return ResponseEntity.ok(variants); + } + catch (PyrisConnectorException e) { + log.error("Could not fetch available variants for feature {}", feature, e); + throw new InternalServerErrorException("Could not fetch available variants for feature " + feature); + } + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/CodeHintService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/CodeHintService.java index ac73fae28454..f04de8a56db9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/CodeHintService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/hestia/CodeHintService.java @@ -13,8 +13,6 @@ import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; -import de.tum.cit.aet.artemis.iris.domain.session.IrisHestiaSession; -import de.tum.cit.aet.artemis.iris.service.session.IrisHestiaSessionService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseSolutionEntry; @@ -29,17 +27,14 @@ public class CodeHintService { private static final Logger log = LoggerFactory.getLogger(CodeHintService.class); - private final Optional irisHestiaSessionService; - private final CodeHintRepository codeHintRepository; private final ProgrammingExerciseTaskRepository taskRepository; private final ProgrammingExerciseSolutionEntryRepository solutionEntryRepository; - public CodeHintService(Optional irisHestiaSessionService, CodeHintRepository codeHintRepository, ProgrammingExerciseTaskRepository taskRepository, + public CodeHintService(CodeHintRepository codeHintRepository, ProgrammingExerciseTaskRepository taskRepository, ProgrammingExerciseSolutionEntryRepository solutionEntryRepository) { - this.irisHestiaSessionService = irisHestiaSessionService; this.codeHintRepository = codeHintRepository; this.taskRepository = taskRepository; this.solutionEntryRepository = solutionEntryRepository; @@ -189,17 +184,4 @@ public void updateSolutionEntriesForCodeHint(CodeHint hint) { codeHintRepository.save(hint); } - - /** - * Generates a description and content for a code hint using the Iris subsystem. - * See {@link IrisHestiaSessionService#executeRequest(IrisHestiaSession)} for more information. - * - * @param codeHint The code hint to be generated - * @return The code hint with description and content - */ - public CodeHint generateDescriptionWithIris(CodeHint codeHint) { - var irisService = irisHestiaSessionService.orElseThrow(); - var session = irisService.getOrCreateSession(codeHint); - return irisService.executeRequest(session); - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/CodeHintResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/CodeHintResource.java index 297e784f1e2b..122aa11b7a17 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/CodeHintResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/hestia/CodeHintResource.java @@ -1,11 +1,9 @@ package de.tum.cit.aet.artemis.programming.web.hestia; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; -import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import org.slf4j.Logger; @@ -23,8 +21,6 @@ import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.exception.ConflictException; import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInExercise.EnforceAtLeastEditorInExercise; -import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; -import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; import de.tum.cit.aet.artemis.programming.domain.hestia.CodeHint; import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository; @@ -50,15 +46,12 @@ public class CodeHintResource { private final CodeHintService codeHintService; - private final Optional irisSettingsService; - public CodeHintResource(ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseSolutionEntryRepository solutionEntryRepository, - CodeHintRepository codeHintRepository, CodeHintService codeHintService, Optional irisSettingsService) { + CodeHintRepository codeHintRepository, CodeHintService codeHintService) { this.programmingExerciseRepository = programmingExerciseRepository; this.solutionEntryRepository = solutionEntryRepository; this.codeHintRepository = codeHintRepository; this.codeHintService = codeHintService; - this.irisSettingsService = irisSettingsService; } /** @@ -98,41 +91,6 @@ public ResponseEntity> generateCodeHintsForExercise(@PathVariable return ResponseEntity.ok(codeHints); } - /** - * {@code POST programming-exercises/:exerciseId/code-hints/:codeHintId/generate-description} : Generate a description for a code hint using Iris. - * - * @param exerciseId The id of the exercise of the code hint - * @param codeHintId The id of the code hint - * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the updated code hint - */ - // TODO: move into some IrisResource - @Profile(PROFILE_IRIS) - @PostMapping("programming-exercises/{exerciseId}/code-hints/{codeHintId}/generate-description") - @EnforceAtLeastEditorInExercise - public ResponseEntity generateDescriptionForCodeHint(@PathVariable Long exerciseId, @PathVariable Long codeHintId) { - log.debug("REST request to generate description with Iris for CodeHint: {}", codeHintId); - - ProgrammingExercise exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); - irisSettingsService.orElseThrow().isEnabledForElseThrow(IrisSubSettingsType.HESTIA, exercise); - - // Hints for exam exercises are not supported at the moment - if (exercise.isExamExercise()) { - throw new AccessForbiddenException("Code hints for exams are currently not supported"); - } - - var codeHint = codeHintRepository.findByIdWithSolutionEntriesElseThrow(codeHintId); - if (!Objects.equals(codeHint.getExercise().getId(), exercise.getId())) { - throw new ConflictException("The code hint does not belong to the exercise", "CodeHint", "codeHintExerciseConflict"); - } - - if (codeHint.getSolutionEntries().isEmpty()) { - throw new ConflictException("The code hint does not have any solution entries", "CodeHint", "codeHintNoSolutionEntries"); - } - - codeHint = codeHintService.generateDescriptionWithIris(codeHint); - return ResponseEntity.ok(codeHint); - } - /** * {@code DELETE programming-exercises/:exerciseId/code-hints/:codeHintId/solution-entries/:solutionEntryId} : * Removes a solution entry from a code hint. diff --git a/src/main/resources/config/liquibase/changelog/20240825191919_changelog.xml b/src/main/resources/config/liquibase/changelog/20240825191919_changelog.xml new file mode 100644 index 000000000000..3ae7fd7ea038 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240825191919_changelog.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + DELETE FROM iris_sub_settings WHERE discriminator = 'HESTIA'; + + + + + + + + + + + UPDATE iris_sub_settings + SET allowed_variants = 'default', selected_variant = 'default' + WHERE id IN ( + SELECT iris_chat_settings_id FROM iris_settings WHERE discriminator = 'GLOBAL' + UNION + SELECT iris_competency_generation_settings_id FROM iris_settings WHERE discriminator = 'GLOBAL' + UNION + SELECT iris_lecture_ingestion_settings_id FROM iris_settings WHERE discriminator = 'GLOBAL' + ); + + + + + + + + + + + + + + DELETE FROM iris_json_message_content WHERE id IN ( + SELECT iris_message_content.id FROM iris_message_content + JOIN iris_message ON iris_message_content.message_id = iris_message.id + JOIN iris_session ON iris_message.session_id = iris_session.id + WHERE iris_session.discriminator = 'HESTIA' + ); + DELETE FROM iris_text_message_content WHERE id IN ( + SELECT iris_message_content.id FROM iris_message_content + JOIN iris_message ON iris_message_content.message_id = iris_message.id + JOIN iris_session ON iris_message.session_id = iris_session.id + WHERE iris_session.discriminator = 'HESTIA' + ); + DELETE FROM iris_message_content WHERE message_id IN ( + SELECT iris_message.id FROM iris_message + JOIN iris_session ON iris_message.session_id = iris_session.id + WHERE iris_session.discriminator = 'HESTIA' + ); + DELETE FROM iris_message WHERE session_id IN ( + SELECT id FROM iris_session WHERE discriminator = 'HESTIA' + ); + DELETE FROM iris_session WHERE discriminator = 'HESTIA'; + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 49f3eeee4d63..b560fb1047fe 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -22,6 +22,7 @@ + diff --git a/src/main/webapp/app/course/manage/detail/course-detail.component.ts b/src/main/webapp/app/course/manage/detail/course-detail.component.ts index 5954f3c0b7c0..29858e79b334 100644 --- a/src/main/webapp/app/course/manage/detail/course-detail.component.ts +++ b/src/main/webapp/app/course/manage/detail/course-detail.component.ts @@ -50,7 +50,6 @@ export class CourseDetailComponent implements OnInit, OnDestroy { communicationEnabled: boolean; irisEnabled = false; irisChatEnabled = false; - irisHestiaEnabled = false; ltiEnabled = false; isAthenaEnabled = false; tutorialEnabled = false; @@ -96,7 +95,6 @@ export class CourseDetailComponent implements OnInit, OnDestroy { if (this.irisEnabled) { const irisSettings = await firstValueFrom(this.irisSettingsService.getGlobalSettings()); this.irisChatEnabled = irisSettings?.irisChatSettings?.enabled ?? false; - this.irisHestiaEnabled = irisSettings?.irisHestiaSettings?.enabled ?? false; } this.route.data.subscribe(({ course }) => { if (course) { diff --git a/src/main/webapp/app/entities/iris/settings/iris-settings.model.ts b/src/main/webapp/app/entities/iris/settings/iris-settings.model.ts index 017c3ea2cffa..ab7f3b475908 100644 --- a/src/main/webapp/app/entities/iris/settings/iris-settings.model.ts +++ b/src/main/webapp/app/entities/iris/settings/iris-settings.model.ts @@ -1,10 +1,5 @@ import { BaseEntity } from 'app/shared/model/base-entity'; -import { - IrisChatSubSettings, - IrisCompetencyGenerationSubSettings, - IrisHestiaSubSettings, - IrisLectureIngestionSubSettings, -} from 'app/entities/iris/settings/iris-sub-settings.model'; +import { IrisChatSubSettings, IrisCompetencyGenerationSubSettings, IrisLectureIngestionSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; export enum IrisSettingsType { GLOBAL = 'global', @@ -17,21 +12,14 @@ export abstract class IrisSettings implements BaseEntity { type: IrisSettingsType; irisChatSettings?: IrisChatSubSettings; irisLectureIngestionSettings?: IrisLectureIngestionSubSettings; - irisHestiaSettings?: IrisHestiaSubSettings; irisCompetencyGenerationSettings?: IrisCompetencyGenerationSubSettings; } export class IrisGlobalSettings implements IrisSettings { id?: number; type = IrisSettingsType.GLOBAL; - currentVersion?: number; - enableAutoUpdateChat?: boolean; - enableAutoUpdateLectureIngestion?: boolean; - enableAutoUpdateHestia?: boolean; - enableAutoUpdateCompetencyGeneration?: boolean; irisChatSettings?: IrisChatSubSettings; irisLectureIngestionSettings?: IrisLectureIngestionSubSettings; - irisHestiaSettings?: IrisHestiaSubSettings; irisCompetencyGenerationSettings?: IrisCompetencyGenerationSubSettings; } @@ -41,7 +29,6 @@ export class IrisCourseSettings implements IrisSettings { courseId?: number; irisChatSettings?: IrisChatSubSettings; irisLectureIngestionSettings?: IrisLectureIngestionSubSettings; - irisHestiaSettings?: IrisHestiaSubSettings; irisCompetencyGenerationSettings?: IrisCompetencyGenerationSubSettings; } diff --git a/src/main/webapp/app/entities/iris/settings/iris-sub-settings.model.ts b/src/main/webapp/app/entities/iris/settings/iris-sub-settings.model.ts index 8848394f1350..626155a43555 100644 --- a/src/main/webapp/app/entities/iris/settings/iris-sub-settings.model.ts +++ b/src/main/webapp/app/entities/iris/settings/iris-sub-settings.model.ts @@ -1,9 +1,7 @@ import { BaseEntity } from 'app/shared/model/base-entity'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; export enum IrisSubSettingsType { CHAT = 'chat', - HESTIA = 'hestia', COMPETENCY_GENERATION = 'competency-generation', LECTURE_INGESTION = 'lecture-ingestion', } @@ -12,13 +10,12 @@ export abstract class IrisSubSettings implements BaseEntity { id?: number; type: IrisSubSettingsType; enabled = false; - allowedModels?: string[]; - preferredModel?: string; + allowedVariants?: string[]; + selectedVariant?: string; } export class IrisChatSubSettings extends IrisSubSettings { type = IrisSubSettingsType.CHAT; - template?: IrisTemplate; rateLimit?: number; rateLimitTimeframeHours?: number; } @@ -28,12 +25,6 @@ export class IrisLectureIngestionSubSettings extends IrisSubSettings { autoIngestOnLectureAttachmentUpload: boolean; } -export class IrisHestiaSubSettings extends IrisSubSettings { - type = IrisSubSettingsType.HESTIA; - template?: IrisTemplate; -} - export class IrisCompetencyGenerationSubSettings extends IrisSubSettings { type = IrisSubSettingsType.COMPETENCY_GENERATION; - template?: IrisTemplate; } diff --git a/src/main/webapp/app/entities/iris/settings/iris-template.ts b/src/main/webapp/app/entities/iris/settings/iris-template.ts deleted file mode 100644 index eb0c8a90041c..000000000000 --- a/src/main/webapp/app/entities/iris/settings/iris-template.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { BaseEntity } from 'app/shared/model/base-entity'; - -export class IrisTemplate implements BaseEntity { - id?: number; - content = ''; -} diff --git a/src/main/webapp/app/entities/iris/settings/iris-model.ts b/src/main/webapp/app/entities/iris/settings/iris-variant.ts similarity index 69% rename from src/main/webapp/app/entities/iris/settings/iris-model.ts rename to src/main/webapp/app/entities/iris/settings/iris-variant.ts index 94a7f9202d92..3fbecfee8c49 100644 --- a/src/main/webapp/app/entities/iris/settings/iris-model.ts +++ b/src/main/webapp/app/entities/iris/settings/iris-variant.ts @@ -1,4 +1,4 @@ -export class IrisModel { +export class IrisVariant { id: string; name: string; description: string; diff --git a/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.html b/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.html index 5c80f6e0c868..6bf170d0bac9 100644 --- a/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-hint/manage/exercise-hint-update.component.html @@ -45,21 +45,6 @@

- @if (exerciseHint.type === HintType.CODE && irisSettings?.irisHestiaSettings?.enabled) { -
- Generate description - -
- }
diff --git a/src/main/webapp/app/iris/iris.module.ts b/src/main/webapp/app/iris/iris.module.ts index 5473966d0275..aacdbd0b64cf 100644 --- a/src/main/webapp/app/iris/iris.module.ts +++ b/src/main/webapp/app/iris/iris.module.ts @@ -14,7 +14,6 @@ import { IrisCommonSubSettingsUpdateComponent } from './settings/iris-settings-u import { IrisCourseSettingsUpdateComponent } from 'app/iris/settings/iris-course-settings-update/iris-course-settings-update.component'; import { IrisExerciseSettingsUpdateComponent } from 'app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component'; import { IrisLogoComponent } from './iris-logo/iris-logo.component'; -import { IrisGlobalAutoupdateSettingsUpdateComponent } from './settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; import { IrisExerciseChatbotButtonComponent } from 'app/iris/exercise-chatbot/exercise-chatbot-button.component'; import { IrisChatbotWidgetComponent } from 'app/iris/exercise-chatbot/widget/chatbot-widget.component'; import { IrisEnabledComponent } from 'app/iris/settings/shared/iris-enabled.component'; @@ -36,7 +35,6 @@ import { CourseChatbotComponent } from 'app/iris/course-chatbot/course-chatbot.c IrisExerciseSettingsUpdateComponent, IrisCommonSubSettingsUpdateComponent, IrisLogoComponent, - IrisGlobalAutoupdateSettingsUpdateComponent, IrisEnabledComponent, ChatStatusBarComponent, IrisLogoButtonComponent, diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html index 4132e9fab587..95238efecaf9 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html @@ -24,39 +24,62 @@ jhiTranslate="artemisApp.iris.settings.subSettings.enabled.off" > -

-: +

+: @if (parentSubSettings) {
}
- @for (model of allIrisModels; track model) { + @for (variant of availableVariants; track variant) {
-
}
+
:
+
+
+ +
+ @if (parentSubSettings) { + + } + @for (model of allowedVariants; track model) { + + } +
+
+ @if (!subSettings?.selectedVariant) { + {{ getSelectedVariantNameParent() }} + } +
diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.ts index 723b8ddb21a9..ba5bd6573691 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.ts +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.ts @@ -1,10 +1,11 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { IrisSubSettings, IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-settings.model'; -import { IrisModel } from 'app/entities/iris/settings/iris-model'; +import { IrisVariant } from 'app/entities/iris/settings/iris-variant'; import { AccountService } from 'app/core/auth/account.service'; import { ButtonType } from 'app/shared/components/button.component'; import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { IrisSettingsType } from 'app/entities/iris/settings/iris-settings.model'; +import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; @Component({ selector: 'jhi-iris-common-sub-settings-update', @@ -17,9 +18,6 @@ export class IrisCommonSubSettingsUpdateComponent implements OnInit, OnChanges { @Input() parentSubSettings?: IrisSubSettings; - @Input() - allIrisModels: IrisModel[]; - @Input() settingsType: IrisSettingsType; @@ -28,9 +26,11 @@ export class IrisCommonSubSettingsUpdateComponent implements OnInit, OnChanges { isAdmin: boolean; - inheritAllowedModels: boolean; + inheritAllowedVariants: boolean; + + availableVariants: IrisVariant[] = []; - allowedIrisModels: IrisModel[]; + allowedVariants: IrisVariant[] = []; enabled: boolean; @@ -42,49 +42,62 @@ export class IrisCommonSubSettingsUpdateComponent implements OnInit, OnChanges { // Icons faTrash = faTrash; - constructor(accountService: AccountService) { + constructor( + accountService: AccountService, + private irisSettingsService: IrisSettingsService, + ) { this.isAdmin = accountService.isAdmin(); } ngOnInit() { this.enabled = this.subSettings?.enabled ?? false; - this.allowedIrisModels = this.getAvailableModels(); - this.inheritAllowedModels = !!(!this.subSettings?.allowedModels && this.parentSubSettings); + this.loadVariants(); + this.inheritAllowedVariants = !!(!this.subSettings?.allowedVariants && this.parentSubSettings); } ngOnChanges(changes: SimpleChanges): void { - if (changes.allIrisModels) { - this.allowedIrisModels = this.getAvailableModels(); + if (changes.availableVariants) { + this.allowedVariants = this.getAllowedVariants(); } if (changes.subSettings) { this.enabled = this.subSettings?.enabled ?? false; } } - getAvailableModels(): IrisModel[] { - return this.allIrisModels.filter((model) => (this.subSettings?.allowedModels ?? this.parentSubSettings?.allowedModels ?? []).includes(model.id)); + loadVariants(): void { + if (!this.subSettings?.type) { + return; + } + this.irisSettingsService.getVariantsForFeature(this.subSettings?.type).subscribe((variants) => { + this.availableVariants = variants ?? this.availableVariants; + this.allowedVariants = this.getAllowedVariants(); + }); + } + + getAllowedVariants(): IrisVariant[] { + return this.availableVariants.filter((variant) => (this.subSettings?.allowedVariants ?? this.parentSubSettings?.allowedVariants ?? []).includes(variant.id)); } - getPreferredModelName(): string | undefined { - return this.allIrisModels.find((model) => model.id === this.subSettings?.preferredModel)?.name ?? this.subSettings?.preferredModel; + getSelectedVariantName(): string | undefined { + return this.availableVariants.find((variant) => variant.id === this.subSettings?.selectedVariant)?.name ?? this.subSettings?.selectedVariant; } - getPreferredModelNameParent(): string | undefined { - return this.allIrisModels.find((model) => model.id === this.parentSubSettings?.preferredModel)?.name ?? this.parentSubSettings?.preferredModel; + getSelectedVariantNameParent(): string | undefined { + return this.availableVariants.find((variant) => variant.id === this.parentSubSettings?.selectedVariant)?.name ?? this.parentSubSettings?.selectedVariant; } - onAllowedIrisModelsSelectionChange(model: IrisModel) { - this.inheritAllowedModels = false; - if (this.allowedIrisModels.includes(model)) { - this.allowedIrisModels = this.allowedIrisModels.filter((m) => m !== model); + onAllowedIrisVariantsSelectionChange(variant: IrisVariant) { + this.inheritAllowedVariants = false; + if (this.allowedVariants.map((variant) => variant.id).includes(variant.id)) { + this.allowedVariants = this.allowedVariants.filter((m) => m.id !== variant.id); } else { - this.allowedIrisModels.push(model); + this.allowedVariants.push(variant); } - this.subSettings!.allowedModels = this.allowedIrisModels.map((model) => model.id); + this.subSettings!.allowedVariants = this.allowedVariants.map((variant) => variant.id); } - setModel(model: IrisModel | undefined) { - this.subSettings!.preferredModel = model?.id; + setVariant(variant: IrisVariant | undefined) { + this.subSettings!.selectedVariant = variant?.id; } onEnabledChange() { @@ -101,12 +114,12 @@ export class IrisCommonSubSettingsUpdateComponent implements OnInit, OnChanges { this.onEnabledChange(); } - onInheritAllowedModelsChange() { - if (this.inheritAllowedModels) { - this.subSettings!.allowedModels = undefined; - this.allowedIrisModels = this.getAvailableModels(); + onInheritAllowedVariantsChange() { + if (this.inheritAllowedVariants) { + this.subSettings!.allowedVariants = undefined; + this.allowedVariants = this.getAllowedVariants(); } else { - this.subSettings!.allowedModels = this.allowedIrisModels.map((model) => model.id); + this.subSettings!.allowedVariants = this.allowedVariants.map((variant) => variant.id); } } diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.html deleted file mode 100644 index efb780138e8c..000000000000 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.html +++ /dev/null @@ -1,20 +0,0 @@ -@if (irisSettings) { -
-
- - -
-
- - -
-
- - -
-
- - -
-
-} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.ts deleted file mode 100644 index 404132633566..000000000000 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { IrisGlobalSettings } from 'app/entities/iris/settings/iris-settings.model'; -import { IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; - -@Component({ - selector: 'jhi-iris-global-autoupdate-settings-update', - templateUrl: './iris-global-autoupdate-settings-update.component.html', -}) -export class IrisGlobalAutoupdateSettingsUpdateComponent { - @Input() - irisSettings?: IrisGlobalSettings; - - @Output() - onChanges = new EventEmitter(); -} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html index b28a3a4c690c..c1e15d609c1f 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html @@ -11,18 +11,11 @@ @if (irisSettings) {
- @if (settingsType === GLOBAL) { -
-

- -
- }

@@ -34,33 +27,21 @@

-

- } - @if (settingsType === COURSE) { -
- - -
- } - @if (settingsType !== EXERCISE) { -
-
-

- +
+ + @if (settingsType === COURSE) { +
+ + +
+ }
} @if (settingsType !== EXERCISE) { @@ -70,7 +51,6 @@

diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts index 5bde2f10f791..aa9adac6be64 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts @@ -6,15 +6,9 @@ import { Observable } from 'rxjs'; import { AlertService } from 'app/core/util/alert.service'; import { ButtonType } from 'app/shared/components/button.component'; import { faRotate, faSave } from '@fortawesome/free-solid-svg-icons'; -import { IrisModel } from 'app/entities/iris/settings/iris-model'; import { ComponentCanDeactivate } from 'app/shared/guard/can-deactivate.model'; import { cloneDeep, isEqual } from 'lodash-es'; -import { - IrisChatSubSettings, - IrisCompetencyGenerationSubSettings, - IrisHestiaSubSettings, - IrisLectureIngestionSubSettings, -} from 'app/entities/iris/settings/iris-sub-settings.model'; +import { IrisChatSubSettings, IrisCompetencyGenerationSubSettings, IrisLectureIngestionSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; import { AccountService } from 'app/core/auth/account.service'; @Component({ @@ -30,7 +24,6 @@ export class IrisSettingsUpdateComponent implements OnInit, DoCheck, ComponentCa public exerciseId?: number; public irisSettings?: IrisSettings; public parentIrisSettings?: IrisSettings; - public allIrisModels?: IrisModel[]; originalIrisSettings?: IrisSettings; @@ -77,13 +70,6 @@ export class IrisSettingsUpdateComponent implements OnInit, DoCheck, ComponentCa return !this.isDirty; } - loadIrisModels(): void { - this.irisSettingsService.getIrisModels().subscribe((models) => { - this.allIrisModels = models; - this.isLoading = false; - }); - } - loadIrisSettings(): void { this.isLoading = true; this.loadIrisSettingsObservable().subscribe((settings) => { @@ -116,9 +102,6 @@ export class IrisSettingsUpdateComponent implements OnInit, DoCheck, ComponentCa if (!this.irisSettings.irisLectureIngestionSettings) { this.irisSettings.irisLectureIngestionSettings = new IrisLectureIngestionSubSettings(); } - if (!this.irisSettings.irisHestiaSettings) { - this.irisSettings.irisHestiaSettings = new IrisHestiaSubSettings(); - } if (!this.irisSettings.irisCompetencyGenerationSettings) { this.irisSettings.irisCompetencyGenerationSettings = new IrisCompetencyGenerationSubSettings(); } diff --git a/src/main/webapp/app/iris/settings/shared/iris-enabled.component.ts b/src/main/webapp/app/iris/settings/shared/iris-enabled.component.ts index ef8d44419841..be64bd5d856b 100644 --- a/src/main/webapp/app/iris/settings/shared/iris-enabled.component.ts +++ b/src/main/webapp/app/iris/settings/shared/iris-enabled.component.ts @@ -56,9 +56,6 @@ export class IrisEnabledComponent implements OnInit { case IrisSubSettingsType.CHAT: this.irisSubSettings = this.irisSettings?.irisChatSettings; break; - case IrisSubSettingsType.HESTIA: - this.irisSubSettings = this.irisSettings?.irisHestiaSettings; - break; case IrisSubSettingsType.COMPETENCY_GENERATION: this.irisSubSettings = this.irisSettings?.irisCompetencyGenerationSettings; break; diff --git a/src/main/webapp/app/iris/settings/shared/iris-settings.service.ts b/src/main/webapp/app/iris/settings/shared/iris-settings.service.ts index 475540bad156..7273e5958cf6 100644 --- a/src/main/webapp/app/iris/settings/shared/iris-settings.service.ts +++ b/src/main/webapp/app/iris/settings/shared/iris-settings.service.ts @@ -3,7 +3,8 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { IrisCourseSettings, IrisExerciseSettings, IrisGlobalSettings } from 'app/entities/iris/settings/iris-settings.model'; -import { IrisModel } from 'app/entities/iris/settings/iris-model'; +import { IrisVariant } from 'app/entities/iris/settings/iris-variant'; +import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-settings.model'; /** * Service for calling the Iris settings endpoints on the server @@ -90,9 +91,11 @@ export class IrisSettingsService { } /** - * Get the global Iris settings + * Get the available variants for a feature */ - getIrisModels(): Observable { - return this.http.get(`${this.resourceUrl}/iris/models`, { observe: 'response' }).pipe(map((res: HttpResponse) => res.body ?? [])); + getVariantsForFeature(feature: IrisSubSettingsType): Observable { + return this.http + .get(`${this.resourceUrl}/iris/variants/${feature}`, { observe: 'response' }) + .pipe(map((res: HttpResponse) => res.body ?? [])); } } diff --git a/src/main/webapp/i18n/de/iris.json b/src/main/webapp/i18n/de/iris.json index 34572e7581bd..4044841119d8 100644 --- a/src/main/webapp/i18n/de/iris.json +++ b/src/main/webapp/i18n/de/iris.json @@ -28,14 +28,14 @@ "hestiaSettings": "Hestia Einstellungen", "competencyGenerationSettings": "Kompetenzgenerierung Einstellungen", "enabled-disabled": "Aktiviert/Deaktiviert", - "models": { - "title": "Modelle", - "allowedModels": { - "title": "Erlaubte Modelle", - "inheritSwitch": "Erbe erlaubte Modelle" + "variants": { + "title": "Varianten", + "allowedVariants": { + "title": "Erlaubte Varianten", + "inheritSwitch": "Erbe erlaubte Varianten" }, - "preferredModel": { - "title": "Präferiertes Modell", + "selectedVariant": { + "title": "Genuzte Variante", "inherit": "Erben" } }, @@ -63,21 +63,13 @@ "global": "Globale Iris Einstellungen", "course": "Kurs Iris Einstellungen", "programmingExercise": "Programmieraufgabe Iris Einstellungen" - }, - "autoUpdate": { - "title": "Auto Update Einstellungen", - "tooltip": "Wenn aktiviert, werden die spezifischen globalen Iris Einstellungen automatisch aktualisiert, wenn eine neue Version von Artemis neue Iris Einstellungen bereitstellt.", - "chatLabel": "Auto Update der Chat Einstellungen", - "hestiaLabel": "Auto Update der Hestia Einstellungen", - "lectureIngestionLabel": "Auto Update der Vorlesungen Erfassung Einstellungen", - "competencyGenerationLabel": "Auto Update der Kompetenzgenerierung Einstellungen" } }, "error": { "forbidden": "Artemis ist nicht konfiguriert, um Iris zu verwenden. (Ungültiges Token)", "internalPyrisError": "Ein interner Fehler beim Kommunizieren mit dem LLM ist aufgetreten. Die Fehlermeldung lautet: {{ pyrisErrorMessage }}.", "invalidTemplate": "Die Vorlage ist ungültig. Die Fehlermeldung lautet: {{ pyrisErrorMessage }}.", - "noModelAvailable": "Das Modell {{ model }} steht nicht zur Verfügung. Bitte kontaktiere einen Administrator, wenn das Problem weiterhin besteht.", + "noVariantAvailable": "Die Variante {{ variant }} steht nicht zur Verfügung. Bitte kontaktiere einen Administrator, wenn das Problem weiterhin besteht.", "noResponse": "Es wurde keine Antwort von Iris empfangen. Bitte kontaktiere einen Administrator, wenn das Problem weiterhin besteht.", "parseResponse": "Ein Fehler ist beim Parsen der Antwort von Iris aufgetreten. Ursache: {{ cause }}" }, diff --git a/src/main/webapp/i18n/en/iris.json b/src/main/webapp/i18n/en/iris.json index 6afbb928d5c9..21aed4b477e7 100644 --- a/src/main/webapp/i18n/en/iris.json +++ b/src/main/webapp/i18n/en/iris.json @@ -28,14 +28,14 @@ "hestiaSettings": "Hestia Settings", "competencyGenerationSettings": "Competency Generation Settings", "enabled-disabled": "Enabled/Disabled", - "models": { - "title": "Models", - "allowedModels": { - "title": "Allowed Models", - "inheritSwitch": "Inherit Allowed Models" + "variants": { + "title": "Variants", + "allowedVariants": { + "title": "Allowed Variants", + "inheritSwitch": "Inherit Allowed Variants" }, - "preferredModel": { - "title": "Preferred Model", + "selectedVariant": { + "title": "Selected Variant", "inherit": "Inherit" } }, @@ -63,21 +63,13 @@ "global": "Global Iris Settings", "course": "Course Iris Settings", "exercise": "Exercise Iris Settings" - }, - "autoUpdate": { - "title": "Auto Update Settings", - "tooltip": "If enabled, the specific global Iris settings will be automatically updated when a new release of Artemis provides new Iris settings.", - "chatLabel": "Auto Update Chat Settings", - "hestiaLabel": "Auto Update Hestia Settings", - "lectureIngestionLabel": "Auto Update Lecture Ingestion Settings", - "competencyGenerationLabel": "Auto Update Competency Generation Settings" } }, "error": { "forbidden": "Artemis is not configured to use Iris. (Invalid token)", "internalPyrisError": "An internal error when communicating with the LLM occurred. Error message is: {{ pyrisErrorMessage }}.", "invalidTemplate": "The template is invalid. Error message is: {{ pyrisErrorMessage }}.", - "noModelAvailable": "Model {{ model }} is not available to use. Please contact your administrator if this problem persists.", + "noVariantAvailable": "Variant {{ variant }} is not available to use. Please contact your administrator if this problem persists.", "noResponse": "No response from Iris was received.", "parseResponse": "An error occurred while parsing the response from Iris. Cause: {{ cause }}" }, diff --git a/src/test/java/de/tum/cit/aet/artemis/core/connector/IrisRequestMockProvider.java b/src/test/java/de/tum/cit/aet/artemis/core/connector/IrisRequestMockProvider.java index 4c7146242014..2e4506485ead 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/connector/IrisRequestMockProvider.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/connector/IrisRequestMockProvider.java @@ -28,8 +28,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisHealthStatusDTO; -import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisModelDTO; +import de.tum.cit.aet.artemis.iris.service.pyris.dto.PyrisVariantDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.chat.exercise.PyrisExerciseChatPipelineExecutionDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.competency.PyrisCompetencyExtractionPipelineExecutionDTO; import de.tum.cit.aet.artemis.iris.service.pyris.dto.lectureingestionwebhook.PyrisWebhookLectureIngestionExecutionDTO; @@ -52,8 +53,8 @@ public class IrisRequestMockProvider { @Value("${artemis.iris.url}/api/v1/webhooks") private URL webhooksApiURL; - @Value("${artemis.iris.url}/api/v1/models") - private URL modelsApiURL; + @Value("${artemis.iris.url}/api/v1/pipelines/") + private String variantsApiBaseURL; @Value("${artemis.iris.url}/api/v1/health/") private URL healthApiURL; @@ -139,11 +140,11 @@ public void mockIngestionWebhookRunError(int httpStatus) { // @formatter:on } - public void mockModelsResponse() throws JsonProcessingException { - var irisModelDTO = new PyrisModelDTO("TEST_MODEL", "Test model", "Test description"); - var irisModelDTOArray = new PyrisModelDTO[] { irisModelDTO }; + public void mockVariantsResponse(IrisSubSettingsType feature) throws JsonProcessingException { + var irisModelDTO = new PyrisVariantDTO("TEST_MODEL", "Test model", "Test description"); + var irisModelDTOArray = new PyrisVariantDTO[] { irisModelDTO }; // @formatter:off - mockServer.expect(ExpectedCount.once(), requestTo(modelsApiURL.toString())) + mockServer.expect(ExpectedCount.once(), requestTo(variantsApiBaseURL + feature.name() + "/variants")) .andExpect(method(HttpMethod.GET)) .andRespond(withSuccess(mapper.writeValueAsString(irisModelDTOArray), MediaType.APPLICATION_JSON)); // @formatter:on @@ -169,9 +170,9 @@ public void mockStatusResponses() throws JsonProcessingException { /** * Mocks a get model error from the Pyris models endpoint */ - public void mockModelsError() { + public void mockVariantsError(IrisSubSettingsType feature) { // @formatter:off - mockServer.expect(ExpectedCount.once(), requestTo(modelsApiURL.toString())) + mockServer.expect(ExpectedCount.once(), requestTo(variantsApiBaseURL + feature.name() + "/variants")) .andExpect(method(HttpMethod.GET)) .andRespond(withRawStatus(418)); // @formatter:on diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/AbstractIrisIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/AbstractIrisIntegrationTest.java index 34337b170baf..28cf037fa204 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/AbstractIrisIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/AbstractIrisIntegrationTest.java @@ -18,7 +18,6 @@ import de.tum.cit.aet.artemis.core.connector.IrisRequestMockProvider; import de.tum.cit.aet.artemis.core.domain.Course; -import de.tum.cit.aet.artemis.iris.domain.IrisTemplate; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettings; import de.tum.cit.aet.artemis.iris.repository.IrisSettingsRepository; import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService; @@ -60,7 +59,6 @@ void tearDown() throws Exception { protected void activateIrisGlobally() { var globalSettings = irisSettingsService.getGlobalSettings(); activateSubSettings(globalSettings.getIrisChatSettings()); - activateSubSettings(globalSettings.getIrisHestiaSettings()); activateSubSettings(globalSettings.getIrisLectureIngestionSettings()); activateSubSettings(globalSettings.getIrisCompetencyGenerationSettings()); irisSettingsRepository.save(globalSettings); @@ -73,21 +71,16 @@ protected void activateIrisGlobally() { */ private void activateSubSettings(IrisSubSettings settings) { settings.setEnabled(true); - settings.setPreferredModel(null); - settings.setAllowedModels(new TreeSet<>(Set.of("dummy"))); + settings.setSelectedVariant("default"); + settings.setAllowedVariants(new TreeSet<>(Set.of("default"))); } protected void activateIrisFor(Course course) { var courseSettings = irisSettingsService.getDefaultSettingsFor(course); activateSubSettings(courseSettings.getIrisChatSettings()); - courseSettings.getIrisChatSettings().setTemplate(createDummyTemplate()); - - activateSubSettings(courseSettings.getIrisHestiaSettings()); - courseSettings.getIrisHestiaSettings().setTemplate(createDummyTemplate()); activateSubSettings(courseSettings.getIrisCompetencyGenerationSettings()); - courseSettings.getIrisCompetencyGenerationSettings().setTemplate(createDummyTemplate()); activateSubSettings(courseSettings.getIrisLectureIngestionSettings()); @@ -97,14 +90,9 @@ protected void activateIrisFor(Course course) { protected void activateIrisFor(ProgrammingExercise exercise) { var exerciseSettings = irisSettingsService.getDefaultSettingsFor(exercise); activateSubSettings(exerciseSettings.getIrisChatSettings()); - exerciseSettings.getIrisChatSettings().setTemplate(createDummyTemplate()); irisSettingsRepository.save(exerciseSettings); } - protected IrisTemplate createDummyTemplate() { - return new IrisTemplate("Hello World"); - } - /** * Verify that the given messages were sent through the websocket for the given user and topic. * diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/PyrisConnectorServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/PyrisConnectorServiceTest.java index 413b0b77c046..de726729a035 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/PyrisConnectorServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/PyrisConnectorServiceTest.java @@ -5,12 +5,13 @@ import java.util.stream.Stream; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; +import de.tum.cit.aet.artemis.iris.domain.settings.IrisSubSettingsType; import de.tum.cit.aet.artemis.iris.exception.IrisForbiddenException; import de.tum.cit.aet.artemis.iris.exception.IrisInternalPyrisErrorException; import de.tum.cit.aet.artemis.iris.service.pyris.PyrisConnectorException; @@ -49,20 +50,22 @@ void testExceptionIngestionV2(int httpStatus, Class exceptionClass) { assertThatThrownBy(() -> pyrisConnectorService.executeLectureWebhook("fullIngestion", null)).isInstanceOf(exceptionClass); } - @Test - void testOfferedModels() throws Exception { - irisRequestMockProvider.mockModelsResponse(); + @ParameterizedTest + @EnumSource(IrisSubSettingsType.class) + void testOfferedModels(IrisSubSettingsType feature) throws Exception { + irisRequestMockProvider.mockVariantsResponse(feature); - var offeredModels = pyrisConnectorService.getOfferedModels(); + var offeredModels = pyrisConnectorService.getOfferedVariants(feature); assertThat(offeredModels).hasSize(1); assertThat(offeredModels.getFirst().id()).isEqualTo("TEST_MODEL"); } - @Test - void testOfferedModelsError() { - irisRequestMockProvider.mockModelsError(); + @ParameterizedTest + @EnumSource(IrisSubSettingsType.class) + void testOfferedModelsError(IrisSubSettingsType feature) { + irisRequestMockProvider.mockVariantsError(feature); - assertThatThrownBy(() -> pyrisConnectorService.getOfferedModels()).isInstanceOf(PyrisConnectorException.class); + assertThatThrownBy(() -> pyrisConnectorService.getOfferedVariants(feature)).isInstanceOf(PyrisConnectorException.class); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/iris/settings/IrisSettingsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/iris/settings/IrisSettingsIntegrationTest.java index 402f6501a3c0..211b0d122d55 100644 --- a/src/test/java/de/tum/cit/aet/artemis/iris/settings/IrisSettingsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/iris/settings/IrisSettingsIntegrationTest.java @@ -17,7 +17,6 @@ import de.tum.cit.aet.artemis.iris.domain.settings.IrisCompetencyGenerationSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisCourseSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisExerciseSettings; -import de.tum.cit.aet.artemis.iris.domain.settings.IrisHestiaSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisLectureIngestionSubSettings; import de.tum.cit.aet.artemis.iris.domain.settings.IrisSettings; import de.tum.cit.aet.artemis.iris.dto.IrisCombinedSettingsDTO; @@ -56,7 +55,7 @@ void getMissingSettingsForCourse() throws Exception { assertThat(loadedSettings2).isNotNull().usingRecursiveComparison().ignoringFieldsOfTypes(HashSet.class, TreeSet.class).ignoringActualNullFields() .isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(course, false)); assertThat(loadedSettings1).isNotNull().usingRecursiveComparison() - .ignoringFields("id", "course", "irisChatSettings.id", "iris_lecture_ingestion_settings_id", "irisHestiaSettings.id", "irisCompetencyGenerationSettings.id") + .ignoringFields("id", "course", "irisChatSettings.id", "iris_lecture_ingestion_settings_id", "irisCompetencyGenerationSettings.id") .isEqualTo(irisSettingsService.getDefaultSettingsFor(course)); } @@ -71,8 +70,8 @@ void getCourseSettings() throws Exception { var loadedSettings2 = request.get("/api/courses/" + course.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); assertThat(loadedSettings1).isNotNull().usingRecursiveComparison() - .ignoringFields("id", "course", "irisChatSettings.id", "irisLectureIngestionSettings.id", "irisHestiaSettings.id", "irisCompetencyGenerationSettings.id") - .ignoringExpectedNullFields().isEqualTo(loadedSettings2); + .ignoringFields("id", "course", "irisChatSettings.id", "irisLectureIngestionSettings.id", "irisCompetencyGenerationSettings.id").ignoringExpectedNullFields() + .isEqualTo(loadedSettings2); assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("course") .isEqualTo(irisSettingsRepository.findCourseSettings(course.getId()).orElseThrow()); } @@ -87,10 +86,9 @@ void getCourseSettingsAsUser() throws Exception { request.get("/api/courses/" + course.getId() + "/raw-iris-settings", HttpStatus.FORBIDDEN, IrisSettings.class); var loadedSettings = request.get("/api/courses/" + course.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); - assertThat(loadedSettings) - .isNotNull().usingRecursiveComparison().ignoringCollectionOrderInFields("irisChatSettings.allowedModels", "irisLectureIngestionSettings.allowedModels", - "irisCompetencyGenerationSettings.allowedModels", "irisHestiaSettings.allowedModels") - .ignoringFields("id").isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(course, true)); + assertThat(loadedSettings).isNotNull().usingRecursiveComparison().ignoringCollectionOrderInFields("irisChatSettings.allowedVariants", + "irisLectureIngestionSettings.allowedVariants", "irisCompetencyGenerationSettings.allowedVariants").ignoringFields("id") + .isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(course, true)); } @Test @@ -103,7 +101,6 @@ void updateCourseSettings1() throws Exception { var loadedSettings1 = request.get("/api/courses/" + course.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); loadedSettings1.getIrisChatSettings().setEnabled(false); - loadedSettings1.getIrisHestiaSettings().setEnabled(false); loadedSettings1.getIrisCompetencyGenerationSettings().setEnabled(false); loadedSettings1.getIrisLectureIngestionSettings().setEnabled(false); @@ -115,7 +112,6 @@ void updateCourseSettings1() throws Exception { assertThat(updatedSettings.getId()).isEqualTo(loadedSettings1.getId()); assertThat(updatedSettings.getIrisLectureIngestionSettings().getId()).isEqualTo(loadedSettings1.getIrisLectureIngestionSettings().getId()); assertThat(updatedSettings.getIrisChatSettings().getId()).isEqualTo(loadedSettings1.getIrisChatSettings().getId()); - assertThat(updatedSettings.getIrisHestiaSettings().getId()).isEqualTo(loadedSettings1.getIrisHestiaSettings().getId()); assertThat(updatedSettings.getIrisCompetencyGenerationSettings().getId()).isEqualTo(loadedSettings1.getIrisCompetencyGenerationSettings().getId()); } @@ -129,12 +125,10 @@ void updateCourseSettings2() throws Exception { var loadedSettings1 = request.get("/api/courses/" + course.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); var chatSubSettingsId = loadedSettings1.getIrisChatSettings().getId(); - var hestiaSubSettingsId = loadedSettings1.getIrisHestiaSettings().getId(); var competencyGenerationSubSettingsId = loadedSettings1.getIrisCompetencyGenerationSettings().getId(); var lectureIngestionSubSettingsId = loadedSettings1.getIrisLectureIngestionSettings().getId(); loadedSettings1.setIrisLectureIngestionSettings(null); loadedSettings1.setIrisChatSettings(null); - loadedSettings1.setIrisHestiaSettings(null); loadedSettings1.setIrisCompetencyGenerationSettings(null); var updatedSettings = request.putWithResponseBody("/api/courses/" + course.getId() + "/raw-iris-settings", loadedSettings1, IrisSettings.class, HttpStatus.OK); @@ -145,7 +139,6 @@ void updateCourseSettings2() throws Exception { // Original subsettings should not exist anymore assertThat(irisSubSettingsRepository.findById(lectureIngestionSubSettingsId)).isEmpty(); assertThat(irisSubSettingsRepository.findById(chatSubSettingsId)).isEmpty(); - assertThat(irisSubSettingsRepository.findById(hestiaSubSettingsId)).isEmpty(); assertThat(irisSubSettingsRepository.findById(competencyGenerationSubSettingsId)).isEmpty(); } @@ -159,18 +152,11 @@ void updateCourseSettings3() throws Exception { courseSettings.setCourse(course); courseSettings.setIrisChatSettings(new IrisChatSubSettings()); courseSettings.getIrisChatSettings().setEnabled(true); - courseSettings.getIrisChatSettings().setTemplate(createDummyTemplate()); - courseSettings.getIrisChatSettings().setPreferredModel(null); - - courseSettings.setIrisHestiaSettings(new IrisHestiaSubSettings()); - courseSettings.getIrisHestiaSettings().setEnabled(true); - courseSettings.getIrisHestiaSettings().setTemplate(createDummyTemplate()); - courseSettings.getIrisHestiaSettings().setPreferredModel(null); + courseSettings.getIrisChatSettings().setSelectedVariant(null); courseSettings.setIrisCompetencyGenerationSettings(new IrisCompetencyGenerationSubSettings()); courseSettings.getIrisCompetencyGenerationSettings().setEnabled(true); - courseSettings.getIrisCompetencyGenerationSettings().setTemplate(createDummyTemplate()); - courseSettings.getIrisCompetencyGenerationSettings().setPreferredModel(null); + courseSettings.getIrisCompetencyGenerationSettings().setSelectedVariant(null); courseSettings.setIrisLectureIngestionSettings(new IrisLectureIngestionSubSettings()); courseSettings.getIrisLectureIngestionSettings().setEnabled(true); @@ -179,10 +165,8 @@ void updateCourseSettings3() throws Exception { var loadedSettings1 = request.get("/api/courses/" + course.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); assertThat(updatedSettings).usingRecursiveComparison().ignoringFields("course").isEqualTo(loadedSettings1); - assertThat(loadedSettings1) - .usingRecursiveComparison().ignoringFields("id", "course", "irisChatSettings.id", "irisChatSettings.template.id", "irisLectureIngestionSettings.id", - "irisHestiaSettings.id", "irisHestiaSettings.template.id", "irisCompetencyGenerationSettings.id", "irisCompetencyGenerationSettings.template.id") - .isEqualTo(courseSettings); + assertThat(loadedSettings1).usingRecursiveComparison().ignoringFields("id", "course", "irisChatSettings.id", "irisChatSettings.template.id", + "irisLectureIngestionSettings.id", "irisCompetencyGenerationSettings.id", "irisCompetencyGenerationSettings.template.id").isEqualTo(courseSettings); } @Test @@ -214,7 +198,6 @@ void getProgrammingExerciseSettings() throws Exception { assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("id", "exercise", "irisChatSettings.id").ignoringExpectedNullFields() .isEqualTo(loadedSettings2); - assertThat(loadedSettings1.getIrisHestiaSettings()).isNull(); assertThat(loadedSettings1.getIrisCompetencyGenerationSettings()).isNull(); assertThat(loadedSettings1.getIrisLectureIngestionSettings()).isNull(); assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("exercise") @@ -233,7 +216,7 @@ void getProgrammingExerciseSettingsAsUser() throws Exception { var loadedSettings = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); assertThat(loadedSettings).isNotNull().usingRecursiveComparison().ignoringFields("id") - .ignoringCollectionOrderInFields("irisChatSettings.allowedModels", "irisCompetencyGenerationSettings.allowedModels", "irisHestiaSettings.allowedModels") + .ignoringCollectionOrderInFields("irisChatSettings.allowedVariants", "irisCompetencyGenerationSettings.allowedVariants") .isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(programmingExercise, true)); } @@ -271,7 +254,6 @@ void updateProgrammingExerciseSettings2() throws Exception { var chatSubSettingsId = loadedSettings1.getIrisChatSettings().getId(); loadedSettings1.setIrisChatSettings(null); - loadedSettings1.setIrisHestiaSettings(null); var updatedSettings = request.putWithResponseBody("/api/programming-exercises/" + programmingExercise.getId() + "/raw-iris-settings", loadedSettings1, IrisSettings.class, HttpStatus.OK); @@ -294,8 +276,7 @@ void updateProgrammingExerciseSettings3() throws Exception { exerciseSettings.setExercise(programmingExercise); exerciseSettings.setIrisChatSettings(new IrisChatSubSettings()); exerciseSettings.getIrisChatSettings().setEnabled(true); - exerciseSettings.getIrisChatSettings().setTemplate(createDummyTemplate()); - exerciseSettings.getIrisChatSettings().setPreferredModel(null); + exerciseSettings.getIrisChatSettings().setSelectedVariant(null); var updatedSettings = request.putWithResponseBody("/api/programming-exercises/" + programmingExercise.getId() + "/raw-iris-settings", exerciseSettings, IrisSettings.class, HttpStatus.OK); diff --git a/src/test/javascript/spec/component/iris/settings/iris-common-sub-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-common-sub-settings-update.component.spec.ts index 0a3bf65f8433..2b36487e4576 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-common-sub-settings-update.component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-common-sub-settings-update.component.spec.ts @@ -1,39 +1,43 @@ import { ArtemisTestModule } from '../../../test.module'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; import { IrisChatSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { MockDirective, MockPipe } from 'ng-mocks'; import { SimpleChange, SimpleChanges } from '@angular/core'; import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; -import { mockModels } from './mock-settings'; +import { mockVariants } from './mock-settings'; import { IrisSettingsType } from 'app/entities/iris/settings/iris-settings.model'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; +import { of } from 'rxjs'; function baseSettings() { - const mockTemplate = new IrisTemplate(); - mockTemplate.id = 1; - mockTemplate.content = 'Hello World'; const irisSubSettings = new IrisChatSubSettings(); irisSubSettings.id = 2; irisSubSettings.enabled = true; - const allowedModels = mockModels(); - allowedModels.pop(); - irisSubSettings.allowedModels = allowedModels.map((model) => model.id!); - irisSubSettings.preferredModel = allowedModels[0].id!; + const allowedVariants = mockVariants(); + allowedVariants.pop(); + irisSubSettings.allowedVariants = allowedVariants.map((model) => model.id!); + irisSubSettings.selectedVariant = allowedVariants[0].id!; return irisSubSettings; } describe('IrisCommonSubSettingsUpdateComponent Component', () => { let comp: IrisCommonSubSettingsUpdateComponent; let fixture: ComponentFixture; + let getVariantsSpy: jest.SpyInstance; beforeEach(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, FormsModule, MockDirective(NgbTooltip), MockPipe(ArtemisTranslatePipe)], declarations: [IrisCommonSubSettingsUpdateComponent], - }).compileComponents(); + }) + .compileComponents() + .then(() => { + const irisSettingsService = TestBed.inject(IrisSettingsService); + getVariantsSpy = jest.spyOn(irisSettingsService, 'getVariantsForFeature').mockReturnValue(of(mockVariants())); + }); fixture = TestBed.createComponent(IrisCommonSubSettingsUpdateComponent); comp = fixture.componentInstance; }); @@ -45,28 +49,29 @@ describe('IrisCommonSubSettingsUpdateComponent Component', () => { it('child setup works', () => { comp.subSettings = baseSettings(); comp.parentSubSettings = baseSettings(); - comp.allIrisModels = mockModels(); + comp.availableVariants = mockVariants(); comp.settingsType = IrisSettingsType.EXERCISE; fixture.detectChanges(); + expect(getVariantsSpy).toHaveBeenCalledOnce(); expect(comp.enabled).toBeTrue(); - expect(comp.inheritAllowedModels).toBeFalse(); - expect(comp.allowedIrisModels).toEqual([mockModels()[0]]); + expect(comp.inheritAllowedVariants).toBeFalse(); + expect(comp.allowedVariants).toEqual([mockVariants()[0]]); }); it('parent setup works', () => { const subSettings = baseSettings(); - subSettings.allowedModels = undefined; - subSettings.preferredModel = undefined; + subSettings.allowedVariants = undefined; + subSettings.selectedVariant = undefined; comp.subSettings = subSettings; comp.parentSubSettings = baseSettings(); - comp.allIrisModels = mockModels(); + comp.availableVariants = mockVariants(); comp.settingsType = IrisSettingsType.EXERCISE; fixture.detectChanges(); expect(comp.enabled).toBeTrue(); - expect(comp.inheritAllowedModels).toBeTrue(); - expect(comp.allowedIrisModels).toEqual([mockModels()[0]]); + expect(comp.inheritAllowedVariants).toBeTrue(); + expect(comp.allowedVariants).toEqual([mockVariants()[0]]); }); it('prevents enabling chat settings if the parent chat settings disabled', () => { @@ -75,7 +80,7 @@ describe('IrisCommonSubSettingsUpdateComponent Component', () => { comp.parentSubSettings.enabled = false; comp.isAdmin = true; comp.settingsType = IrisSettingsType.EXERCISE; - comp.allIrisModels = mockModels(); + comp.availableVariants = mockVariants(); fixture.detectChanges(); expect(comp.inheritDisabled).toBeTrue(); @@ -88,7 +93,7 @@ describe('IrisCommonSubSettingsUpdateComponent Component', () => { comp.parentSubSettings.enabled = false; comp.isAdmin = true; comp.settingsType = IrisSettingsType.COURSE; - comp.allIrisModels = mockModels(); + comp.availableVariants = mockVariants(); fixture.detectChanges(); expect(comp.inheritDisabled).toBeTrue(); @@ -96,34 +101,34 @@ describe('IrisCommonSubSettingsUpdateComponent Component', () => { }); it('change allowed model', () => { - const allIrisModels = mockModels(); + const availableVariants = mockVariants(); comp.subSettings = baseSettings(); comp.parentSubSettings = baseSettings(); - comp.allIrisModels = allIrisModels; + comp.availableVariants = availableVariants; comp.settingsType = IrisSettingsType.EXERCISE; fixture.detectChanges(); - comp.onAllowedIrisModelsSelectionChange(allIrisModels[1]); - expect(comp.allowedIrisModels).toEqual([allIrisModels[0], allIrisModels[1]]); - comp.onAllowedIrisModelsSelectionChange(allIrisModels[0]); - expect(comp.allowedIrisModels).toEqual([allIrisModels[1]]); + comp.onAllowedIrisVariantsSelectionChange(availableVariants[1]); + expect(comp.allowedVariants).toEqual([availableVariants[0], availableVariants[1]]); + comp.onAllowedIrisVariantsSelectionChange(availableVariants[0]); + expect(comp.allowedVariants).toEqual([availableVariants[1]]); }); it('change preferred model', () => { comp.subSettings = baseSettings(); comp.parentSubSettings = baseSettings(); - comp.allIrisModels = mockModels(); + comp.availableVariants = mockVariants(); comp.settingsType = IrisSettingsType.EXERCISE; fixture.detectChanges(); - comp.setModel(mockModels()[1]); - expect(comp.subSettings!.preferredModel).toBe(mockModels()[1].id); + comp.setVariant(mockVariants()[1]); + expect(comp.subSettings!.selectedVariant).toBe(mockVariants()[1].id); }); it('change enabled', () => { comp.subSettings = baseSettings(); comp.parentSubSettings = baseSettings(); - comp.allIrisModels = mockModels(); + comp.availableVariants = mockVariants(); comp.settingsType = IrisSettingsType.EXERCISE; fixture.detectChanges(); @@ -139,41 +144,41 @@ describe('IrisCommonSubSettingsUpdateComponent Component', () => { it('change inherit allowed models', () => { comp.subSettings = baseSettings(); comp.parentSubSettings = baseSettings(); - comp.allIrisModels = mockModels(); + comp.availableVariants = mockVariants(); comp.settingsType = IrisSettingsType.EXERCISE; fixture.detectChanges(); - comp.inheritAllowedModels = true; - comp.onInheritAllowedModelsChange(); - expect(comp.subSettings!.allowedModels).toBeUndefined(); - expect(comp.allowedIrisModels).toEqual(comp.getAvailableModels()); + comp.inheritAllowedVariants = true; + comp.onInheritAllowedVariantsChange(); + expect(comp.subSettings!.allowedVariants).toBeUndefined(); + expect(comp.allowedVariants).toEqual(comp.getAllowedVariants()); - comp.inheritAllowedModels = false; - comp.onInheritAllowedModelsChange(); - expect(comp.subSettings!.allowedModels).toEqual(comp.allowedIrisModels.map((model) => model.id)); + comp.inheritAllowedVariants = false; + comp.onInheritAllowedVariantsChange(); + expect(comp.subSettings!.allowedVariants).toEqual(comp.allowedVariants.map((model) => model.id)); }); it('ngOnChanges works', () => { comp.subSettings = baseSettings(); comp.parentSubSettings = baseSettings(); - comp.allIrisModels = mockModels(); + comp.availableVariants = mockVariants(); comp.settingsType = IrisSettingsType.EXERCISE; fixture.detectChanges(); const newSubSettings = baseSettings(); newSubSettings.enabled = false; - const newModels = mockModels(); + const newModels = mockVariants(); newModels.pop(); const changes: SimpleChanges = { subSettings: new SimpleChange(comp.subSettings, newSubSettings, false), - allIrisModels: new SimpleChange(comp.allIrisModels, newModels, false), + availableVariants: new SimpleChange(comp.availableVariants, newModels, false), }; comp.subSettings = newSubSettings; - comp.allIrisModels = mockModels(); + comp.availableVariants = mockVariants(); comp.ngOnChanges(changes); expect(comp.enabled).toBeFalse(); - expect(comp.allowedIrisModels).toEqual(newModels); + expect(comp.allowedVariants).toEqual(newModels); }); }); diff --git a/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts index 87c4fcb5989d..d31ecbaba114 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts @@ -6,7 +6,6 @@ import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; import { BehaviorSubject, of } from 'rxjs'; import { ButtonComponent } from 'app/shared/components/button.component'; import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; -import { IrisGlobalAutoupdateSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; import { mockEmptySettings, mockSettings } from './mock-settings'; import { ActivatedRoute, Params, provideRouter } from '@angular/router'; import { NgModel } from '@angular/forms'; @@ -23,7 +22,6 @@ describe('IrisCourseSettingsUpdateComponent Component', () => { const route = { parent: { params: routeParamsSubject.asObservable() } } as ActivatedRoute; let paramsSpy: jest.SpyInstance; let getSettingsSpy: jest.SpyInstance; - //let getModelsSpy: jest.SpyInstance; let getParentSettingsSpy: jest.SpyInstance; beforeEach(() => { @@ -33,7 +31,6 @@ describe('IrisCourseSettingsUpdateComponent Component', () => { IrisCourseSettingsUpdateComponent, IrisSettingsUpdateComponent, MockComponent(IrisCommonSubSettingsUpdateComponent), - MockComponent(IrisGlobalAutoupdateSettingsUpdateComponent), MockComponent(ButtonComponent), MockDirective(NgModel), ], @@ -49,7 +46,6 @@ describe('IrisCourseSettingsUpdateComponent Component', () => { const irisSettings = mockSettings(); getSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedCourseSettings').mockReturnValue(of(irisSettings)); - //getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); getParentSettingsSpy = jest.spyOn(irisSettingsService, 'getGlobalSettings').mockReturnValue(of(irisSettings)); }); fixture = TestBed.createComponent(IrisCourseSettingsUpdateComponent); @@ -66,11 +62,9 @@ describe('IrisCourseSettingsUpdateComponent Component', () => { expect(comp.courseId).toBe(1); expect(comp.settingsUpdateComponent).toBeTruthy(); expect(getSettingsSpy).toHaveBeenCalledWith(1); - //expect(getModelsSpy).toHaveBeenCalledOnce(); expect(getParentSettingsSpy).toHaveBeenCalledOnce(); - expect(fixture.debugElement.query(By.directive(IrisGlobalAutoupdateSettingsUpdateComponent))).toBeFalsy(); - expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(4); + expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(3); }); it('Can deactivate correctly', () => { @@ -99,7 +93,6 @@ describe('IrisCourseSettingsUpdateComponent Component', () => { comp.settingsUpdateComponent!.fillEmptyIrisSubSettings(); expect(comp.settingsUpdateComponent!.irisSettings.irisChatSettings).toBeTruthy(); expect(comp.settingsUpdateComponent!.irisSettings.irisLectureIngestionSettings).toBeTruthy(); - expect(comp.settingsUpdateComponent!.irisSettings.irisHestiaSettings).toBeTruthy(); expect(comp.settingsUpdateComponent!.irisSettings.irisCompetencyGenerationSettings).toBeTruthy(); }); }); diff --git a/src/test/javascript/spec/component/iris/settings/iris-enabled.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-enabled.component.spec.ts index f4aba8617eee..c47b91fdd5b1 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-enabled.component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-enabled.component.spec.ts @@ -47,7 +47,7 @@ describe('IrisEnabledComponent', () => { expect(comp).toBeDefined(); }); - it.each([IrisSubSettingsType.CHAT, IrisSubSettingsType.HESTIA, IrisSubSettingsType.COMPETENCY_GENERATION])('should load exercise', async (subSettingstype) => { + it.each([IrisSubSettingsType.CHAT, IrisSubSettingsType.COMPETENCY_GENERATION])('should load exercise', async (subSettingstype) => { const getExerciseSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedProgrammingExerciseSettings').mockReturnValue(of(irisSettings)); comp.exercise = exercise; comp.irisSubSettingsType = subSettingstype; @@ -58,19 +58,16 @@ describe('IrisEnabledComponent', () => { expect(comp.irisSubSettings).toBeDefined(); }); - it.each([IrisSubSettingsType.CHAT, IrisSubSettingsType.HESTIA, IrisSubSettingsType.COMPETENCY_GENERATION, IrisSubSettingsType.LECTURE_INGESTION])( - 'should load course', - async (subSettingstype) => { - const getExerciseSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedCourseSettings').mockReturnValue(of(irisSettings)); - comp.course = course; - comp.irisSubSettingsType = subSettingstype; - fixture.detectChanges(); - expect(getExerciseSettingsSpy).toHaveBeenCalledOnce(); - await Promise.resolve(); - expect(comp.irisSettings).toBe(irisSettings); - expect(comp.irisSubSettings).toBeDefined(); - }, - ); + it.each([IrisSubSettingsType.CHAT, IrisSubSettingsType.COMPETENCY_GENERATION, IrisSubSettingsType.LECTURE_INGESTION])('should load course', async (subSettingstype) => { + const getExerciseSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedCourseSettings').mockReturnValue(of(irisSettings)); + comp.course = course; + comp.irisSubSettingsType = subSettingstype; + fixture.detectChanges(); + expect(getExerciseSettingsSpy).toHaveBeenCalledOnce(); + await Promise.resolve(); + expect(comp.irisSettings).toBe(irisSettings); + expect(comp.irisSubSettings).toBeDefined(); + }); it('should set exercise enabled', async () => { const setSettingsSpy = jest.spyOn(irisSettingsService, 'setProgrammingExerciseSettings').mockReturnValue(of(new HttpResponse({ body: null as any as IrisSettings }))); @@ -89,8 +86,8 @@ describe('IrisEnabledComponent', () => { const setSettingsSpy = jest.spyOn(irisSettingsService, 'setCourseSettings').mockReturnValue(of(new HttpResponse({ body: null as any as IrisSettings }))); comp.course = course; comp.irisSettings = irisSettings; - comp.irisSubSettingsType = IrisSubSettingsType.HESTIA; - comp.irisSubSettings = irisSettings.irisHestiaSettings; + comp.irisSubSettingsType = IrisSubSettingsType.CHAT; + comp.irisSubSettings = irisSettings.irisChatSettings; comp.setEnabled(true); expect(setSettingsSpy).toHaveBeenCalledOnce(); diff --git a/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts index 98d0ff3f8770..140940787933 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts @@ -6,7 +6,6 @@ import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; import { BehaviorSubject, of } from 'rxjs'; import { ButtonComponent } from 'app/shared/components/button.component'; import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; -import { IrisGlobalAutoupdateSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; import { mockSettings } from './mock-settings'; import { IrisExerciseSettingsUpdateComponent } from 'app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component'; import { ActivatedRoute, Params, provideRouter } from '@angular/router'; @@ -23,7 +22,6 @@ describe('IrisExerciseSettingsUpdateComponent Component', () => { const route = { parent: { params: routeParamsSubject.asObservable() } } as ActivatedRoute; let paramsSpy: jest.SpyInstance; let getSettingsSpy: jest.SpyInstance; - //let getModelsSpy: jest.SpyInstance; let getParentSettingsSpy: jest.SpyInstance; beforeEach(() => { @@ -33,7 +31,6 @@ describe('IrisExerciseSettingsUpdateComponent Component', () => { IrisExerciseSettingsUpdateComponent, IrisSettingsUpdateComponent, MockComponent(IrisCommonSubSettingsUpdateComponent), - MockComponent(IrisGlobalAutoupdateSettingsUpdateComponent), MockComponent(ButtonComponent), MockDirective(NgModel), ], @@ -49,7 +46,6 @@ describe('IrisExerciseSettingsUpdateComponent Component', () => { const irisSettings = mockSettings(); getSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedProgrammingExerciseSettings').mockReturnValue(of(irisSettings)); - //getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); getParentSettingsSpy = jest.spyOn(irisSettingsService, 'getCombinedCourseSettings').mockReturnValue(of(irisSettings)); }); fixture = TestBed.createComponent(IrisExerciseSettingsUpdateComponent); @@ -67,10 +63,8 @@ describe('IrisExerciseSettingsUpdateComponent Component', () => { expect(comp.exerciseId).toBe(2); expect(comp.settingsUpdateComponent).toBeTruthy(); expect(getSettingsSpy).toHaveBeenCalledWith(2); - //expect(getModelsSpy).toHaveBeenCalledOnce(); expect(getParentSettingsSpy).toHaveBeenCalledWith(1); - expect(fixture.debugElement.query(By.directive(IrisGlobalAutoupdateSettingsUpdateComponent))).toBeFalsy(); expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(1); }); diff --git a/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts index a00353681299..44783963ac05 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts @@ -6,7 +6,6 @@ import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ButtonComponent } from 'app/shared/components/button.component'; import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; -import { IrisGlobalAutoupdateSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; import { mockSettings } from './mock-settings'; import { NgModel } from '@angular/forms'; import { IrisGlobalSettingsUpdateComponent } from 'app/iris/settings/iris-global-settings-update/iris-global-settings-update.component'; @@ -19,7 +18,6 @@ describe('IrisGlobalSettingsUpdateComponent Component', () => { let fixture: ComponentFixture; let irisSettingsService: IrisSettingsService; let getSettingsSpy: jest.SpyInstance; - //let getModelsSpy: jest.SpyInstance; beforeEach(() => { TestBed.configureTestingModule({ @@ -28,7 +26,6 @@ describe('IrisGlobalSettingsUpdateComponent Component', () => { IrisGlobalSettingsUpdateComponent, IrisSettingsUpdateComponent, MockComponent(IrisCommonSubSettingsUpdateComponent), - MockComponent(IrisGlobalAutoupdateSettingsUpdateComponent), MockComponent(ButtonComponent), MockDirective(NgModel), ], @@ -41,7 +38,6 @@ describe('IrisGlobalSettingsUpdateComponent Component', () => { // Setup const irisSettings = mockSettings(); getSettingsSpy = jest.spyOn(irisSettingsService, 'getGlobalSettings').mockReturnValue(of(irisSettings)); - //getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); }); fixture = TestBed.createComponent(IrisGlobalSettingsUpdateComponent); comp = fixture.componentInstance; @@ -55,10 +51,8 @@ describe('IrisGlobalSettingsUpdateComponent Component', () => { fixture.detectChanges(); expect(comp.settingsUpdateComponent).toBeTruthy(); expect(getSettingsSpy).toHaveBeenCalledOnce(); - //expect(getModelsSpy).toHaveBeenCalledOnce(); - expect(fixture.debugElement.query(By.directive(IrisGlobalAutoupdateSettingsUpdateComponent))).toBeTruthy(); - expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(4); + expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(3); }); it('Can deactivate correctly', () => { diff --git a/src/test/javascript/spec/component/iris/settings/iris-settings-update-component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-settings-update-component.spec.ts index 1c3a6fb751b4..4fe2f213f845 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-settings-update-component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-settings-update-component.spec.ts @@ -2,12 +2,11 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testin import { By } from '@angular/platform-browser'; import { IrisSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; import { IrisSettingsType } from 'app/entities/iris/settings/iris-settings.model'; -import { mockSettings } from './mock-settings'; +import { mockSettings, mockVariants } from './mock-settings'; import { ArtemisTestModule } from '../../../test.module'; import { NgModel } from '@angular/forms'; import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; -import { IrisGlobalAutoupdateSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; import { ButtonComponent } from 'app/shared/components/button.component'; import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; import { of } from 'rxjs'; @@ -17,12 +16,12 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; describe('IrisSettingsUpdateComponent', () => { let component: IrisSettingsUpdateComponent; let fixture: ComponentFixture; + let getVariantsSpy: jest.SpyInstance; beforeEach(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule], declarations: [ - IrisGlobalAutoupdateSettingsUpdateComponent, IrisCourseSettingsUpdateComponent, IrisSettingsUpdateComponent, IrisCommonSubSettingsUpdateComponent, @@ -42,15 +41,12 @@ describe('IrisSettingsUpdateComponent', () => { .then(() => { fixture = TestBed.createComponent(IrisSettingsUpdateComponent); component = fixture.componentInstance; + + const irisSettingsService = TestBed.inject(IrisSettingsService); + getVariantsSpy = jest.spyOn(irisSettingsService, 'getVariantsForFeature').mockReturnValue(of(mockVariants())); }); }); - it('should display global auto-update settings only if settingsType is GLOBAL', () => { - component.irisSettings = mockSettings(); - component.settingsType = IrisSettingsType.GLOBAL; - fixture.detectChanges(); - const globalSettingsElement = fixture.debugElement.query(By.css('jhi-iris-global-autoupdate-settings-update')); - expect(globalSettingsElement).toBeTruthy(); - }); + it('should display the checkbox for lecture ingestion when settingsType is COURSE', fakeAsync(() => { component.irisSettings = mockSettings(); component.settingsType = IrisSettingsType.COURSE; @@ -65,5 +61,6 @@ describe('IrisSettingsUpdateComponent', () => { expect(lectureIngestionElement).not.toBeNull(); expect(checkboxElement).toBeTruthy(); expect(labelElement).toBeTruthy(); + expect(getVariantsSpy).toHaveBeenCalled(); })); }); diff --git a/src/test/javascript/spec/component/iris/settings/mock-settings.ts b/src/test/javascript/spec/component/iris/settings/mock-settings.ts index f9338cd78cc4..6c542caf9a91 100644 --- a/src/test/javascript/spec/component/iris/settings/mock-settings.ts +++ b/src/test/javascript/spec/component/iris/settings/mock-settings.ts @@ -1,51 +1,33 @@ -import { IrisModel } from 'app/entities/iris/settings/iris-model'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { - IrisChatSubSettings, - IrisCompetencyGenerationSubSettings, - IrisHestiaSubSettings, - IrisLectureIngestionSubSettings, -} from 'app/entities/iris/settings/iris-sub-settings.model'; +import { IrisVariant } from 'app/entities/iris/settings/iris-variant'; +import { IrisChatSubSettings, IrisCompetencyGenerationSubSettings, IrisLectureIngestionSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; import { IrisGlobalSettings } from 'app/entities/iris/settings/iris-settings.model'; export function mockSettings() { - const mockTemplate = new IrisTemplate(); - mockTemplate.id = 1; - mockTemplate.content = 'Hello World'; const mockChatSettings = new IrisChatSubSettings(); mockChatSettings.id = 1; - mockChatSettings.template = mockTemplate; mockChatSettings.enabled = true; const mockLectureIngestionSettings = new IrisLectureIngestionSubSettings(); mockLectureIngestionSettings.id = 7; mockLectureIngestionSettings.enabled = true; mockLectureIngestionSettings.autoIngestOnLectureAttachmentUpload = true; - const mockHestiaSettings = new IrisHestiaSubSettings(); - mockHestiaSettings.id = 2; - mockHestiaSettings.template = mockTemplate; - mockHestiaSettings.enabled = true; const mockCompetencyGenerationSettings = new IrisCompetencyGenerationSubSettings(); mockCompetencyGenerationSettings.id = 5; mockCompetencyGenerationSettings.enabled = false; const irisSettings = new IrisGlobalSettings(); irisSettings.id = 1; irisSettings.irisChatSettings = mockChatSettings; - irisSettings.irisHestiaSettings = mockHestiaSettings; irisSettings.irisCompetencyGenerationSettings = mockCompetencyGenerationSettings; irisSettings.irisLectureIngestionSettings = mockLectureIngestionSettings; return irisSettings; } export function mockEmptySettings() { - const mockTemplate = new IrisTemplate(); - mockTemplate.id = 1; - mockTemplate.content = 'Hello World'; const irisSettings = new IrisGlobalSettings(); irisSettings.id = 1; return irisSettings; } -export function mockModels() { +export function mockVariants() { return [ { id: '1', @@ -57,5 +39,5 @@ export function mockModels() { name: 'Model 2', description: 'Model 2 Description', }, - ] as IrisModel[]; + ] as IrisVariant[]; } From da03f27a4c9aef1be9b4c6c6edb92be36445c287 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Sat, 12 Oct 2024 11:37:20 +0200 Subject: [PATCH 08/14] Development: Update client dependencies --- jest.config.js | 8 +- package-lock.json | 1216 +++++++++++++++++++++++++++++---------------- package.json | 48 +- 3 files changed, 803 insertions(+), 469 deletions(-) diff --git a/jest.config.js b/jest.config.js index 3dab49d4b7e0..48cac393d596 100644 --- a/jest.config.js +++ b/jest.config.js @@ -102,10 +102,10 @@ module.exports = { coverageThreshold: { global: { // TODO: in the future, the following values should increase to at least 90% - statements: 87.37, - branches: 73.68, - functions: 81.93, - lines: 87.42, + statements: 87.43, + branches: 73.72, + functions: 82.05, + lines: 87.49, }, }, coverageReporters: ['clover', 'json', 'lcov', 'text-summary'], diff --git a/package-lock.json b/package-lock.json index 4b7bc1437ba6..91ee03d72aaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,18 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "18.2.7", - "@angular/cdk": "18.2.7", - "@angular/common": "18.2.7", - "@angular/compiler": "18.2.7", - "@angular/core": "18.2.7", - "@angular/forms": "18.2.7", - "@angular/localize": "18.2.7", - "@angular/material": "18.2.7", - "@angular/platform-browser": "18.2.7", - "@angular/platform-browser-dynamic": "18.2.7", - "@angular/router": "18.2.7", - "@angular/service-worker": "18.2.7", + "@angular/animations": "18.2.8", + "@angular/cdk": "18.2.8", + "@angular/common": "18.2.8", + "@angular/compiler": "18.2.8", + "@angular/core": "18.2.8", + "@angular/forms": "18.2.8", + "@angular/localize": "18.2.8", + "@angular/material": "18.2.8", + "@angular/platform-browser": "18.2.8", + "@angular/platform-browser-dynamic": "18.2.8", + "@angular/router": "18.2.8", + "@angular/service-worker": "18.2.8", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.5.0", @@ -33,7 +33,7 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular": "8.33.1", + "@sentry/angular": "8.34.0", "@siemens/ngx-datatable": "22.4.1", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", @@ -59,8 +59,8 @@ "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", - "pdfjs-dist": "4.6.82", - "posthog-js": "1.166.1", + "pdfjs-dist": "4.7.76", + "posthog-js": "1.167.0", "rxjs": "7.8.1", "showdown": "2.1.0", "showdown-highlight": "3.1.0", @@ -78,29 +78,29 @@ }, "devDependencies": { "@angular-builders/jest": "18.0.0", - "@angular-devkit/build-angular": "18.2.7", + "@angular-devkit/build-angular": "18.2.8", "@angular-eslint/builder": "18.3.1", "@angular-eslint/eslint-plugin": "18.3.1", "@angular-eslint/eslint-plugin-template": "18.3.1", "@angular-eslint/schematics": "18.3.1", "@angular-eslint/template-parser": "18.3.1", - "@angular/cli": "18.2.7", - "@angular/compiler-cli": "18.2.7", - "@angular/language-service": "18.2.7", - "@sentry/types": "8.33.1", + "@angular/cli": "18.2.8", + "@angular/compiler-cli": "18.2.8", + "@angular/language-service": "18.2.8", + "@sentry/types": "8.34.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", "@types/jest": "29.5.13", "@types/lodash-es": "4.17.12", - "@types/node": "22.7.4", + "@types/node": "22.7.5", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.8.0", - "@typescript-eslint/parser": "8.8.0", + "@typescript-eslint/eslint-plugin": "8.8.1", + "@typescript-eslint/parser": "8.8.1", "eslint": "9.12.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", @@ -121,7 +121,7 @@ "ngxtension": "4.0.0", "prettier": "3.3.3", "rimraf": "6.0.1", - "sass": "1.79.4", + "sass": "1.79.5", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" @@ -212,13 +212,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.7.tgz", - "integrity": "sha512-kpcgXnepEXcoxDTbqbGj7Hg1WJLWj1HLR3/FKmC5TbpBf1xiLxiqfkQNwz3BbE/W9JWMLdrXr3GI9O3O2gWPLg==", + "version": "0.1802.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.8.tgz", + "integrity": "sha512-/rtFQEKgS7LlB9oHr4NCBSdKnvP5kr8L5Hbd3Vl8hZOYK9QWjxKPEXnryA2d5+PCE98bBzZswCNXqELZCPTgIQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.7", + "@angular-devkit/core": "18.2.8", "rxjs": "7.8.1" }, "engines": { @@ -228,17 +228,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.7.tgz", - "integrity": "sha512-u8PriYdgddK7k+OS/pOFPD1v4Iu5bztUJZXZVcGeXBZFFdnGFFzKmQw9mfcyGvTMJp2ABgBuuJT0YqYgNfAhzw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.8.tgz", + "integrity": "sha512-qK/iLk7A8vQp1CyiJV4DpwfLjPKoiOlTtFqoO5vD8Tyxmc+R06FQp6GJTsZ7JtrTLYSiH+QAWiY6NgF/Rj/hHg==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.7", - "@angular-devkit/build-webpack": "0.1802.7", - "@angular-devkit/core": "18.2.7", - "@angular/build": "18.2.7", + "@angular-devkit/architect": "0.1802.8", + "@angular-devkit/build-webpack": "0.1802.8", + "@angular-devkit/core": "18.2.8", + "@angular/build": "18.2.8", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -249,7 +249,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.7", + "@ngtools/webpack": "18.2.8", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -382,13 +382,13 @@ "license": "0BSD" }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.7.tgz", - "integrity": "sha512-VrtbrhZ+dht3f0GjtfRLRGRN4XHN/W+/bA9DqckdxVS6SydsrCWNHonvEPmOs4jJmGIGXIu6tUBMcWleTao2sg==", + "version": "0.1802.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.8.tgz", + "integrity": "sha512-uPpopkXkO66SSdjtVr7xCyQCPs/x6KUC76xkDc4j0b8EEHifTbi/fNpbkcZ6wBmoAfjKLWXfKvtkh0TqKK5Hkw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.7", + "@angular-devkit/architect": "0.1802.8", "rxjs": "7.8.1" }, "engines": { @@ -402,9 +402,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.7.tgz", - "integrity": "sha512-1ZTi4A6tEC2bkJ/puCIdIPYhesnlCVOMSDJL/lZAd0hC6X22T4pwu0AEvue7mcP5NbXpQDiBaXOZ3MmCA8PwOA==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.8.tgz", + "integrity": "sha512-4o2T6wsmXGE/v53+F8L7kGoN2+qzt03C9rtjLVQpOljzpJVttQ8bhvfWxyYLWwcl04RWqRa+82fpIZtBkOlZJw==", "dev": true, "license": "MIT", "dependencies": { @@ -430,13 +430,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.7.tgz", - "integrity": "sha512-j7198lpkOXMG+Gyfln/5aDgBZV7m4pWMzHFhkO3+w3cbCNUN1TVZW0SyJcF+CYaxANzTbuumfvpsYc/fTeAGLw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.8.tgz", + "integrity": "sha512-i/h2Oji5FhJMC7wDSnIl5XUe/qym+C1ZwScaATJwDyRLCUIynZkj5rLgdG/uK6l+H0PgvxigkF+akWpokkwW6w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.7", + "@angular-devkit/core": "18.2.8", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -549,9 +549,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.7.tgz", - "integrity": "sha512-5B7qD1K+kKOf9lgJT4VNMft3IK2BnRHjN1S6l38ywzQ/nxpmCG7f+qKAAU6CpCywhNUBeXW0hVXTMuMNPVOcQQ==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.8.tgz", + "integrity": "sha512-dMSn2hg70siv3lhP+vqhMbgc923xw6XBUvnpCPEzhZqFHvPXfh/LubmsD5RtqHmjWebXtgVcgS+zg3Gq3jB2lg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -560,18 +560,18 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.7" + "@angular/core": "18.2.8" } }, "node_modules/@angular/build": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.7.tgz", - "integrity": "sha512-oq6JsVxLP9/w9F2IjKroJwPB9CdlMblu2Xhfq/qQZRSUuM8Ppt1svr2FBTo1HrLIbosqukkVcSSdmKYDneo+cg==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.8.tgz", + "integrity": "sha512-ufuA4vHJSrL9SQW7bKV61DOoN1mm0t0ILTHaxSoCG3YF70cZJOX7+HNp3cK2uoldRMwbTOKSvCWBw54KKDRd5Q==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.7", + "@angular-devkit/architect": "0.1802.8", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -651,9 +651,9 @@ } }, "node_modules/@angular/cdk": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.7.tgz", - "integrity": "sha512-Dfl37WBLeEUURQrDeuMcOgX2bkQJ+BGMOlr1qsFXzUWHH+qgYW2YwO1rbna/rjxyeFzc2Sy569dYRzNPqMewzg==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.8.tgz", + "integrity": "sha512-J8A2FkwTBzLleAEWz6EgW73dEoeq87GREBPjTv8+2JV09LX+V3hnbgNk6zWq5k4OXtQNg9WrWP9QyRbUyA597g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -668,18 +668,18 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.7.tgz", - "integrity": "sha512-KoWgSvhRsU05A2m6B7jw1kdpyoS+Ce5GGLW6xcnX7VF2AckW54vYd/8ZkgpzQrKfvIpVblYd4KJGizKoaLZ5jA==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.8.tgz", + "integrity": "sha512-GKXG7F7z5rxwZ8/bnW/Bp8/zsfE/BpHmIP/icLfUIOwv2kaY5OD2tfQssWXPEuqZzYq2AYz+wjVSbWjxGoja8A==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.7", - "@angular-devkit/core": "18.2.7", - "@angular-devkit/schematics": "18.2.7", + "@angular-devkit/architect": "0.1802.8", + "@angular-devkit/core": "18.2.8", + "@angular-devkit/schematics": "18.2.8", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.7", + "@schematics/angular": "18.2.8", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -702,9 +702,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.7.tgz", - "integrity": "sha512-5vDBmBR2JcIxHVEDunKXNU+T+OvTGiHZTSo35GFOHJxKFgX5g6+0tJBZunK04oBZGbJQUmp3pg2kMvuKKjZnkQ==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.8.tgz", + "integrity": "sha512-TYsKtE5nVaIScWSLGSO34Skc+s3hB/BujSddnfQHoNFvPT/WR0dfmdlpVCTeLj+f50htFoMhW11tW99PbK+whQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -713,14 +713,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.7", + "@angular/core": "18.2.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.7.tgz", - "integrity": "sha512-XemlYyRGnu/HrICtXwTPmGtyOrI8BhbGg/HMiJ7sVx40AeEIX0uyDgnu9Gc5OjmtDqZZ8Qftg1sQAxaCVjLb1w==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.8.tgz", + "integrity": "sha512-JRedHNfK1CCPVyeGQB5w3WBYqMA6X8Q240CkvjlGfn0pVXihf9DWk3nkSQJVgYxpvpHfxdgjaYZ5IpMzlkmkhw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -729,7 +729,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.7" + "@angular/core": "18.2.8" }, "peerDependenciesMeta": { "@angular/core": { @@ -738,14 +738,14 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.7.tgz", - "integrity": "sha512-U7cveObj+rrXH5EC8egAhATCeAAcOceEQDTVIOWmBa0qMR4hOMjtI2XUS2QRuI1Q+fQZ2hVEOW95WVLvEMsANA==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.8.tgz", + "integrity": "sha512-OksDE4LWQUCcIvMjtZF7eiDCdIMrcMMpC1+Q0PIYi7KmnqXFGs4/Y0NdJvtn/LrQznzz5WaKM3ZDVNZTRX4wmw==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^3.0.0", + "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", @@ -761,14 +761,42 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.7", + "@angular/compiler": "18.2.8", "typescript": ">=5.4 <5.6" } }, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/compiler-cli/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular/core": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.7.tgz", - "integrity": "sha512-hLOxgxLiyWm9iVHBsUsJfx1hDsXWZnfJBlr+N7cev53f0CDoPfbshqq6KV+JFqXFDguzR9dKHm1ewT1jK3e6Tw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.8.tgz", + "integrity": "sha512-NwIuX/Iby1jT6Iv1/s6S3wOFf8xfuQR3MPGvKhGgNtjXLbHG+TXceK9+QPZC0s9/Z8JR/hz+li34B79GrIKgUg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -782,9 +810,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.7.tgz", - "integrity": "sha512-WO3c9/OA7ekBnDBgmvi5TlHshOt5S4NREIP+/VVyuRgg28BwUWyO/Nqh19nguE1UNNRt6OMLkT6NSV2ewhcXUg==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.8.tgz", + "integrity": "sha512-JCLki7KC6D5vF6dE6yGlBmW33khIgpHs8N9SzuiJtkQqNDTIQA8cPsGV6qpLpxflxASynQOX5lDkWYdQyfm77Q==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -793,16 +821,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.7", - "@angular/core": "18.2.7", - "@angular/platform-browser": "18.2.7", + "@angular/common": "18.2.8", + "@angular/core": "18.2.8", + "@angular/platform-browser": "18.2.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.7.tgz", - "integrity": "sha512-gFsme3y5uC/dQGBBX05VnmT2KAEAZ6gsNk8m1b226LYvh8Oc+JQ4sXv7THGq1x5VnrTzRcCIELbkNHCiFdvL1Q==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.8.tgz", + "integrity": "sha512-IueQ57CPP0Dt0z2n8B1A6JTwTq6m/AJVObZzrkSfXlzY1rY2qRuTJmAbZpTJ3iAxVzNYoaGh+NFHmJL8fRiXKQ==", "dev": true, "license": "MIT", "engines": { @@ -810,9 +838,9 @@ } }, "node_modules/@angular/localize": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.7.tgz", - "integrity": "sha512-qYozomhO+1BlvtoMEEgKhaKz8thoztqNZEYPq9RmfkTB5uW7Q8h6rr1Sc2YAzJ6+ZA0McwabdJSX1TDxWyZx0Q==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.8.tgz", + "integrity": "sha512-1T7aXEdgVyeYnHOfQUuIDO8Lsamg1ZLrJrA5zUv61asPJp6HCcMjXy9vDQ1XvHm5+CdDjKk/rczlN4lSMZ0QRw==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -829,21 +857,21 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.7", - "@angular/compiler-cli": "18.2.7" + "@angular/compiler": "18.2.8", + "@angular/compiler-cli": "18.2.8" } }, "node_modules/@angular/material": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.7.tgz", - "integrity": "sha512-mgPj2TCIrsngmu3iNnoaPc6su7uPv+NPCv9HaiKhTx4QGae8EW+RvUxEZJvh4Qaym1fJTi3hjnVeWvQDLQt4CA==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.8.tgz", + "integrity": "sha512-wQGMVsfQ9lQfih2VsWAvV4z3S3uBxrxc61owlE+K0T1BxH9u/jo3A/rnRitIdvR/L4NnYlfhCnmrW9K+Pl+WCg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^18.0.0 || ^19.0.0", - "@angular/cdk": "18.2.7", + "@angular/cdk": "18.2.8", "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "@angular/forms": "^18.0.0 || ^19.0.0", @@ -852,9 +880,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.7.tgz", - "integrity": "sha512-xgj2DH/isFrMZ73dJJm89NRnWBI3AHtugQrZbIapkKBdEt/C1o4SR2W2cV4mPb9o+ELnWurfrxFt9o/q2vnVLw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.8.tgz", + "integrity": "sha512-EPai4ZPqSq3ilLJUC85kPi9wo5j5suQovwtgRyjM/75D9Qy4TV19g8hkVM5Co/zrltO8a2G6vDscCNI5BeGw2A==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -863,9 +891,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.7", - "@angular/common": "18.2.7", - "@angular/core": "18.2.7" + "@angular/animations": "18.2.8", + "@angular/common": "18.2.8", + "@angular/core": "18.2.8" }, "peerDependenciesMeta": { "@angular/animations": { @@ -874,9 +902,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.7.tgz", - "integrity": "sha512-BDldzUKjnUjo0NW5gHjBY6CeJP1bWVfF1h/T3idyYG+F4Lxlb3aykRgLWXg4srNLY1KqE7XOYUmgc5cV613bgw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.8.tgz", + "integrity": "sha512-poZoapDqyN/rxGKQ3C6esdPiPLMkSpP2v12hoEa12KHgfPk7T1e+a+NMyJjV8HeOY3WyvL7tGRhW0NPTajTkhw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -885,16 +913,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.7", - "@angular/compiler": "18.2.7", - "@angular/core": "18.2.7", - "@angular/platform-browser": "18.2.7" + "@angular/common": "18.2.8", + "@angular/compiler": "18.2.8", + "@angular/core": "18.2.8", + "@angular/platform-browser": "18.2.8" } }, "node_modules/@angular/router": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.7.tgz", - "integrity": "sha512-TXE8Aw63hDp3PEaNu4B1DMNvlS0uCzs36o/OSCCmewmLnzyJygkgi4jeEj20FsWPAQOUj5g5tnCYgxz1IRrCUg==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.8.tgz", + "integrity": "sha512-L+olYgxIiBq+tbfayVI0cv1yOuymsw33msnGC2l/vpc9sSVfqGzESFnB4yMVU3vHtE9v6v2Y6O+iV44/b79W/g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -903,16 +931,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.7", - "@angular/core": "18.2.7", - "@angular/platform-browser": "18.2.7", + "@angular/common": "18.2.8", + "@angular/core": "18.2.8", + "@angular/platform-browser": "18.2.8", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.7.tgz", - "integrity": "sha512-1t8PUWmZi32i/SG/r12vz+cfn0l3xVEa0FY7GXaZK7hlfDL34js1HZXHkvGUuRZRw/4L1jl7AwPoxwGeWr2ldg==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.8.tgz", + "integrity": "sha512-LQktgS2Hn845ASWNyjde18V+CHkkPeCzORfh0ChYKiOmXYFtj/myEik5o/QI/G13Kaymy+vcuwQKiUuZjZiD1w==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -924,8 +952,8 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.7", - "@angular/core": "18.2.7" + "@angular/common": "18.2.8", + "@angular/core": "18.2.8" } }, "node_modules/@babel/code-frame": { @@ -942,9 +970,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.7.tgz", - "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1353,12 +1381,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", - "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7" + "@babel/types": "^7.25.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -1864,15 +1892,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.7.tgz", - "integrity": "sha512-rvUUtoVlkDWtDWxGAiiQj0aNktTPn3eFynBcMC2IhsXweehwgdI9ODe+XjWw515kEmv22sSOTp/rxIRuTiB7zg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz", + "integrity": "sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.7", - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1999,14 +2026,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.7.tgz", - "integrity": "sha512-UvcLuual4h7/GfylKm2IAA3aph9rwvAM2XBA0uPKU3lca+Maai4jBjjEVUS568ld6kJcgbouuumCBhMd/Yz17w==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz", + "integrity": "sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2033,14 +2059,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.7.tgz", - "integrity": "sha512-h3MDAP5l34NQkkNulsTNyjdaR+OiB0Im67VU//sFupouP8Q6m9Spy7l66DcaAQxtmCqGdanPByLsnwFttxKISQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz", + "integrity": "sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2085,14 +2110,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.7.tgz", - "integrity": "sha512-Ot43PrL9TEAiCe8C/2erAjXMeVSnE/BLEx6eyrKLNFCCw5jvhTHKyHxdI1pA0kz5njZRYAnMO2KObGqOCRDYSA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz", + "integrity": "sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2118,14 +2142,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.7.tgz", - "integrity": "sha512-iImzbA55BjiovLyG2bggWS+V+OLkaBorNvc/yJoeeDQGztknRnDdYfp2d/UPmunZYEnZi6Lg8QcTmNMHOB0lGA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz", + "integrity": "sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2255,14 +2278,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.7.tgz", - "integrity": "sha512-FbuJ63/4LEL32mIxrxwYaqjJxpbzxPVQj5a+Ebrc8JICV6YX8nE53jY+K0RZT3um56GoNWgkS2BQ/uLGTjtwfw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz", + "integrity": "sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2272,14 +2294,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.7.tgz", - "integrity": "sha512-8CbutzSSh4hmD+jJHIA8vdTNk15kAzOnFLVVgBSMGr28rt85ouT01/rezMecks9pkU939wDInImwCKv4ahU4IA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz", + "integrity": "sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2289,15 +2310,14 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.7.tgz", - "integrity": "sha512-1JdVKPhD7Y5PvgfFy0Mv2brdrolzpzSoUq2pr6xsR+m+3viGGeHEokFKsCgOkbeFOQxfB1Vt2F0cPJLRpFI4Zg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz", + "integrity": "sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.7", "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.25.7" }, "engines": { @@ -2325,14 +2345,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.7.tgz", - "integrity": "sha512-m9obYBA39mDPN7lJzD5WkGGb0GO54PPLXsbcnj1Hyeu8mSRz7Gb4b1A6zxNX32ZuUySDK4G6it8SDFWD1nCnqg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz", + "integrity": "sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2342,15 +2361,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.7.tgz", - "integrity": "sha512-h39agClImgPWg4H8mYVAbD1qP9vClFbEjqoJmt87Zen8pjqK8FTPUwrOXAvqu5soytwxrLMd2fx2KSCp2CHcNg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz", + "integrity": "sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2393,16 +2411,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.7.tgz", - "integrity": "sha512-LzA5ESzBy7tqj00Yjey9yWfs3FKy4EmJyKOSWld144OxkTji81WWnUT8nkLUn+imN/zHL8ZQlOu/MTUAhHaX3g==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz", + "integrity": "sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.7", "@babel/helper-create-class-features-plugin": "^7.25.7", - "@babel/helper-plugin-utils": "^7.25.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -2827,9 +2844,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", - "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.7", @@ -2908,9 +2925,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.2.0.tgz", - "integrity": "sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", + "integrity": "sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==", "dev": true, "license": "MIT", "dependencies": { @@ -2919,9 +2936,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", - "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", "dev": true, "license": "MIT", "dependencies": { @@ -3793,9 +3810,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.6.tgz", - "integrity": "sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", + "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", "dev": true, "license": "MIT", "engines": { @@ -4990,9 +5007,9 @@ } }, "node_modules/@jsonjoy.com/util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.3.0.tgz", - "integrity": "sha512-Cebt4Vk7k1xHy87kHY7KSPLT77A7Ev7IfOblyLZhtYEhrdQ6fX4EoLq3xOQ3O/DRMEh2ok5nyC180E+ABS8Wmw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5359,9 +5376,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.7.tgz", - "integrity": "sha512-BmnFxss6zGobGyq9Mi7736golbK8RLgF+zYCQZ+4/OfMMA1jKVoELnyJqNyAx+DQn3m1qKVBjtGEL7pTNpPzOw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.8.tgz", + "integrity": "sha512-sq0kI8gEen4QlM6X8XqOYy7j4B8iLCYNo+iKxatV36ts4AXH0MuVkP56+oMaoH5oZNoSqd0RlfnotEHfvJAr8A==", "dev": true, "license": "MIT", "engines": { @@ -5691,23 +5708,23 @@ } }, "node_modules/@nrwl/devkit": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-19.8.3.tgz", - "integrity": "sha512-67vZJRMCEA543A0uz8dPTZ5lX4wsAlgsr24KJafsUxBC2WCf9z4BqcLj0jVWfmRdKJmu2UwaxtD2UB1bekt3sg==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-19.8.4.tgz", + "integrity": "sha512-OoIqDjj2mWzLs3aSF6w5OiC2xywYi/jBxHc7t7Lyi56Vc4dQq8vJMELa9WtG6qH0k05fF7N+jAoKlfvLgbbEFA==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "19.8.3" + "@nx/devkit": "19.8.4" } }, "node_modules/@nrwl/tao": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-19.8.3.tgz", - "integrity": "sha512-byjBtOXx+xGjMu1wKopJSJbrR3gKqTsCEgp1+YSZ45+iFKxFdXLJrGsyhVqBovCKVBM+5/KtGuEkZoUPlP8JWg==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-19.8.4.tgz", + "integrity": "sha512-03/+QZ4/6HmKbEmvzCutLI1XIclBspNYtiVHmGPRWuwhnZViqYfnyl8J7RWVdFEoKKA5fhJqpg7e28aGuoMBvQ==", "dev": true, "license": "MIT", "dependencies": { - "nx": "19.8.3", + "nx": "19.8.4", "tslib": "^2.3.0" }, "bin": { @@ -5715,13 +5732,13 @@ } }, "node_modules/@nx/devkit": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-19.8.3.tgz", - "integrity": "sha512-uX50CAM11tzhwswf0ftN0QfzW2FM3M4Mf/pD/nRRnmsTkcPTdMXVu4LHuLVTp4CMsaO+cOQlqgHXujHYfOIctg==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-19.8.4.tgz", + "integrity": "sha512-FPFT8gVDFRSEmU0n7nRkT4Rnqy7OMznfPXLfDZtVuzEi5Cl6ftG3UBUvCgJcJFCYJVAZAUuv6vRSRarHd51XFQ==", "dev": true, "license": "MIT", "dependencies": { - "@nrwl/devkit": "19.8.3", + "@nrwl/devkit": "19.8.4", "ejs": "^3.1.7", "enquirer": "~2.3.6", "ignore": "^5.0.4", @@ -5762,9 +5779,9 @@ } }, "node_modules/@nx/nx-darwin-arm64": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-19.8.3.tgz", - "integrity": "sha512-ORHFFWMZcvFi0xcpCaXccXVEhFwAevSHOIKfW359+12H9w7VW2O42B+2NcVMK1mrDTOjlXTd+0AmAu7P4NzWFA==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-19.8.4.tgz", + "integrity": "sha512-mbSGt63hYcVCSQ54kpHl0lFqr5CsbkGJ4L3liWE30Da7vXZJwUBr9f+b9DnQ64IZzlu6vAhNcaiYQXa9lAk0yQ==", "cpu": [ "arm64" ], @@ -5779,9 +5796,9 @@ } }, "node_modules/@nx/nx-darwin-x64": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-19.8.3.tgz", - "integrity": "sha512-Ji9DPA0tuzygMcypD/FHRDQSPipcRqMNmSaNKxVpcCbozVTWHvqXFk0rloDIUnxnE0+zvE9LN71H2sS4ZHdTQA==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-19.8.4.tgz", + "integrity": "sha512-lTcXUCXNvqHdLmrNCOyDF+u6pDx209Ew7nSR47sQPvkycIHYi0gvgk0yndFn1Swah0lP4OxWg7rzAfmOlZd6ew==", "cpu": [ "x64" ], @@ -5796,9 +5813,9 @@ } }, "node_modules/@nx/nx-freebsd-x64": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-19.8.3.tgz", - "integrity": "sha512-Ys+PqtBZCS+QBNs7he3fnxVhMWz/lSSaBVUlVHoQcV1Y4clEpP2TWNQSsbaVnnpcB7pdmKN5ymWdaCaAQuqCMw==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-19.8.4.tgz", + "integrity": "sha512-4BUplOxPZeUwlUNfzHHMmebNVgDFW/jNX6TWRS+jINwOHnpWLkLFAXu27G80/S3OaniVCzEQklXO9b+1UsdgXw==", "cpu": [ "x64" ], @@ -5813,9 +5830,9 @@ } }, "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-19.8.3.tgz", - "integrity": "sha512-hGOlML60ELXkgkqLHB/w/sXbTbXFhOQGSXC72CjaP5G0u1gj8eTQKJ7WEsqPAFMk5SLFFxqM7eid0LmAYYuZWQ==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-19.8.4.tgz", + "integrity": "sha512-Wahul8oz9huEm/Jv3wud5IGWdZxkGG4tdJm9i5TV5wxfUMAWbKU9v2nzZZins452UYESWvwvDkiuBPZqSto3qw==", "cpu": [ "arm" ], @@ -5830,9 +5847,9 @@ } }, "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-19.8.3.tgz", - "integrity": "sha512-K/5iVbLbhsx28YtZHvveJgF41rbr2kMdabooZeFqy6VReN7U/zGJMjpV1FzDlf3TNr9jyjPDZgVQRS+qXau2qA==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-19.8.4.tgz", + "integrity": "sha512-L0RVCZkNAtZDplLT7uJV7M9cXxq2Fxw+8ex3eb9XSp7eyLeFO21T0R6vTouJ42E/PEvGApCAcyGqtnyPNMZFfw==", "cpu": [ "arm64" ], @@ -5847,9 +5864,9 @@ } }, "node_modules/@nx/nx-linux-arm64-musl": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-19.8.3.tgz", - "integrity": "sha512-zqzWjFniZDXiI/3MYxbJ0yIenUKr56apLy70oABTBHx++dsUA3/DxLMNypMA82a8KQtsbePWUi3Pgtr+JIMNXw==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-19.8.4.tgz", + "integrity": "sha512-0q8r8I8WCsY3xowDI2j109SCUSkFns/BJ40aCfRh9hhrtaIIc5qXUw2YFTjxUZNcRJXx9j9+hTe9jBkUSIGvCw==", "cpu": [ "arm64" ], @@ -5864,9 +5881,9 @@ } }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-19.8.3.tgz", - "integrity": "sha512-W1RRCqsQvpur4BxP5g5cQwjZB6jhxYLSSXi3QQDaU5ITkaV5Pdj/L7D/G6YgRB8lzKZrXc57aLJ5UKY/Z+di7w==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-19.8.4.tgz", + "integrity": "sha512-XcRBNe0ws7KB0PMcUlpQqzzjjxMP8VdqirBz7CfB2XQ8xKmP3370p0cDvqs/4oKDHK4PCkmvVFX60tzakutylA==", "cpu": [ "x64" ], @@ -5881,9 +5898,9 @@ } }, "node_modules/@nx/nx-linux-x64-musl": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-19.8.3.tgz", - "integrity": "sha512-waTo0zBBGnmU7fS87IpOnVGx7EHa0umzSMlGG0LUoU6swOeNODezsBn1Vbvaw1o7sStWBzdEBlxLxHOQXRAidg==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-19.8.4.tgz", + "integrity": "sha512-JB4tAuZBCF0yqSnKF3pHXa0b7LA3ebi3Bw08QmMr//ON4aU+eXURGBuj9XvULD2prY+gpBrvf+MsG1XJAHL6Zg==", "cpu": [ "x64" ], @@ -5898,9 +5915,9 @@ } }, "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-19.8.3.tgz", - "integrity": "sha512-lio7ulblEMs1otMtVIrdfdMTBqKRZEHim57AcMHSVnwmtl2ENP6TR3YIgyigjfLlkPanNU7i0QQ4h6Nk2I/FRw==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-19.8.4.tgz", + "integrity": "sha512-WvQag/pN9ofRWRDvOZxj3jvJoTetlvV1uyirnDrhupRgi+Fj67OlGGt2zVUHaXFGEa1MfCEG6Vhk6152m4KyaQ==", "cpu": [ "arm64" ], @@ -5915,9 +5932,9 @@ } }, "node_modules/@nx/nx-win32-x64-msvc": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-19.8.3.tgz", - "integrity": "sha512-RU11iXJzdrw5CmogT2AwsjxK7g8vWf6Oy23NlrvsQFODtavjqAWoD5qpUY/H16s9lVDwrpzCbGbAXph0lbgLKA==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-19.8.4.tgz", + "integrity": "sha512-//JntLrN3L7WL/WgP3D0FE34caYTPcG/GIMBguC9w7YDyTlEikLgLbobjdCPz+2f9OWGvIZbJgGmtHNjnETM/g==", "cpu": [ "x64" ], @@ -5931,6 +5948,312 @@ "node": ">= 10" } }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6247,14 +6570,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.7.tgz", - "integrity": "sha512-WOBzO11qstznHbC9tZXQf6/8+PqmaRI6QYcdTspqXNh9q9nNglvi43Xn4tSIpEhW8aSHea9hgWZV8sG+i/4W9Q==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.8.tgz", + "integrity": "sha512-62Sr7/j/dlhZorxH4GzQgpJy0s162BVts0Q7knZuEacP4VL+IWOUE1NS9OFkh/cbomoyXBdoewkZ5Zd1dVX78w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.7", - "@angular-devkit/schematics": "18.2.7", + "@angular-devkit/core": "18.2.8", + "@angular-devkit/schematics": "18.2.8", "jsonc-parser": "3.3.1" }, "engines": { @@ -6264,73 +6587,73 @@ } }, "node_modules/@sentry-internal/browser-utils": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.33.1.tgz", - "integrity": "sha512-TW6/r+Gl5jiXv54iK1xZ3mlVgTS/jaBp4vcQ0xGMdgiQ3WchEPcFSeYovL+YHT3tSud0GZqVtDQCz+5i76puqA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.34.0.tgz", + "integrity": "sha512-4AcYOzPzD1tL5eSRQ/GpKv5enquZf4dMVUez99/Bh3va8qiJrNP55AcM7UzZ7WZLTqKygIYruJTU5Zu2SpEAPQ==", "license": "MIT", "dependencies": { - "@sentry/core": "8.33.1", - "@sentry/types": "8.33.1", - "@sentry/utils": "8.33.1" + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.33.1.tgz", - "integrity": "sha512-qauMRTm3qDaLqZ3ibI03cj4gLF40y0ij65nj+cns6iWxGCtPrO8tjvXFWuQsE7Aye9dGMnBgmv7uN+NTUtC3RA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.34.0.tgz", + "integrity": "sha512-aYSM2KPUs0FLPxxbJCFSwCYG70VMzlT04xepD1Y/tTlPPOja/02tSv2tyOdZbv8Uw7xslZs3/8Lhj74oYcTBxw==", "license": "MIT", "dependencies": { - "@sentry/core": "8.33.1", - "@sentry/types": "8.33.1", - "@sentry/utils": "8.33.1" + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.33.1.tgz", - "integrity": "sha512-fm4coIOjmanU29NOVN9MyaP4fUCOYytbtFqVSKRFNZQ/xAgNeySiBIbUd6IjujMmnOk9bY0WEUMcdm3Uotjdog==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.34.0.tgz", + "integrity": "sha512-EoMh9NYljNewZK1quY23YILgtNdGgrkzJ9TPsj6jXUG0LZ0Q7N7eFWd0xOEDBvFxrmI3cSXF1i4d1sBb+eyKRw==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.33.1", - "@sentry/core": "8.33.1", - "@sentry/types": "8.33.1", - "@sentry/utils": "8.33.1" + "@sentry-internal/browser-utils": "8.34.0", + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.33.1.tgz", - "integrity": "sha512-nsxTFTPCT10Ty/v6+AiST3+yotGP1sUb8xqfKB9fPnS1hZHFryp0NnEls7xFjBsBbZPU1GpFkzrk/E6JFzixDQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.34.0.tgz", + "integrity": "sha512-x8KhZcCDpbKHqFOykYXiamX6x0LRxv6N1OJHoH+XCrMtiDBZr4Yo30d/MaS6rjmKGMtSRij30v+Uq+YWIgxUrg==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "8.33.1", - "@sentry/core": "8.33.1", - "@sentry/types": "8.33.1", - "@sentry/utils": "8.33.1" + "@sentry-internal/replay": "8.34.0", + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/angular": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.33.1.tgz", - "integrity": "sha512-jt4oViLMl/eqOALQmD0dPzXsy75Xp8amfRExgXoPdyDg6sLDNdEzpzrX2p7nGl7vsW/0Vm8NZ2TkbEBCll5wfQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.34.0.tgz", + "integrity": "sha512-FjBN5s+SFzTFHQh5DqWUGUp19p3V7p86I7Dq1a7MBCzmQukGM1bcW8+n6wLj6CxlEoyLCPPZpTIXIO4ulheIwg==", "license": "MIT", "dependencies": { - "@sentry/browser": "8.33.1", - "@sentry/core": "8.33.1", - "@sentry/types": "8.33.1", - "@sentry/utils": "8.33.1", + "@sentry/browser": "8.34.0", + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0", "tslib": "^2.4.1" }, "engines": { @@ -6344,52 +6667,52 @@ } }, "node_modules/@sentry/browser": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.33.1.tgz", - "integrity": "sha512-c6zI/igexkLwZuGk+u8Rj26ChjxGgkhe6ZbKFsXCYaKAp5ep5X7HQRkkqgbxApiqlC0LduHdd/ymzh139JLg8w==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.34.0.tgz", + "integrity": "sha512-3HHG2NXxzHq1lVmDy2uRjYjGNf9NsJsTPlOC70vbQdOb+S49EdH/XMPy+J3ruIoyv6Cu0LwvA6bMOM6rHZOgNQ==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.33.1", - "@sentry-internal/feedback": "8.33.1", - "@sentry-internal/replay": "8.33.1", - "@sentry-internal/replay-canvas": "8.33.1", - "@sentry/core": "8.33.1", - "@sentry/types": "8.33.1", - "@sentry/utils": "8.33.1" + "@sentry-internal/browser-utils": "8.34.0", + "@sentry-internal/feedback": "8.34.0", + "@sentry-internal/replay": "8.34.0", + "@sentry-internal/replay-canvas": "8.34.0", + "@sentry/core": "8.34.0", + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/core": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.33.1.tgz", - "integrity": "sha512-3SS41suXLFzxL3OQvTMZ6q92ZapELVq2l2SoWlZopcamWhog2Ru0dp2vkunq97kFHb2TzKRTlFH4+4gbT8SJug==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.34.0.tgz", + "integrity": "sha512-adrXCTK/zsg5pJ67lgtZqdqHvyx6etMjQW3P82NgWdj83c8fb+zH+K79Z47pD4zQjX0ou2Ws5nwwi4wJbz4bfA==", "license": "MIT", "dependencies": { - "@sentry/types": "8.33.1", - "@sentry/utils": "8.33.1" + "@sentry/types": "8.34.0", + "@sentry/utils": "8.34.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/types": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.33.1.tgz", - "integrity": "sha512-GjoAMvwtpIemoF/IiwZ7A60g4nQv3qwzR21GvJqDVUoKD0e8pv9OLX+HyXoUat4wEDGSuDUcUyUKD2G+od73QA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.34.0.tgz", + "integrity": "sha512-zLRc60CzohGCo6zNsNeQ9JF3SiEeRE4aDCP9fDDdIVCOKovS+mn1rtSip0qd0Vp2fidOu0+2yY0ALCz1A3PJSQ==", "license": "MIT", "engines": { "node": ">=14.18" } }, "node_modules/@sentry/utils": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.33.1.tgz", - "integrity": "sha512-uzuYpiiJuFY3N4WNHMBWUQX5oNv2t/TbG0OHRp3Rr7yeu+HSfD542TIp9/gMZ+G0Cxd8AmVO3wkKIFbk0TL4Qg==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.34.0.tgz", + "integrity": "sha512-W1KoRlFUjprlh3t86DZPFxLfM6mzjRzshVfMY7vRlJFymBelJsnJ3A1lPeBZM9nCraOSiw6GtOWu6k5BAkiGIg==", "license": "MIT", "dependencies": { - "@sentry/types": "8.33.1" + "@sentry/types": "8.34.0" }, "engines": { "node": ">=14.18" @@ -7037,9 +7360,9 @@ } }, "node_modules/@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7243,17 +7566,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", - "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", + "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/type-utils": "8.8.0", - "@typescript-eslint/utils": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/type-utils": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -7277,16 +7600,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", - "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", + "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "debug": "^4.3.4" }, "engines": { @@ -7306,14 +7629,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", - "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0" + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7324,14 +7647,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", - "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", + "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.8.0", - "@typescript-eslint/utils": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/utils": "8.8.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -7349,9 +7672,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", - "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", "dev": true, "license": "MIT", "engines": { @@ -7363,14 +7686,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", - "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -7392,16 +7715,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", - "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0" + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7415,13 +7738,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", - "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/types": "8.8.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -7929,6 +8252,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -7942,6 +8266,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -8441,6 +8766,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8785,9 +9111,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001666", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz", - "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==", + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", "funding": [ { "type": "opencollective", @@ -8855,6 +9181,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -8875,6 +9202,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -9114,9 +9454,9 @@ } }, "node_modules/code-block-writer": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.2.tgz", - "integrity": "sha512-XfXzAGiStXSmCIwrkdfvc7FS5Dtj8yelCtyOf2p2skCAfvLd6zu0rGzuS9NSCO3bq1JKpFZ7tbKdKlcd5occQA==", + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "dev": true, "license": "MIT" }, @@ -9366,19 +9706,6 @@ "webpack": "^5.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/core-js": { "version": "3.38.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz", @@ -10497,9 +10824,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.32", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz", - "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", "license": "ISC" }, "node_modules/emittery": { @@ -11372,19 +11699,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/eslint/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -11737,6 +12051,18 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-patch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", @@ -12018,9 +12344,9 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "dev": true, "license": "MIT", "dependencies": { @@ -12151,6 +12477,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -12346,15 +12673,16 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/glob-to-regexp": { @@ -13107,6 +13435,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -16558,9 +16887,9 @@ } }, "node_modules/memfs": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.12.0.tgz", - "integrity": "sha512-74wDsex5tQDSClVkeK1vtxqYCAgCoXxx+K4NSHzgU/muYVYByFqa+0RnrPO9NM6naWm1+G9JmZ0p6QHhXmeYfA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.13.0.tgz", + "integrity": "sha512-dIs5KGy24fbdDhIAg0RxXpFqQp3RwL6wgSMRF9OSuphL/Uc9a4u2/SDJKPLj/zUgtOGKuHrRMrj563+IErj4Cg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -16960,7 +17289,8 @@ "node_modules/monaco-editor": { "version": "0.52.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", - "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==" + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "license": "MIT" }, "node_modules/moo-color": { "version": "1.0.3", @@ -17063,9 +17393,9 @@ } }, "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "license": "MIT", "optional": true }, @@ -17455,6 +17785,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17619,15 +17950,15 @@ "license": "MIT" }, "node_modules/nx": { - "version": "19.8.3", - "resolved": "https://registry.npmjs.org/nx/-/nx-19.8.3.tgz", - "integrity": "sha512-/3FF4tgwPGRu4bV6O+aHqhTnOGHKF0/HNVkApUwjimSC+YzOX9VH1uBx2eReb4XC1scxDWkIzVi9gkFSXSQDjQ==", + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/nx/-/nx-19.8.4.tgz", + "integrity": "sha512-fc833c3UKo6kuoG4z0kSKet17yWym3VzcQ+yPWYspxxxd8GFVVk42+9wieyVQDi9YqtKZQ6PdQfSEPm59/M7SA==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", - "@nrwl/tao": "19.8.3", + "@nrwl/tao": "19.8.4", "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "3.0.0-rc.46", "@zkochan/js-yaml": "0.0.7", @@ -17666,16 +17997,16 @@ "nx-cloud": "bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "19.8.3", - "@nx/nx-darwin-x64": "19.8.3", - "@nx/nx-freebsd-x64": "19.8.3", - "@nx/nx-linux-arm-gnueabihf": "19.8.3", - "@nx/nx-linux-arm64-gnu": "19.8.3", - "@nx/nx-linux-arm64-musl": "19.8.3", - "@nx/nx-linux-x64-gnu": "19.8.3", - "@nx/nx-linux-x64-musl": "19.8.3", - "@nx/nx-win32-arm64-msvc": "19.8.3", - "@nx/nx-win32-x64-msvc": "19.8.3" + "@nx/nx-darwin-arm64": "19.8.4", + "@nx/nx-darwin-x64": "19.8.4", + "@nx/nx-freebsd-x64": "19.8.4", + "@nx/nx-linux-arm-gnueabihf": "19.8.4", + "@nx/nx-linux-arm64-gnu": "19.8.4", + "@nx/nx-linux-arm64-musl": "19.8.4", + "@nx/nx-linux-x64-gnu": "19.8.4", + "@nx/nx-linux-x64-musl": "19.8.4", + "@nx/nx-win32-arm64-msvc": "19.8.4", + "@nx/nx-win32-x64-msvc": "19.8.4" }, "peerDependencies": { "@swc-node/register": "^1.8.0", @@ -18554,9 +18885,9 @@ } }, "node_modules/pdfjs-dist": { - "version": "4.6.82", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.6.82.tgz", - "integrity": "sha512-BUOryeRFwvbLe0lOU6NhkJNuVQUp06WxlJVVCsxdmJ4y5cU3O3s3/0DunVdK1PMm7v2MUw52qKYaidhDH1Z9+w==", + "version": "4.7.76", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.7.76.tgz", + "integrity": "sha512-8y6wUgC/Em35IumlGjaJOCm3wV4aY/6sqnIT3fVW/67mXsOZ9HWBn8GDKmJUK0GSzpbmX3gQqwfoFayp78Mtqw==", "license": "Apache-2.0", "engines": { "node": ">=18" @@ -18891,9 +19222,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.166.1", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.166.1.tgz", - "integrity": "sha512-K8IpV8FJTCdwhsXFSbKj5vZ6IXNV079lukpG3cRtst2q5vMmUXRQiks7W3lOZLrjWyuJLKZDUiCeeDIUFORRuQ==", + "version": "1.167.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.167.0.tgz", + "integrity": "sha512-/zXQ6tuJgiF1d4mgg3UsAi/uoyg7UnfFNQtikuALmaE53xFExpcAKbMfHPG/f54QgTvLxSHyGL1kFl/1uspkGg==", "license": "MIT", "dependencies": { "fflate": "^0.4.8", @@ -18902,9 +19233,9 @@ } }, "node_modules/preact": { - "version": "10.24.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.1.tgz", - "integrity": "sha512-PnBAwFI3Yjxxcxw75n6VId/5TFxNW/81zexzWD9jn1+eSrOP84NdsS38H5IkF/UH3frqRPT+MvuCoVHjTDTnDw==", + "version": "10.24.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", + "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==", "license": "MIT", "funding": { "type": "opencollective", @@ -19342,6 +19673,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -19354,6 +19686,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -19470,9 +19803,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.0.tgz", - "integrity": "sha512-vTbzVAjQDzwQdKuvj7qEq6OlAprCjE656khuGQ4QaBLg7abQ9I9ISpmLuc6inWe7zP75AECjqUa4g4sdQvOXhg==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", + "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -19894,12 +20227,13 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.79.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.4.tgz", - "integrity": "sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "dev": true, "license": "MIT", "dependencies": { + "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" @@ -21075,9 +21409,9 @@ "license": "MIT" }, "node_modules/synckit": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", - "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", "dev": true, "license": "MIT", "dependencies": { @@ -21429,22 +21763,22 @@ "license": "MIT" }, "node_modules/tldts": { - "version": "6.1.50", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.50.tgz", - "integrity": "sha512-q9GOap6q3KCsLMdOjXhWU5jVZ8/1dIib898JBRLsN+tBhENpBDcAVQbE0epADOjw11FhQQy9AcbqKGBQPUfTQA==", + "version": "6.1.51", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.51.tgz", + "integrity": "sha512-33lfQoL0JsDogIbZ8fgRyvv77GnRtwkNE/MOKocwUgPO1WrSfsq7+vQRKxRQZai5zd+zg97Iv9fpFQSzHyWdLA==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^6.1.50" + "tldts-core": "^6.1.51" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.50", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.50.tgz", - "integrity": "sha512-na2EcZqmdA2iV9zHV7OHQDxxdciEpxrjbkp+aHmZgnZKHzoElLajP59np5/4+sare9fQBfixgvXKx8ev1d7ytw==", + "version": "6.1.51", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.51.tgz", + "integrity": "sha512-bu9oCYYWC1iRjx+3UnAjqCsfrWNZV1ghNQf49b3w5xE8J/tNShHTzp5syWJfwGH+pxUgTTLUnzHnfuydW7wmbg==", "dev": true, "license": "MIT" }, @@ -22872,9 +23206,9 @@ } }, "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b1063f9a90e0..755adfae3d3e 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,18 @@ "node_modules" ], "dependencies": { - "@angular/animations": "18.2.7", - "@angular/cdk": "18.2.7", - "@angular/common": "18.2.7", - "@angular/compiler": "18.2.7", - "@angular/core": "18.2.7", - "@angular/forms": "18.2.7", - "@angular/localize": "18.2.7", - "@angular/material": "18.2.7", - "@angular/platform-browser": "18.2.7", - "@angular/platform-browser-dynamic": "18.2.7", - "@angular/router": "18.2.7", - "@angular/service-worker": "18.2.7", + "@angular/animations": "18.2.8", + "@angular/cdk": "18.2.8", + "@angular/common": "18.2.8", + "@angular/compiler": "18.2.8", + "@angular/core": "18.2.8", + "@angular/forms": "18.2.8", + "@angular/localize": "18.2.8", + "@angular/material": "18.2.8", + "@angular/platform-browser": "18.2.8", + "@angular/platform-browser-dynamic": "18.2.8", + "@angular/router": "18.2.8", + "@angular/service-worker": "18.2.8", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.5.0", @@ -36,7 +36,7 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular": "8.33.1", + "@sentry/angular": "8.34.0", "@siemens/ngx-datatable": "22.4.1", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", @@ -62,8 +62,8 @@ "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", - "pdfjs-dist": "4.6.82", - "posthog-js": "1.166.1", + "pdfjs-dist": "4.7.76", + "posthog-js": "1.167.0", "rxjs": "7.8.1", "showdown": "2.1.0", "showdown-highlight": "3.1.0", @@ -119,29 +119,29 @@ }, "devDependencies": { "@angular-builders/jest": "18.0.0", - "@angular-devkit/build-angular": "18.2.7", + "@angular-devkit/build-angular": "18.2.8", "@angular-eslint/builder": "18.3.1", "@angular-eslint/eslint-plugin": "18.3.1", "@angular-eslint/eslint-plugin-template": "18.3.1", "@angular-eslint/schematics": "18.3.1", "@angular-eslint/template-parser": "18.3.1", - "@angular/cli": "18.2.7", - "@angular/compiler-cli": "18.2.7", - "@angular/language-service": "18.2.7", - "@sentry/types": "8.33.1", + "@angular/cli": "18.2.8", + "@angular/compiler-cli": "18.2.8", + "@angular/language-service": "18.2.8", + "@sentry/types": "8.34.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", "@types/jest": "29.5.13", "@types/lodash-es": "4.17.12", - "@types/node": "22.7.4", + "@types/node": "22.7.5", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.8.0", - "@typescript-eslint/parser": "8.8.0", + "@typescript-eslint/eslint-plugin": "8.8.1", + "@typescript-eslint/parser": "8.8.1", "eslint": "9.12.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", @@ -162,7 +162,7 @@ "ng-mocks": "14.13.1", "prettier": "3.3.3", "rimraf": "6.0.1", - "sass": "1.79.4", + "sass": "1.79.5", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" From 36f12605a68c8061ad503203c523be114035b9cd Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Sat, 12 Oct 2024 12:52:28 +0200 Subject: [PATCH 09/14] Development: Update server dependencies --- build.gradle | 2 +- gradle.properties | 8 ++++---- jest.config.js | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index cdad264bac58..163b2517748f 100644 --- a/build.gradle +++ b/build.gradle @@ -535,7 +535,7 @@ dependencies { testImplementation "org.awaitility:awaitility:4.2.2" testImplementation "org.apache.maven.shared:maven-invoker:3.3.0" testImplementation "org.gradle:gradle-tooling-api:8.10.2" - testImplementation "org.apache.maven.surefire:surefire-report-parser:3.5.0" + testImplementation "org.apache.maven.surefire:surefire-report-parser:3.5.1" testImplementation "com.opencsv:opencsv:5.9" testImplementation("io.zonky.test:embedded-database-spring-test:2.5.1") { exclude group: "org.testcontainers", module: "mariadb" diff --git a/gradle.properties b/gradle.properties index 46526dddaf56..07ee79d07d25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,17 +25,17 @@ jplag_version=5.1.0 # NOTE: we do not need to use the latest version 9.x here as long as Stanford CoreNLP does not reference it lucene_version=8.11.4 slf4j_version=2.0.16 -sentry_version=7.14.0 +sentry_version=7.15.0 liquibase_version=4.29.2 docker_java_version=3.4.0 -logback_version=1.5.8 +logback_version=1.5.10 java_parser_version=3.26.2 -byte_buddy_version=1.15.3 +byte_buddy_version=1.15.4 # testing # make sure both versions are compatible junit_version=5.11.0 -junit_platform_version=1.11.1 +junit_platform_version=1.11.2 mockito_version=5.14.1 diff --git a/jest.config.js b/jest.config.js index 48cac393d596..77fd322375d8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -102,9 +102,9 @@ module.exports = { coverageThreshold: { global: { // TODO: in the future, the following values should increase to at least 90% - statements: 87.43, - branches: 73.72, - functions: 82.05, + statements: 87.44, + branches: 73.74, + functions: 82.10, lines: 87.49, }, }, From 3fd3dc54650282e41d7a187e4f43b03125bb7505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20St=C3=B6hr?= <38322605+JohannesStoehr@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:23:26 +0200 Subject: [PATCH 10/14] Development: Use @EnforceAdmin annotation on class level (#9422) --- .../atlas/web/LearningPathResource.java | 12 +--- .../artemis/atlas/web/ScienceResource.java | 2 +- .../web/StandardizedCompetencyResource.java | 5 +- .../AdminStandardizedCompetencyResource.java | 18 +----- .../AdminSystemNotificationResource.java | 4 +- .../security/annotations/AnnotationUtils.java | 62 ++++++++++++++++--- .../security/annotations/EnforceAdmin.java | 5 +- .../service/feature/FeatureToggleAspect.java | 20 +++--- ...tResource.java => AdminAuditResource.java} | 8 +-- .../web/admin/AdminBuildJobQueueResource.java | 11 +--- .../core/web/admin/AdminCourseResource.java | 4 +- .../web/admin/AdminDataExportResource.java | 2 +- ...e.java => AdminFeatureToggleResource.java} | 6 +- .../core/web/admin/AdminImprintResource.java | 3 +- ...LogResource.java => AdminLogResource.java} | 5 +- .../web/admin/AdminOrganizationResource.java | 15 +---- .../admin/AdminPrivacyStatementResource.java | 3 +- .../web/admin/AdminStatisticsResource.java | 2 +- .../core/web/admin/AdminUserResource.java | 11 +--- .../exam/web/admin/AdminExamResource.java | 2 +- .../web/admin/AdminExerciseResource.java | 2 +- .../web/admin/AdminIrisSettingsResource.java | 2 +- .../admin/AdminLtiConfigurationResource.java | 6 +- .../admin/AdminModelingExerciseResource.java | 4 +- ...ogrammingExerciseExportImportResource.java | 17 +++-- ...ProgrammingExercisePlagiarismResource.java | 5 +- .../AdminTextAssessmentEventResource.java | 2 +- .../web/TutorialGroupFreePeriodResource.java | 5 +- .../web/TutorialGroupResource.java | 15 +---- .../web/TutorialGroupSessionResource.java | 8 +-- .../TutorialGroupsConfigurationResource.java | 4 +- .../AuthorizationArchitectureTest.java | 2 +- ...bstractModuleResourceArchitectureTest.java | 27 ++++++++ 33 files changed, 137 insertions(+), 162 deletions(-) rename src/main/java/de/tum/cit/aet/artemis/core/web/admin/{AuditResource.java => AdminAuditResource.java} (96%) rename src/main/java/de/tum/cit/aet/artemis/core/web/admin/{FeatureToggleResource.java => AdminFeatureToggleResource.java} (91%) rename src/main/java/de/tum/cit/aet/artemis/core/web/admin/{LogResource.java => AdminLogResource.java} (96%) diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java index d940e50acc51..f69dae28f80c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/LearningPathResource.java @@ -59,6 +59,7 @@ import de.tum.cit.aet.artemis.lecture.service.LearningObjectService; @Profile(PROFILE_CORE) +@FeatureToggle(Feature.LearningPaths) @RestController @RequestMapping("api/") public class LearningPathResource { @@ -108,7 +109,6 @@ public LearningPathResource(CourseService courseService, CourseRepository course * @return the ResponseEntity with status 200 (OK) */ @PutMapping("courses/{courseId}/learning-paths/enable") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructorInCourse public ResponseEntity enableLearningPathsForCourse(@PathVariable long courseId) { log.debug("REST request to enable learning paths for course with id: {}", courseId); @@ -129,7 +129,6 @@ public ResponseEntity enableLearningPathsForCourse(@PathVariable long cour * @return the ResponseEntity with status 200 (OK) */ @PutMapping("courses/{courseId}/learning-paths/generate-missing") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructorInCourse public ResponseEntity generateMissingLearningPathsForCourse(@PathVariable long courseId) { log.debug("REST request to generate missing learning paths for course with id: {}", courseId); @@ -147,7 +146,6 @@ public ResponseEntity generateMissingLearningPathsForCourse(@PathVariable * @return the ResponseEntity with status 200 (OK) and with body the desired page, sorted and matching the given query */ @GetMapping("courses/{courseId}/learning-paths") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructorInCourse public ResponseEntity> getLearningPathsOnPage(@PathVariable long courseId, SearchTermPageableSearchDTO search) { log.debug("REST request to get learning paths for course with id: {}", courseId); @@ -162,7 +160,6 @@ public ResponseEntity> getLearni * @return the ResponseEntity with status 200 (OK) and with body the health status */ @GetMapping("courses/{courseId}/learning-path-health") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastInstructorInCourse public ResponseEntity getHealthStatusForCourse(@PathVariable long courseId) { log.debug("REST request to get health status of learning paths in course with id: {}", courseId); @@ -177,7 +174,6 @@ public ResponseEntity getHealthStatusForCourse(@PathVaria * @return the ResponseEntity with status 200 (OK) and with body the learning path */ @GetMapping("learning-path/{learningPathId}") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPath(@PathVariable long learningPathId) { log.debug("REST request to get learning path with id: {}", learningPathId); @@ -196,7 +192,6 @@ public ResponseEntity getLearningPath(@PathVariable * @return the ResponseEntity with status 200 (OK) and with body the graph */ @GetMapping("learning-path/{learningPathId}/competency-graph") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathCompetencyGraph(@PathVariable long learningPathId) { log.debug("REST request to get competency graph for learning path with id: {}", learningPathId); @@ -215,7 +210,6 @@ public ResponseEntity getLearningPathCompetencyG * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path */ @GetMapping("learning-path/{learningPathId}/graph") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathNgxGraph(@PathVariable long learningPathId) { log.debug("REST request to get ngx graph representation of learning path with id: {}", learningPathId); @@ -229,7 +223,6 @@ public ResponseEntity getLearningPathNgxGraph(@PathVariable * @return the ResponseEntity with status 200 (OK) and with body the ngx representation of the learning path */ @GetMapping("learning-path/{learningPathId}/path") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathNgxPath(@PathVariable long learningPathId) { log.debug("REST request to get ngx path representation of learning path with id: {}", learningPathId); @@ -246,7 +239,6 @@ public ResponseEntity getLearningPathNgxPath(@PathVariable l * @return the ResponseEntity with status 200 (OK) and with body the navigation information */ @GetMapping("learning-path/{learningPathId}/relative-navigation") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getRelativeLearningPathNavigation(@PathVariable @Valid long learningPathId, @RequestParam long learningObjectId, @RequestParam LearningObjectType learningObjectType, @RequestParam long competencyId) { @@ -265,7 +257,6 @@ public ResponseEntity getRelativeLearningPathNavigati * @return the ResponseEntity with status 200 (OK) and with body the navigation information */ @GetMapping("learning-path/{learningPathId}/navigation") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathNavigation(@PathVariable long learningPathId) { log.debug("REST request to get navigation for learning path with id: {}", learningPathId); @@ -281,7 +272,6 @@ public ResponseEntity getLearningPathNavigation(@Path * @return the ResponseEntity with status 200 (OK) and with body the navigation overview */ @GetMapping("learning-path/{learningPathId}/navigation-overview") - @FeatureToggle(Feature.LearningPaths) @EnforceAtLeastStudent public ResponseEntity getLearningPathNavigationOverview(@PathVariable @Valid long learningPathId) { log.debug("REST request to get navigation overview for learning path with id: {}", learningPathId); diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/ScienceResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/ScienceResource.java index 7312cc824997..d4e1563df41c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/ScienceResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/ScienceResource.java @@ -21,6 +21,7 @@ * REST controller providing the science related endpoints. */ @Profile(PROFILE_CORE) +@FeatureToggle(Feature.Science) @RestController @RequestMapping("api/") public class ScienceResource { @@ -40,7 +41,6 @@ public ScienceResource(ScienceEventService scienceEventService) { * @return the ResponseEntity with status 200 (OK) */ @PutMapping(value = "science") - @FeatureToggle(Feature.Science) @EnforceAtLeastStudent public ResponseEntity science(@RequestBody ScienceEventDTO event) { log.debug("REST request to log science event of type {}", event); diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/StandardizedCompetencyResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/StandardizedCompetencyResource.java index 8b6316fcc51c..b205db140bc3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/StandardizedCompetencyResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/StandardizedCompetencyResource.java @@ -29,6 +29,7 @@ * REST controller for managing {@link StandardizedCompetency} entities. */ @Profile(PROFILE_CORE) +@FeatureToggle(Feature.StandardizedCompetencies) @RestController @RequestMapping("api/standardized-competencies/") public class StandardizedCompetencyResource { @@ -58,7 +59,6 @@ public StandardizedCompetencyResource(StandardizedCompetencyService standardized * @return the ResponseEntity with status 200 (OK) and with body containing the standardized competency, or with status 404 (Not Found) */ @GetMapping("{competencyId}") - @FeatureToggle(Feature.StandardizedCompetencies) @EnforceAtLeastInstructor public ResponseEntity getStandardizedCompetency(@PathVariable long competencyId) { log.debug("REST request to get standardized competency with id : {}", competencyId); @@ -74,7 +74,6 @@ public ResponseEntity getStandardizedCompetency(@PathVar * @return the ResponseEntity with status 200 (OK) and with body containing the knowledge areas */ @GetMapping("for-tree-view") - @FeatureToggle(Feature.StandardizedCompetencies) @EnforceAtLeastInstructor public ResponseEntity> getAllForTreeView() { log.debug("REST request to all knowledge areas for tree view"); @@ -91,7 +90,6 @@ public ResponseEntity> getAllForTreeView() { * @return the ResponseEntity with status 200 (OK) and with body containing the knowledge area, or with status 404 (Not Found) */ @GetMapping("knowledge-areas/{knowledgeAreaId}") - @FeatureToggle(Feature.StandardizedCompetencies) @EnforceAtLeastInstructor public ResponseEntity getKnowledgeArea(@PathVariable long knowledgeAreaId) { log.debug("REST request to get knowledge area with id : {}", knowledgeAreaId); @@ -107,7 +105,6 @@ public ResponseEntity getKnowledgeArea(@PathVariable long knowled * @return the ResponseEntity with status 200 (OK) and with body containing the list of sources */ @GetMapping("sources") - @FeatureToggle(Feature.StandardizedCompetencies) @EnforceAtLeastInstructor public ResponseEntity> getSources() { log.debug("REST request to get all sources"); diff --git a/src/main/java/de/tum/cit/aet/artemis/atlas/web/admin/AdminStandardizedCompetencyResource.java b/src/main/java/de/tum/cit/aet/artemis/atlas/web/admin/AdminStandardizedCompetencyResource.java index 376cedb132dc..5c974101f8bb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/atlas/web/admin/AdminStandardizedCompetencyResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/atlas/web/admin/AdminStandardizedCompetencyResource.java @@ -38,6 +38,8 @@ * Admin REST controller for managing {@link StandardizedCompetency} entities. */ @Profile(PROFILE_CORE) +@FeatureToggle(Feature.StandardizedCompetencies) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminStandardizedCompetencyResource { @@ -61,8 +63,6 @@ public AdminStandardizedCompetencyResource(StandardizedCompetencyService standar * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("standardized-competencies") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity createStandardizedCompetency(@RequestBody @Valid StandardizedCompetencyRequestDTO competency) throws URISyntaxException { log.debug("REST request to create standardized competency : {}", competency); @@ -79,8 +79,6 @@ public ResponseEntity createStandardizedCompete * @return the ResponseEntity with status 200 (OK) and with body the updated standardized competency */ @PutMapping("standardized-competencies/{competencyId}") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity updateStandardizedCompetency(@PathVariable long competencyId, @RequestBody @Valid StandardizedCompetencyRequestDTO competency) { log.debug("REST request to update standardized competency : {}", competency); @@ -97,8 +95,6 @@ public ResponseEntity updateStandardizedCompete * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("standardized-competencies/{competencyId}") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity deleteStandardizedCompetency(@PathVariable long competencyId) { log.debug("REST request to delete standardized competency : {}", competencyId); @@ -115,8 +111,6 @@ public ResponseEntity deleteStandardizedCompetency(@PathVariable long comp * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("standardized-competencies/knowledge-areas") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity createKnowledgeArea(@RequestBody @Valid KnowledgeAreaRequestDTO knowledgeArea) throws URISyntaxException { log.debug("REST request to create knowledge area : {}", knowledgeArea); @@ -134,8 +128,6 @@ public ResponseEntity createKnowledgeArea(@RequestBody @ * @return the ResponseEntity with status 200 (OK) and with body the updated knowledge area */ @PutMapping("standardized-competencies/knowledge-areas/{knowledgeAreaId}") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity updateKnowledgeArea(@PathVariable long knowledgeAreaId, @RequestBody @Valid KnowledgeAreaRequestDTO knowledgeArea) { log.debug("REST request to update knowledge area : {}", knowledgeArea); @@ -151,8 +143,6 @@ public ResponseEntity updateKnowledgeArea(@PathVariable * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("standardized-competencies/knowledge-areas/{knowledgeAreaId}") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity deleteKnowledgeArea(@PathVariable long knowledgeAreaId) { log.debug("REST request to delete knowledge area : {}", knowledgeAreaId); @@ -168,8 +158,6 @@ public ResponseEntity deleteKnowledgeArea(@PathVariable long knowledgeArea * @return the ResponseEntity with status 200 (OK) */ @PutMapping("standardized-competencies/import") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity importStandardizedCompetencyCatalog(@RequestBody @Valid StandardizedCompetencyCatalogDTO standardizedCompetencyCatalogDTO) { log.debug("REST request to import standardized competency catalog"); @@ -184,8 +172,6 @@ public ResponseEntity importStandardizedCompetencyCatalog(@RequestBody @Va * @return the ResponseEntity with status 200 (OK) and the body containing the JSON string of the standardized competency catalog */ @GetMapping("standardized-competencies/export") - @FeatureToggle(Feature.StandardizedCompetencies) - @EnforceAdmin public ResponseEntity exportStandardizedCompetencyCatalog() { log.debug("REST request to export standardized competency catalog"); diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/web/admin/AdminSystemNotificationResource.java b/src/main/java/de/tum/cit/aet/artemis/communication/web/admin/AdminSystemNotificationResource.java index 6850598633e9..4e94766284d6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/web/admin/AdminSystemNotificationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/web/admin/AdminSystemNotificationResource.java @@ -30,6 +30,7 @@ * REST controller for administrating system notifications. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminSystemNotificationResource { @@ -58,7 +59,6 @@ public AdminSystemNotificationResource(SystemNotificationRepository systemNotifi * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("system-notifications") - @EnforceAdmin public ResponseEntity createSystemNotification(@RequestBody SystemNotification systemNotification) throws URISyntaxException { log.debug("REST request to save SystemNotification : {}", systemNotification); if (systemNotification.getId() != null) { @@ -79,7 +79,6 @@ public ResponseEntity createSystemNotification(@RequestBody System * status 500 (Internal Server Error) if the system notification couldn't be updated */ @PutMapping("system-notifications") - @EnforceAdmin public ResponseEntity updateSystemNotification(@RequestBody SystemNotification systemNotification) { log.debug("REST request to update SystemNotification : {}", systemNotification); if (systemNotification.getId() == null) { @@ -101,7 +100,6 @@ public ResponseEntity updateSystemNotification(@RequestBody * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("system-notifications/{notificationId}") - @EnforceAdmin public ResponseEntity deleteSystemNotification(@PathVariable Long notificationId) { log.debug("REST request to delete SystemNotification : {}", notificationId); systemNotificationRepository.deleteById(notificationId); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/AnnotationUtils.java b/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/AnnotationUtils.java index c5aef335defb..028342bfeee9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/AnnotationUtils.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/AnnotationUtils.java @@ -3,7 +3,9 @@ import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Optional; import jakarta.validation.constraints.NotBlank; @@ -22,7 +24,8 @@ private AnnotationUtils() { } /** - * Extracts the annotation from the method or type + * Extracts the annotation from the method or type and all super classes. + * In case multiple versions of the annotation are present, the one closest to the method is returned. * * @param clazz the annotation class * @param joinPoint the join point @@ -33,26 +36,71 @@ private AnnotationUtils() { public static Optional getAnnotation(@NotNull Class clazz, @NotNull ProceedingJoinPoint joinPoint) { final var method = ((MethodSignature) joinPoint.getSignature()).getMethod(); T annotation = method.getAnnotation(clazz); + + Optional foundAnnotation = getAnnotation(clazz, method.getDeclaredAnnotations(), annotation); + if (foundAnnotation.isPresent()) { + return foundAnnotation; + } + + for (Class declaringClass = method.getDeclaringClass(); declaringClass != null; declaringClass = declaringClass.getSuperclass()) { + annotation = declaringClass.getAnnotation(clazz); + foundAnnotation = getAnnotation(clazz, declaringClass.getDeclaredAnnotations(), annotation); + if (foundAnnotation.isPresent()) { + return foundAnnotation; + } + } + + return Optional.empty(); + } + + private static Optional getAnnotation(Class clazz, Annotation[] declaredAnnotations, T annotation) { if (annotation != null) { return Optional.of(annotation); } - for (Annotation a : method.getDeclaredAnnotations()) { + for (Annotation a : declaredAnnotations) { annotation = a.annotationType().getAnnotation(clazz); if (annotation != null) { return Optional.of(annotation); } } - annotation = method.getDeclaringClass().getAnnotation(clazz); + return Optional.empty(); + } + + /** + * Extracts the annotations from the method or type and all super classes. + * In case multiple versions of the annotation are present, all are returned. + * + * @param clazz the annotation class + * @param joinPoint the join point + * @param the type of the annotation + * @return the annotations if they are present, empty otherwise + */ + public static List getAnnotations(@NotNull Class clazz, @NotNull ProceedingJoinPoint joinPoint) { + List annotations = new ArrayList<>(); + + final var method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + T annotation = method.getAnnotation(clazz); + + addAnnotations(clazz, method.getDeclaredAnnotations(), annotation, annotations); + + for (Class declaringClass = method.getDeclaringClass(); declaringClass != null; declaringClass = declaringClass.getSuperclass()) { + annotation = declaringClass.getAnnotation(clazz); + addAnnotations(clazz, declaringClass.getDeclaredAnnotations(), annotation, annotations); + } + + return annotations; + } + + private static void addAnnotations(Class clazz, Annotation[] declaredAnnotations, T annotation, List annotations) { if (annotation != null) { - return Optional.of(annotation); + annotations.add(annotation); } - for (Annotation a : method.getDeclaringClass().getDeclaredAnnotations()) { + for (Annotation a : declaredAnnotations) { annotation = a.annotationType().getAnnotation(clazz); if (annotation != null) { - return Optional.of(annotation); + annotations.add(annotation); } } - return Optional.empty(); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/EnforceAdmin.java b/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/EnforceAdmin.java index 5adbcd73c16c..9fdbf88d9d82 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/EnforceAdmin.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/annotations/EnforceAdmin.java @@ -10,11 +10,8 @@ /** * This annotation is used to enforce that the user is an admin. * It should only be used with endpoints starting with {@code /api/admin/} - *

- * It's only addable to methods. The intention is that a developer can see the required role without the need to scroll up. - * This also prevents overrides of the annotation. */ -@Target(ElementType.METHOD) +@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasRole('ADMIN')") public @interface EnforceAdmin { diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleAspect.java b/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleAspect.java index 87785e123cec..ffeed9cfa513 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleAspect.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/feature/FeatureToggleAspect.java @@ -1,8 +1,11 @@ package de.tum.cit.aet.artemis.core.service.feature; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import static de.tum.cit.aet.artemis.core.security.annotations.AnnotationUtils.getAnnotations; import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @@ -26,25 +29,24 @@ public FeatureToggleAspect(FeatureToggleService featureToggleService) { /** * Pointcut around all methods or classes annotated with {@link FeatureToggle}. - * - * @param featureToggle The feature toggle annotation containing the relevant features */ - @Pointcut("@within(featureToggle) || @annotation(featureToggle)") - public void callAt(FeatureToggle featureToggle) { + @Pointcut("@within(de.tum.cit.aet.artemis.core.service.feature.FeatureToggle) || @annotation(de.tum.cit.aet.artemis.core.service.feature.FeatureToggle) || execution(@(@de.tum.cit.aet.artemis.core.service.feature.FeatureToggle *) * *(..))") + protected void callAt() { } /** * Aspect around all methods for which a feature toggle has been activated. Will check all specified features and only * execute the underlying method if all features are enabled. Will otherwise return forbidden (as response entity) * - * @param joinPoint Proceeding join point of the aspect - * @param featureToggle The feature toggle annotation containing all features that should get checked + * @param joinPoint Proceeding join point of the aspect * @return The original return value of the called method, if all features are enabled, a forbidden response entity otherwise * @throws Throwable If there was any error during method execution (both the aspect or the actual called method) */ - @Around(value = "callAt(featureToggle)", argNames = "joinPoint,featureToggle") - public Object around(ProceedingJoinPoint joinPoint, FeatureToggle featureToggle) throws Throwable { - if (Arrays.stream(featureToggle.value()).allMatch(featureToggleService::isFeatureEnabled)) { + @Around(value = "callAt()", argNames = "joinPoint") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + List featureToggleAnnotations = getAnnotations(FeatureToggle.class, joinPoint); + Stream features = featureToggleAnnotations.stream().flatMap(featureToggle -> Arrays.stream(featureToggle.value())); + if (features.allMatch(featureToggleService::isFeatureEnabled)) { return joinPoint.proceed(); } else { diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AuditResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminAuditResource.java similarity index 96% rename from src/main/java/de/tum/cit/aet/artemis/core/web/admin/AuditResource.java rename to src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminAuditResource.java index fe8af9ff7eb0..7c0a339d9717 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AuditResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminAuditResource.java @@ -30,13 +30,14 @@ * REST controller for getting the audit events. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") -public class AuditResource { +public class AdminAuditResource { private final AuditEventService auditEventService; - public AuditResource(AuditEventService auditEventService) { + public AdminAuditResource(AuditEventService auditEventService) { this.auditEventService = auditEventService; } @@ -47,7 +48,6 @@ public AuditResource(AuditEventService auditEventService) { * @return the ResponseEntity with status 200 (OK) and the list of AuditEvents in body */ @GetMapping("audits") - @EnforceAdmin public ResponseEntity> getAll(Pageable pageable) { Page page = auditEventService.findAll(pageable); HttpHeaders headers = generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); @@ -63,7 +63,6 @@ public ResponseEntity> getAll(Pageable pageable) { * @return the ResponseEntity with status 200 (OK) and the list of AuditEvents in body */ @GetMapping(value = "audits", params = { "fromDate", "toDate" }) - @EnforceAdmin public ResponseEntity> getByDates(@RequestParam(value = "fromDate") LocalDate fromDate, @RequestParam(value = "toDate") LocalDate toDate, Pageable pageable) { Instant from = fromDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); @@ -81,7 +80,6 @@ public ResponseEntity> getByDates(@RequestParam(value = "fromDa * @return the ResponseEntity with status 200 (OK) and the AuditEvent in body, or status 404 (Not Found) */ @GetMapping("audits/{id:.+}") - @EnforceAdmin public ResponseEntity get(@PathVariable Long id) { return ResponseUtil.wrapOrNotFound(auditEventService.find(id)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java index bd1bcc4dea1b..db71ab34c05a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminBuildJobQueueResource.java @@ -33,6 +33,7 @@ import tech.jhipster.web.util.PaginationUtil; @Profile(PROFILE_LOCALCI) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminBuildJobQueueResource { @@ -54,7 +55,6 @@ public AdminBuildJobQueueResource(SharedQueueManagementService localCIBuildJobQu * @return the queued build jobs */ @GetMapping("queued-jobs") - @EnforceAdmin public ResponseEntity> getQueuedBuildJobs() { log.debug("REST request to get the queued build jobs"); List buildJobQueue = localCIBuildJobQueueService.getQueuedJobs(); @@ -67,7 +67,6 @@ public ResponseEntity> getQueuedBuildJobs() { * @return the running build jobs */ @GetMapping("running-jobs") - @EnforceAdmin public ResponseEntity> getRunningBuildJobs() { log.debug("REST request to get the running build jobs"); List runningBuildJobs = localCIBuildJobQueueService.getProcessingJobs(); @@ -80,7 +79,6 @@ public ResponseEntity> getRunningBuildJobs() { * @return list of build agents information */ @GetMapping("build-agents") - @EnforceAdmin public ResponseEntity> getBuildAgentSummary() { log.debug("REST request to get information on available build agents"); List buildAgentSummary = localCIBuildJobQueueService.getBuildAgentInformationWithoutRecentBuildJobs(); @@ -94,7 +92,6 @@ public ResponseEntity> getBuildAgentSummary() { * @return the build agent information */ @GetMapping("build-agent") - @EnforceAdmin public ResponseEntity getBuildAgentDetails(@RequestParam String agentName) { log.debug("REST request to get information on build agent {}", agentName); BuildAgentInformation buildAgentDetails = localCIBuildJobQueueService.getBuildAgentInformation().stream().filter(agent -> agent.name().equals(agentName)).findFirst() @@ -109,7 +106,6 @@ public ResponseEntity getBuildAgentDetails(@RequestParam * @return the ResponseEntity with the result of the cancellation */ @DeleteMapping("cancel-job/{buildJobId}") - @EnforceAdmin public ResponseEntity cancelBuildJob(@PathVariable String buildJobId) { log.debug("REST request to cancel the build job with id {}", buildJobId); // Call the cancelBuildJob method in LocalCIBuildJobManagementService @@ -124,7 +120,6 @@ public ResponseEntity cancelBuildJob(@PathVariable String buildJobId) { * @return the ResponseEntity with the result of the cancellation */ @DeleteMapping("cancel-all-queued-jobs") - @EnforceAdmin public ResponseEntity cancelAllQueuedBuildJobs() { log.debug("REST request to cancel all queued build jobs"); // Call the cancelAllQueuedBuildJobs method in LocalCIBuildJobManagementService @@ -139,7 +134,6 @@ public ResponseEntity cancelAllQueuedBuildJobs() { * @return the ResponseEntity with the result of the cancellation */ @DeleteMapping("cancel-all-running-jobs") - @EnforceAdmin public ResponseEntity cancelAllRunningBuildJobs() { log.debug("REST request to cancel all running build jobs"); // Call the cancelAllRunningBuildJobs method in LocalCIBuildJobManagementService @@ -155,7 +149,6 @@ public ResponseEntity cancelAllRunningBuildJobs() { * @return the ResponseEntity with the result of the cancellation */ @DeleteMapping("cancel-all-running-jobs-for-agent") - @EnforceAdmin public ResponseEntity cancelAllRunningBuildJobsForAgent(@RequestParam String agentName) { log.debug("REST request to cancel all running build jobs for agent {}", agentName); // Call the cancelAllRunningBuildJobsForAgent method in LocalCIBuildJobManagementService @@ -171,7 +164,6 @@ public ResponseEntity cancelAllRunningBuildJobsForAgent(@RequestParam Stri * @return the page of finished build jobs */ @GetMapping("finished-jobs") - @EnforceAdmin public ResponseEntity> getFinishedBuildJobs(FinishedBuildJobPageableSearchDTO search) { log.debug("REST request to get a page of finished build jobs with build status {}, build agent address {}, start date {} and end date {}", search.buildStatus(), search.buildAgentAddress(), search.startDate(), search.endDate()); @@ -190,7 +182,6 @@ public ResponseEntity> getFinishedBuildJobs(FinishedBu * @return the build job statistics */ @GetMapping("build-job-statistics") - @EnforceAdmin public ResponseEntity getBuildJobStatistics(@RequestParam(required = false, defaultValue = "7") int span) { log.debug("REST request to get the build job statistics"); List buildJobResultCountDtos = buildJobRepository.getBuildJobsResultsStatistics(ZonedDateTime.now().minusDays(span), null); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminCourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminCourseResource.java index a75b2e2d4a0a..9cdcc9486f52 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminCourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminCourseResource.java @@ -48,6 +48,7 @@ * REST controller for managing Course. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminCourseResource { @@ -90,7 +91,6 @@ public AdminCourseResource(UserRepository userRepository, CourseService courseSe * @return the list of groups (the user has access to) */ @GetMapping("courses/groups") - @EnforceAdmin public ResponseEntity> getAllGroupsForAllCourses() { log.debug("REST request to get all Groups for all Courses"); List courses = courseRepository.findAll(); @@ -113,7 +113,6 @@ public ResponseEntity> getAllGroupsForAllCourses() { * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping(value = "courses", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - @EnforceAdmin public ResponseEntity createCourse(@RequestPart Course course, @RequestPart(required = false) MultipartFile file) throws URISyntaxException { log.debug("REST request to save Course : {}", course); if (course.getId() != null) { @@ -167,7 +166,6 @@ public ResponseEntity createCourse(@RequestPart Course course, @RequestP * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("courses/{courseId}") - @EnforceAdmin public ResponseEntity deleteCourse(@PathVariable long courseId) { log.info("REST request to delete Course : {}", courseId); Course course = courseRepository.findByIdWithExercisesAndLecturesAndLectureUnitsAndCompetenciesElseThrow(courseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminDataExportResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminDataExportResource.java index 0e1f17de42c1..6547fd7ef57a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminDataExportResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminDataExportResource.java @@ -17,6 +17,7 @@ * REST controller for requesting data exports for another user as admin. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminDataExportResource { @@ -34,7 +35,6 @@ public AdminDataExportResource(DataExportService dataExportService) { * @return the ResponseEntity with status 200 (OK) and with body a DTO containing the id, the state and the request date of the data export */ @PostMapping("data-exports/{login}") - @EnforceAdmin public ResponseEntity requestDataExportForUser(@PathVariable String login) { return ResponseEntity.ok(dataExportService.requestDataExportForUserAsAdmin(login)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/FeatureToggleResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminFeatureToggleResource.java similarity index 91% rename from src/main/java/de/tum/cit/aet/artemis/core/web/admin/FeatureToggleResource.java rename to src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminFeatureToggleResource.java index 84a58f7918aa..e2d24720efbd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/FeatureToggleResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminFeatureToggleResource.java @@ -18,13 +18,14 @@ import de.tum.cit.aet.artemis.core.service.feature.FeatureToggleService; @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") -public class FeatureToggleResource { +public class AdminFeatureToggleResource { private final FeatureToggleService featureToggleService; - public FeatureToggleResource(FeatureToggleService featureToggleService) { + public AdminFeatureToggleResource(FeatureToggleService featureToggleService) { this.featureToggleService = featureToggleService; } @@ -36,7 +37,6 @@ public FeatureToggleResource(FeatureToggleService featureToggleService) { * @see FeatureToggleService */ @PutMapping("feature-toggle") - @EnforceAdmin public ResponseEntity> toggleFeatures(@RequestBody Map features) { featureToggleService.updateFeatureToggles(features); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminImprintResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminImprintResource.java index 1e705926232c..58c99393679a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminImprintResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminImprintResource.java @@ -22,6 +22,7 @@ * REST controller for editing the imprint as an admin. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminImprintResource { @@ -40,7 +41,6 @@ public AdminImprintResource(LegalDocumentService legalDocumentService) { * @return the ResponseEntity with status 200 (OK) and with body the imprint with the given language */ @GetMapping("imprint-for-update") - @EnforceAdmin public ResponseEntity getImprintForUpdate(@RequestParam("language") String language) { if (!Language.isValidShortName(language)) { throw new BadRequestException("Language not supported"); @@ -55,7 +55,6 @@ public ResponseEntity getImprintForUpdate(@RequestParam("language") * @return the ResponseEntity with status 200 (OK) and with body the updated imprint */ @PutMapping("imprint") - @EnforceAdmin public ResponseEntity updateImprint(@RequestBody ImprintDTO imprint) { return ResponseEntity.ok(legalDocumentService.updateImprint(imprint)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/LogResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminLogResource.java similarity index 96% rename from src/main/java/de/tum/cit/aet/artemis/core/web/admin/LogResource.java rename to src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminLogResource.java index 375deea352f6..a0855aa92929 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/LogResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminLogResource.java @@ -23,9 +23,10 @@ * Controller for view and managing Log Level at runtime. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") -public class LogResource { +public class AdminLogResource { /** * GET logs -- Gets the current log levels. @@ -33,7 +34,6 @@ public class LogResource { * @return A list of all loggers with their log level */ @GetMapping("logs") - @EnforceAdmin public ResponseEntity> getList() { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); return ResponseEntity.ok(context.getLoggerList().stream().map(LoggerVM::new).toList()); @@ -46,7 +46,6 @@ public ResponseEntity> getList() { * @return The updated logger */ @PutMapping("logs") - @EnforceAdmin public ResponseEntity changeLevel(@RequestBody LoggerVM jsonLogger) { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); Logger logger = context.getLogger(jsonLogger.getName()); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminOrganizationResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminOrganizationResource.java index 75d3719b9c6e..c62673b6178f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminOrganizationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminOrganizationResource.java @@ -36,6 +36,7 @@ * REST controller for administrating the Organization entities */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminOrganizationResource { @@ -72,7 +73,6 @@ public AdminOrganizationResource(OrganizationService organizationService, Organi * @return empty ResponseEntity with status 200 (OK), or 404 (Not Found) otherwise */ @PostMapping("organizations/{organizationId}/courses/{courseId}") - @EnforceAdmin public ResponseEntity addCourseToOrganization(@PathVariable Long courseId, @PathVariable Long organizationId) { log.debug("REST request to add course to organization : {}", organizationId); Organization organization = organizationRepository.findByIdElseThrow(organizationId); @@ -90,7 +90,6 @@ public ResponseEntity addCourseToOrganization(@PathVariable Long courseId, * @return empty ResponseEntity with status 200 (OK), or 404 (Not Found) otherwise */ @DeleteMapping("organizations/{organizationId}/courses/{courseId}") - @EnforceAdmin public ResponseEntity removeCourseFromOrganization(@PathVariable Long courseId, @PathVariable Long organizationId) { Organization organization = organizationRepository.findByIdElseThrow(organizationId); courseRepository.removeOrganizationFromCourse(courseId, organization); @@ -107,7 +106,6 @@ public ResponseEntity removeCourseFromOrganization(@PathVariable Long cour * @return empty ResponseEntity with status 200 (OK), or 404 (Not Found) otherwise */ @PostMapping("organizations/{organizationId}/users/{userLogin}") - @EnforceAdmin public ResponseEntity addUserToOrganization(@PathVariable String userLogin, @PathVariable Long organizationId) { User user = userRepository.getUserByLoginElseThrow(userLogin); Organization organization = organizationRepository.findByIdElseThrow(organizationId); @@ -128,7 +126,6 @@ public ResponseEntity addUserToOrganization(@PathVariable String userLogin * @return empty ResponseEntity with status 200 (OK), or 404 (Not Found) otherwise */ @DeleteMapping("organizations/{organizationId}/users/{userLogin}") - @EnforceAdmin public ResponseEntity removeUserFromOrganization(@PathVariable String userLogin, @PathVariable Long organizationId) { log.debug("REST request to remove course to organization : {}", organizationId); User user = userRepository.getUserByLoginElseThrow(userLogin); @@ -145,7 +142,6 @@ public ResponseEntity removeUserFromOrganization(@PathVariable String user * @return the ResponseEntity containing the added organization with status 200 (OK), or 404 (Not Found) otherwise */ @PostMapping("organizations") - @EnforceAdmin public ResponseEntity addOrganization(@RequestBody Organization organization) { log.debug("REST request to add new organization : {}", organization); Organization created = organizationService.add(organization); @@ -161,7 +157,6 @@ public ResponseEntity addOrganization(@RequestBody Organization or * @return the ResponseEntity containing the updated organization with status 200 (OK), or 404 (Not Found) otherwise */ @PutMapping("organizations/{organizationId}") - @EnforceAdmin public ResponseEntity updateOrganization(@PathVariable Long organizationId, @RequestBody Organization organization) { log.debug("REST request to update organization : {}", organization); if (organization.getId() == null) { @@ -182,7 +177,6 @@ public ResponseEntity updateOrganization(@PathVariable Long organi * @return empty ResponseEntity with status 200 (OK), or 404 (Not Found) otherwise */ @DeleteMapping("organizations/{organizationId}") - @EnforceAdmin public ResponseEntity deleteOrganization(@PathVariable Long organizationId) { log.debug("REST request to delete organization : {}", organizationId); organizationService.deleteOrganization(organizationId); @@ -195,7 +189,6 @@ public ResponseEntity deleteOrganization(@PathVariable Long organizationId * @return ResponseEntity containing a list of all organizations with status 200 (OK) */ @GetMapping("organizations") - @EnforceAdmin public ResponseEntity> getAllOrganizations() { log.debug("REST request to get all organizations"); // TODO: we should avoid findAll() and instead load batches of organizations @@ -210,7 +203,6 @@ public ResponseEntity> getAllOrganizations() { * @return ResponseEntity containing a map containing the numbers of users and courses */ @GetMapping("organizations/{organizationId}/count") - @EnforceAdmin public ResponseEntity getNumberOfUsersAndCoursesByOrganization(@PathVariable long organizationId) { log.debug("REST request to get number of users and courses of organization : {}", organizationId); @@ -227,7 +219,6 @@ public ResponseEntity getNumberOfUsersAndCoursesByOrganiza * containing their relative numbers of users and courses */ @GetMapping("organizations/count-all") - @EnforceAdmin public ResponseEntity> getNumberOfUsersAndCoursesOfAllOrganizations() { log.debug("REST request to get number of users and courses of all organizations"); @@ -250,7 +241,6 @@ public ResponseEntity> getNumberOfUsersAndCoursesOfAl * if exists, else with status 404 (Not Found) */ @GetMapping("organizations/{organizationId}") - @EnforceAdmin public ResponseEntity getOrganizationById(@PathVariable long organizationId) { log.debug("REST request to get organization : {}", organizationId); Organization organization = organizationRepository.findByIdElseThrow(organizationId); @@ -265,7 +255,6 @@ public ResponseEntity getOrganizationById(@PathVariable long organ * if exists, else with status 404 (Not Found) */ @GetMapping("organizations/{organizationId}/full") - @EnforceAdmin public ResponseEntity getOrganizationByIdWithUsersAndCourses(@PathVariable long organizationId) { log.debug("REST request to get organization with users and courses : {}", organizationId); Organization organization = organizationRepository.findByIdWithEagerUsersAndCoursesElseThrow(organizationId); @@ -279,7 +268,6 @@ public ResponseEntity getOrganizationByIdWithUsersAndCourses(@Path * @return ResponseEntity containing a set of organizations containing the given user */ @GetMapping("organizations/users/{userId}") - @EnforceAdmin public ResponseEntity> getAllOrganizationsByUser(@PathVariable Long userId) { log.debug("REST request to get all organizations of user : {}", userId); Set organizations = organizationRepository.findAllOrganizationsByUserId(userId); @@ -293,7 +281,6 @@ public ResponseEntity> getAllOrganizationsByUser(@PathVariable * @return the title of the organization wrapped in an ResponseEntity or 404 Not Found if no organization with that id exists */ @GetMapping("organizations/{organizationId}/title") - @EnforceAdmin public ResponseEntity getOrganizationTitle(@PathVariable Long organizationId) { final var title = organizationRepository.getOrganizationTitle(organizationId); return title == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(title); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminPrivacyStatementResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminPrivacyStatementResource.java index af48dc8f3565..736d926218ab 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminPrivacyStatementResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminPrivacyStatementResource.java @@ -22,6 +22,7 @@ * REST controller for editing the Privacy Statement as an admin. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminPrivacyStatementResource { @@ -39,7 +40,6 @@ public AdminPrivacyStatementResource(LegalDocumentService legalDocumentService) * @param language the language of the privacy statement * @return the ResponseEntity with status 200 (OK) and with body the privacy statement */ - @EnforceAdmin @GetMapping("privacy-statement-for-update") public ResponseEntity getPrivacyStatementForUpdate(@RequestParam("language") String language) { if (!Language.isValidShortName(language)) { @@ -54,7 +54,6 @@ public ResponseEntity getPrivacyStatementForUpdate(@Request * @param privacyStatement the privacy statement to update * @return the ResponseEntity with status 200 (OK) and with body the updated privacy statement */ - @EnforceAdmin @PutMapping("privacy-statement") public ResponseEntity updatePrivacyStatement(@RequestBody PrivacyStatementDTO privacyStatement) { return ResponseEntity.ok(legalDocumentService.updatePrivacyStatement(privacyStatement)); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminStatisticsResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminStatisticsResource.java index 9c6db4cc7616..c91d7df176c3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminStatisticsResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminStatisticsResource.java @@ -23,6 +23,7 @@ * REST controller for administrating statistics. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminStatisticsResource { @@ -44,7 +45,6 @@ public AdminStatisticsResource(StatisticsService statisticsService) { * @return the ResponseEntity with status 200 (OK) and the data in body, or status 404 (Not Found) */ @GetMapping("management/statistics/data") - @EnforceAdmin public ResponseEntity> getChartData(@RequestParam SpanType span, @RequestParam Integer periodIndex, @RequestParam GraphType graphType) { log.debug("REST request to get graph data"); return ResponseEntity.ok(this.statisticsService.getChartData(span, periodIndex, graphType, StatisticsView.ARTEMIS, null)); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java index 289d296c5624..b2fb3b4fadd2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/admin/AdminUserResource.java @@ -69,6 +69,7 @@ * Another option would be to have a specific JPA entity graph to handle this case. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminUserResource { @@ -108,7 +109,6 @@ public AdminUserResource(UserRepository userRepository, UserService userService, * @throws BadRequestAlertException 400 (Bad Request) if the login or email is already in use */ @PostMapping("users") - @EnforceAdmin public ResponseEntity createUser(@Valid @RequestBody ManagedUserVM managedUserVM) throws URISyntaxException { this.userService.checkUsernameAndPasswordValidityElseThrow(managedUserVM.getLogin(), managedUserVM.getPassword()); @@ -144,7 +144,6 @@ else if (userRepository.findOneByEmailIgnoreCase(managedUserVM.getEmail()).isPre * @throws LoginAlreadyUsedException 400 (Bad Request) if the login is already in use */ @PutMapping("users") - @EnforceAdmin public ResponseEntity updateUser(@Valid @RequestBody ManagedUserVM managedUserVM) { this.userService.checkUsernameAndPasswordValidityElseThrow(managedUserVM.getLogin(), managedUserVM.getPassword()); log.debug("REST request to update User : {}", managedUserVM); @@ -181,7 +180,6 @@ public ResponseEntity updateUser(@Valid @RequestBody ManagedUserVM mana * @return the ResponseEntity with status 200 (OK) and with body the "login" user, or with status 404 (Not Found) */ @GetMapping("users/{login:" + Constants.LOGIN_REGEX + "}") - @EnforceAdmin public ResponseEntity getUser(@PathVariable String login) { log.debug("REST request to get User : {}", login); return ResponseUtil.wrapOrNotFound(userRepository.findOneWithGroupsAndAuthoritiesByLogin(login).map(user -> { @@ -201,7 +199,6 @@ public ResponseEntity getUser(@PathVariable String login) { * @return the list of users who could not be imported, because they could NOT be found in the Artemis database and could NOT be found in the connected LDAP */ @PostMapping("users/import") - @EnforceAdmin public ResponseEntity> importUsers(@RequestBody List userDtos) { log.debug("REST request to import {} to Artemis", userDtos); List notFoundStudentsDtos = userService.importUsers(userDtos); @@ -215,7 +212,6 @@ public ResponseEntity> importUsers(@RequestBody List syncUserViaLdap(@PathVariable Long userId) { log.debug("REST request to update ldap information User : {}", userId); @@ -235,7 +231,6 @@ public ResponseEntity syncUserViaLdap(@PathVariable Long userId) { * @return the ResponseEntity with status 200 (OK) and with body all users */ @GetMapping("users") - @EnforceAdmin public ResponseEntity> getAllUsers(UserPageableSearchDTO userSearch) { final Page page = userRepository.getAllManagedUsers(userSearch); HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); @@ -248,7 +243,6 @@ public ResponseEntity> getAllUsers(UserPageableSearchDTO userSearc * @return the ResponseEntity with status 200 (OK) and with body all logins of not enrolled users */ @GetMapping("users/not-enrolled") - @EnforceAdmin public ResponseEntity> getNotEnrolledUsers() { List logins = userRepository.findAllNotEnrolledUsers(); return new ResponseEntity<>(logins, HttpStatus.OK); @@ -260,7 +254,6 @@ public ResponseEntity> getNotEnrolledUsers() { * @return the ResponseEntity with status 200 (OK) and with body a string list of the all the roles */ @GetMapping("users/authorities") - @EnforceAdmin public ResponseEntity> getAuthorities() { return ResponseEntity.ok(authorityRepository.getAuthorities()); } @@ -272,7 +265,6 @@ public ResponseEntity> getAuthorities() { * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("users/{login:" + Constants.LOGIN_REGEX + "}") - @EnforceAdmin public ResponseEntity deleteUser(@PathVariable String login) { log.debug("REST request to delete User: {}", login); if (userRepository.isCurrentUser(login)) { @@ -289,7 +281,6 @@ public ResponseEntity deleteUser(@PathVariable String login) { * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("users") - @EnforceAdmin public ResponseEntity> deleteUsers(@RequestParam(name = "login") List logins) { log.debug("REST request to delete {} users", logins.size()); List deletedUsers = Collections.synchronizedList(new java.util.ArrayList<>()); diff --git a/src/main/java/de/tum/cit/aet/artemis/exam/web/admin/AdminExamResource.java b/src/main/java/de/tum/cit/aet/artemis/exam/web/admin/AdminExamResource.java index add9135f8c54..d1f70035e858 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exam/web/admin/AdminExamResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exam/web/admin/AdminExamResource.java @@ -20,6 +20,7 @@ * REST controller for administrating Exam. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminExamResource { @@ -38,7 +39,6 @@ public AdminExamResource(ExamRepository examRepository) { * @return the ResponseEntity with status 200 (OK) and a list of exams. */ @GetMapping("courses/upcoming-exams") - @EnforceAdmin public ResponseEntity> getCurrentAndUpcomingExams() { log.debug("REST request to get all upcoming exams"); diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/admin/AdminExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/admin/AdminExerciseResource.java index 306ad29ddd4f..76546cbe6289 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/admin/AdminExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/admin/AdminExerciseResource.java @@ -20,6 +20,7 @@ * REST controller for administrating Exercise. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminExerciseResource { @@ -38,7 +39,6 @@ public AdminExerciseResource(ExerciseRepository exerciseRepository) { * @return the ResponseEntity with status 200 (OK) and a list of exercises. */ @GetMapping("exercises/upcoming") - @EnforceAdmin public ResponseEntity> getUpcomingExercises() { log.debug("REST request to get all upcoming exercises"); Set upcomingExercises = exerciseRepository.findAllExercisesWithCurrentOrUpcomingDueDate(); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/admin/AdminIrisSettingsResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/admin/AdminIrisSettingsResource.java index b8ea1f92ba31..40da3e5ee431 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/admin/AdminIrisSettingsResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/admin/AdminIrisSettingsResource.java @@ -17,6 +17,7 @@ * REST controller for managing {@link IrisSettings}. */ @Profile(PROFILE_IRIS) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminIrisSettingsResource { @@ -34,7 +35,6 @@ public AdminIrisSettingsResource(IrisSettingsService irisSettingsService) { * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the updated settings. */ @PutMapping("iris/global-iris-settings") - @EnforceAdmin public ResponseEntity updateGlobalSettings(@RequestBody IrisSettings settings) { var updatedSettings = irisSettingsService.saveIrisSettings(settings); return ResponseEntity.ok(updatedSettings); diff --git a/src/main/java/de/tum/cit/aet/artemis/lti/web/admin/AdminLtiConfigurationResource.java b/src/main/java/de/tum/cit/aet/artemis/lti/web/admin/AdminLtiConfigurationResource.java index 654facd44302..0b5d3f71c2f3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lti/web/admin/AdminLtiConfigurationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/lti/web/admin/AdminLtiConfigurationResource.java @@ -33,6 +33,7 @@ * Handles administrative actions for LTI platforms, including configuration, deletion, and dynamic registration. */ @RestController +@EnforceAdmin @RequestMapping("api/admin/") @Profile(PROFILE_LTI) public class AdminLtiConfigurationResource { @@ -75,7 +76,6 @@ public AdminLtiConfigurationResource(LtiPlatformConfigurationRepository ltiPlatf * @return a {@code ResponseEntity} with an {@code Optional} and HTTP status. */ @GetMapping("lti-platform/{platformId}") - @EnforceAdmin public ResponseEntity getLtiPlatformConfiguration(@PathVariable("platformId") String platformId) { log.debug("REST request to configured lti platform"); LtiPlatformConfiguration platform = ltiPlatformConfigurationRepository.findByIdElseThrow(Long.parseLong(platformId)); @@ -89,7 +89,6 @@ public ResponseEntity getLtiPlatformConfiguration(@Pat * @return a {@code ResponseEntity} with status {@code 200 (OK)} and a header indicating the deletion. */ @DeleteMapping("lti-platform/{platformId}") - @EnforceAdmin public ResponseEntity deleteLtiPlatformConfiguration(@PathVariable("platformId") String platformId) { log.debug("REST request to configured lti platform"); LtiPlatformConfiguration platform = ltiPlatformConfigurationRepository.findByIdElseThrow(Long.parseLong(platformId)); @@ -105,7 +104,6 @@ public ResponseEntity deleteLtiPlatformConfiguration(@PathVariable("platfo * or with status 400 (Bad Request) if the provided platform configuration is invalid (e.g., missing ID) */ @PutMapping("lti-platform") - @EnforceAdmin public ResponseEntity updateLtiPlatformConfiguration(@RequestBody LtiPlatformConfiguration platform) { log.debug("REST request to update configured lti platform"); @@ -125,7 +123,6 @@ public ResponseEntity updateLtiPlatformConfiguration(@RequestBody LtiPlatf * or with status 400 (Bad Request) if the provided platform configuration is invalid (e.g., missing ID) */ @PostMapping("lti-platform") - @EnforceAdmin public ResponseEntity addLtiPlatformConfiguration(@RequestBody LtiPlatformConfiguration platform) { log.debug("REST request to add new lti platform"); @@ -147,7 +144,6 @@ public ResponseEntity addLtiPlatformConfiguration(@RequestBody LtiPlatform * @return a {@link ResponseEntity} with status 200 (OK) if the dynamic registration process was successful. */ @PostMapping("lti13/dynamic-registration") - @EnforceAdmin public ResponseEntity lti13DynamicRegistration(@RequestParam(name = "openid_configuration") String openIdConfiguration, @RequestParam(name = "registration_token", required = false) String registrationToken) { diff --git a/src/main/java/de/tum/cit/aet/artemis/modeling/web/admin/AdminModelingExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/modeling/web/admin/AdminModelingExerciseResource.java index 215ea0a05293..cf3c6d2ee627 100644 --- a/src/main/java/de/tum/cit/aet/artemis/modeling/web/admin/AdminModelingExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/modeling/web/admin/AdminModelingExerciseResource.java @@ -25,6 +25,7 @@ * REST controller for administrating ModelingExercise. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminModelingExerciseResource { @@ -59,7 +60,6 @@ public AdminModelingExerciseResource(ModelingExerciseRepository modelingExercise * @return the ResponseEntity with status 200 (OK) */ @GetMapping("modeling-exercises/{exerciseId}/check-clusters") - @EnforceAdmin public ResponseEntity checkClusters(@PathVariable Long exerciseId) { log.info("REST request to check clusters of ModelingExercise : {}", exerciseId); int clusterCount = modelClusterRepository.countByExerciseIdWithEagerElements(exerciseId); @@ -73,7 +73,6 @@ public ResponseEntity checkClusters(@PathVariable Long exerciseId) { * @return the ResponseEntity with status 200 (OK) */ @DeleteMapping("modeling-exercises/{exerciseId}/clusters") - @EnforceAdmin public ResponseEntity deleteModelingExerciseClustersAndElements(@PathVariable Long exerciseId) { log.info("REST request to delete ModelingExercise : {}", exerciseId); var modelingExercise = modelingExerciseRepository.findByIdElseThrow(exerciseId); @@ -91,7 +90,6 @@ public ResponseEntity deleteModelingExerciseClustersAndElements(@PathVaria * @return the ResponseEntity with status 200 (OK) */ @PostMapping("modeling-exercises/{exerciseId}/trigger-automatic-assessment") - @EnforceAdmin public ResponseEntity triggerAutomaticAssessment(@PathVariable Long exerciseId) { instanceMessageSendService.sendModelingExerciseInstantClustering(exerciseId); return ResponseEntity.ok().build(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java index 2222e4a5f3d9..a55c44871619 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseExportImportResource.java @@ -86,6 +86,7 @@ * REST controller for managing ProgrammingExercise. */ @Profile(PROFILE_CORE) +@FeatureToggle(Feature.ProgrammingExercises) @RestController @RequestMapping("api/") public class ProgrammingExerciseExportImportResource { @@ -184,7 +185,6 @@ private void validateStaticCodeAnalysisSettings(ProgrammingExercise programmingE */ @PostMapping("programming-exercises/import/{sourceExerciseId}") @EnforceAtLeastEditor - @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity importProgrammingExercise(@PathVariable long sourceExerciseId, @RequestBody ProgrammingExercise newExercise, @RequestParam(defaultValue = "false") boolean recreateBuildPlans, @RequestParam(defaultValue = "false") boolean updateTemplate, @RequestParam(defaultValue = "false") boolean setTestCaseVisibilityToAfterDueDate) throws JsonProcessingException { @@ -291,7 +291,6 @@ public ResponseEntity importProgrammingExercise(@PathVariab */ @PostMapping("courses/{courseId}/programming-exercises/import-from-file") @EnforceAtLeastEditor - @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity importProgrammingExerciseFromFile(@PathVariable long courseId, @RequestPart("programmingExercise") ProgrammingExercise programmingExercise, @RequestPart("file") MultipartFile zipFile) { final var user = userRepository.getUserWithGroupsAndAuthorities(); @@ -318,7 +317,7 @@ public ResponseEntity importProgrammingExerciseFromFile(@Pa */ @GetMapping("programming-exercises/{exerciseId}/export-instructor-exercise") @EnforceAtLeastInstructor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportInstructorExercise(@PathVariable long exerciseId) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdWithPlagiarismDetectionConfigTeamConfigAndBuildConfigElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, programmingExercise, null); @@ -352,7 +351,7 @@ public ResponseEntity exportInstructorExercise(@PathVariable long exer */ @GetMapping("programming-exercises/{exerciseId}/export-instructor-repository/{repositoryType}") @EnforceAtLeastTutor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportInstructorRepository(@PathVariable long exerciseId, @PathVariable RepositoryType repositoryType) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.TEACHING_ASSISTANT, programmingExercise, null); @@ -373,7 +372,7 @@ public ResponseEntity exportInstructorRepository(@PathVariable long ex */ @GetMapping("programming-exercises/{exerciseId}/export-instructor-auxiliary-repository/{repositoryId}") @EnforceAtLeastTutor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportInstructorAuxiliaryRepository(@PathVariable long exerciseId, @PathVariable long repositoryId) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.TEACHING_ASSISTANT, programmingExercise, null); @@ -419,7 +418,7 @@ private ResponseEntity returnZipFileForRepositoryExport(Optional */ @PostMapping("programming-exercises/{exerciseId}/export-repos-by-participant-identifiers/{participantIdentifiers}") @EnforceAtLeastTutor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportSubmissionsByStudentLogins(@PathVariable long exerciseId, @PathVariable String participantIdentifiers, @RequestBody RepositoryExportOptionsDTO repositoryExportOptions) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdWithStudentParticipationsAndLegalSubmissionsElseThrow(exerciseId); @@ -464,7 +463,7 @@ public ResponseEntity exportSubmissionsByStudentLogins(@PathVariable l */ @PostMapping("programming-exercises/{exerciseId}/export-repos-by-participation-ids/{participationIds}") @EnforceAtLeastTutor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportSubmissionsByParticipationIds(@PathVariable long exerciseId, @PathVariable String participationIds, @RequestBody RepositoryExportOptionsDTO repositoryExportOptions) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdWithStudentParticipationsAndLegalSubmissionsElseThrow(exerciseId); @@ -523,7 +522,7 @@ private ResponseEntity provideZipForParticipations(@NotNull List exportStudentRequestedRepository(@PathVariable long exerciseId, @RequestParam() boolean includeTests) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); if (programmingExercise.isExamExercise()) { @@ -550,7 +549,7 @@ public ResponseEntity exportStudentRequestedRepository(@PathVariable l */ @GetMapping("programming-exercises/{exerciseId}/export-student-repository/{participationId}") @EnforceAtLeastStudent - @FeatureToggle({ Feature.ProgrammingExercises, Feature.Exports }) + @FeatureToggle(Feature.Exports) public ResponseEntity exportStudentRepository(@PathVariable long exerciseId, @PathVariable long participationId) throws IOException { var programmingExercise = programmingExerciseRepository.findByIdWithStudentParticipationsAndLegalSubmissionsElseThrow(exerciseId); var studentParticipation = programmingExercise.getStudentParticipations().stream().filter(p -> p.getId().equals(participationId)) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExercisePlagiarismResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExercisePlagiarismResource.java index 4388e386ab39..95e60a0ec89c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExercisePlagiarismResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExercisePlagiarismResource.java @@ -40,6 +40,7 @@ * REST controller for managing ProgrammingExercise. */ @Profile(PROFILE_CORE) +@FeatureToggle(Feature.ProgrammingExercises) @RestController @RequestMapping("api/") public class ProgrammingExercisePlagiarismResource { @@ -72,7 +73,6 @@ public ProgrammingExercisePlagiarismResource(ProgrammingExerciseRepository progr */ @GetMapping("programming-exercises/{exerciseId}/plagiarism-result") @EnforceAtLeastEditor - @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity> getPlagiarismResult(@PathVariable long exerciseId) { log.debug("REST request to get the latest plagiarism result for the programming exercise with id: {}", exerciseId); ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); @@ -95,7 +95,7 @@ public ResponseEntity> getPlagiarismRe */ @GetMapping("programming-exercises/{exerciseId}/check-plagiarism") @EnforceAtLeastEditor - @FeatureToggle({ Feature.ProgrammingExercises, Feature.PlagiarismChecks }) + @FeatureToggle(Feature.PlagiarismChecks) public ResponseEntity> checkPlagiarism(@PathVariable long exerciseId, @RequestParam int similarityThreshold, @RequestParam int minimumScore, @RequestParam int minimumSize) throws ExitException, IOException { ProgrammingExercise programmingExercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); @@ -128,7 +128,6 @@ public ResponseEntity> checkPlagiarism */ @GetMapping(value = "programming-exercises/{exerciseId}/check-plagiarism-jplag-report") @EnforceAtLeastEditor - @FeatureToggle(Feature.ProgrammingExercises) public ResponseEntity checkPlagiarismWithJPlagReport(@PathVariable long exerciseId, @RequestParam int similarityThreshold, @RequestParam int minimumScore, @RequestParam int minimumSize) throws IOException { log.debug("REST request to check plagiarism for ProgrammingExercise with id: {}", exerciseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/text/web/admin/AdminTextAssessmentEventResource.java b/src/main/java/de/tum/cit/aet/artemis/text/web/admin/AdminTextAssessmentEventResource.java index 78b6a9f8871b..acecfb3b1e56 100644 --- a/src/main/java/de/tum/cit/aet/artemis/text/web/admin/AdminTextAssessmentEventResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/text/web/admin/AdminTextAssessmentEventResource.java @@ -19,6 +19,7 @@ * REST controller for administrating TextAssessmentEventResource. */ @Profile(PROFILE_CORE) +@EnforceAdmin @RestController @RequestMapping("api/admin/") public class AdminTextAssessmentEventResource { @@ -36,7 +37,6 @@ public AdminTextAssessmentEventResource(TextAssessmentEventRepository textAssess * @return returns a List of TextAssessmentEvent's */ @GetMapping("event-insights/text-assessment/events/{courseId}") - @EnforceAdmin public ResponseEntity> getEventsByCourseId(@PathVariable Long courseId) { List events = textAssessmentEventRepository.findAllByCourseId(courseId); return ResponseEntity.ok().body(events); diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupFreePeriodResource.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupFreePeriodResource.java index d8a59414d71c..791eea50d82e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupFreePeriodResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupFreePeriodResource.java @@ -37,6 +37,7 @@ import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupFreePeriodService; @Profile(PROFILE_CORE) +@FeatureToggle(Feature.TutorialGroups) @RestController @RequestMapping("api/") public class TutorialGroupFreePeriodResource { @@ -73,7 +74,6 @@ public TutorialGroupFreePeriodResource(TutorialGroupsConfigurationRepository tut */ @GetMapping("courses/{courseId}/tutorial-groups-configuration/{tutorialGroupsConfigurationId}/tutorial-free-periods/{tutorialGroupFreePeriodId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity getOneOfConfiguration(@PathVariable Long courseId, @PathVariable Long tutorialGroupsConfigurationId, @PathVariable Long tutorialGroupFreePeriodId) { log.debug("REST request to get tutorial group free period: {} of tutorial group configuration {} of course: {}", tutorialGroupFreePeriodId, tutorialGroupsConfigurationId, @@ -96,7 +96,6 @@ public ResponseEntity getOneOfConfiguration(@PathVariab */ @PutMapping("courses/{courseId}/tutorial-groups-configuration/{tutorialGroupsConfigurationId}/tutorial-free-periods/{tutorialGroupFreePeriodId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity update(@PathVariable Long courseId, @PathVariable Long tutorialGroupsConfigurationId, @PathVariable Long tutorialGroupFreePeriodId, @RequestBody @Valid TutorialGroupFreePeriodDTO tutorialGroupFreePeriod) throws URISyntaxException { log.debug("REST request to update TutorialGroupFreePeriod: {} for tutorial group configuration: {} of course: {}", tutorialGroupFreePeriodId, tutorialGroupsConfigurationId, @@ -144,7 +143,6 @@ public ResponseEntity update(@PathVariable Long courseI */ @PostMapping("courses/{courseId}/tutorial-groups-configuration/{tutorialGroupsConfigurationId}/tutorial-free-periods") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity create(@PathVariable Long courseId, @PathVariable Long tutorialGroupsConfigurationId, @RequestBody @Valid TutorialGroupFreePeriodDTO tutorialGroupFreePeriod) throws URISyntaxException { log.debug("REST request to create TutorialGroupFreePeriod: {} for tutorial group configuration: {} of course: {}", tutorialGroupFreePeriod, tutorialGroupsConfigurationId, @@ -189,7 +187,6 @@ public ResponseEntity create(@PathVariable Long courseI */ @DeleteMapping("courses/{courseId}/tutorial-groups-configuration/{tutorialGroupsConfigurationId}/tutorial-free-periods/{tutorialGroupFreePeriodId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity delete(@PathVariable Long courseId, @PathVariable Long tutorialGroupsConfigurationId, @PathVariable Long tutorialGroupFreePeriodId) throws URISyntaxException { log.debug("REST request to delete TutorialGroupFreePeriod: {} of tutorial group configuration {} of course: {}", tutorialGroupFreePeriodId, tutorialGroupsConfigurationId, diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupResource.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupResource.java index daa50435cf94..a82e0c9297ad 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupResource.java @@ -71,6 +71,7 @@ import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupService; @Profile(PROFILE_CORE) +@FeatureToggle(Feature.TutorialGroups) @RestController @RequestMapping("api/") public class TutorialGroupResource { @@ -131,7 +132,6 @@ public TutorialGroupResource(AuthorizationCheckService authorizationCheckService */ @GetMapping("tutorial-groups/{tutorialGroupId}/title") @EnforceAtLeastStudent - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity getTitle(@PathVariable Long tutorialGroupId) { log.debug("REST request to get title of TutorialGroup : {}", tutorialGroupId); return tutorialGroupRepository.getTutorialGroupTitle(tutorialGroupId).map(ResponseEntity::ok) @@ -147,7 +147,6 @@ public ResponseEntity getTitle(@PathVariable Long tutorialGroupId) { */ @GetMapping("courses/{courseId}/tutorial-groups/campus-values") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity> getUniqueCampusValues(@PathVariable Long courseId) { log.debug("REST request to get unique campus values used for tutorial groups in course : {}", courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -165,7 +164,6 @@ public ResponseEntity> getUniqueCampusValues(@PathVariable Long cour */ @GetMapping("courses/{courseId}/tutorial-groups/language-values") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity> getUniqueLanguageValues(@PathVariable Long courseId) { log.debug("REST request to get unique language values used for tutorial groups in course : {}", courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -182,7 +180,6 @@ public ResponseEntity> getUniqueLanguageValues(@PathVariable Long co */ @GetMapping("courses/{courseId}/tutorial-groups") @EnforceAtLeastStudent - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity> getAllForCourse(@PathVariable Long courseId) { log.debug("REST request to get all tutorial groups of course with id: {}", courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -202,7 +199,6 @@ public ResponseEntity> getAllForCourse(@PathVariable Long co */ @GetMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}") @EnforceAtLeastStudent - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity getOneOfCourse(@PathVariable Long courseId, @PathVariable Long tutorialGroupId) { log.debug("REST request to get tutorial group: {} of course: {}", tutorialGroupId, courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -221,7 +217,6 @@ public ResponseEntity getOneOfCourse(@PathVariable Long courseId, */ @PostMapping("courses/{courseId}/tutorial-groups") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity create(@PathVariable Long courseId, @RequestBody @Valid TutorialGroup tutorialGroup) throws URISyntaxException { log.debug("REST request to create TutorialGroup: {} in course: {}", tutorialGroup, courseId); if (tutorialGroup.getId() != null) { @@ -282,7 +277,6 @@ public ResponseEntity create(@PathVariable Long courseId, @Reques */ @DeleteMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity delete(@PathVariable Long courseId, @PathVariable Long tutorialGroupId) { log.info("REST request to delete a TutorialGroup: {} of course: {}", tutorialGroupId, courseId); var tutorialGroupFromDatabase = this.tutorialGroupRepository.findByIdWithTeachingAssistantAndRegistrationsElseThrow(tutorialGroupId); @@ -317,7 +311,6 @@ public record TutorialGroupUpdateDTO(@Valid @NotNull TutorialGroup tutorialGroup */ @PutMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity update(@PathVariable long courseId, @PathVariable long tutorialGroupId, @RequestBody @Valid TutorialGroupUpdateDTO tutorialGroupUpdateDTO) { TutorialGroup updatedTutorialGroup = tutorialGroupUpdateDTO.tutorialGroup(); @@ -405,7 +398,6 @@ public ResponseEntity update(@PathVariable long courseId, @PathVa */ @DeleteMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/deregister/{studentLogin:" + Constants.LOGIN_REGEX + "}") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity deregisterStudent(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable String studentLogin) { log.debug("REST request to deregister {} student from tutorial group : {}", studentLogin, tutorialGroupId); var tutorialGroupFromDatabase = this.tutorialGroupRepository.findByIdElseThrow(tutorialGroupId); @@ -427,7 +419,6 @@ public ResponseEntity deregisterStudent(@PathVariable Long courseId, @Path */ @PostMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/register/{studentLogin:" + Constants.LOGIN_REGEX + "}") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity registerStudent(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable String studentLogin) { log.debug("REST request to register {} student to tutorial group : {}", studentLogin, tutorialGroupId); var tutorialGroupFromDatabase = this.tutorialGroupRepository.findByIdElseThrow(tutorialGroupId); @@ -454,7 +445,6 @@ public ResponseEntity registerStudent(@PathVariable Long courseId, @PathVa */ @PostMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/register-multiple") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity> registerMultipleStudentsToTutorialGroup(@PathVariable long courseId, @PathVariable long tutorialGroupId, @RequestBody Set studentDtos) { log.debug("REST request to register {} to tutorial group {}", studentDtos, tutorialGroupId); @@ -476,7 +466,6 @@ public ResponseEntity> registerMultipleStudentsToTutorialGroup(@ */ @PostMapping("courses/{courseId}/tutorial-groups/import") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity> importRegistrations(@PathVariable Long courseId, @RequestBody @Valid Set importDTOs) { log.debug("REST request to import registrations {} to course {}", importDTOs, courseId); @@ -549,7 +538,6 @@ private void checkEntityIdMatchesPathIds(TutorialGroup tutorialGroup, Optional exportTutorialGroupsToCSV(@PathVariable Long courseId, @RequestParam List fields) { log.debug("REST request to export TutorialGroups to CSV for course: {}", courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -580,7 +568,6 @@ public ResponseEntity exportTutorialGroupsToCSV(@PathVariable Long cours */ @GetMapping(value = "courses/{courseId}/tutorial-groups/export/json", produces = MediaType.APPLICATION_JSON_VALUE) @EnforceAtLeastInstructorInCourse - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity exportTutorialGroupsToJSON(@PathVariable Long courseId, @RequestParam List fields) { log.debug("REST request to export TutorialGroups to JSON for course: {}", courseId); try { diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupSessionResource.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupSessionResource.java index 54790e628ba9..4d9c9459f65c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupSessionResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupSessionResource.java @@ -56,6 +56,7 @@ import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupService; @Profile(PROFILE_CORE) +@FeatureToggle(Feature.TutorialGroups) @RestController @RequestMapping("api/") public class TutorialGroupSessionResource { @@ -101,7 +102,6 @@ public TutorialGroupSessionResource(TutorialGroupSessionRepository tutorialGroup */ @GetMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}") @EnforceAtLeastStudent - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity getOneOfTutorialGroup(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable Long sessionId) { log.debug("REST request to get session: {} of tutorial group: {} of course {}", sessionId, tutorialGroupId, courseId); var session = tutorialGroupSessionRepository.findByIdElseThrow(sessionId); @@ -124,7 +124,6 @@ public ResponseEntity getOneOfTutorialGroup(@PathVariable */ @PutMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity update(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable Long sessionId, @RequestBody @Valid TutorialGroupSessionDTO tutorialGroupSessionDTO) { log.debug("REST request to update session: {} of tutorial group: {} of course {}", sessionId, tutorialGroupId, courseId); @@ -170,7 +169,6 @@ public ResponseEntity update(@PathVariable Long courseId, */ @PatchMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}/attendance-count") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity updateAttendanceCount(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable Long sessionId, @RequestParam(required = false) @Min(0) @Max(3000) Integer attendanceCount) { log.debug("REST request to update attendance count of session: {} of tutorial group: {} of course {} to {}", sessionId, tutorialGroupId, courseId, attendanceCount); @@ -192,7 +190,6 @@ public ResponseEntity updateAttendanceCount(@PathVariable */ @DeleteMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity deleteSession(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable Long sessionId) { log.debug("REST request to delete session: {} of tutorial group: {} of course {}", sessionId, tutorialGroupId, courseId); var sessionFromDatabase = this.tutorialGroupSessionRepository.findByIdElseThrow(sessionId); @@ -212,7 +209,6 @@ public ResponseEntity deleteSession(@PathVariable Long courseId, @PathVari */ @PostMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity create(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @RequestBody @Valid TutorialGroupSessionDTO tutorialGroupSessionDTO) throws URISyntaxException { log.debug("REST request to create TutorialGroupSession: {} for tutorial group: {}", tutorialGroupSessionDTO, tutorialGroupId); @@ -255,7 +251,6 @@ private TutorialGroupsConfiguration validateTutorialGroupConfiguration(@PathVari */ @PostMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}/cancel") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity cancel(@PathVariable Long courseId, @PathVariable Long tutorialGroupId, @PathVariable Long sessionId, @RequestBody TutorialGroupStatusDTO tutorialGroupStatusDTO) throws URISyntaxException { log.debug("REST request to cancel session: {} of tutorial group: {} of course {}", sessionId, tutorialGroupId, courseId); @@ -283,7 +278,6 @@ public ResponseEntity cancel(@PathVariable Long courseId, */ @PostMapping("courses/{courseId}/tutorial-groups/{tutorialGroupId}/sessions/{sessionId}/activate") @EnforceAtLeastTutor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity activate(@PathVariable long courseId, @PathVariable long tutorialGroupId, @PathVariable long sessionId) throws URISyntaxException { log.debug("REST request to activate session: {} of tutorial group: {} of course {}", sessionId, tutorialGroupId, courseId); var sessionToActivate = tutorialGroupSessionRepository.findByIdElseThrow(sessionId); diff --git a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupsConfigurationResource.java b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupsConfigurationResource.java index 4e9c9212ee4b..dc26c5bd4e09 100644 --- a/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupsConfigurationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/tutorialgroup/web/TutorialGroupsConfigurationResource.java @@ -36,6 +36,7 @@ import de.tum.cit.aet.artemis.tutorialgroup.service.TutorialGroupChannelManagementService; @Profile(PROFILE_CORE) +@FeatureToggle(Feature.TutorialGroups) @RestController @RequestMapping("api/") public class TutorialGroupsConfigurationResource { @@ -68,7 +69,6 @@ public TutorialGroupsConfigurationResource(TutorialGroupsConfigurationRepository */ @GetMapping("courses/{courseId}/tutorial-groups-configuration") @EnforceAtLeastStudent - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity getOneOfCourse(@PathVariable Long courseId) { log.debug("REST request to get tutorial groups configuration of course: {}", courseId); var course = courseRepository.findByIdElseThrow(courseId); @@ -85,7 +85,6 @@ public ResponseEntity getOneOfCourse(@PathVariable */ @PostMapping("courses/{courseId}/tutorial-groups-configuration") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity create(@PathVariable Long courseId, @RequestBody @Valid TutorialGroupsConfiguration tutorialGroupsConfiguration) throws URISyntaxException { log.debug("REST request to create TutorialGroupsConfiguration: {} for course: {}", tutorialGroupsConfiguration, courseId); @@ -120,7 +119,6 @@ public ResponseEntity create(@PathVariable Long cou */ @PutMapping("courses/{courseId}/tutorial-groups-configuration/{tutorialGroupsConfigurationId}") @EnforceAtLeastInstructor - @FeatureToggle(Feature.TutorialGroups) public ResponseEntity update(@PathVariable Long courseId, @PathVariable Long tutorialGroupsConfigurationId, @RequestBody @Valid TutorialGroupsConfiguration updatedTutorialGroupConfiguration) { log.debug("REST request to update TutorialGroupsConfiguration: {} of course: {}", updatedTutorialGroupConfiguration, courseId); diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authorization/AuthorizationArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authorization/AuthorizationArchitectureTest.java index f1bce920c4ef..a5426daad3b8 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authorization/AuthorizationArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authorization/AuthorizationArchitectureTest.java @@ -47,7 +47,7 @@ void testNoPreAuthorizeOnRestEndpoints() { void testEnforceAdminAnnotations() { ArchRule rule = methods().that().areAnnotatedWith(EnforceAdmin.class).and().areNotAnnotatedWith(ManualConfig.class).should().beDeclaredInClassesThat() .resideInAPackage(REST_ADMIN_PACKAGE + ".."); - rule.check(productionClasses); + rule.allowEmptyShould(true).check(productionClasses); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleResourceArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleResourceArchitectureTest.java index b90faedcaabf..5442e6d26c4b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleResourceArchitectureTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleResourceArchitectureTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -32,6 +33,7 @@ import com.tngtech.archunit.lang.ConditionEvents; import de.tum.cit.aet.artemis.communication.web.LinkPreviewResource; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAdmin; import de.tum.cit.aet.artemis.shared.architecture.AbstractArchitectureTest; public abstract class AbstractModuleResourceArchitectureTest extends AbstractArchitectureTest implements ModuleArchitectureTest { @@ -150,4 +152,29 @@ private & HasDescription & HasSourceCodeLocation> v tester.accept(value); } } + + @Test + void testClassWithEnforceAdminInCorrectlyNamed() { + ArchRule annotationToNameRule = classesOfThisModuleThat().areAnnotatedWith(EnforceAdmin.class).should().haveSimpleNameStartingWith("Admin") + .andShould(new ArchCondition<>("Have package name ending with .admin") { + + @Override + public void check(JavaClass item, ConditionEvents events) { + if (!item.getPackage().getName().endsWith(".admin")) { + events.add(violated(item, "Classes annotated with @EnforceAdmin should be in an admin subpackage.")); + } + } + }); + annotationToNameRule.allowEmptyShould(true).check(productionClasses); + + ArchRule nameToAnnotationRule = classesOfThisModuleThat().haveSimpleNameStartingWith("Admin").should().beAnnotatedWith(EnforceAdmin.class); + nameToAnnotationRule.allowEmptyShould(true).check(productionClasses); + } + + @Test + void testNoOverrideOfEnforceAdmin() { + ArchRule rule = methodsOfThisModuleThat().areDeclaredInClassesThat().areAnnotatedWith(EnforceAdmin.class).should().notBeAnnotatedWith(EnforceAdmin.class).andShould() + .notBeMetaAnnotatedWith(PreAuthorize.class); + rule.allowEmptyShould(true).check(productionClasses); + } } From 4dacc21806553c012b93560764ac61249bea0089 Mon Sep 17 00:00:00 2001 From: Raphael Klein <134242785+raffifasaro@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:26:42 +0200 Subject: [PATCH 11/14] General: Improve exercise view when using LTI (#9329) --- .../lti/lti13-exercise-launch.component.ts | 10 +- .../course-exercises.component.html | 4 +- .../course-exercises.component.ts | 9 + .../overview/course-overview.component.html | 360 +++++++++--------- .../app/overview/course-overview.component.ts | 14 +- .../shared/layouts/main/main.component.html | 6 +- .../app/shared/layouts/main/main.component.ts | 11 +- .../webapp/app/shared/service/lti.service.ts | 15 + .../course/course-overview.component.spec.ts | 27 +- 9 files changed, 264 insertions(+), 192 deletions(-) create mode 100644 src/main/webapp/app/shared/service/lti.service.ts diff --git a/src/main/webapp/app/lti/lti13-exercise-launch.component.ts b/src/main/webapp/app/lti/lti13-exercise-launch.component.ts index 6c9af31bc0c4..bbb1757c77f5 100644 --- a/src/main/webapp/app/lti/lti13-exercise-launch.component.ts +++ b/src/main/webapp/app/lti/lti13-exercise-launch.component.ts @@ -4,6 +4,8 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { AccountService } from 'app/core/auth/account.service'; import { captureException } from '@sentry/angular'; import { SessionStorageService } from 'ngx-webstorage'; +import { LtiService } from 'app/shared/service/lti.service'; +import { Theme, ThemeService } from 'app/core/theme/theme.service'; type LtiLaunchResponse = { targetLinkUri: string; @@ -24,6 +26,8 @@ export class Lti13ExerciseLaunchComponent implements OnInit { private accountService: AccountService, private router: Router, private sessionStorageService: SessionStorageService, + private ltiService: LtiService, + private themeService: ThemeService, ) { this.isLaunching = true; } @@ -142,6 +146,10 @@ export class Lti13ExerciseLaunchComponent implements OnInit { } replaceWindowLocationWrapper(url: string): void { - window.location.replace(url); + this.ltiService.setLti(true); + this.themeService.applyThemeExplicitly(Theme.LIGHT); + const path = new URL(url).pathname; + + this.router.navigate([path], { replaceUrl: true }); } } diff --git a/src/main/webapp/app/overview/course-exercises/course-exercises.component.html b/src/main/webapp/app/overview/course-exercises/course-exercises.component.html index 830d3863c55e..4b32dedb50f4 100644 --- a/src/main/webapp/app/overview/course-exercises/course-exercises.component.html +++ b/src/main/webapp/app/overview/course-exercises/course-exercises.component.html @@ -1,11 +1,11 @@

@if (course) { -
+
@if (exerciseSelected) { -
+
} @else { diff --git a/src/main/webapp/app/overview/course-exercises/course-exercises.component.ts b/src/main/webapp/app/overview/course-exercises/course-exercises.component.ts index 2238ede4702c..a8bfc240b99d 100644 --- a/src/main/webapp/app/overview/course-exercises/course-exercises.component.ts +++ b/src/main/webapp/app/overview/course-exercises/course-exercises.component.ts @@ -9,6 +9,7 @@ import { Exercise } from 'app/entities/exercise.model'; import { CourseStorageService } from 'app/course/manage/course-storage.service'; import { AccordionGroups, CollapseState, SidebarCardElement, SidebarData } from 'app/types/sidebar'; import { CourseOverviewService } from '../course-overview.service'; +import { LtiService } from 'app/shared/service/lti.service'; const DEFAULT_UNIT_GROUPS: AccordionGroups = { future: { entityData: [] }, @@ -34,6 +35,7 @@ const DEFAULT_COLLAPSE_STATE: CollapseState = { export class CourseExercisesComponent implements OnInit, OnDestroy { private parentParamSubscription: Subscription; private courseUpdatesSubscription: Subscription; + private ltiSubscription: Subscription; course?: Course; courseId: number; @@ -46,6 +48,7 @@ export class CourseExercisesComponent implements OnInit, OnDestroy { sidebarExercises: SidebarCardElement[] = []; isCollapsed: boolean = false; readonly DEFAULT_COLLAPSE_STATE = DEFAULT_COLLAPSE_STATE; + isLti: boolean = false; constructor( private courseStorageService: CourseStorageService, @@ -54,6 +57,7 @@ export class CourseExercisesComponent implements OnInit, OnDestroy { private programmingSubmissionService: ProgrammingSubmissionService, private router: Router, private courseOverviewService: CourseOverviewService, + private ltiService: LtiService, ) {} ngOnInit() { @@ -74,6 +78,10 @@ export class CourseExercisesComponent implements OnInit, OnDestroy { this.exerciseForGuidedTour = this.guidedTourService.enableTourForCourseExerciseComponent(this.course, courseExerciseOverviewTour, true); + this.ltiSubscription = this.ltiService.isLti$.subscribe((isLti) => { + this.isLti = isLti; + }); + // If no exercise is selected navigate to the lastSelected or upcoming exercise this.navigateToExercise(); } @@ -143,5 +151,6 @@ export class CourseExercisesComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.courseUpdatesSubscription?.unsubscribe(); this.parentParamSubscription?.unsubscribe(); + this.ltiSubscription?.unsubscribe(); } } diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index e5641f6db02f..958137c657b6 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -1,200 +1,214 @@ -