From 2a6c44b45b4a212fcc5cd68237bb577da0c9ea16 Mon Sep 17 00:00:00 2001 From: Tobias Lippert <84102468+tobias-lippert@users.noreply.github.com> Date: Fri, 15 Sep 2023 07:19:52 +0200 Subject: [PATCH 01/16] Programming exercises: Export files embedded with HTML syntax (#7187) --- .../ProgrammingExerciseExportService.java | 127 ++++++++++++++---- .../ProgrammingExerciseTestService.java | 8 +- 2 files changed, 107 insertions(+), 28 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java index ac7bed156470..de1a21404714 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseExportService.java @@ -50,7 +50,8 @@ import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.enumeration.RepositoryType; -import de.tum.in.www1.artemis.domain.participation.*; +import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; +import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.exception.GitException; import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; @@ -90,7 +91,9 @@ public class ProgrammingExerciseExportService { public static final String EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX = "Problem-Statement"; - private static final String EMBEDDED_FILE_REGEX = "\\[.*] *\\(/api/files/markdown/.*\\)"; + private static final String EMBEDDED_FILE_MARKDOWN_SYNTAX_REGEX = "\\[.*] *\\(/api/files/markdown/.*\\)"; + + private static final String EMBEDDED_FILE_HTML_SYNTAX_REGEX = ""; private static final String API_MARKDOWN_FILE_PATH = "/api/files/markdown/"; @@ -152,6 +155,14 @@ public Path exportProgrammingExerciseInstructorMaterial(ProgrammingExercise exer return pathToZippedExercise; } + /** + * Export problem statement and embedded files for a given programming exercise. + * + * @param exercise the programming exercise that is exported + * @param exportErrors List of failures that occurred during the export + * @param exportDir the directory where the content of the export is stored + * @param pathsToBeZipped the paths that should be included in the zip file + */ private void exportProblemStatementAndEmbeddedFiles(ProgrammingExercise exercise, List exportErrors, Path exportDir, List pathsToBeZipped) { var problemStatementFileExtension = ".md"; String problemStatementFileName = EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX + "-" + exercise.getTitle() + problemStatementFileExtension; @@ -168,15 +179,97 @@ private void exportProblemStatementAndEmbeddedFiles(ProgrammingExercise exercise * @param outputDir the directory where the content of the export is stored * @param pathsToBeZipped the paths that should be included in the zip file */ - private void copyEmbeddedFiles(ProgrammingExercise exercise, Path outputDir, List pathsToBeZipped, List exportErrors) { - Set embeddedFiles = new HashSet<>(); + Set embeddedFilesWithMarkdownSyntax = new HashSet<>(); + Set embeddedFilesWithHtmlSyntax = new HashSet<>(); + + Matcher matcherForMarkdownSyntax = Pattern.compile(EMBEDDED_FILE_MARKDOWN_SYNTAX_REGEX).matcher(exercise.getProblemStatement()); + Matcher matcherForHtmlSyntax = Pattern.compile(EMBEDDED_FILE_HTML_SYNTAX_REGEX).matcher(exercise.getProblemStatement()); + checkForMatchesInProblemStatementAndCreateDirectoryForFiles(outputDir, pathsToBeZipped, exportErrors, embeddedFilesWithMarkdownSyntax, matcherForMarkdownSyntax); + Path embeddedFilesDir = checkForMatchesInProblemStatementAndCreateDirectoryForFiles(outputDir, pathsToBeZipped, exportErrors, embeddedFilesWithHtmlSyntax, + matcherForHtmlSyntax); + // if the returned path is null the directory could not be created + if (embeddedFilesDir == null) { + return; + } + copyFilesEmbeddedWithMarkdownSyntax(exercise, exportErrors, embeddedFilesWithMarkdownSyntax, embeddedFilesDir); + copyFilesEmbeddedWithHtmlSyntax(exercise, exportErrors, embeddedFilesWithHtmlSyntax, embeddedFilesDir); + + } + + /** + * Copies the files that are embedded with Markdown syntax to the embedded files' directory. + * + * @param exercise the programming exercise that is exported + * @param exportErrors List of failures that occurred during the export + * @param embeddedFilesWithMarkdownSyntax the files that are embedded with Markdown syntax + * @param embeddedFilesDir the directory where the embedded files are stored + */ + private void copyFilesEmbeddedWithMarkdownSyntax(ProgrammingExercise exercise, List exportErrors, Set embeddedFilesWithMarkdownSyntax, Path embeddedFilesDir) { + for (String embeddedFile : embeddedFilesWithMarkdownSyntax) { + // avoid matching other closing ] or () in the squared brackets by getting the index of the last ] + String lastPartOfMatchedString = embeddedFile.substring(embeddedFile.lastIndexOf("]") + 1); + String filePath = lastPartOfMatchedString.substring(lastPartOfMatchedString.indexOf("(") + 1, lastPartOfMatchedString.indexOf(")")); + constructFilenameAndCopyFile(exercise, exportErrors, embeddedFilesDir, filePath); + } + } + + /** + * Copies the files that are embedded with html syntax to the embedded files' directory. + * + * @param exercise the programming exercise that is exported + * @param exportErrors List of failures that occurred during the export + * @param embeddedFilesWithHtmlSyntax the files that are embedded with html syntax + * @param embeddedFilesDir the directory where the embedded files are stored + */ + private void copyFilesEmbeddedWithHtmlSyntax(ProgrammingExercise exercise, List exportErrors, Set embeddedFilesWithHtmlSyntax, Path embeddedFilesDir) { + for (String embeddedFile : embeddedFilesWithHtmlSyntax) { + int indexOfFirstQuotationMark = embeddedFile.indexOf('"'); + String filePath = embeddedFile.substring(embeddedFile.indexOf("src=") + 5, embeddedFile.indexOf('"', indexOfFirstQuotationMark + 1)); + constructFilenameAndCopyFile(exercise, exportErrors, embeddedFilesDir, filePath); + } + } - Matcher matcher = Pattern.compile(EMBEDDED_FILE_REGEX).matcher(exercise.getProblemStatement()); + /** + * Extracts the filename from the matched string and copies the file to the embedded files' directory. + * + * @param exercise the programming exercise that is exported + * @param exportErrors List of failures that occurred during the export + * @param embeddedFilesDir the directory where the embedded files are stored + * @param filePath the path of the file that should be copied + */ + private void constructFilenameAndCopyFile(ProgrammingExercise exercise, List exportErrors, Path embeddedFilesDir, String filePath) { + String fileName = filePath.replace(API_MARKDOWN_FILE_PATH, ""); + Path imageFilePath = Path.of(FilePathService.getMarkdownFilePath(), fileName); + Path imageExportPath = embeddedFilesDir.resolve(fileName); + // we need this check as it might be that the matched string is different and not filtered out above but the file is already copied + if (!Files.exists(imageExportPath)) { + try { + Files.copy(imageFilePath, imageExportPath); + } + catch (IOException e) { + exportErrors.add("Failed to copy embedded files: " + e.getMessage()); + log.warn("Could not copy embedded file {} for exercise with id {}", fileName, exercise.getId()); + } + } + } + + /** + * Checks for matches in the problem statement and creates a directory for the embedded files. + * + * @param outputDir the directory where the content of the export is stored + * @param pathsToBeZipped the paths that should be included in the zip file + * @param exportErrors List of failures that occurred during the export + * @param embeddedFiles the files that are embedded in the problem statement + * @param matcher the matcher that is used to find the embedded files + * @return the path to the embedded files directory or null if the directory could not be created + */ + private Path checkForMatchesInProblemStatementAndCreateDirectoryForFiles(Path outputDir, List pathsToBeZipped, List exportErrors, Set embeddedFiles, + Matcher matcher) { while (matcher.find()) { embeddedFiles.add(matcher.group()); } - log.debug("Found embedded files:{} ", embeddedFiles); + log.debug("Found embedded files: {} ", embeddedFiles); Path embeddedFilesDir = outputDir.resolve("files"); if (!embeddedFiles.isEmpty()) { if (!Files.exists(embeddedFilesDir)) { @@ -186,30 +279,12 @@ private void copyEmbeddedFiles(ProgrammingExercise exercise, Path outputDir, Lis catch (IOException e) { exportErrors.add("Could not create directory for embedded files: " + e.getMessage()); log.warn("Could not create directory for embedded files. Won't include embedded files: " + e.getMessage()); - return; + return null; } } pathsToBeZipped.add(embeddedFilesDir); } - for (String embeddedFile : embeddedFiles) { - // avoid matching other closing ] or () in the squared brackets by getting the index of the last ] - String lastPartOfMatchedString = embeddedFile.substring(embeddedFile.lastIndexOf("]") + 1); - String filePath = lastPartOfMatchedString.substring(lastPartOfMatchedString.indexOf("(") + 1, lastPartOfMatchedString.indexOf(")")); - String fileName = filePath.replace(API_MARKDOWN_FILE_PATH, ""); - Path imageFilePath = Path.of(FilePathService.getMarkdownFilePath(), fileName); - Path imageExportPath = embeddedFilesDir.resolve(fileName); - // we need this check as it might be that the matched string is different and not filtered out above but the file is already copied - if (!Files.exists(imageExportPath)) { - try { - Files.copy(imageFilePath, imageExportPath); - } - catch (IOException e) { - exportErrors.add("Failed to copy embedded files: " + e.getMessage()); - log.warn("Could not copy embedded file {} for exercise with id {}", fileName, exercise.getId()); - } - } - } - + return embeddedFilesDir; } /** diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java index 88c0ca71d2cf..64a93b2726b6 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTestService.java @@ -1,6 +1,7 @@ package de.tum.in.www1.artemis.exercise.programmingexercise; -import static de.tum.in.www1.artemis.domain.enumeration.ExerciseMode.*; +import static de.tum.in.www1.artemis.domain.enumeration.ExerciseMode.INDIVIDUAL; +import static de.tum.in.www1.artemis.domain.enumeration.ExerciseMode.TEAM; import static de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage.*; import static de.tum.in.www1.artemis.service.programming.ProgrammingExerciseExportService.EXPORTED_EXERCISE_DETAILS_FILE_PREFIX; import static de.tum.in.www1.artemis.service.programming.ProgrammingExerciseExportService.EXPORTED_EXERCISE_PROBLEM_STATEMENT_FILE_PREFIX; @@ -17,6 +18,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.*; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -48,6 +50,8 @@ import de.tum.in.www1.artemis.config.StaticCodeAnalysisConfigurer; import de.tum.in.www1.artemis.course.CourseUtilService; import de.tum.in.www1.artemis.domain.*; +import de.tum.in.www1.artemis.domain.Authority; +import de.tum.in.www1.artemis.domain.AuxiliaryRepository; import de.tum.in.www1.artemis.domain.enumeration.*; import de.tum.in.www1.artemis.domain.exam.Exam; import de.tum.in.www1.artemis.domain.exam.ExamUser; @@ -1492,7 +1496,7 @@ private void generateProgrammingExerciseForExport(boolean saveEmbeddedFiles) thr exercise.setProblemStatement(String.format(""" Problem statement ![mountain.jpg](/api/files/markdown/%s) - ![matterhorn.jpg](/api/files/markdown/%s) + """, embeddedFileName1, embeddedFileName2)); if (saveEmbeddedFiles) { Files.write(Path.of(FilePathService.getMarkdownFilePath(), embeddedFileName1), From c2d29a2e09bbd059d2de82ff4298181b52bb9465 Mon Sep 17 00:00:00 2001 From: Tobias Lippert <84102468+tobias-lippert@users.noreply.github.com> Date: Fri, 15 Sep 2023 07:21:14 +0200 Subject: [PATCH 02/16] Exam mode: Allow instructors to notify students about programming exam exercise updates and highlight the changes (#7177) --- .../participate/exam-participation.module.ts | 4 +- ...m-exercise-update-highlighter.component.ts | 60 +++++++++++++++---- ...exam-exercise-update-highlighter.module.ts | 10 ++++ .../file-upload-exercise-management.module.ts | 2 + ...file-upload-exercise-update.component.html | 5 +- .../modeling-exercise-update.component.html | 5 +- .../manage/modeling-exercise.module.ts | 2 + ...programming-exercise-update.component.html | 6 ++ .../programming-exercise-update.module.ts | 2 + ...amming-exercise-instruction.component.html | 4 ++ ...gramming-exercise-instruction.component.ts | 16 ++++- ...ing-exercise-instructions-render.module.ts | 3 +- ...xercise-update-notification.component.html | 4 ++ .../exercise-update-notification.component.ts | 17 ++++++ .../exercise-update-notification.module.ts | 10 ++++ .../text-exercise-update.component.html | 5 +- .../text-exercise/text-exercise.module.ts | 2 + ...rcise-update-highlighter.component.spec.ts | 39 ++++++++++-- ...cise-update-notification.component.spec.ts | 30 ++++++++++ ...ing-exercise-instruction.component.spec.ts | 11 ++++ ...gramming-exercise-update.component.spec.ts | 2 + 21 files changed, 208 insertions(+), 31 deletions(-) create mode 100644 src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts create mode 100644 src/main/webapp/app/exercises/shared/exercise-update-notification/exercise-update-notification.component.html create mode 100644 src/main/webapp/app/exercises/shared/exercise-update-notification/exercise-update-notification.component.ts create mode 100644 src/main/webapp/app/exercises/shared/exercise-update-notification/exercise-update-notification.module.ts create mode 100644 src/test/javascript/spec/component/exercises/shared/exercise-update-notification.component.spec.ts diff --git a/src/main/webapp/app/exam/participate/exam-participation.module.ts b/src/main/webapp/app/exam/participate/exam-participation.module.ts index d57cef937a87..3ba594471fd3 100644 --- a/src/main/webapp/app/exam/participate/exam-participation.module.ts +++ b/src/main/webapp/app/exam/participate/exam-participation.module.ts @@ -29,8 +29,8 @@ import { ArtemisHeaderExercisePageWithDetailsModule } from 'app/exercises/shared import { ArtemisMarkdownModule } from 'app/shared/markdown.module'; import { FileUploadExamSubmissionComponent } from 'app/exam/participate/exercises/file-upload/file-upload-exam-submission.component'; import { ExamExerciseOverviewPageComponent } from 'app/exam/participate/exercises/exercise-overview-page/exam-exercise-overview-page.component'; -import { ExamExerciseUpdateHighlighterComponent } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component'; import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; +import { ExamExerciseUpdateHighlighterModule } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module'; const ENTITY_STATES = [...examParticipationState]; @@ -55,6 +55,7 @@ const ENTITY_STATES = [...examParticipationState]; ArtemisParticipationSummaryModule, ArtemisMarkdownModule, SubmissionResultStatusModule, + ExamExerciseUpdateHighlighterModule, ], declarations: [ ExamParticipationComponent, @@ -67,7 +68,6 @@ const ENTITY_STATES = [...examParticipationState]; ExamNavigationBarComponent, ExamTimerComponent, ExamExerciseOverviewPageComponent, - ExamExerciseUpdateHighlighterComponent, ], }) export class ArtemisExamParticipationModule {} diff --git a/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component.ts b/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component.ts index 9938725296e7..0b607e7ec78b 100644 --- a/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component.ts @@ -1,7 +1,7 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Subscription } from 'rxjs'; import { ExamExerciseUpdateService } from 'app/exam/manage/exam-exercise-update.service'; -import { Exercise } from 'app/entities/exercise.model'; +import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { Diff, DiffMatchPatch, DiffOperation } from 'diff-match-patch-typescript'; @Component({ @@ -9,13 +9,13 @@ import { Diff, DiffMatchPatch, DiffOperation } from 'diff-match-patch-typescript templateUrl: './exam-exercise-update-highlighter.component.html', styleUrls: ['./exam-exercise-update-highlighter.component.scss'], }) -export class ExamExerciseUpdateHighlighterComponent implements OnInit { +export class ExamExerciseUpdateHighlighterComponent implements OnInit, OnDestroy { subscriptionToLiveExamExerciseUpdates: Subscription; + themeSubscription: Subscription; previousProblemStatementUpdate: string; updatedProblemStatementWithHighlightedDifferences: string; updatedProblemStatement: string; showHighlightedDifferences = true; - @Input() exercise: Exercise; @Output() problemStatementUpdateEvent: EventEmitter = new EventEmitter(); @@ -28,6 +28,11 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit { }); } + ngOnDestroy(): void { + this.subscriptionToLiveExamExerciseUpdates?.unsubscribe(); + this.themeSubscription?.unsubscribe(); + } + /** * Switches the view between the new(updated) problem statement without the difference * with the view showing the difference between the new and old problem statement and vice versa. @@ -79,14 +84,49 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit { } this.previousProblemStatementUpdate = this.updatedProblemStatement; - + let removedDiagrams: string[] = []; + let diff: Diff[]; + if (this.exercise.type === ExerciseType.PROGRAMMING) { + const updatedProblemStatementAndRemovedDiagrams = this.removeAnyPlantUmlDiagramsInProblemStatement(this.updatedProblemStatement); + const outdatedProblemStatementAndRemovedDiagrams = this.removeAnyPlantUmlDiagramsInProblemStatement(outdatedProblemStatement); + const updatedProblemStatementWithoutDiagrams = updatedProblemStatementAndRemovedDiagrams.problemStatementWithoutPlantUmlDiagrams; + const outdatedProblemStatementWithoutDiagrams = outdatedProblemStatementAndRemovedDiagrams.problemStatementWithoutPlantUmlDiagrams; + removedDiagrams = updatedProblemStatementAndRemovedDiagrams.removedDiagrams; + diff = dmp.diff_main(outdatedProblemStatementWithoutDiagrams!, updatedProblemStatementWithoutDiagrams); + } else { + diff = dmp.diff_main(outdatedProblemStatement!, this.updatedProblemStatement); + } // finds the initial difference then cleans the text with added html & css elements - const diff = dmp.diff_main(outdatedProblemStatement!, this.updatedProblemStatement); dmp.diff_cleanupEfficiency(diff); this.updatedProblemStatementWithHighlightedDifferences = this.diffPrettyHtml(diff); + + if (this.exercise.type === ExerciseType.PROGRAMMING) { + this.addPlantUmlToProblemStatementWithDiffHighlightAgain(removedDiagrams); + } return this.updatedProblemStatementWithHighlightedDifferences; } + private addPlantUmlToProblemStatementWithDiffHighlightAgain(removedDiagrams: string[]) { + removedDiagrams.forEach((text) => { + this.updatedProblemStatementWithHighlightedDifferences = this.updatedProblemStatementWithHighlightedDifferences.replace('@startuml', '@startuml\n' + text + '\n'); + }); + } + + private removeAnyPlantUmlDiagramsInProblemStatement(problemStatement: string): { problemStatementWithoutPlantUmlDiagrams: string; removedDiagrams: string[] } { + // Regular expression to match content between @startuml and @enduml + const plantUmlSequenceRegex = /@startuml([\s\S]*?)@enduml/g; + const removedDiagrams: string[] = []; + const problemStatementWithoutPlantUmlDiagrams = problemStatement.replace(plantUmlSequenceRegex, (match, content) => { + removedDiagrams.push(content); + // we have to keep the markers, otherwise we cannot add the diagrams back later + return '@startuml\n@enduml'; + }); + return { + problemStatementWithoutPlantUmlDiagrams, + removedDiagrams, + }; + } + /** * Convert a diff array into a pretty HTML report. * Keeps markdown styling intact (not like the original method) @@ -98,17 +138,17 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit { * @param diffs Array of diff tuples. (from DiffMatchPatch) * @return the HTML representation as string with markdown intact. */ - diffPrettyHtml = function (diffs: Diff[]): string { + private diffPrettyHtml(diffs: Diff[]): string { const html: any[] = []; diffs.forEach((diff: Diff, index: number) => { const op = diffs[index][0]; // Operation (insert, delete, equal) const text = diffs[index][1]; // Text of change. switch (op) { case DiffOperation.DIFF_INSERT: - html[index] = '' + text + ''; + html[index] = '' + text + ''; break; case DiffOperation.DIFF_DELETE: - html[index] = '' + text + ''; + html[index] = '' + text + ''; break; case DiffOperation.DIFF_EQUAL: html[index] = text; @@ -116,5 +156,5 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit { } }); return html.join(''); - }; + } } diff --git a/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts b/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts new file mode 100644 index 000000000000..30e5255744b1 --- /dev/null +++ b/src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { ExamExerciseUpdateHighlighterComponent } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component'; + +@NgModule({ + declarations: [ExamExerciseUpdateHighlighterComponent], + imports: [ArtemisSharedCommonModule], + exports: [ExamExerciseUpdateHighlighterComponent], +}) +export class ExamExerciseUpdateHighlighterModule {} diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-management.module.ts b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-management.module.ts index 0b704a7bc039..2e0795a8afb2 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-management.module.ts +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-management.module.ts @@ -20,6 +20,7 @@ import { NonProgrammingExerciseDetailCommonActionsModule } from 'app/exercises/s import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercise-categories.module'; import { ExerciseTitleChannelNameModule } from 'app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.module'; +import { ExerciseUpdateNotificationModule } from 'app/exercises/shared/exercise-update-notification/exercise-update-notification.module'; @NgModule({ imports: [ @@ -41,6 +42,7 @@ import { ExerciseTitleChannelNameModule } from 'app/exercises/shared/exercise-ti ArtemisSharedComponentModule, ExerciseCategoriesModule, ExerciseTitleChannelNameModule, + ExerciseUpdateNotificationModule, ], declarations: [FileUploadExerciseComponent, FileUploadExerciseDetailComponent, FileUploadExerciseUpdateComponent], exports: [FileUploadExerciseComponent], diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.html b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.html index e4740153ec82..71b190057fb9 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.html +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise-update.component.html @@ -197,10 +197,7 @@

Assessment Instructions -
- - -
+
-
- - -
+
- + @@ -40,7 +40,7 @@ [buttonIcon]="faFolderOpen" class="open-code-editor" [jhiFeatureToggle]="FeatureToggle.ProgrammingExercises" - [buttonLabel]="'artemisApp.exerciseActions.' + (activeParticipation.testRun ? 'openPracticeCodeEditor' : 'openGradedCodeEditor') | artemisTranslate" + [buttonLabel]="'artemisApp.exerciseActions.' + (isPracticeMode ? 'openPracticeCodeEditor' : 'openGradedCodeEditor') | artemisTranslate" [buttonLoading]="loading" [smallButton]="smallButtons" [hideLabelMobile]="false" diff --git a/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.ts b/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.ts index 039ae8ddf45b..4871d89ba095 100644 --- a/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.ts +++ b/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.ts @@ -25,6 +25,7 @@ export class OpenCodeEditorButtonComponent implements OnChanges { courseAndExerciseNavigationUrl: string; activeParticipation: ProgrammingExerciseStudentParticipation; + isPracticeMode: boolean | undefined; // Icons faFolderOpen = faFolderOpen; @@ -38,6 +39,7 @@ export class OpenCodeEditorButtonComponent implements OnChanges { } switchPracticeMode() { - this.activeParticipation = this.participationService.getSpecificStudentParticipation(this.participations!, !this.activeParticipation.testRun)!; + this.isPracticeMode = !this.isPracticeMode; + this.activeParticipation = this.participationService.getSpecificStudentParticipation(this.participations!, this.isPracticeMode)!; } } diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java index e071bf93380e..3dfa4d907083 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ResultServiceIntegrationTest.java @@ -238,7 +238,7 @@ void setProgrammingExerciseResultRated(boolean shouldBeRated, ZonedDateTime buil @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void testTestRunsNonRated() { - programmingExerciseStudentParticipation.setTestRun(true); + programmingExerciseStudentParticipation.setPracticeMode(true); programmingExerciseStudentParticipation = programmingExerciseStudentParticipationRepository.save(programmingExerciseStudentParticipation); var submission = (ProgrammingSubmission) new ProgrammingSubmission().commitHash("abc").type(SubmissionType.MANUAL).submitted(true); diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index bd77cb71ecaa..ed857f5a52ee 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -946,7 +946,7 @@ public void testGetCoursesForDashboardPracticeRepositories() throws Exception { programmingExerciseUtilService.addProgrammingSubmissionToResultAndParticipation(gradedResult, gradedParticipation, "asdf"); StudentParticipation practiceParticipation = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INITIALIZED, programmingExercise, student1); - practiceParticipation.setTestRun(true); + practiceParticipation.setPracticeMode(true); participationRepository.save(practiceParticipation); Result practiceResult = participationUtilService.addResultToParticipation(AssessmentType.AUTOMATIC, ZonedDateTime.now().minusHours(1), practiceParticipation); practiceResult.setRated(false); diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseUtilService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseUtilService.java index fe523238414d..cc68fa3725bf 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseUtilService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseUtilService.java @@ -290,7 +290,7 @@ public List createCoursesWithExercisesAndLectures(String prefix, boolean StudentParticipation participation3 = ParticipationFactory.generateStudentParticipation(InitializationState.UNINITIALIZED, modelingExercise, user); StudentParticipation participation4 = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.FINISHED, programmingExercise, user); StudentParticipation participation5 = ParticipationFactory.generateProgrammingExerciseStudentParticipation(InitializationState.INITIALIZED, programmingExercise, user); - participation5.setTestRun(true); + participation5.setPracticeMode(true); Submission modelingSubmission1 = ParticipationFactory.generateModelingSubmission("model1", true); Submission modelingSubmission2 = ParticipationFactory.generateModelingSubmission("model2", true); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java index 564dbd60afb2..8b3a6699c17d 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseIntegrationTestService.java @@ -303,8 +303,8 @@ List exportSubmissionsWithPracticeSubmissionByParticipationIds(boolean exc doReturn(repository2).when(gitService).getOrCheckoutRepository(eq(participation2.getVcsRepositoryUrl()), anyString(), anyBoolean()); // Set one of the participations to practice mode - participation1.setTestRun(false); - participation2.setTestRun(true); + participation1.setPracticeMode(false); + participation2.setPracticeMode(true); final var participations = List.of(participation1, participation2); programmingExerciseStudentParticipationRepository.saveAll(participations); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java index bed74e3d4160..79ebb1edaadf 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseParticipationIntegrationTest.java @@ -427,7 +427,7 @@ void checkResetRepository_noAccess_forbidden() throws Exception { void checkResetRepository_noAccessToGradedParticipation_forbidden() throws Exception { var gradedParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student2"); var practiceParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student1"); - practiceParticipation.setTestRun(true); + practiceParticipation.setPracticeMode(true); participationRepository.save(practiceParticipation); request.put("/api/programming-exercise-participations/" + practiceParticipation.getId() + "/reset-repository?gradedParticipationId=" + gradedParticipation.getId(), null, diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTest.java index 2e8c72b59314..00a610275474 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingExerciseTest.java @@ -219,7 +219,7 @@ void testFindRelevantParticipations() { gradedParticipationFinished.setInitializationState(InitializationState.FINISHED); gradedParticipationFinished.setExercise(exercise); StudentParticipation practiceParticipation = new StudentParticipation(); - practiceParticipation.setTestRun(true); + practiceParticipation.setPracticeMode(true); practiceParticipation.setExercise(exercise); List allParticipations = List.of(gradedParticipationInitialized, gradedParticipationFinished, practiceParticipation); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultBitbucketBambooIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultBitbucketBambooIntegrationTest.java index 8a94261d32cd..2350f2b45477 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultBitbucketBambooIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultBitbucketBambooIntegrationTest.java @@ -949,7 +949,7 @@ void shouldCreateGradleFeedback() throws Exception { @ParameterizedTest(name = "{displayName} [{index}] {argumentsWithNames}") @MethodSource("testSubmissionAfterDueDateValues") @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void testSubmissionAfterDueDate(ZonedDateTime dueDate, SubmissionType expectedType, boolean expectedRated, boolean testRun) throws Exception { + void testSubmissionAfterDueDate(ZonedDateTime dueDate, SubmissionType expectedType, boolean expectedRated, boolean practiceMode) throws Exception { var user = userRepository.findUserWithGroupsAndAuthoritiesByLogin(TEST_PREFIX + "student1").orElseThrow(); Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise(); @@ -959,8 +959,8 @@ void testSubmissionAfterDueDate(ZonedDateTime dueDate, SubmissionType expectedTy // Add a participation for the programming exercise var participation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, user.getLogin()); - if (testRun) { - participation.setTestRun(testRun); + if (practiceMode) { + participation.setPracticeMode(practiceMode); participation = participationRepository.save(participation); } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java index 88baf78befc3..f2f2f9cc0494 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/RepositoryIntegrationTest.java @@ -888,7 +888,7 @@ void testCommitChangesAllowedForPracticeModeAfterDueDate() throws Exception { programmingExercise.setAssessmentType(AssessmentType.MANUAL); programmingExerciseRepository.save(programmingExercise); - participation.setTestRun(true); + participation.setPracticeMode(true); studentParticipationRepository.save(participation); testCommitChanges(); diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java index a071df4a63c6..2b763da0c2fc 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIIntegrationTest.java @@ -636,7 +636,7 @@ void testFetchPush_studentPracticeRepository() throws Exception { // Create practice participation. ProgrammingExerciseStudentParticipation practiceParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, student1Login); - practiceParticipation.setTestRun(true); + practiceParticipation.setPracticeMode(true); practiceParticipation.setRepositoryUrl(localVCLocalCITestService.constructLocalVCUrl("", "", projectKey1, practiceRepositorySlug)); programmingExerciseStudentParticipationRepository.save(practiceParticipation); @@ -682,7 +682,7 @@ void testFetchPush_teachingAssistantPracticeRepository() throws Exception { // Create practice participation. ProgrammingExerciseStudentParticipation practiceParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, tutor1Login); - practiceParticipation.setTestRun(true); + practiceParticipation.setPracticeMode(true); programmingExerciseStudentParticipationRepository.save(practiceParticipation); // Students should not be able to access, teaching assistants should be able to fetch and push and editors and higher should be able to fetch and push. @@ -721,7 +721,7 @@ void testFetchPush_instructorPracticeRepository() throws Exception { // Create practice participation. ProgrammingExerciseStudentParticipation practiceParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, instructor1Login); - practiceParticipation.setTestRun(true); + practiceParticipation.setPracticeMode(true); programmingExerciseStudentParticipationRepository.save(practiceParticipation); // Students should not be able to access, teaching assistants should be able to fetch, and editors and higher should be able to fetch and push. diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java index 9f93d1fa34e1..512daad3da8f 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCLocalCIParticipationIntegrationTest.java @@ -55,7 +55,7 @@ void testStartParticipation() throws Exception { StudentParticipation participation = request.postWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/participations", null, StudentParticipation.class, HttpStatus.CREATED); assertThat(participation).isNotNull(); - assertThat(participation.isTestRun()).isFalse(); + assertThat(participation.isPracticeMode()).isFalse(); assertThat(participation.getStudent()).contains(user); LocalVCRepositoryUrl studentAssignmentRepositoryUrl = new LocalVCRepositoryUrl(projectKey, projectKey.toLowerCase() + "-" + TEST_PREFIX + "student1", localVCBaseUrl); assertThat(studentAssignmentRepositoryUrl.getLocalRepositoryPath(localVCBasePath)).exists(); diff --git a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java index 4255a6dec31a..c3b757d87240 100644 --- a/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/participation/ParticipationIntegrationTest.java @@ -306,7 +306,7 @@ void participateInProgrammingExerciseAsEditorDueDatePassed() throws Exception { HttpStatus.CREATED); var participationUsers = participation.getStudents(); assertThat(participation).isNotNull(); - assertThat(participation.isTestRun()).isFalse(); + assertThat(participation.isPracticeMode()).isFalse(); assertThat(participationUsers).contains(user); } @@ -339,7 +339,7 @@ void practiceProgrammingExercise_successful() throws Exception { StudentParticipation participation = request.postWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/participations/practice", null, StudentParticipation.class, HttpStatus.CREATED); assertThat(participation).isNotNull(); - assertThat(participation.isTestRun()).isTrue(); + assertThat(participation.isPracticeMode()).isTrue(); assertThat(participation.getStudent()).contains(user); } @@ -354,7 +354,7 @@ void participateInProgrammingExercise_successful() throws Exception { StudentParticipation participation = request.postWithResponseBody("/api/exercises/" + programmingExercise.getId() + "/participations", null, StudentParticipation.class, HttpStatus.CREATED); assertThat(participation).isNotNull(); - assertThat(participation.isTestRun()).isFalse(); + assertThat(participation.isPracticeMode()).isFalse(); assertThat(participation.getStudent()).contains(user); } @@ -593,7 +593,7 @@ void getAllParticipationsForExercise() throws Exception { participationUtilService.createAndSaveParticipationForExercise(textExercise, TEST_PREFIX + "student1"); participationUtilService.createAndSaveParticipationForExercise(textExercise, TEST_PREFIX + "student2"); StudentParticipation testParticipation = participationUtilService.createAndSaveParticipationForExercise(textExercise, TEST_PREFIX + "student3"); - testParticipation.setTestRun(true); + testParticipation.setPracticeMode(true); participationRepo.save(testParticipation); var participations = request.getList("/api/exercises/" + textExercise.getId() + "/participations", HttpStatus.OK, StudentParticipation.class); assertThat(participations).as("Exactly 3 participations are returned").hasSize(3).as("Only participation that has student are returned") @@ -617,7 +617,7 @@ void getAllParticipationsForExercise_withLatestResults() throws Exception { Submission onlySubmission = textExerciseUtilService.createSubmissionForTextExercise(textExercise, students.get(2), "asdf"); StudentParticipation testParticipation = participationUtilService.createAndSaveParticipationForExercise(textExercise, TEST_PREFIX + "student4"); - testParticipation.setTestRun(true); + testParticipation.setPracticeMode(true); participationRepo.save(testParticipation); final var params = new LinkedMultiValueMap(); @@ -1066,7 +1066,7 @@ void getSubmissionOfParticipation() throws Exception { @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") void cleanupBuildPlan(boolean practiceMode, boolean afterDueDate) throws Exception { var participation = participationUtilService.addStudentParticipationForProgrammingExercise(programmingExercise, TEST_PREFIX + "student1"); - participation.setTestRun(practiceMode); + participation.setPracticeMode(practiceMode); participationRepo.save(participation); if (afterDueDate) { programmingExercise.setDueDate(ZonedDateTime.now().minusHours(1)); diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java index 2e87e4cca224..083965aa0378 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java @@ -183,7 +183,7 @@ void testStartPracticeMode(boolean useGradedParticipation) throws URISyntaxExcep StudentParticipation studentParticipationReceived = participationService.startPracticeMode(programmingExercise, participant, Optional.of((StudentParticipation) gradedResult.getParticipation()), useGradedParticipation); - assertThat(studentParticipationReceived.isTestRun()).isTrue(); + assertThat(studentParticipationReceived.isPracticeMode()).isTrue(); assertThat(studentParticipationReceived.getExercise()).isEqualTo(programmingExercise); assertThat(studentParticipationReceived.getStudent()).isPresent(); assertThat(studentParticipationReceived.getStudent().get()).isEqualTo(participant); From 233dbd13be7c5727edaf131960eb39ad2a20463b Mon Sep 17 00:00:00 2001 From: Nityananda Zbil Date: Fri, 15 Sep 2023 10:06:36 +0200 Subject: [PATCH 07/16] Communication: Configure messaging code of conduct (#7118) --- .../java/de/tum/in/www1/artemis/domain/Course.java | 10 ++++++++++ .../liquibase/changelog/20230907225501_changelog.xml | 10 ++++++++++ src/main/resources/config/liquibase/master.xml | 1 + .../app/course/manage/course-update.component.html | 12 ++++++++++++ .../app/course/manage/course-update.component.ts | 9 +++++++++ src/main/webapp/app/entities/course.model.ts | 1 + src/main/webapp/i18n/de/course.json | 6 +++++- src/main/webapp/i18n/en/course.json | 6 +++++- .../component/course/course-update.component.spec.ts | 10 ++++++---- 9 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 src/main/resources/config/liquibase/changelog/20230907225501_changelog.xml diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index f67803ed8073..952dd81e6af6 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -131,6 +131,9 @@ public class Course extends DomainObject { @JsonView(QuizView.Before.class) private CourseInformationSharingConfiguration courseInformationSharingConfiguration = CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING; // default value + @Column(name = "info_sharing_messaging_code_of_conduct") + private String courseInformationSharingMessagingCodeOfConduct; + @Column(name = "max_complaints", nullable = false) @JsonView(QuizView.Before.class) private Integer maxComplaints = 3; // default value @@ -1005,4 +1008,11 @@ public void setCourseInformationSharingConfiguration(CourseInformationSharingCon this.courseInformationSharingConfiguration = courseInformationSharingConfiguration; } + public String getCourseInformationSharingMessagingCodeOfConduct() { + return this.courseInformationSharingMessagingCodeOfConduct; + } + + public void setCourseInformationSharingMessagingCodeOfConduct(String courseInformationSharingMessagingCodeOfConduct) { + this.courseInformationSharingMessagingCodeOfConduct = courseInformationSharingMessagingCodeOfConduct; + } } diff --git a/src/main/resources/config/liquibase/changelog/20230907225501_changelog.xml b/src/main/resources/config/liquibase/changelog/20230907225501_changelog.xml new file mode 100644 index 000000000000..6ce1a1de6303 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20230907225501_changelog.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index eb868ed1b3bf..103a5319cfba 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -53,6 +53,7 @@ + diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index 180893a4d23b..b943661e4fa1 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -336,6 +336,18 @@
ngbTooltip="{{ 'artemisApp.course.courseCommunicationSetting.messagingEnabled.tooltip' | artemisTranslate }}" >
+
+ + + + +
diff --git a/src/main/webapp/app/course/manage/course-update.component.ts b/src/main/webapp/app/course/manage/course-update.component.ts index beb9d3162bb1..315fe2e8c2d1 100644 --- a/src/main/webapp/app/course/manage/course-update.component.ts +++ b/src/main/webapp/app/course/manage/course-update.component.ts @@ -156,6 +156,7 @@ export class CourseUpdateComponent implements OnInit { editorGroupName: new FormControl(this.course.editorGroupName), instructorGroupName: new FormControl(this.course.instructorGroupName), description: new FormControl(this.course.description), + courseInformationSharingMessagingCodeOfConduct: new FormControl(this.course.courseInformationSharingMessagingCodeOfConduct), organizations: new FormControl(this.courseOrganizations), startDate: new FormControl(this.course.startDate), endDate: new FormControl(this.course.endDate), @@ -520,6 +521,14 @@ export class CourseUpdateComponent implements OnInit { this.courseForm.controls['registrationConfirmationMessage'].setValue(message); } + /** + * Updates courseInformationSharingMessagingCodeOfConduct on markdown change + * @param message new courseInformationSharingMessagingCodeOfConduct + */ + updateCourseInformationSharingMessagingCodeOfConduct(message: string) { + this.courseForm.controls['courseInformationSharingMessagingCodeOfConduct'].setValue(message); + } + /** * Auxiliary method checking if online course is currently true */ diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index aae8b0627951..c155170c05e5 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -97,6 +97,7 @@ export class Course implements BaseEntity { public tutorialGroups?: TutorialGroup[]; public onlineCourseConfiguration?: OnlineCourseConfiguration; public courseInformationSharingConfiguration?: CourseInformationSharingConfiguration; + public courseInformationSharingMessagingCodeOfConduct?: string; // helper attributes public isAtLeastTutor?: boolean; diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index 642561a24be8..3390637f6d32 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -93,7 +93,11 @@ }, "messagingEnabled": { "label": "Nachrichten aktiviert", - "tooltip": "Ermöglicht den Nachrichtenaustausch zwischen Nutzer:innen in privaten oder öffentlichen Kanälen, Gruppenchats oder Direktnachrichten. Kanäle können nur von Lehrenden und Tutor:innen erstellt werden. Nutzer:innen können selbst öffentlichen Kanälen beitreten und müssen zu privaten Kanälen hinzugefügt werden. Alle Nutzer:innen können einen privaten Gruppenchat starten und andere Nutzer:innen hinzufügen. Ein Gruppenchat ist auf zehn Mitglieder:innen begrenzt. Alle Nutzer:innen können Direktnachrichten an andere Nutzer:innen senden. Die Chats finden im Nachrichtenbereich des Kurses statt." + "tooltip": "Ermöglicht den Nachrichtenaustausch zwischen Nutzer:innen in privaten oder öffentlichen Kanälen, Gruppenchats oder Direktnachrichten. Kanäle können nur von Lehrenden und Tutor:innen erstellt werden. Nutzer:innen können selbst öffentlichen Kanälen beitreten und müssen zu privaten Kanälen hinzugefügt werden. Alle Nutzer:innen können einen privaten Gruppenchat starten und andere Nutzer:innen hinzufügen. Ein Gruppenchat ist auf zehn Mitglieder:innen begrenzt. Alle Nutzer:innen können Direktnachrichten an andere Nutzer:innen senden. Die Chats finden im Nachrichtenbereich des Kurses statt.", + "codeOfConduct": { + "label": "Verhaltenskodex", + "tooltip": "Der Verhaltenskodex gibt Nutzer:innen an, wie sie miteinander kommunizieren sollen und welche Konsequenzen bei Fehlverhalten drohen können, sowie einen Kontakt zur Berichterstattung." + } } }, "registrationEnabled": { diff --git a/src/main/webapp/i18n/en/course.json b/src/main/webapp/i18n/en/course.json index 978785393729..6e8695f31d42 100644 --- a/src/main/webapp/i18n/en/course.json +++ b/src/main/webapp/i18n/en/course.json @@ -93,7 +93,11 @@ }, "messagingEnabled": { "label": "Messaging Enabled", - "tooltip": "Enables messaging between course users in private or public channels, group chats or direct messages. Channels can only be created by instructors and tutors. Users can self-join public channels and must be invited to private channels. Every user can start a private group chat and add other users. A group chat is limited to 10 members. Every user can start a private one-to-one chat with another user. The chats happens in the messaging space of the course." + "tooltip": "Enables messaging between course users in private or public channels, group chats or direct messages. Channels can only be created by instructors and tutors. Users can self-join public channels and must be invited to private channels. Every user can start a private group chat and add other users. A group chat is limited to 10 members. Every user can start a private one-to-one chat with another user. The chats happens in the messaging space of the course.", + "codeOfConduct": { + "label": "Code of Conduct", + "tooltip": "The Code of Conduct describes to users how best to communicate and which consequences might be raised if there is misconduct, as well as, contact information for reporting." + } } }, "registrationEnabled": { diff --git a/src/test/javascript/spec/component/course/course-update.component.spec.ts b/src/test/javascript/spec/component/course/course-update.component.spec.ts index bc5e82b7cff1..cee5192b8d4b 100644 --- a/src/test/javascript/spec/component/course/course-update.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-update.component.spec.ts @@ -14,6 +14,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { HasAnyAuthorityDirective } from 'app/shared/auth/has-any-authority.directive'; import { ColorSelectorComponent } from 'app/shared/color-selector/color-selector.component'; import { FormDateTimePickerComponent } from 'app/shared/date-time-picker/date-time-picker.component'; +import { HelpIconComponent } from 'app/shared/components/help-icon.component'; import { SecuredImageComponent } from 'app/shared/image/secured-image.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; @@ -107,13 +108,14 @@ describe('Course Management Update Component', () => { declarations: [ CourseUpdateComponent, MarkdownEditorStubComponent, - MockPipe(ArtemisTranslatePipe), - MockComponent(SecuredImageComponent), - MockComponent(FormDateTimePickerComponent), MockComponent(ColorSelectorComponent), + MockComponent(FormDateTimePickerComponent), + MockComponent(HelpIconComponent), + MockComponent(SecuredImageComponent), + MockDirective(FeatureToggleHideDirective), MockDirective(HasAnyAuthorityDirective), MockDirective(TranslateDirective), - MockDirective(FeatureToggleHideDirective), + MockPipe(ArtemisTranslatePipe), MockPipe(RemoveKeysPipe), ], }) From 700ff5e1e360ba3a47701324c15dd736c2d17422 Mon Sep 17 00:00:00 2001 From: Laurenz Blumentritt <38919977+laurenzfb@users.noreply.github.com> Date: Fri, 15 Sep 2023 14:29:23 +0200 Subject: [PATCH 08/16] Development: Add auxiliary repository support for Local VC CI (#7064) --- .../service/RepositoryAccessService.java | 14 +- .../LocalCIBuildJobExecutionService.java | 53 ++++++- .../LocalCIBuildJobManagementService.java | 2 - .../localci/LocalCIContainerService.java | 139 +++++++++++++++--- ...alCIProgrammingLanguageFeatureService.java | 2 +- .../localvc/LocalVCServletService.java | 14 +- .../AuxiliaryRepositoryService.java | 17 +++ ...ogrammingExerciseParticipationService.java | 10 +- .../repository/TestRepositoryResource.java | 8 +- ...programming-exercise-detail.component.html | 8 +- .../programming-exercise-detail.component.ts | 1 - .../service/RepositoryAccessServiceTest.java | 2 +- 12 files changed, 225 insertions(+), 45 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java b/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java index 95bd3d359866..4b0bc207736c 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/RepositoryAccessService.java @@ -147,20 +147,22 @@ else if (!isStudent && !isOwner) { * Checks if the user has access to the test repository of the given programming exercise. * Throws an {@link AccessForbiddenException} otherwise. * - * @param atLeastEditor if true, the user needs at least editor permissions, otherwise only teaching assistant permissions are required. - * @param exercise the programming exercise the test repository belongs to. - * @param user the user that wants to access the test repository. + * @param atLeastEditor if true, the user needs at least editor permissions, otherwise only teaching assistant permissions are required. + * @param exercise the programming exercise the test repository belongs to. + * @param user the user that wants to access the test repository. + * @param repositoryType the type of the repository. */ - public void checkAccessTestRepositoryElseThrow(boolean atLeastEditor, ProgrammingExercise exercise, User user) { + public void checkAccessTestOrAuxRepositoryElseThrow(boolean atLeastEditor, ProgrammingExercise exercise, User user, String repositoryType) { if (atLeastEditor) { if (!authorizationCheckService.isAtLeastEditorInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), user)) { - throw new AccessForbiddenException("You are not allowed to access the test repository of this programming exercise."); + throw new AccessForbiddenException("You are not allowed to push to the " + repositoryType + " repository of this programming exercise."); } } else { if (!authorizationCheckService.isAtLeastTeachingAssistantInCourse(exercise.getCourseViaExerciseGroupOrCourseMember(), user)) { - throw new AccessForbiddenException("You are not allowed to push to the test repository of this programming exercise."); + throw new AccessForbiddenException("You are not allowed to access the " + repositoryType + " repository of this programming exercise."); } } } + } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java index b9502e7df63e..0924bc65e3d6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobExecutionService.java @@ -17,6 +17,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.io.IOUtils; +import org.hibernate.Hibernate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -28,9 +29,11 @@ import com.github.dockerjava.api.model.HostConfig; import de.tum.in.www1.artemis.config.localvcci.LocalCIConfiguration; +import de.tum.in.www1.artemis.domain.AuxiliaryRepository; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.LocalCIException; import de.tum.in.www1.artemis.exception.localvc.LocalVCInternalException; +import de.tum.in.www1.artemis.repository.AuxiliaryRepositoryRepository; import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService; import de.tum.in.www1.artemis.service.connectors.localci.dto.LocalCIBuildResult; import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUrl; @@ -54,6 +57,8 @@ public class LocalCIBuildJobExecutionService { private final LocalCIContainerService localCIContainerService; + private final AuxiliaryRepositoryRepository auxiliaryRepositoryRepository; + /** * Instead of creating a new XMLInputFactory for every build job, it is created once and provided as a Bean (see {@link LocalCIConfiguration#localCIXMLInputFactory()}). */ @@ -66,16 +71,17 @@ public class LocalCIBuildJobExecutionService { private String localVCBasePath; public LocalCIBuildJobExecutionService(LocalCIBuildPlanService localCIBuildPlanService, Optional versionControlService, - LocalCIContainerService localCIContainerService, XMLInputFactory localCIXMLInputFactory) { + LocalCIContainerService localCIContainerService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, XMLInputFactory localCIXMLInputFactory) { this.localCIBuildPlanService = localCIBuildPlanService; this.versionControlService = versionControlService; this.localCIContainerService = localCIContainerService; + this.auxiliaryRepositoryRepository = auxiliaryRepositoryRepository; this.localCIXMLInputFactory = localCIXMLInputFactory; } public enum LocalCIBuildJobRepositoryType { - ASSIGNMENT("assignment"), TEST("test"); + ASSIGNMENT("assignment"), TEST("test"), AUXILIARY("auxiliary"); private final String name; @@ -104,14 +110,47 @@ public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participa // Update the build plan status to "BUILDING". localCIBuildPlanService.updateBuildPlanStatus(participation, ContinuousIntegrationService.BuildStatus.BUILDING); + List auxiliaryRepositories; + + // If the auxiliary repositories are not initialized, we need to fetch them from the database. + if (Hibernate.isInitialized(participation.getProgrammingExercise().getAuxiliaryRepositories())) { + auxiliaryRepositories = participation.getProgrammingExercise().getAuxiliaryRepositories(); + } + else { + auxiliaryRepositories = auxiliaryRepositoryRepository.findByExerciseId(participation.getProgrammingExercise().getId()); + } + + // Prepare script + Path buildScriptPath = localCIContainerService.createBuildScript(participation.getProgrammingExercise(), auxiliaryRepositories); + // Retrieve the paths to the repositories that the build job needs. // This includes the assignment repository (the one to be tested, e.g. the student's repository, or the template repository), and the tests repository which includes // the tests to be executed. LocalVCRepositoryUrl assignmentRepositoryUrl; LocalVCRepositoryUrl testsRepositoryUrl; + LocalVCRepositoryUrl[] auxiliaryRepositoriesUrls; + Path[] auxiliaryRepositoriesPaths; + String[] auxiliaryRepositoryNames; + try { assignmentRepositoryUrl = new LocalVCRepositoryUrl(participation.getRepositoryUrl(), localVCBaseUrl); testsRepositoryUrl = new LocalVCRepositoryUrl(participation.getProgrammingExercise().getTestRepositoryUrl(), localVCBaseUrl); + + if (!auxiliaryRepositories.isEmpty()) { + auxiliaryRepositoriesUrls = new LocalVCRepositoryUrl[auxiliaryRepositories.size()]; + auxiliaryRepositoriesPaths = new Path[auxiliaryRepositories.size()]; + auxiliaryRepositoryNames = new String[auxiliaryRepositories.size()]; + + for (int i = 0; i < auxiliaryRepositories.size(); i++) { + auxiliaryRepositoriesUrls[i] = new LocalVCRepositoryUrl(auxiliaryRepositories.get(i).getRepositoryUrl(), localVCBaseUrl); + auxiliaryRepositoriesPaths[i] = auxiliaryRepositoriesUrls[i].getLocalRepositoryPath(localVCBasePath).toAbsolutePath(); + auxiliaryRepositoryNames[i] = auxiliaryRepositories.get(i).getName(); + } + } + else { + auxiliaryRepositoriesPaths = new Path[0]; + auxiliaryRepositoryNames = new String[0]; + } } catch (LocalVCInternalException e) { throw new LocalCIException("Error while creating LocalVCRepositoryUrl", e); @@ -130,7 +169,8 @@ public LocalCIBuildResult runBuildJob(ProgrammingExerciseParticipation participa // Create the volume configuration for the container. The assignment repository, the tests repository, and the build script are bound into the container to be used by // the build job. - HostConfig volumeConfig = localCIContainerService.createVolumeConfig(assignmentRepositoryPath, testsRepositoryPath); + HostConfig volumeConfig = localCIContainerService.createVolumeConfig(assignmentRepositoryPath, testsRepositoryPath, auxiliaryRepositoriesPaths, auxiliaryRepositoryNames, + buildScriptPath); // Create the container from the "ls1tum/artemis-maven-template" image with the local paths to the Git repositories and the shell script bound to it. Also give the // container information about the branch and commit hash to be used. @@ -182,6 +222,8 @@ private LocalCIBuildResult runScriptAndParseResults(ProgrammingExerciseParticipa // Could not read commit hash from .git folder. Stop the container and return a build result that indicates that the build failed (empty list for failed tests and // empty list for successful tests). localCIContainerService.stopContainer(containerName); + // Delete script file from host system + localCIContainerService.deleteScriptFile(participation.getProgrammingExercise().getId().toString()); return constructFailedBuildResult(branch, assignmentRepoCommitHash, testRepoCommitHash, buildCompletedDate); } @@ -198,11 +240,16 @@ private LocalCIBuildResult runScriptAndParseResults(ProgrammingExerciseParticipa // If the test results are not found, this means that something went wrong during the build and testing of the submission. // Stop the container and return a build results that indicates that the build failed. localCIContainerService.stopContainer(containerName); + // Delete script file from host system + localCIContainerService.deleteScriptFile(participation.getProgrammingExercise().getId().toString()); return constructFailedBuildResult(branch, assignmentRepoCommitHash, testRepoCommitHash, buildCompletedDate); } localCIContainerService.stopContainer(containerName); + // Delete script file from host system + localCIContainerService.deleteScriptFile(participation.getProgrammingExercise().getId().toString()); + LocalCIBuildResult buildResult; try { buildResult = parseTestResults(testResultsTarInputStream, branch, assignmentRepoCommitHash, testRepoCommitHash, buildCompletedDate); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobManagementService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobManagementService.java index 24e208c14e4a..85133a964ca4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobManagementService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIBuildJobManagementService.java @@ -73,8 +73,6 @@ public LocalCIBuildJobManagementService(LocalCIBuildJobExecutionService localCIB * @throws LocalCIException If the build job could not be submitted to the executor service. */ public CompletableFuture addBuildJobToQueue(ProgrammingExerciseParticipation participation, String commitHash) { - - // It should not be possible to create a programming exercise with a different project type than Gradle. This is just a sanity check. ProjectType projectType = participation.getProgrammingExercise().getProjectType(); if (projectType == null || !projectType.isGradle()) { throw new LocalCIException("Project type must be Gradle."); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java index 344f9d1c2ece..5d27a0717c92 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java @@ -1,7 +1,10 @@ package de.tum.in.www1.artemis.service.connectors.localci; +import java.io.BufferedWriter; +import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Optional; @@ -24,7 +27,8 @@ import com.github.dockerjava.api.model.HostConfig; import com.github.dockerjava.api.model.Volume; -import de.tum.in.www1.artemis.config.localvcci.LocalCIConfiguration; +import de.tum.in.www1.artemis.domain.AuxiliaryRepository; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.exception.LocalCIException; /** @@ -39,33 +43,35 @@ public class LocalCIContainerService { private final DockerClient dockerClient; - /** - * The Path to the script file located in the resources folder. The script file contains the steps that run the tests on the Docker container. - * This path is provided as a Bean, because the retrieval is quite costly in the production environment (see {@link LocalCIConfiguration#buildScriptFilePath()}). - */ - private final Path buildScriptFilePath; - @Value("${artemis.continuous-integration.build.images.java.default}") String dockerImage; - public LocalCIContainerService(DockerClient dockerClient, Path buildScriptFilePath) { + public LocalCIContainerService(DockerClient dockerClient) { this.dockerClient = dockerClient; - this.buildScriptFilePath = buildScriptFilePath; } /** * Configure the volumes of the container such that it can access the assignment repository, the test repository, and the build script. * - * @param assignmentRepositoryPath the path to the assignment repository in the file system - * @param testRepositoryPath the path to the test repository in the file system + * @param assignmentRepositoryPath the path to the assignment repository in the file system + * @param testRepositoryPath the path to the test repository in the file system + * @param auxiliaryRepositoriesPaths the paths to the auxiliary repositories in the file system + * @param auxiliaryRepositoryNames the names of the auxiliary repositories + * @param buildScriptPath the path to the build script in the file system * @return the host configuration for the container containing the binds to the assignment repository, the test repository, and the build script */ - public HostConfig createVolumeConfig(Path assignmentRepositoryPath, Path testRepositoryPath) { - return HostConfig.newHostConfig().withAutoRemove(true) // Automatically remove the container when it exits. - .withBinds( - new Bind(assignmentRepositoryPath.toString(), new Volume("/" + LocalCIBuildJobExecutionService.LocalCIBuildJobRepositoryType.ASSIGNMENT + "-repository")), - new Bind(testRepositoryPath.toString(), new Volume("/" + LocalCIBuildJobExecutionService.LocalCIBuildJobRepositoryType.TEST + "-repository")), - new Bind(buildScriptFilePath.toString(), new Volume("/script.sh"))); + public HostConfig createVolumeConfig(Path assignmentRepositoryPath, Path testRepositoryPath, Path[] auxiliaryRepositoriesPaths, String[] auxiliaryRepositoryNames, + Path buildScriptPath) { + + Bind[] binds = new Bind[3 + auxiliaryRepositoriesPaths.length]; + binds[0] = new Bind(assignmentRepositoryPath.toString(), new Volume("/" + LocalCIBuildJobExecutionService.LocalCIBuildJobRepositoryType.ASSIGNMENT + "-repository")); + binds[1] = new Bind(testRepositoryPath.toString(), new Volume("/" + LocalCIBuildJobExecutionService.LocalCIBuildJobRepositoryType.TEST + "-repository")); + for (int i = 0; i < auxiliaryRepositoriesPaths.length; i++) { + binds[2 + i] = new Bind(auxiliaryRepositoriesPaths[i].toString(), new Volume("/" + auxiliaryRepositoryNames[i] + "-repository")); + } + binds[2 + auxiliaryRepositoriesPaths.length] = new Bind(buildScriptPath.toString(), new Volume("/script.sh")); + + return HostConfig.newHostConfig().withAutoRemove(true).withBinds(binds); // Automatically remove the container when it exits. } /** @@ -196,4 +202,103 @@ public void stopContainer(String containerName) { ExecCreateCmdResponse createStopContainerFileCmdResponse = dockerClient.execCreateCmd(containerId).withCmd("touch", "stop_container.txt").exec(); dockerClient.execStartCmd(createStopContainerFileCmdResponse.getId()).exec(new ResultCallback.Adapter<>()); } + + /** + * Creates a build script for a given programming exercise. + * The build script is stored in a file in the local-ci-scripts directory. + * The build script is used to build the programming exercise in a Docker container. + * + * @param programmingExercise the programming exercise for which to create the build script + * @param auxiliaryRepositories the auxiliary repositories of the programming exercise + * @return the path to the build script file + */ + public Path createBuildScript(ProgrammingExercise programmingExercise, List auxiliaryRepositories) { + Long programmingExerciseId = programmingExercise.getId(); + boolean hasAuxiliaryRepositories = auxiliaryRepositories != null && !auxiliaryRepositories.isEmpty(); + + Path scriptsPath = Path.of("local-ci-scripts"); + + if (!Files.exists(scriptsPath)) { + try { + Files.createDirectory(scriptsPath); + } + catch (IOException e) { + throw new LocalCIException("Failed to create directory for local CI scripts", e); + } + } + + String buildScriptPath = scriptsPath.toAbsolutePath() + "/" + programmingExerciseId.toString() + "-build.sh"; + + StringBuilder buildScript = new StringBuilder(""" + #!/bin/bash + mkdir /repositories + cd /repositories + """); + + // Checkout tasks + buildScript.append(""" + git clone --depth 1 --branch $ARTEMIS_DEFAULT_BRANCH file:///test-repository + git clone --depth 1 --branch $ARTEMIS_DEFAULT_BRANCH file:///assignment-repository + """); + + if (hasAuxiliaryRepositories) { + for (AuxiliaryRepository auxiliaryRepository : auxiliaryRepositories) { + buildScript.append("git clone --depth 1 --branch $ARTEMIS_DEFAULT_BRANCH file:///").append(auxiliaryRepository.getName()).append("-repository\n"); + } + } + + buildScript.append(""" + cd assignment-repository + if [ -n "$ARTEMIS_ASSIGNMENT_REPOSITORY_COMMIT_HASH" ]; then + git fetch --depth 1 origin "$ARTEMIS_ASSIGNMENT_REPOSITORY_COMMIT_HASH" + git checkout "$ARTEMIS_ASSIGNMENT_REPOSITORY_COMMIT_HASH" + fi + mkdir /repositories/test-repository/assignment + cp -a /repositories/assignment-repository/. /repositories/test-repository/assignment/ + """); + + // Copy auxiliary repositories to checkout directories + if (hasAuxiliaryRepositories) { + for (AuxiliaryRepository auxiliaryRepository : auxiliaryRepositories) { + buildScript.append("cp -a /repositories/").append(auxiliaryRepository.getName()).append("-repository/. /repositories/test-repository/") + .append(auxiliaryRepository.getCheckoutDirectory()).append("/\n"); + } + } + + buildScript.append("cd /repositories/test-repository\n"); + + // programming language specific tasks + buildScript.append(""" + chmod +x gradlew + sed -i -e 's/\\r$//' gradlew + ./gradlew clean test"""); + + try { + BufferedWriter writer = new BufferedWriter(new FileWriter(buildScriptPath)); + writer.write(buildScript.toString()); + writer.close(); + } + catch (IOException e) { + throw new LocalCIException("Failed to create build script file", e); + } + + return Path.of(buildScriptPath); + } + + /** + * Deletes the build script for a given programming exercise. + * The build script is stored in a file in the local-ci-scripts directory. + * + * @param exerciseID the ID of the programming exercise for which to delete the build script + */ + public void deleteScriptFile(String exerciseID) { + Path scriptsPath = Path.of("local-ci-scripts"); + String buildScriptPath = scriptsPath.toAbsolutePath() + "/" + exerciseID + "-build.sh"; + try { + Files.deleteIfExists(Path.of(buildScriptPath)); + } + catch (IOException e) { + throw new LocalCIException("Failed to delete build script file", e); + } + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIProgrammingLanguageFeatureService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIProgrammingLanguageFeatureService.java index 3aa694c12115..040bd2d13a7b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIProgrammingLanguageFeatureService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIProgrammingLanguageFeatureService.java @@ -21,7 +21,7 @@ public class LocalCIProgrammingLanguageFeatureService extends ProgrammingLanguag public LocalCIProgrammingLanguageFeatureService() { // Must be extended once a new programming language is added // TODO LOCALVC_CI: Local CI is not supporting EMPTY at the moment. - programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, false, false, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE), false, false, false)); + programmingLanguageFeatures.put(JAVA, new ProgrammingLanguageFeature(JAVA, false, false, true, true, false, List.of(PLAIN_GRADLE, GRADLE_GRADLE), false, false, true)); // TODO LOCALVC_CI: Local CI is not supporting Python at the moment. // TODO LOCALVC_CI: Local CI is not supporting C at the moment. // TODO LOCALVC_CI: Local CI is not supporting Haskell at the moment. diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCServletService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCServletService.java index c287dfd6fcd5..18d8be3ae303 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCServletService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCServletService.java @@ -40,6 +40,7 @@ import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.RepositoryAccessService; import de.tum.in.www1.artemis.service.connectors.localci.LocalCIConnectorService; +import de.tum.in.www1.artemis.service.programming.AuxiliaryRepositoryService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseParticipationService; import de.tum.in.www1.artemis.service.util.TimeLogUtil; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; @@ -71,6 +72,8 @@ public class LocalVCServletService { private final ProgrammingExerciseParticipationService programmingExerciseParticipationService; + private final AuxiliaryRepositoryService auxiliaryRepositoryService; + @Value("${artemis.version-control.url}") private URL localVCBaseUrl; @@ -88,7 +91,8 @@ public class LocalVCServletService { public LocalVCServletService(AuthenticationManagerBuilder authenticationManagerBuilder, UserRepository userRepository, ProgrammingExerciseRepository programmingExerciseRepository, RepositoryAccessService repositoryAccessService, AuthorizationCheckService authorizationCheckService, - Optional localCIConnectorService, ProgrammingExerciseParticipationService programmingExerciseParticipationService) { + Optional localCIConnectorService, ProgrammingExerciseParticipationService programmingExerciseParticipationService, + AuxiliaryRepositoryService auxiliaryRepositoryService) { this.authenticationManagerBuilder = authenticationManagerBuilder; this.userRepository = userRepository; this.programmingExerciseRepository = programmingExerciseRepository; @@ -96,6 +100,7 @@ public LocalVCServletService(AuthenticationManagerBuilder authenticationManagerB this.authorizationCheckService = authorizationCheckService; this.localCIConnectorService = localCIConnectorService; this.programmingExerciseParticipationService = programmingExerciseParticipationService; + this.auxiliaryRepositoryService = auxiliaryRepositoryService; } /** @@ -231,10 +236,10 @@ private String checkAuthorizationHeader(String authorizationHeader) throws Local private void authorizeUser(String repositoryTypeOrUserName, User user, ProgrammingExercise exercise, RepositoryActionType repositoryActionType, boolean isPracticeRepository) throws LocalVCAuthException, LocalVCForbiddenException { - if (repositoryTypeOrUserName.equals(RepositoryType.TESTS.toString())) { + if (repositoryTypeOrUserName.equals(RepositoryType.TESTS.toString()) || auxiliaryRepositoryService.isAuxiliaryRepositoryOfExercise(repositoryTypeOrUserName, exercise)) { + // Test and auxiliary repositories are only accessible by instructors and higher. try { - // Only editors and higher are able to push. Teaching assistants can only fetch. - repositoryAccessService.checkAccessTestRepositoryElseThrow(repositoryActionType == RepositoryActionType.WRITE, exercise, user); + repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(repositoryActionType == RepositoryActionType.WRITE, exercise, user, repositoryTypeOrUserName); } catch (AccessForbiddenException e) { throw new LocalVCAuthException(e); @@ -243,7 +248,6 @@ private void authorizeUser(String repositoryTypeOrUserName, User user, Programmi } ProgrammingExerciseParticipation participation; - try { participation = programmingExerciseParticipationService.getParticipationForRepository(exercise, repositoryTypeOrUserName, isPracticeRepository, false); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/AuxiliaryRepositoryService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/AuxiliaryRepositoryService.java index 28332fa5f022..940d694f4ff0 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/AuxiliaryRepositoryService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/AuxiliaryRepositoryService.java @@ -208,4 +208,21 @@ private void validateAuxiliaryRepository(AuxiliaryRepository auxiliaryRepository // limited to 500 characters. validateAuxiliaryRepositoryDescriptionLength(auxiliaryRepository); } + + /** + * Checks if the given repository is an auxiliary repository of the given exercise. + * + * @param repositoryName the name of the repository to check. + * @param exercise the exercise to check. + * @return true if the repository is an auxiliary repository of the exercise, false otherwise. + */ + public boolean isAuxiliaryRepositoryOfExercise(String repositoryName, ProgrammingExercise exercise) { + List auxiliaryRepositories = auxiliaryRepositoryRepository.findByExerciseId(exercise.getId()); + for (AuxiliaryRepository repo : auxiliaryRepositories) { + if (repo.getName().equals(repositoryName)) { + return true; + } + } + return false; + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java index ca3f102721b2..f389bb27840f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseParticipationService.java @@ -52,10 +52,12 @@ public class ProgrammingExerciseParticipationService { private final UserRepository userRepository; + private final AuxiliaryRepositoryService auxiliaryRepositoryService; + public ProgrammingExerciseParticipationService(SolutionProgrammingExerciseParticipationRepository solutionParticipationRepository, TemplateProgrammingExerciseParticipationRepository templateParticipationRepository, ProgrammingExerciseStudentParticipationRepository studentParticipationRepository, ParticipationRepository participationRepository, TeamRepository teamRepository, GitService gitService, Optional versionControlService, - AuthorizationCheckService authorizationCheckService, UserRepository userRepository) { + AuthorizationCheckService authorizationCheckService, UserRepository userRepository, AuxiliaryRepositoryService auxiliaryRepositoryService) { this.studentParticipationRepository = studentParticipationRepository; this.solutionParticipationRepository = solutionParticipationRepository; this.templateParticipationRepository = templateParticipationRepository; @@ -65,6 +67,7 @@ public ProgrammingExerciseParticipationService(SolutionProgrammingExercisePartic this.gitService = gitService; this.authorizationCheckService = authorizationCheckService; this.userRepository = userRepository; + this.auxiliaryRepositoryService = auxiliaryRepositoryService; } /** @@ -408,8 +411,11 @@ public void resetRepository(VcsRepositoryUrl targetURL, VcsRepositoryUrl sourceU public ProgrammingExerciseParticipation getParticipationForRepository(ProgrammingExercise exercise, String repositoryTypeOrUserName, boolean isPracticeRepository, boolean withSubmissions) { + boolean isAuxiliaryRepository = auxiliaryRepositoryService.isAuxiliaryRepositoryOfExercise(repositoryTypeOrUserName, exercise); + // For pushes to the tests repository, the solution repository is built first, and thus we need the solution participation. - if (repositoryTypeOrUserName.equals(RepositoryType.SOLUTION.toString()) || repositoryTypeOrUserName.equals(RepositoryType.TESTS.toString())) { + // Can possibly be used by auxiliary repositories + if (repositoryTypeOrUserName.equals(RepositoryType.SOLUTION.toString()) || repositoryTypeOrUserName.equals(RepositoryType.TESTS.toString()) || isAuxiliaryRepository) { if (withSubmissions) { return solutionParticipationRepository.findWithEagerResultsAndSubmissionsByProgrammingExerciseIdElseThrow(exercise.getId()); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java index 2889542c4f22..5b64d4528f45 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/repository/TestRepositoryResource.java @@ -51,7 +51,7 @@ public TestRepositoryResource(ProfileService profileService, UserRepository user Repository getRepository(Long exerciseId, RepositoryActionType repositoryActionType, boolean pullOnGet) throws GitAPIException { final var exercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId); User user = userRepository.getUserWithGroupsAndAuthorities(); - repositoryAccessService.checkAccessTestRepositoryElseThrow(false, exercise, user); + repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(false, exercise, user, "test"); final var repoUrl = exercise.getVcsTestRepositoryUrl(); return gitService.getOrCheckoutRepository(repoUrl, pullOnGet); } @@ -65,8 +65,8 @@ VcsRepositoryUrl getRepositoryUrl(Long exerciseId) { @Override boolean canAccessRepository(Long exerciseId) { try { - repositoryAccessService.checkAccessTestRepositoryElseThrow(true, programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId), - userRepository.getUserWithGroupsAndAuthorities()); + repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(true, programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(exerciseId), + userRepository.getUserWithGroupsAndAuthorities(), "test"); } catch (AccessForbiddenException e) { return false; @@ -178,7 +178,7 @@ public ResponseEntity> updateTestFiles(@PathVariable("exerci Repository repository; try { - repositoryAccessService.checkAccessTestRepositoryElseThrow(true, exercise, userRepository.getUserWithGroupsAndAuthorities(principal.getName())); + repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(true, exercise, userRepository.getUserWithGroupsAndAuthorities(principal.getName()), "test"); repository = gitService.getOrCheckoutRepository(exercise.getVcsTestRepositoryUrl(), true); } catch (AccessForbiddenException e) { diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html index 4fc3b5ad8561..92953ae5fadd 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html @@ -380,9 +380,11 @@

Exercise Details

- {{ - auxiliaryRepository.repositoryUrl - }} + + {{ + auxiliaryRepository.repositoryUrl + }} +
diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts index 019af757325e..ebce5ca9c451 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts @@ -184,7 +184,6 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.programmingExercise.solutionParticipation.buildPlanId, ); } - this.supportsAuxiliaryRepositories = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(programmingExercise.programmingLanguage).auxiliaryRepositoriesSupported ?? false; this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); diff --git a/src/test/java/de/tum/in/www1/artemis/service/RepositoryAccessServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/RepositoryAccessServiceTest.java index 726d0fe471c4..861aa69cf398 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/RepositoryAccessServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/RepositoryAccessServiceTest.java @@ -107,6 +107,6 @@ void testShouldEnforceLockRepositoryPolicy() throws Exception { // Student should not have access to the tests repository. void testShouldDenyAccessToTestRepository(boolean atLeastEditor) { assertThatExceptionOfType(AccessForbiddenException.class) - .isThrownBy(() -> repositoryAccessService.checkAccessTestRepositoryElseThrow(atLeastEditor, programmingExercise, student)); + .isThrownBy(() -> repositoryAccessService.checkAccessTestOrAuxRepositoryElseThrow(atLeastEditor, programmingExercise, student, "test")); } } From a6a9890bdb9b9657b318cfe582b5c54e55d42884 Mon Sep 17 00:00:00 2001 From: Lucas Welscher Date: Sat, 16 Sep 2023 10:48:49 +0200 Subject: [PATCH 09/16] Development: Fix modernizer warning (#7208) --- .../localci/LocalCIContainerService.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java index 5d27a0717c92..920471153db6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIContainerService.java @@ -1,7 +1,5 @@ package de.tum.in.www1.artemis.service.connectors.localci; -import java.io.BufferedWriter; -import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -128,7 +126,7 @@ public void onComplete() { }); try { - log.info("Started running the build script for build job in container with id " + containerId); + log.info("Started running the build script for build job in container with id {}", containerId); // Block until the latch reaches 0 or until the thread is interrupted. latch.await(); } @@ -227,7 +225,7 @@ public Path createBuildScript(ProgrammingExercise programmingExercise, List Date: Sat, 16 Sep 2023 10:51:18 +0200 Subject: [PATCH 10/16] Development: Use a DTO when sending result update notifications (#7070) --- .../artemis/domain/FileUploadSubmission.java | 2 +- .../artemis/domain/ProgrammingSubmission.java | 2 +- .../de/tum/in/www1/artemis/domain/Result.java | 27 +++++-- .../in/www1/artemis/domain/Submission.java | 7 ++ .../www1/artemis/domain/TextSubmission.java | 2 +- .../domain/modeling/ModelingSubmission.java | 2 +- .../domain/participation/Participation.java | 3 + ...ogrammingExerciseStudentParticipation.java | 5 ++ ...utionProgrammingExerciseParticipation.java | 5 ++ .../participation/StudentParticipation.java | 5 ++ ...plateProgrammingExerciseParticipation.java | 5 ++ .../artemis/domain/quiz/QuizSubmission.java | 2 +- .../service/WebsocketMessagingService.java | 81 +++---------------- .../ProgrammingMessagingService.java | 21 ++--- .../rest/ProgrammingAssessmentResource.java | 2 +- .../web/rest/QuizSubmissionResource.java | 2 +- .../web/rest/dto/DomainObjectIdDTO.java | 13 +++ .../web/rest/dto/ParticipationDTO.java | 14 ++++ .../www1/artemis/web/rest/dto/ResultDTO.java | 55 +++++++++++++ .../artemis/web/rest/dto/SubmissionDTO.java | 36 +++++++++ .../entities/programming-submission.model.ts | 2 +- .../webapp/app/entities/submission.model.ts | 2 +- .../programming-exam-submission.component.ts | 6 +- ...code-editor-student-container.component.ts | 2 +- .../exercises/shared/result/result.utils.ts | 2 +- .../result/updating-result.component.ts | 4 +- .../FileUploadAssessmentIntegrationTest.java | 3 +- .../ModelingAssessmentIntegrationTest.java | 3 +- .../ProgrammingAssessmentIntegrationTest.java | 3 +- ...dResultBitbucketBambooIntegrationTest.java | 18 +---- .../ProgrammingSubmissionIntegrationTest.java | 5 +- .../text/TextAssessmentIntegrationTest.java | 3 +- 32 files changed, 219 insertions(+), 125 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/DomainObjectIdDTO.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/ParticipationDTO.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/ResultDTO.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/SubmissionDTO.java diff --git a/src/main/java/de/tum/in/www1/artemis/domain/FileUploadSubmission.java b/src/main/java/de/tum/in/www1/artemis/domain/FileUploadSubmission.java index e98ad42167c0..2fee2ecf8998 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/FileUploadSubmission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/FileUploadSubmission.java @@ -20,7 +20,7 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class FileUploadSubmission extends Submission { - // used to distinguish the type when used in collections (e.g. SearchResultPageDTO --> resultsOnPage) + @Override public String getSubmissionExerciseType() { return "file-upload"; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingSubmission.java b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingSubmission.java index 58a095ed810d..0c3a1262c3be 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingSubmission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingSubmission.java @@ -26,7 +26,7 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class ProgrammingSubmission extends Submission { - // used to distinguish the type when used in collections (e.g. SearchResultPageDTO --> resultsOnPage) + @Override public String getSubmissionExerciseType() { return "programming"; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Result.java b/src/main/java/de/tum/in/www1/artemis/domain/Result.java index a19e5f2f84b2..f7cca4885cc0 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Result.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Result.java @@ -8,6 +8,7 @@ import java.time.Instant; import java.time.ZonedDateTime; import java.util.*; +import java.util.stream.Collectors; import javax.persistence.*; import javax.validation.constraints.NotNull; @@ -31,6 +32,7 @@ import de.tum.in.www1.artemis.domain.view.QuizView; import de.tum.in.www1.artemis.service.ExerciseDateService; import de.tum.in.www1.artemis.service.listeners.ResultListener; +import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; /** * A Result. @@ -480,14 +482,11 @@ public void filterSensitiveInformation() { /** * Removes all feedback details that should not be passed to the student. * - * @param isBeforeDueDate if feedbacks marked with visibility 'after due date' should also be removed. + * @param removeHiddenFeedback if feedbacks marked with visibility 'after due date' should also be removed. */ - public void filterSensitiveFeedbacks(boolean isBeforeDueDate) { - feedbacks.removeIf(Feedback::isInvisible); - - if (isBeforeDueDate) { - feedbacks.removeIf(Feedback::isAfterDueDate); - } + public void filterSensitiveFeedbacks(boolean removeHiddenFeedback) { + var filteredFeedback = createFilteredFeedbacks(removeHiddenFeedback); + setFeedbacks(filteredFeedback); // TODO: this is not good code! var testCaseFeedback = feedbacks.stream().filter(Feedback::isTestFeedback).toList(); @@ -495,6 +494,20 @@ public void filterSensitiveFeedbacks(boolean isBeforeDueDate) { setPassedTestCaseCount((int) testCaseFeedback.stream().filter(feedback -> Boolean.TRUE.equals(feedback.isPositive())).count()); } + /** + * Returns a new list that only contains feedback that should be passed to the student. + * Does not change the feedbacks attribute of this entity. + * + * @see ResultDTO + * + * @param removeHiddenFeedback if feedbacks marked with visibility 'after due date' should also be removed. + * @return the new filtered list + */ + public List createFilteredFeedbacks(boolean removeHiddenFeedback) { + return feedbacks.stream().filter(feedback -> !feedback.isInvisible()).filter(feedback -> !removeHiddenFeedback || !feedback.isAfterDueDate()) + .collect(Collectors.toCollection(ArrayList::new)); + } + /** * Checks whether the result is a manual result. A manual result can be from type MANUAL or SEMI_AUTOMATIC * diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Submission.java b/src/main/java/de/tum/in/www1/artemis/domain/Submission.java index d9c0704f7ab8..1f2a5873ec6f 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Submission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Submission.java @@ -307,6 +307,13 @@ public void setExampleSubmission(Boolean exampleSubmission) { */ public abstract boolean isEmpty(); + /** + * used to distinguish the type when used in collections or DTOs + * + * @return the exercise type (e.g. programming, text) + */ + public abstract String getSubmissionExerciseType(); + /** * In case user calls for correctionRound 0, but more manual results already exists * and he has not requested a specific result, remove any other results diff --git a/src/main/java/de/tum/in/www1/artemis/domain/TextSubmission.java b/src/main/java/de/tum/in/www1/artemis/domain/TextSubmission.java index 774166ba53d8..a227d12cf9f1 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/TextSubmission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/TextSubmission.java @@ -22,7 +22,7 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class TextSubmission extends Submission { - // used to distinguish the type when used in collections (e.g. SearchResultPageDTO --> resultsOnPage) + @Override public String getSubmissionExerciseType() { return "text"; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/modeling/ModelingSubmission.java b/src/main/java/de/tum/in/www1/artemis/domain/modeling/ModelingSubmission.java index c7fd81c54d4c..cd2600d916b2 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/modeling/ModelingSubmission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/modeling/ModelingSubmission.java @@ -26,7 +26,7 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class ModelingSubmission extends Submission { - // used to distinguish the type when used in collections (e.g. SearchResultPageDTO --> resultsOnPage) + @Override public String getSubmissionExerciseType() { return "modeling"; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java b/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java index 7a6e8bfda395..cbc09c674a5d 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/participation/Participation.java @@ -320,4 +320,7 @@ public String toString() { public abstract Participation copyParticipationId(); public abstract void filterSensitiveInformation(); + + @JsonIgnore + public abstract String getType(); } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseStudentParticipation.java b/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseStudentParticipation.java index 6c924d1559a0..9005fa049173 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseStudentParticipation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/participation/ProgrammingExerciseStudentParticipation.java @@ -108,6 +108,11 @@ public void setProgrammingExercise(ProgrammingExercise programmingExercise) { setExercise(programmingExercise); } + @Override + public String getType() { + return "programming"; + } + @Override public String toString() { return getClass().getSimpleName() + "{" + "id=" + getId() + ", repositoryUrl='" + getRepositoryUrl() + "'" + ", buildPlanId='" + getBuildPlanId() + "'" diff --git a/src/main/java/de/tum/in/www1/artemis/domain/participation/SolutionProgrammingExerciseParticipation.java b/src/main/java/de/tum/in/www1/artemis/domain/participation/SolutionProgrammingExerciseParticipation.java index 3dc378527fba..c9b6a4fb5627 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/participation/SolutionProgrammingExerciseParticipation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/participation/SolutionProgrammingExerciseParticipation.java @@ -28,6 +28,11 @@ public void setProgrammingExercise(ProgrammingExercise programmingExercise) { this.programmingExercise = programmingExercise; } + @Override + public String getType() { + return "solution"; + } + @Override public Participation copyParticipationId() { var participation = new SolutionProgrammingExerciseParticipation(); diff --git a/src/main/java/de/tum/in/www1/artemis/domain/participation/StudentParticipation.java b/src/main/java/de/tum/in/www1/artemis/domain/participation/StudentParticipation.java index f7ccd68f99dd..20a8f1a4b3b0 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/participation/StudentParticipation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/participation/StudentParticipation.java @@ -56,6 +56,11 @@ public Participant getParticipant() { return Optional.ofNullable((Participant) student).orElse(team); } + @Override + public String getType() { + return "student"; + } + /** * allows to set the participant independent whether it is a team or user * diff --git a/src/main/java/de/tum/in/www1/artemis/domain/participation/TemplateProgrammingExerciseParticipation.java b/src/main/java/de/tum/in/www1/artemis/domain/participation/TemplateProgrammingExerciseParticipation.java index 2849b2c8b707..e0dffb493a2f 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/participation/TemplateProgrammingExerciseParticipation.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/participation/TemplateProgrammingExerciseParticipation.java @@ -28,6 +28,11 @@ public void setProgrammingExercise(ProgrammingExercise programmingExercise) { this.programmingExercise = programmingExercise; } + @Override + public String getType() { + return "template"; + } + @Override public Participation copyParticipationId() { var participation = new TemplateProgrammingExerciseParticipation(); diff --git a/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizSubmission.java b/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizSubmission.java index 124782dcfc61..565dd60ab60b 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizSubmission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/quiz/QuizSubmission.java @@ -12,7 +12,7 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) public class QuizSubmission extends AbstractQuizSubmission { - // used to distinguish the type when used in collections (e.g. SearchResultPageDTO --> resultsOnPage) + @Override public String getSubmissionExerciseType() { return "quiz"; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/WebsocketMessagingService.java b/src/main/java/de/tum/in/www1/artemis/service/WebsocketMessagingService.java index 59b0d2cb0912..d57f77530901 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/WebsocketMessagingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/WebsocketMessagingService.java @@ -3,14 +3,11 @@ import static de.tum.in.www1.artemis.config.Constants.*; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.hibernate.Hibernate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; @@ -19,12 +16,12 @@ import org.springframework.stereotype.Service; import de.tum.in.www1.artemis.domain.Exercise; -import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.domain.Result; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.service.exam.ExamDateService; +import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; /** * This service sends out websocket messages. @@ -111,19 +108,6 @@ public CompletableFuture sendMessageToUser(String user, String topic, Obje } } - /** - * Broadcast a new result to the client. - * Waits until all notifications are sent and the result properties are restored. - * This allows the caller to reuse the passed result object again after calling. - * - * @param participation used to find the receivers of the notification - * @param result the result object to publish - */ - public void awaitBroadcastNewResult(Participation participation, Result result) { - // Wait until all notifications got send and the objects were reconnected. - broadcastNewResult(participation, result).join(); - } - /** * Broadcast a new result to the client. * @@ -131,55 +115,18 @@ public void awaitBroadcastNewResult(Participation participation, Result result) * @param result the new result that should be sent to the client. It typically includes feedback, its participation will be cut off here to reduce the payload size. * As the participation is already known to the client, we do not need to send it. This also cuts of the exercise (including the potentially huge * problem statement and the course with all potential attributes - * @return a CompletableFuture allowing to wait until all messages got send. */ - public CompletableFuture broadcastNewResult(Participation participation, Result result) { - // remove unnecessary properties to reduce the data sent to the client (we should not send the exercise and its potentially huge problem statement) - var originalParticipation = result.getParticipation(); - result.setParticipation(originalParticipation.copyParticipationId()); - List originalResults = null; - if (Hibernate.isInitialized(result.getSubmission()) && result.getSubmission() != null) { - var submission = result.getSubmission(); - submission.setParticipation(null); - if (Hibernate.isInitialized(submission.getResults())) { - originalResults = submission.getResults(); - submission.setResults(null); - } - if (submission instanceof ProgrammingSubmission programmingSubmission && programmingSubmission.isBuildFailed()) { - programmingSubmission.setBuildLogEntries(null); - } - } - - final var originalAssessor = result.getAssessor(); - final var originalFeedback = new ArrayList<>(result.getFeedbacks()); - - CompletableFuture[] allFutures = new CompletableFuture[0]; - + public void broadcastNewResult(Participation participation, Result result) { // TODO: Are there other cases that must be handled here? if (participation instanceof StudentParticipation studentParticipation) { - allFutures = broadcastNewResultToParticipants(studentParticipation, result); + broadcastNewResultToParticipants(studentParticipation, result); } - final List finalOriginalResults = originalResults; - return CompletableFuture.allOf(allFutures).thenCompose(v -> { - // Restore information that should not go to students but tutors, instructors, and admins should still see - // only add these values after the async broadcast is done to not publish it mistakenly - result.setAssessor(originalAssessor); - result.setFeedbacks(originalFeedback); - - // Send to tutors, instructors and admins - return sendMessage(getNonPersonalExerciseResultDestination(participation.getExercise().getId()), result).thenAccept(v2 -> { - // recover the participation and submission because we might want to use this result object again - result.setParticipation(originalParticipation); - if (Hibernate.isInitialized(result.getSubmission()) && result.getSubmission() != null) { - result.getSubmission().setParticipation(originalParticipation); - result.getSubmission().setResults(finalOriginalResults); - } - }); - }); + // Send to tutors, instructors and admins + sendMessage(getNonPersonalExerciseResultDestination(participation.getExercise().getId()), ResultDTO.of(result)); } - private CompletableFuture[] broadcastNewResultToParticipants(StudentParticipation studentParticipation, Result result) { + private void broadcastNewResultToParticipants(StudentParticipation studentParticipation, Result result) { final Exercise exercise = studentParticipation.getExercise(); boolean isWorkingPeriodOver; if (exercise.isExamExercise()) { @@ -194,21 +141,19 @@ private CompletableFuture[] broadcastNewResultToParticipants(StudentPartic boolean isAutomaticAssessmentOrDueDateOver = AssessmentType.AUTOMATIC == result.getAssessmentType() || exercise.getAssessmentDueDate() == null || ZonedDateTime.now().isAfter(exercise.getAssessmentDueDate()); - List> allFutures = new ArrayList<>(); if (isAutomaticAssessmentOrDueDateOver && !isAfterExamEnd) { var students = studentParticipation.getStudents(); - result.filterSensitiveInformation(); - - allFutures.addAll(students.stream().filter(student -> authCheckService.isAtLeastTeachingAssistantForExercise(exercise, student)) - .map(user -> sendMessageToUser(user.getLogin(), NEW_RESULT_TOPIC, result)).toList()); + var resultDTO = ResultDTO.of(result); + students.stream().filter(student -> authCheckService.isAtLeastTeachingAssistantForExercise(exercise, student)) + .forEach(user -> sendMessageToUser(user.getLogin(), NEW_RESULT_TOPIC, resultDTO)); - result.filterSensitiveFeedbacks(!isWorkingPeriodOver); + var filteredFeedback = result.createFilteredFeedbacks(!isWorkingPeriodOver); + var filteredFeedbackResultDTO = ResultDTO.of(result, filteredFeedback); - allFutures.addAll(students.stream().filter(student -> !authCheckService.isAtLeastTeachingAssistantForExercise(exercise, student)) - .map(user -> sendMessageToUser(user.getLogin(), NEW_RESULT_TOPIC, result)).toList()); + students.stream().filter(student -> !authCheckService.isAtLeastTeachingAssistantForExercise(exercise, student)) + .forEach(user -> sendMessageToUser(user.getLogin(), NEW_RESULT_TOPIC, filteredFeedbackResultDTO)); } - return allFutures.toArray(CompletableFuture[]::new); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingMessagingService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingMessagingService.java index f4f3d4377f09..ceae098dfc1a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingMessagingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingMessagingService.java @@ -11,6 +11,7 @@ import de.tum.in.www1.artemis.service.WebsocketMessagingService; import de.tum.in.www1.artemis.service.connectors.lti.LtiNewResultService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; +import de.tum.in.www1.artemis.web.rest.dto.SubmissionDTO; import de.tum.in.www1.artemis.web.websocket.programmingSubmission.BuildTriggerWebsocketError; @Service @@ -47,28 +48,18 @@ public void notifyInstructorAboutCompletedExerciseBuildRun(ProgrammingExercise p * Notify user on a new programming submission. * * @param submission ProgrammingSubmission - * @param exerciseId + * @param exerciseId used to build the correct topic */ public void notifyUserAboutSubmission(ProgrammingSubmission submission, Long exerciseId) { + var submissionDTO = SubmissionDTO.of(submission); if (submission.getParticipation() instanceof StudentParticipation studentParticipation) { - - // TODO: we should reduce the amount of data sent to the client and use a DTO - - // TODO LOCALVC_CI: Find a way to set the exercise to null (submission.getParticipation().setExercise(null)) as it is not necessary to send all these details here. - // Just removing it causes issues with the local CI system that calls this method and in some places expects the exercise to be set on the submission's participation - // afterwards (call by reference). - // Removing it and immediately setting it back to the original value after sending the message here, is not working either, because some steps in the local CI system - // happen in parallel to this and the exercise needs to be set at all times. - // Creating a deep copy of the submission and setting the exercise to null there is also not working, because 'java.time.ZoneRegion' is not open to external libraries - // (like Jackson) so it cannot be serialized using 'objectMapper.readValue()'. - // You could look into some kind of ProgrammingSubmissionDTO here that only gets the values set that the client actually needs. - studentParticipation.getStudents().forEach(user -> websocketMessagingService.sendMessageToUser(user.getLogin(), NEW_SUBMISSION_TOPIC, submission)); + studentParticipation.getStudents().forEach(user -> websocketMessagingService.sendMessageToUser(user.getLogin(), NEW_SUBMISSION_TOPIC, submissionDTO)); } // send an update to tutors, editors and instructors about submissions for template and solution participations if (!(submission.getParticipation() instanceof StudentParticipation)) { var topicDestination = getExerciseTopicForTAAndAbove(exerciseId); - websocketMessagingService.sendMessage(topicDestination, submission); + websocketMessagingService.sendMessage(topicDestination, submissionDTO); } } @@ -141,7 +132,7 @@ private static String getProgrammingExerciseAllExerciseBuildsTriggeredTopic(Long public void notifyUserAboutNewResult(Result result, ProgrammingExerciseParticipation participation) { log.debug("Send result to client over websocket. Result: {}, Submission: {}, Participation: {}", result, result.getSubmission(), result.getParticipation()); // notify user via websocket - websocketMessagingService.awaitBroadcastNewResult((Participation) participation, result); + websocketMessagingService.broadcastNewResult((Participation) participation, result); if (participation instanceof ProgrammingExerciseStudentParticipation studentParticipation) { // do not try to report results for template or solution participations diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java index b5ee28ac65b7..eb73ae90f741 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java @@ -204,7 +204,7 @@ public ResponseEntity saveProgrammingAssessment(@PathVariable Long parti // Note: we always need to report the result over LTI, otherwise it might never become visible in the external system ltiNewResultService.onNewResult((StudentParticipation) newManualResult.getParticipation()); if (submit && ExerciseDateService.isAfterAssessmentDueDate(programmingExercise)) { - messagingService.awaitBroadcastNewResult(newManualResult.getParticipation(), newManualResult); + messagingService.broadcastNewResult(newManualResult.getParticipation(), newManualResult); } var isManualFeedbackRequest = programmingExercise.getAllowManualFeedbackRequests() && participation.getIndividualDueDate() != null diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java index 4a7d540ac32a..fa4cc3ee2499 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java @@ -157,7 +157,7 @@ public ResponseEntity submitForPractice(@PathVariable Long exerciseId, @ quizExercise.setQuizPointStatistic(null); - messagingService.awaitBroadcastNewResult(result.getParticipation(), result); + messagingService.broadcastNewResult(result.getParticipation(), result); quizExercise.setCourse(null); // return result with quizSubmission, participation and quiz exercise (including the solution) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/DomainObjectIdDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/DomainObjectIdDTO.java new file mode 100644 index 000000000000..6c4fb52c656a --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/DomainObjectIdDTO.java @@ -0,0 +1,13 @@ +package de.tum.in.www1.artemis.web.rest.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.DomainObject; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record DomainObjectIdDTO(Long id) { + + public DomainObjectIdDTO(DomainObject domainObject) { + this(domainObject.getId()); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ParticipationDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ParticipationDTO.java new file mode 100644 index 000000000000..5775e47fcb1f --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ParticipationDTO.java @@ -0,0 +1,14 @@ +package de.tum.in.www1.artemis.web.rest.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.participation.Participation; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record ParticipationDTO(Long id, boolean testRun, String type) { + + public static ParticipationDTO of(Participation participation) { + return new ParticipationDTO(participation.getId(), participation.isTestRun(), participation.getType()); + } + +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ResultDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ResultDTO.java new file mode 100644 index 000000000000..2590d44f30d9 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ResultDTO.java @@ -0,0 +1,55 @@ +package de.tum.in.www1.artemis.web.rest.dto; + +import java.time.ZonedDateTime; +import java.util.List; + +import org.hibernate.Hibernate; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.Feedback; +import de.tum.in.www1.artemis.domain.Result; +import de.tum.in.www1.artemis.domain.enumeration.*; + +/** + * DTO containing {@link Result} information. + * This does not include large reference attributes in order to send minimal data to the client. + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record ResultDTO(Long id, ZonedDateTime completionDate, Boolean successful, Double score, Boolean rated, SubmissionDTO submission, ParticipationDTO participation, + List feedbacks, AssessmentType assessmentType, Boolean hasComplaint, Boolean exampleResult, Integer testCaseCount, Integer passedTestCaseCount, + Integer codeIssueCount) { + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public record FeedbackDTO(String text, String detailText, boolean hasLongFeedbackText, String reference, Double credits, Boolean positive, FeedbackType type, + Visibility visibility) { + + public static FeedbackDTO of(Feedback feedback) { + return new FeedbackDTO(feedback.getText(), feedback.getDetailText(), feedback.getHasLongFeedbackText(), feedback.getReference(), feedback.getCredits(), + feedback.isPositive(), feedback.getType(), feedback.getVisibility()); + + } + } + + public static ResultDTO of(Result result) { + return of(result, result.getFeedbacks()); + } + + /** + * Converts a Result into a ResultDTO + * + * @param result to convert + * @param filteredFeedback feedback that should get send to the client, will get converted into {@link FeedbackDTO} objects. + * @return the converted DTO + */ + public static ResultDTO of(Result result, List filteredFeedback) { + SubmissionDTO submissionDTO = null; + if (Hibernate.isInitialized(result.getSubmission()) && result.getSubmission() != null) { + submissionDTO = SubmissionDTO.of(result.getSubmission()); + } + var feedbackDTOs = filteredFeedback.stream().map(FeedbackDTO::of).toList(); + return new ResultDTO(result.getId(), result.getCompletionDate(), result.isSuccessful(), result.getScore(), result.isRated(), submissionDTO, + ParticipationDTO.of(result.getParticipation()), feedbackDTOs, result.getAssessmentType(), result.hasComplaint(), result.isExampleResult(), + result.getTestCaseCount(), result.getPassedTestCaseCount(), result.getCodeIssueCount()); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/SubmissionDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/SubmissionDTO.java new file mode 100644 index 000000000000..4fbe933beabe --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/SubmissionDTO.java @@ -0,0 +1,36 @@ +package de.tum.in.www1.artemis.web.rest.dto; + +import java.time.ZonedDateTime; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.ProgrammingSubmission; +import de.tum.in.www1.artemis.domain.Submission; +import de.tum.in.www1.artemis.domain.enumeration.SubmissionType; + +/** + * DTO containing {@link Submission} information. + * This does not include large reference attributes in order to send minimal data to the client. + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record SubmissionDTO(Long id, Boolean submitted, SubmissionType type, Boolean exampleSubmission, ZonedDateTime submissionDate, String commitHash, Boolean buildFailed, + Boolean buildArtifact, ParticipationDTO participation, String submissionExerciseType) { + + /** + * Converts a Submission into a SubmissionDTO. + * + * @param submission to convert + * @return the converted DTO + */ + public static SubmissionDTO of(Submission submission) { + if (submission instanceof ProgrammingSubmission programmingSubmission) { + // For programming submissions we need to extract additional information (e.g. the commit hash) and send it to the client + return new SubmissionDTO(programmingSubmission.getId(), programmingSubmission.isSubmitted(), programmingSubmission.getType(), + programmingSubmission.isExampleSubmission(), programmingSubmission.getSubmissionDate(), programmingSubmission.getCommitHash(), + programmingSubmission.isBuildFailed(), programmingSubmission.isBuildArtifact(), ParticipationDTO.of(programmingSubmission.getParticipation()), + programmingSubmission.getSubmissionExerciseType()); + } + return new SubmissionDTO(submission.getId(), submission.isSubmitted(), submission.getType(), submission.isExampleSubmission(), submission.getSubmissionDate(), null, null, + null, ParticipationDTO.of(submission.getParticipation()), submission.getSubmissionExerciseType()); + } +} diff --git a/src/main/webapp/app/entities/programming-submission.model.ts b/src/main/webapp/app/entities/programming-submission.model.ts index 3a37874befa4..fb9bb4c9632f 100644 --- a/src/main/webapp/app/entities/programming-submission.model.ts +++ b/src/main/webapp/app/entities/programming-submission.model.ts @@ -3,7 +3,7 @@ import { Submission, SubmissionExerciseType } from 'app/entities/submission.mode export class ProgrammingSubmission extends Submission { public commitHash?: string; public buildFailed?: boolean; - public buildArtifact?: boolean; // default value (whether the result includes a build artifact or not) + public buildArtifact?: boolean; // whether the result includes a build artifact or not constructor() { super(SubmissionExerciseType.PROGRAMMING); diff --git a/src/main/webapp/app/entities/submission.model.ts b/src/main/webapp/app/entities/submission.model.ts index 1713d5fce273..ced9e0276d88 100644 --- a/src/main/webapp/app/entities/submission.model.ts +++ b/src/main/webapp/app/entities/submission.model.ts @@ -38,7 +38,7 @@ export abstract class Submission implements BaseEntity { // Helper Attributes // latestResult is undefined until setLatestSubmissionResult() is called - public latestResult?: undefined | Result; + public latestResult?: Result; // only used for exam to check if it is saved to server public isSynced?: boolean; diff --git a/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.ts index 78383239add7..4ac469effa65 100644 --- a/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core'; +import { Submission } from 'app/entities/submission.model'; import { ExamSubmissionComponent } from 'app/exam/participate/exercises/exam-submission.component'; import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; @@ -52,10 +53,11 @@ export class ProgrammingExamSubmissionComponent extends ExamSubmissionComponent readonly IncludedInOverallScore = IncludedInOverallScore; readonly getCourseFromExercise = getCourseFromExercise; - getSubmission() { - if (this.studentParticipation && this.studentParticipation.submissions && this.studentParticipation.submissions.length > 0) { + getSubmission(): Submission | undefined { + if (this.studentParticipation?.submissions?.length) { return this.studentParticipation.submissions[0]; } + return undefined; } getExercise(): Exercise { 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 1fc55d5bbf81..826902c2058a 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 @@ -169,7 +169,7 @@ export class CodeEditorStudentContainerComponent implements OnInit, OnDestroy { * Check whether a latestResult exists and if, returns the unreferenced feedback of it */ get unreferencedFeedback(): Feedback[] { - if (this.latestResult && this.latestResult.feedbacks) { + if (this.latestResult?.feedbacks) { checkSubsequentFeedbackInAssessment(this.latestResult.feedbacks); return getUnreferencedFeedback(this.latestResult.feedbacks) ?? []; } 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 6df74495060f..57049db61057 100644 --- a/src/main/webapp/app/exercises/shared/result/result.utils.ts +++ b/src/main/webapp/app/exercises/shared/result/result.utils.ts @@ -109,7 +109,7 @@ export const evaluateTemplateStatus = ( result: Result | undefined, isBuilding: boolean, missingResultInfo = MissingResultInformation.NONE, -) => { +): ResultTemplateStatus => { // Fallback if participation is not set if (!participation || !exercise) { if (!result) { 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 effd1796252a..dce6b52af890 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 @@ -63,14 +63,14 @@ export class UpdatingResultComponent implements OnChanges, OnDestroy { this.participation.results = _orderBy(this.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 = this.participation.results && this.participation.results.find(({ rated }) => this.showUngradedResults || rated === true); + const latestResult = this.participation.results?.find(({ rated }) => this.showUngradedResults || rated === true); // Make sure that the participation result is connected to the newest result. this.result = latestResult ? { ...latestResult, participation: this.participation } : undefined; this.missingResultInfo = MissingResultInformation.NONE; this.subscribeForNewResults(); // Currently submissions are only used for programming exercises to visualize the build process. - if (this.exercise && this.exercise.type === ExerciseType.PROGRAMMING) { + if (this.exercise?.type === ExerciseType.PROGRAMMING) { this.subscribeForNewSubmissions(); } } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java index de85bd556523..b9f5e5a0071e 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/fileuploadexercise/FileUploadAssessmentIntegrationTest.java @@ -32,6 +32,7 @@ import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class FileUploadAssessmentIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -633,7 +634,7 @@ void multipleCorrectionRoundsForExam() throws Exception { assertThat(assessedSubmissionList).isEmpty(); // Student should not have received a result over WebSocket as manual correction is ongoing - verify(websocketMessagingService, never()).sendMessageToUser(notNull(), eq(Constants.NEW_RESULT_TOPIC), isA(Result.class)); + verify(websocketMessagingService, never()).sendMessageToUser(notNull(), eq(Constants.NEW_RESULT_TOPIC), isA(ResultDTO.class)); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java index ad0dc9b3dfaf..57996459c6dd 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/modelingexercise/ModelingAssessmentIntegrationTest.java @@ -39,6 +39,7 @@ import de.tum.in.www1.artemis.service.compass.CompassService; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.FileUtils; +import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; class ModelingAssessmentIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -1468,7 +1469,7 @@ void multipleCorrectionRoundsForExam() throws Exception { assertThat(assessedSubmissionList).isEmpty(); // Student should not have received a result over WebSocket as manual correction is ongoing - verify(websocketMessagingService, never()).sendMessageToUser(notNull(), eq(Constants.NEW_RESULT_TOPIC), isA(Result.class)); + verify(websocketMessagingService, never()).sendMessageToUser(notNull(), eq(Constants.NEW_RESULT_TOPIC), isA(ResultDTO.class)); } private void assessmentDueDatePassed() { diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java index 660022bdd770..74b3eec32ed4 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingAssessmentIntegrationTest.java @@ -35,6 +35,7 @@ import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.FileUtils; +import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; class ProgrammingAssessmentIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -799,7 +800,7 @@ void multipleCorrectionRoundsForExam() throws Exception { assertThat(assessedSubmissionList).isEmpty(); // Student should not have received a result over WebSocket as manual correction is ongoing - verify(websocketMessagingService, never()).sendMessageToUser(notNull(), eq(Constants.NEW_RESULT_TOPIC), isA(Result.class)); + verify(websocketMessagingService, never()).sendMessageToUser(notNull(), eq(Constants.NEW_RESULT_TOPIC), isA(ResultDTO.class)); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultBitbucketBambooIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultBitbucketBambooIntegrationTest.java index 2350f2b45477..a25e6791ecfa 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultBitbucketBambooIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionAndResultBitbucketBambooIntegrationTest.java @@ -6,14 +6,12 @@ import static de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingSubmissionConstants.*; import static de.tum.in.www1.artemis.util.TestConstants.COMMIT_HASH_OBJECT_ID; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.AdditionalAnswers.answer; import static org.mockito.Mockito.*; import java.time.Duration; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.*; -import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import javax.validation.constraints.NotNull; @@ -58,6 +56,7 @@ import de.tum.in.www1.artemis.service.connectors.bamboo.dto.BambooBuildResultNotificationDTO.BambooTestJobDTO; import de.tum.in.www1.artemis.service.exam.ExamDateService; import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; class ProgrammingSubmissionAndResultBitbucketBambooIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -851,7 +850,7 @@ void shouldCreateIllegalSubmissionOnNotifyPushForExamProgrammingExerciseAfterDue createdResult = resultRepository.findByIdWithEagerFeedbacksAndAssessor(createdResult.getId()).orElseThrow(); // Student should not receive a result over WebSocket, the exam is over and therefore test after due date would be visible - verify(websocketMessagingService, never()).sendMessageToUser(eq(user.getLogin()), eq(NEW_RESULT_TOPIC), isA(Result.class)); + verify(websocketMessagingService, never()).sendMessageToUser(eq(user.getLogin()), eq(NEW_RESULT_TOPIC), isA(ResultDTO.class)); // Assert that the submission is illegal assertThat(submission.getParticipation().getId()).isEqualTo(participation.getId()); @@ -883,17 +882,6 @@ void shouldCreateLegalSubmissionOnNotifyPushForExamProgrammingExerciseAfterDueDa // set the author name to "Artemis" ProgrammingSubmission submission = mockCommitInfoAndPostSubmission(participation.getId()); - doAnswer(answer((u, topic, arg) -> { - // Verify that the passed result is minimal - // We cannot use an ArgumentCaptor since the passed object gets modified afterwards, but we want to verify that state when calling the method. - assertThat(arg).isInstanceOf(Result.class); - Result result = (Result) arg; - assertThat(result.getSubmission().getResults()).isNull(); - assertThat(result.getSubmission().getParticipation()).isNull(); - assertThat(result.getParticipation().getExercise()).isNull(); - return CompletableFuture.completedFuture(null); - })).when(websocketMessagingService).sendMessageToUser(eq(user.getLogin()), eq(NEW_RESULT_TOPIC), isA(Result.class)); - // Mock result from bamboo assertThat(examDateService.getLatestIndividualExamEndDateWithGracePeriod(studentExam.getExam())).isAfter(ZonedDateTime.now()); postResult(participation.getBuildPlanId(), HttpStatus.OK, false); @@ -905,7 +893,7 @@ void shouldCreateLegalSubmissionOnNotifyPushForExamProgrammingExerciseAfterDueDa createdResult = resultRepository.findByIdWithEagerFeedbacksAndAssessor(createdResult.getId()).orElseThrow(); // Student should receive a result over WebSocket, the exam not over (grace period still active) - verify(websocketMessagingService, timeout(2000)).sendMessageToUser(eq(user.getLogin()), eq(NEW_RESULT_TOPIC), isA(Result.class)); + verify(websocketMessagingService, timeout(2000)).sendMessageToUser(eq(user.getLogin()), eq(NEW_RESULT_TOPIC), isA(ResultDTO.class)); // Assert that the submission is illegal assertThat(submission.getParticipation().getId()).isEqualTo(participation.getId()); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java index 1b4c28d7cefa..d2f4a49b9cbc 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/ProgrammingSubmissionIntegrationTest.java @@ -51,6 +51,7 @@ import de.tum.in.www1.artemis.user.UserUtilService; import de.tum.in.www1.artemis.util.FileUtils; import de.tum.in.www1.artemis.util.TestConstants; +import de.tum.in.www1.artemis.web.rest.dto.SubmissionDTO; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; class ProgrammingSubmissionIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -377,7 +378,9 @@ void triggerFailedBuildResultPresentInCIOk() throws Exception { String url = Constants.PROGRAMMING_SUBMISSION_RESOURCE_API_PATH + participation.getId() + "/trigger-failed-build"; request.postWithoutLocation(url, null, HttpStatus.OK, null); - verify(websocketMessagingService, timeout(2000)).sendMessageToUser(user.getLogin(), NEW_SUBMISSION_TOPIC, submission); + final Long submissionId = submission.getId(); + verify(websocketMessagingService, timeout(2000)).sendMessageToUser(eq(user.getLogin()), eq(NEW_SUBMISSION_TOPIC), + argThat(arg -> arg instanceof SubmissionDTO submissionDTO && submissionDTO.id().equals(submissionId))); // Perform the request again and make sure no new submission was created request.postWithoutLocation(url, null, HttpStatus.OK, null); diff --git a/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java index 196aa22e7ce1..015bf3bfd296 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java @@ -42,6 +42,7 @@ import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.service.TextAssessmentService; import de.tum.in.www1.artemis.user.UserUtilService; +import de.tum.in.www1.artemis.web.rest.dto.ResultDTO; import de.tum.in.www1.artemis.web.rest.dto.TextAssessmentDTO; import de.tum.in.www1.artemis.web.rest.dto.TextAssessmentUpdateDTO; @@ -1230,7 +1231,7 @@ void multipleCorrectionRoundsForExam(AssessmentType assessmentType) throws Excep assertThat(assessedSubmissionList).isEmpty(); // Student should not have received a result over WebSocket as manual correction is ongoing - verify(websocketMessagingService, never()).sendMessageToUser(notNull(), eq(Constants.NEW_RESULT_TOPIC), isA(Result.class)); + verify(websocketMessagingService, never()).sendMessageToUser(notNull(), eq(Constants.NEW_RESULT_TOPIC), isA(ResultDTO.class)); } private void addAssessmentFeedbackAndCheckScore(TextSubmission submissionWithoutAssessment, TextAssessmentDTO textAssessmentDTO, List feedbacks, double pointsAwarded, From 927308ab9b08b83c8c945540b39e729478db6442 Mon Sep 17 00:00:00 2001 From: pogobanane <38314551+pogobanane@users.noreply.github.com> Date: Sat, 16 Sep 2023 11:17:33 +0200 Subject: [PATCH 11/16] Development: Add additional binary file extensions for handling file replacements in programming exercises (#7192) --- src/main/java/de/tum/in/www1/artemis/service/FileService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/FileService.java b/src/main/java/de/tum/in/www1/artemis/service/FileService.java index 662e15d40ebf..b9a1649a5ba4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/FileService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/FileService.java @@ -63,7 +63,7 @@ public class FileService implements DisposableBean { * Extensions must be lower-case without leading dots. */ private static final Set binaryFileExtensions = Set.of("png", "jpg", "jpeg", "heic", "gif", "tiff", "psd", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pages", - "numbers", "key", "odt", "zip", "rar", "7z", "tar", "iso", "mdb", "sqlite", "exe", "jar"); + "numbers", "key", "odt", "zip", "rar", "7z", "tar", "iso", "mdb", "sqlite", "exe", "jar", "bin", "so", "dll"); /** * The list of file extensions that are allowed to be uploaded in a Markdown editor. From 9567be416dbfb2554d1eace4e8468c8c118751e9 Mon Sep 17 00:00:00 2001 From: Andreas Pfurtscheller <1051396+aplr@users.noreply.github.com> Date: Sat, 16 Sep 2023 11:19:11 +0200 Subject: [PATCH 12/16] Communication: Prevent post submission if already in progress (#7130) --- .../message-inline-input.component.html | 4 ++-- .../message-inline-input.component.ts | 5 +++++ .../message-reply-inline-input.component.html | 5 +++-- .../message-reply-inline-input.component.ts | 10 +++++----- .../answer-post-create-edit-modal.component.html | 2 +- .../post-create-edit-modal.component.html | 2 +- .../app/shared/metis/posting-create-edit.directive.ts | 1 + 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/webapp/app/shared/metis/message/message-inline-input/message-inline-input.component.html b/src/main/webapp/app/shared/metis/message/message-inline-input/message-inline-input.component.html index 327530044869..328b92348d2f 100644 --- a/src/main/webapp/app/shared/metis/message/message-inline-input/message-inline-input.component.html +++ b/src/main/webapp/app/shared/metis/message/message-inline-input/message-inline-input.component.html @@ -17,7 +17,7 @@

{{ 'artemisApp.messageWarning.headerText' | artemisTra

{{ 'artemisApp.messageWarning.lastParagraph' | artemisTranslate }}

diff --git a/src/main/webapp/app/shared/metis/message/message-reply-inline-input/message-reply-inline-input.component.ts b/src/main/webapp/app/shared/metis/message/message-reply-inline-input/message-reply-inline-input.component.ts index 992fc62eb60d..1fe543496ff5 100644 --- a/src/main/webapp/app/shared/metis/message/message-reply-inline-input/message-reply-inline-input.component.ts +++ b/src/main/webapp/app/shared/metis/message/message-reply-inline-input/message-reply-inline-input.component.ts @@ -16,11 +16,6 @@ import { LocalStorageService } from 'ngx-webstorage'; export class MessageReplyInlineInputComponent extends PostingCreateEditDirective implements OnInit { warningDismissed = false; - ngOnInit(): void { - super.ngOnInit(); - this.warningDismissed = !!this.localStorageService.retrieve('chatWarningDismissed'); - } - constructor( protected metisService: MetisService, protected modalService: NgbModal, @@ -30,6 +25,11 @@ export class MessageReplyInlineInputComponent extends PostingCreateEditDirective super(metisService, modalService, formBuilder); } + ngOnInit(): void { + super.ngOnInit(); + this.warningDismissed = !!this.localStorageService.retrieve('chatWarningDismissed'); + } + /** * resets the answer post content */ diff --git a/src/main/webapp/app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component.html b/src/main/webapp/app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component.html index 514ab9cd54bb..6294a19b0276 100644 --- a/src/main/webapp/app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component.html +++ b/src/main/webapp/app/shared/metis/posting-create-edit-modal/answer-post-create-edit-modal/answer-post-create-edit-modal.component.html @@ -8,7 +8,7 @@