From 1348a86de1ce91d19422687567a7964fc9d9cc95 Mon Sep 17 00:00:00 2001 From: Florian Glombik <63976129+florian-glombik@users.noreply.github.com> Date: Sat, 1 Jun 2024 15:05:16 +0200 Subject: [PATCH] Programming exercises: Display checkout directories in build plan preview (#8597) --- .../de/tum/in/www1/artemis/security/Role.java | 2 +- .../ci/ContinuousIntegrationService.java | 17 +- .../connectors/gitlabci/GitLabCIService.java | 7 + .../connectors/jenkins/JenkinsService.java | 7 + .../connectors/localci/LocalCIService.java | 50 ++++ .../dto/BuildPlanCheckoutDirectoriesDTO.java | 8 + .../web/rest/dto/CheckoutDirectoriesDTO.java | 7 + .../ProgrammingExerciseResource.java | 20 ++ .../resources/templates/ocaml/test/run.sh | 6 +- .../detail-overview-list.component.html | 14 +- .../detail-overview-list.component.ts | 1 + .../app/detail-overview-list/detail.model.ts | 16 +- .../app/detail-overview-list/detail.module.ts | 4 + .../build-log-statistics-dto.ts | 0 .../build-plan-checkout-directories-dto.ts | 5 + .../app/entities/checkout-directories-dto.ts | 6 + .../entities/programming-exercise.model.ts | 2 +- .../exam-exercise-import.component.ts | 9 +- .../programming-exercise-detail.component.ts | 146 +++++++----- .../services/programming-exercise.service.ts | 14 +- ...ns-and-repositories-preview.component.html | 67 ------ ...lans-and-repositories-preview.component.ts | 20 -- .../programming-exercise-update.component.ts | 19 +- .../programming-exercise-update.module.ts | 8 +- ...amming-exercise-information.component.html | 15 +- ...d-plan-checkout-directories.component.html | 19 ++ ...ild-plan-checkout-directories.component.ts | 16 ++ ...tory-and-build-plan-details.component.html | 115 ++++++++++ ...sitory-and-build-plan-details.component.ts | 72 ++++++ .../app/shared/constants/input.constants.ts | 3 + .../webapp/i18n/de/programmingExercise.json | 8 +- .../webapp/i18n/en/programmingExercise.json | 10 +- .../ContinuousIntegrationTestService.java | 10 +- ...ExerciseLocalVCLocalCIIntegrationTest.java | 37 ++- .../artemis/localvcci/LocalCIServiceTest.java | 51 ++++ ...gramming-exercise-detail.component.spec.ts | 2 +- ...y-and-build-plan-details.component.spec.ts | 217 ++++++++++++++++++ ...gramming-exercise-update.component.spec.ts | 4 +- ...ing-exercise-information.component.spec.ts | 4 +- .../mock-programming-exercise.service.ts | 2 + 40 files changed, 841 insertions(+), 199 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/BuildPlanCheckoutDirectoriesDTO.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/dto/CheckoutDirectoriesDTO.java rename src/main/webapp/app/{exercises/programming/manage => entities}/build-log-statistics-dto.ts (100%) create mode 100644 src/main/webapp/app/entities/build-plan-checkout-directories-dto.ts create mode 100644 src/main/webapp/app/entities/checkout-directories-dto.ts delete mode 100644 src/main/webapp/app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component.html delete mode 100644 src/main/webapp/app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component.ts create mode 100644 src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component.html create mode 100644 src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component.ts create mode 100644 src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.html create mode 100644 src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts create mode 100644 src/test/javascript/spec/component/programming-exercise/programming-exercise-repository-and-build-plan-details.component.spec.ts diff --git a/src/main/java/de/tum/in/www1/artemis/security/Role.java b/src/main/java/de/tum/in/www1/artemis/security/Role.java index bbd95ce02751..e2c87d502059 100644 --- a/src/main/java/de/tum/in/www1/artemis/security/Role.java +++ b/src/main/java/de/tum/in/www1/artemis/security/Role.java @@ -5,7 +5,7 @@ */ public enum Role { - // NOTE: we will soon rename "USER" to "STUDENT" in the database and add a new role "EDITOR" + // NOTE: we will soon rename "USER" to "STUDENT" in the database ADMIN("ADMIN"), INSTRUCTOR("INSTRUCTOR"), EDITOR("EDITOR"), TEACHING_ASSISTANT("TA"), STUDENT("USER"), ANONYMOUS("ANONYMOUS"); public static final String ROLE_PREFIX = "ROLE_"; diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java index 12260b2e7448..35876ac08e9a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/ci/ContinuousIntegrationService.java @@ -17,6 +17,7 @@ import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.service.connectors.ConnectorHealth; +import de.tum.in.www1.artemis.web.rest.dto.CheckoutDirectoriesDTO; /** * Abstract service for managing entities related to continuous integration. @@ -41,7 +42,7 @@ enum BuildStatus { * @param planKey the key of the plan * @param repositoryUri the URI of the assignment repository (used to separate between exercise and solution) * @param testRepositoryUri the URI of the test repository - * @param solutionRepositoryUri the URI of the solution repository. Only used for HASKELL exercises with checkoutSolutionRepository=true. Otherwise ignored. + * @param solutionRepositoryUri the URI of the solution repository. Only used for HASKELL exercises with checkoutSolutionRepository=true. Otherwise, ignored. */ void createBuildPlanForExercise(ProgrammingExercise exercise, String planKey, VcsRepositoryUri repositoryUri, VcsRepositoryUri testRepositoryUri, VcsRepositoryUri solutionRepositoryUri); @@ -124,7 +125,7 @@ String copyBuildPlan(ProgrammingExercise sourceExercise, String sourcePlanName, * Get the build artifact (JAR/WAR), if any, of the latest build * * @param participation participation for which to get the build artifact - * @return the binary build artifact. Typically a JAR/WAR ResponseEntity. + * @return the binary build artifact. Typically, a JAR/WAR ResponseEntity. */ ResponseEntity retrieveLatestArtifact(ProgrammingExerciseParticipation participation); @@ -238,7 +239,7 @@ public String forProgrammingLanguage(ProgrammingLanguage language) { public String forProgrammingLanguage(ProgrammingLanguage language) { return switch (language) { case HASKELL, OCAML -> "solution"; - default -> throw new IllegalArgumentException("Repository checkout path for solution repo has not yet been defined for " + language); + default -> throw new IllegalArgumentException("The solution repository is not checked out during the template/submission build plan for " + language); }; } } @@ -255,4 +256,14 @@ interface CustomizableCheckoutPath { */ String forProgrammingLanguage(ProgrammingLanguage language); } + + /** + * Get the checkout directories for the template and submission build plan for a given programming language. + * + * @param programmingLanguage for which the checkout directories should be retrieved + * @param checkoutSolution whether the checkout solution repository shall be checked out during the template and submission build plan + * @return the paths of the checkout directories for the default repositories (exercise, solution, tests) for the + * template and submission build plan + */ + CheckoutDirectoriesDTO getCheckoutDirectories(ProgrammingLanguage programmingLanguage, boolean checkoutSolution); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java index c5d53ff178b6..f338a26e910f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlabci/GitLabCIService.java @@ -28,6 +28,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.ProgrammingSubmission; import de.tum.in.www1.artemis.domain.VcsRepositoryUri; +import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; import de.tum.in.www1.artemis.exception.GitLabCIException; @@ -37,6 +38,7 @@ import de.tum.in.www1.artemis.service.connectors.ci.AbstractContinuousIntegrationService; import de.tum.in.www1.artemis.service.connectors.ci.CIPermission; import de.tum.in.www1.artemis.service.connectors.ci.notification.dto.TestResultsDTO; +import de.tum.in.www1.artemis.web.rest.dto.CheckoutDirectoriesDTO; @Profile("gitlabci") @Service @@ -322,4 +324,9 @@ public Optional getWebHookUrl(String projectKey, String buildPlanId) { log.error("Unsupported action: GitLabCIService.getWebHookUrl()"); return Optional.empty(); } + + @Override + public CheckoutDirectoriesDTO getCheckoutDirectories(ProgrammingLanguage programmingLanguage, boolean checkoutSolution) { + throw new UnsupportedOperationException("Method not implemented, consult the build plans in GitLab for more information on the checkout directories."); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java index 48e8830096e1..5f251df9de94 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/jenkins/JenkinsService.java @@ -22,6 +22,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.VcsRepositoryUri; import de.tum.in.www1.artemis.domain.enumeration.BuildPlanType; +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.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.ContinuousIntegrationException; @@ -36,6 +37,7 @@ import de.tum.in.www1.artemis.service.connectors.ci.notification.dto.TestResultsDTO; import de.tum.in.www1.artemis.service.connectors.jenkins.build_plan.JenkinsBuildPlanService; import de.tum.in.www1.artemis.service.connectors.jenkins.jobs.JenkinsJobService; +import de.tum.in.www1.artemis.web.rest.dto.CheckoutDirectoriesDTO; @Profile("jenkins") @Service @@ -252,4 +254,9 @@ public void createProjectForExercise(ProgrammingExercise programmingExercise) th throw new JenkinsException("Error creating folder for exercise " + programmingExercise, e); } } + + @Override + public CheckoutDirectoriesDTO getCheckoutDirectories(ProgrammingLanguage programmingLanguage, boolean checkoutSolution) { + throw new UnsupportedOperationException("Method not implemented, consult the build plans in Jenkins for more information on the checkout directories."); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java index f17c4c1edcfd..4c2dde4578b2 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localci/LocalCIService.java @@ -18,6 +18,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.VcsRepositoryUri; +import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseParticipation; import de.tum.in.www1.artemis.exception.LocalCIException; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; @@ -27,6 +28,9 @@ import de.tum.in.www1.artemis.service.connectors.aeolus.Windfile; import de.tum.in.www1.artemis.service.connectors.ci.AbstractContinuousIntegrationService; import de.tum.in.www1.artemis.service.connectors.ci.CIPermission; +import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService; +import de.tum.in.www1.artemis.web.rest.dto.BuildPlanCheckoutDirectoriesDTO; +import de.tum.in.www1.artemis.web.rest.dto.CheckoutDirectoriesDTO; /** * Implementation of ContinuousIntegrationService for local CI. Contains methods for communication with the local CI system. @@ -219,4 +223,50 @@ public boolean checkIfBuildPlanExists(String projectKey, String buildPlanId) { private String getCleanPlanName(String name) { return name.toUpperCase().replaceAll("[^A-Z0-9]", ""); } + + @Override + public CheckoutDirectoriesDTO getCheckoutDirectories(ProgrammingLanguage programmingLanguage, boolean checkoutSolution) { + BuildPlanCheckoutDirectoriesDTO submissionBuildPlanCheckoutDirectories = getSubmissionBuildPlanCheckoutDirectories(programmingLanguage, checkoutSolution); + BuildPlanCheckoutDirectoriesDTO solutionBuildPlanCheckoutDirectories = getSolutionBuildPlanCheckoutDirectories(submissionBuildPlanCheckoutDirectories); + + return new CheckoutDirectoriesDTO(submissionBuildPlanCheckoutDirectories, solutionBuildPlanCheckoutDirectories); + } + + private BuildPlanCheckoutDirectoriesDTO getSubmissionBuildPlanCheckoutDirectories(ProgrammingLanguage programmingLanguage, boolean checkoutSolution) { + String exerciseCheckoutDirectory = ContinuousIntegrationService.RepositoryCheckoutPath.ASSIGNMENT.forProgrammingLanguage(programmingLanguage); + String testCheckoutDirectory = ContinuousIntegrationService.RepositoryCheckoutPath.TEST.forProgrammingLanguage(programmingLanguage); + + exerciseCheckoutDirectory = startPathWithRootDirectory(exerciseCheckoutDirectory); + testCheckoutDirectory = startPathWithRootDirectory(testCheckoutDirectory); + + String solutionCheckoutDirectory = null; + + if (checkoutSolution) { + try { + String solutionCheckoutDirectoryPath = ContinuousIntegrationService.RepositoryCheckoutPath.SOLUTION.forProgrammingLanguage(programmingLanguage); + solutionCheckoutDirectory = startPathWithRootDirectory(solutionCheckoutDirectoryPath); + } + catch (IllegalArgumentException exception) { + // not checked out during template & submission build + } + } + + return new BuildPlanCheckoutDirectoriesDTO(exerciseCheckoutDirectory, solutionCheckoutDirectory, testCheckoutDirectory); + } + + private String startPathWithRootDirectory(String checkoutDirectoryPath) { + final String ROOT_DIRECTORY = "/"; + if (checkoutDirectoryPath == null || checkoutDirectoryPath.isEmpty()) { + return ROOT_DIRECTORY; + } + + return checkoutDirectoryPath.startsWith(ROOT_DIRECTORY) ? checkoutDirectoryPath : ROOT_DIRECTORY + checkoutDirectoryPath; + } + + private BuildPlanCheckoutDirectoriesDTO getSolutionBuildPlanCheckoutDirectories(BuildPlanCheckoutDirectoriesDTO submissionBuildPlanCheckoutDirectories) { + String solutionCheckoutDirectory = submissionBuildPlanCheckoutDirectories.exerciseCheckoutDirectory(); + String testCheckoutDirectory = submissionBuildPlanCheckoutDirectories.testCheckoutDirectory(); + + return new BuildPlanCheckoutDirectoriesDTO(null, solutionCheckoutDirectory, testCheckoutDirectory); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/BuildPlanCheckoutDirectoriesDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/BuildPlanCheckoutDirectoriesDTO.java new file mode 100644 index 000000000000..f83aaae3ed2f --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/BuildPlanCheckoutDirectoriesDTO.java @@ -0,0 +1,8 @@ +package de.tum.in.www1.artemis.web.rest.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record BuildPlanCheckoutDirectoriesDTO(String exerciseCheckoutDirectory, String solutionCheckoutDirectory, String testCheckoutDirectory) { + +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/CheckoutDirectoriesDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/CheckoutDirectoriesDTO.java new file mode 100644 index 000000000000..8cd4f34f434e --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/CheckoutDirectoriesDTO.java @@ -0,0 +1,7 @@ +package de.tum.in.www1.artemis.web.rest.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record CheckoutDirectoriesDTO(BuildPlanCheckoutDirectoriesDTO submissionBuildPlanCheckoutDirectories, BuildPlanCheckoutDirectoriesDTO solutionBuildPlanCheckoutDirectories) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java index 656c47bf937d..a3013e07ae10 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java @@ -81,6 +81,7 @@ import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseService; import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseTestCaseService; import de.tum.in.www1.artemis.web.rest.dto.BuildLogStatisticsDTO; +import de.tum.in.www1.artemis.web.rest.dto.CheckoutDirectoriesDTO; import de.tum.in.www1.artemis.web.rest.dto.ProgrammingExerciseResetOptionsDTO; import de.tum.in.www1.artemis.web.rest.dto.SearchResultPageDTO; import de.tum.in.www1.artemis.web.rest.dto.pageablesearch.SearchTermPageableSearchDTO; @@ -878,4 +879,23 @@ public ResponseEntity getBuildLogStatistics(@PathVariable return ResponseEntity.ok(buildLogStatistics); } + /** + * GET programming-exercises/repository-checkout-directories + * + * @param programmingLanguage for which the checkout directories should be retrieved + * @param checkoutSolution whether the checkout solution repository shall be checked out during the template and submission build plan, + * if not supplied set to true as default + * @return a DTO containing the checkout directories for the exercise, solution, and tests repository + * for the requested programming language for the submission and solution build. + */ + @GetMapping("programming-exercises/repository-checkout-directories") + @EnforceAtLeastEditor + @FeatureToggle(Feature.ProgrammingExercises) + public ResponseEntity getRepositoryCheckoutDirectories(@RequestParam(value = "programmingLanguage") ProgrammingLanguage programmingLanguage, + @RequestParam(value = "checkoutSolution", defaultValue = "true") boolean checkoutSolution) { + log.debug("REST request to get checkout directories for programming language: {}", programmingLanguage); + + CheckoutDirectoriesDTO repositoriesCheckoutDirectoryDTO = continuousIntegrationService.orElseThrow().getCheckoutDirectories(programmingLanguage, checkoutSolution); + return ResponseEntity.ok(repositoriesCheckoutDirectoryDTO); + } } diff --git a/src/main/resources/templates/ocaml/test/run.sh b/src/main/resources/templates/ocaml/test/run.sh index 6b831445f61f..fe73a2830c98 100755 --- a/src/main/resources/templates/ocaml/test/run.sh +++ b/src/main/resources/templates/ocaml/test/run.sh @@ -52,7 +52,7 @@ cp_code solution echo 'include Assignment' > solution/solution.ml cp_code assignment -# select if tests are run by generated source code as studen toplevel code may run before the tests and be able to spoof a runtime signal +# select if tests are run by generated source code as student toplevel code may run before the tests and be able to spoof a runtime signal echo "let runHidden = $RUN_HIDDEN" > test/runHidden.ml # shellcheck disable=SC2046 @@ -69,8 +69,8 @@ if ! timeout -s SIGTERM $BUILD_TIMEOUT checker/checker.exe; then echo "Unable to build submission, please ensure that your code builds and matches the provided interface" >&2 exit 0 fi -# build the sudent submission -# don't reference the tests or solution, so that we can show the build output to the sudent and not leak test / solution code +# build the student submission +# don't reference the tests or solution, so that we can show the build output to the student and not leak test / solution code if ! timeout -s SIGTERM $BUILD_TIMEOUT dune build --force assignment; then echo "Unable to build submission, please ensure that your code builds and matches the provided interface" >&2 exit 0 diff --git a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html index 5271ec9101c0..dbabc394771a 100644 --- a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html +++ b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.html @@ -102,9 +102,7 @@

{{ section }
- @if (auxiliaryRepository.checkoutDirectory) { - Checkout Directory: {{ auxiliaryRepository.checkoutDirectory }} - } @else { + @if (!auxiliaryRepository.checkoutDirectory) { } @@ -285,6 +283,16 @@

{{ section } } + @case (DetailType.ProgrammingCheckoutDirectories) { +
+ +
+ } } } } diff --git a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts index 8bdf8e132958..1c8d9a73343e 100644 --- a/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts +++ b/src/main/webapp/app/detail-overview-list/detail-overview-list.component.ts @@ -37,6 +37,7 @@ export enum DetailType { ProgrammingProblemStatement = 'detail-problem-statement', ProgrammingTimeline = 'detail-timeline', ProgrammingBuildStatistics = 'detail-build-statistics', + ProgrammingCheckoutDirectories = 'detail-checkout-directories', } @Component({ diff --git a/src/main/webapp/app/detail-overview-list/detail.model.ts b/src/main/webapp/app/detail-overview-list/detail.model.ts index e87603a5b13b..9c95158e0307 100644 --- a/src/main/webapp/app/detail-overview-list/detail.model.ts +++ b/src/main/webapp/app/detail-overview-list/detail.model.ts @@ -1,12 +1,12 @@ import { GradingCriterion } from 'app/exercises/shared/structured-grading-criterion/grading-criterion.model'; -import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; +import type { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming-exercise.model'; import { TemplateProgrammingExerciseParticipation } from 'app/entities/participation/template-programming-exercise-participation.model'; import { SolutionProgrammingExerciseParticipation } from 'app/entities/participation/solution-programming-exercise-participation.model'; import { ProgrammingExerciseInstructorRepositoryType } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { AuxiliaryRepository } from 'app/entities/programming-exercise-auxiliary-repository-model'; import { ProgrammingExerciseParticipationType } from 'app/entities/programming-exercise-participation.model'; import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; -import { BuildLogStatisticsDTO } from 'app/exercises/programming/manage/build-log-statistics-dto'; +import { BuildLogStatisticsDTO } from 'app/entities/build-log-statistics-dto'; import { DetailType } from 'app/detail-overview-list/detail-overview-list.component'; import { SafeHtml } from '@angular/platform-browser'; import { UMLDiagramType, UMLModel } from '@ls1intum/apollon'; @@ -33,7 +33,8 @@ type ShownDetail = | ProgrammingDiffReportDetail | ProgrammingProblemStatementDetail | ProgrammingTimelineDetail - | ProgrammingBuildStatisticsDetail; + | ProgrammingBuildStatisticsDetail + | ProgrammingCheckoutDirectoriesDetail; interface DetailBase { type: DetailType; @@ -129,3 +130,12 @@ interface ProgrammingBuildStatisticsDetail extends DetailBase { buildLogStatistics: BuildLogStatisticsDTO; }; } + +interface ProgrammingCheckoutDirectoriesDetail extends DetailBase { + type: DetailType.ProgrammingCheckoutDirectories; + data: { + exercise: ProgrammingExercise; + programmingLanguage?: ProgrammingLanguage; + isLocal: boolean; + }; +} diff --git a/src/main/webapp/app/detail-overview-list/detail.module.ts b/src/main/webapp/app/detail-overview-list/detail.module.ts index b849ae5e67e4..217146a1fd88 100644 --- a/src/main/webapp/app/detail-overview-list/detail.module.ts +++ b/src/main/webapp/app/detail-overview-list/detail.module.ts @@ -12,6 +12,8 @@ import { ArtemisProgrammingExerciseLifecycleModule } from 'app/exercises/program import { AssessmentInstructionsModule } from 'app/assessment/assessment-instructions/assessment-instructions.module'; import { IrisModule } from 'app/iris/iris.module'; import { ArtemisModelingEditorModule } from 'app/exercises/modeling/shared/modeling-editor.module'; +import { ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component'; +import { ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component'; @NgModule({ imports: [ @@ -27,6 +29,8 @@ import { ArtemisModelingEditorModule } from 'app/exercises/modeling/shared/model AssessmentInstructionsModule, IrisModule, ArtemisModelingEditorModule, + ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent, + ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent, ], declarations: [DetailOverviewListComponent], exports: [DetailOverviewListComponent], diff --git a/src/main/webapp/app/exercises/programming/manage/build-log-statistics-dto.ts b/src/main/webapp/app/entities/build-log-statistics-dto.ts similarity index 100% rename from src/main/webapp/app/exercises/programming/manage/build-log-statistics-dto.ts rename to src/main/webapp/app/entities/build-log-statistics-dto.ts diff --git a/src/main/webapp/app/entities/build-plan-checkout-directories-dto.ts b/src/main/webapp/app/entities/build-plan-checkout-directories-dto.ts new file mode 100644 index 000000000000..73d8d1dec2a2 --- /dev/null +++ b/src/main/webapp/app/entities/build-plan-checkout-directories-dto.ts @@ -0,0 +1,5 @@ +export class BuildPlanCheckoutDirectoriesDTO { + exerciseCheckoutDirectory?: string; + solutionCheckoutDirectory?: string; + testCheckoutDirectory: string; +} diff --git a/src/main/webapp/app/entities/checkout-directories-dto.ts b/src/main/webapp/app/entities/checkout-directories-dto.ts new file mode 100644 index 000000000000..71b05a32d187 --- /dev/null +++ b/src/main/webapp/app/entities/checkout-directories-dto.ts @@ -0,0 +1,6 @@ +import { BuildPlanCheckoutDirectoriesDTO } from 'app/entities/build-plan-checkout-directories-dto'; + +export class CheckoutDirectoriesDto { + submissionBuildPlanCheckoutDirectories?: BuildPlanCheckoutDirectoriesDTO; + solutionBuildPlanCheckoutDirectories?: BuildPlanCheckoutDirectoriesDTO; +} diff --git a/src/main/webapp/app/entities/programming-exercise.model.ts b/src/main/webapp/app/entities/programming-exercise.model.ts index 653a6bfaf778..e14bc12a0beb 100644 --- a/src/main/webapp/app/entities/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming-exercise.model.ts @@ -8,7 +8,7 @@ import { AuxiliaryRepository } from 'app/entities/programming-exercise-auxiliary import { SubmissionPolicy } from 'app/entities/submission-policy.model'; import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; import { ExerciseHint } from 'app/entities/hestia/exercise-hint.model'; -import { BuildLogStatisticsDTO } from 'app/exercises/programming/manage/build-log-statistics-dto'; +import { BuildLogStatisticsDTO } from 'app/entities/build-log-statistics-dto'; export class BuildAction { name: string; diff --git a/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.ts b/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.ts index 0f212060dccf..342c7b6fb1c1 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-exercise-import/exam-exercise-import.component.ts @@ -3,7 +3,7 @@ import { Exam } from 'app/entities/exam.model'; import { faCheckDouble, faFont } from '@fortawesome/free-solid-svg-icons'; import { Exercise, ExerciseType, getIcon } from 'app/entities/exercise.model'; import { ExerciseGroup } from 'app/entities/exercise-group.model'; -import { SHORT_NAME_PATTERN } from 'app/shared/constants/input.constants'; +import { EXERCISE_TITLE_NAME_REGEX, SHORT_NAME_PATTERN } from 'app/shared/constants/input.constants'; @Component({ selector: 'jhi-exam-exercise-import', @@ -33,8 +33,7 @@ export class ExamExerciseImportComponent implements OnInit { // Patterns // length of < 3 is also accepted in order to provide more accurate validation error messages - readonly shortNamePattern = RegExp('(^(?![\\s\\S]))|^[a-zA-Z][a-zA-Z0-9]*$|' + SHORT_NAME_PATTERN); // must start with a letter and cannot contain special characters - readonly titleNamePattern = RegExp('^[a-zA-Z0-9-_ ]+'); // must only contain alphanumeric characters, or whitespaces, or '_' or '-' + readonly SHORT_NAME_REGEX = RegExp('(^(?![\\s\\S]))|^[a-zA-Z][a-zA-Z0-9]*$|' + SHORT_NAME_PATTERN); // must start with a letter and cannot contain special characters // Icons faCheckDouble = faCheckDouble; @@ -217,7 +216,7 @@ export class ExamExerciseImportComponent implements OnInit { validateTitleOfProgrammingExercise(exercise: Exercise): boolean { return ( !!exercise.title?.length && - this.titleNamePattern.test(exercise.title!) && + EXERCISE_TITLE_NAME_REGEX.test(exercise.title!) && !this.exercisesWithDuplicatedTitles.has(exercise.id!) && (exercise.title !== this.getBlocklistTitleOfProgrammingExercise(exercise.id!) || this.getBlocklistShortNameOfProgrammingExercise(exercise.id!) === '') ); @@ -231,7 +230,7 @@ export class ExamExerciseImportComponent implements OnInit { return ( // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain exercise.shortName?.length! > 2 && - this.shortNamePattern.test(exercise.shortName!) && + this.SHORT_NAME_REGEX.test(exercise.shortName!) && !this.exercisesWithDuplicatedShortNames.has(exercise.id!) && exercise.shortName !== this.getBlocklistShortNameOfProgrammingExercise(exercise.id!) ); 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 5caa177d4960..86791687e2f5 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 @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { SafeHtml } from '@angular/platform-browser'; -import { Subject } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming-exercise.model'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { AlertService, AlertType } from 'app/core/util/alert.service'; @@ -102,6 +102,14 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { plagiarismCheckSupported = false; // default value + private activatedRouteSubscription: Subscription; + private templateAndSolutionParticipationSubscription: Subscription; + private profileInfoSubscription: Subscription; + private irisSettingsSubscription: Subscription; + private submissionPolicySubscription: Subscription; + private buildLogsSubscription: Subscription; + private exerciseStatisticsSubscription: Subscription; + private dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); @@ -148,7 +156,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { ngOnInit() { this.checkBuildPlanEditable(); - this.activatedRoute.data.subscribe(({ programmingExercise }) => { + this.activatedRouteSubscription = this.activatedRoute.data.subscribe(({ programmingExercise }) => { this.programmingExercise = programmingExercise; this.competencies = programmingExercise.competencies; const exerciseId = this.programmingExercise.id!; @@ -171,69 +179,74 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { `/exercise-groups/${this.programmingExercise.exerciseGroup?.id}/exercises/${this.programmingExercise.exerciseGroup?.exam?.id}/`; } - this.programmingExerciseService.findWithTemplateAndSolutionParticipationAndLatestResults(programmingExercise.id!).subscribe((updatedProgrammingExercise) => { - this.programmingExercise = updatedProgrammingExercise.body!; - - this.setLatestCoveredLineRatio(); - this.loadingTemplateParticipationResults = false; - this.loadingSolutionParticipationResults = false; - this.profileService.getProfileInfo().subscribe(async (profileInfo) => { - if (profileInfo) { - if (this.programmingExercise.projectKey && this.programmingExercise.templateParticipation && this.programmingExercise.templateParticipation.buildPlanId) { - this.programmingExercise.templateParticipation.buildPlanUrl = createBuildPlanUrl( - profileInfo.buildPlanURLTemplate, - this.programmingExercise.projectKey, - this.programmingExercise.templateParticipation.buildPlanId, - ); - } - if (this.programmingExercise.projectKey && this.programmingExercise.solutionParticipation && this.programmingExercise.solutionParticipation.buildPlanId) { - this.programmingExercise.solutionParticipation.buildPlanUrl = createBuildPlanUrl( - profileInfo.buildPlanURLTemplate, - this.programmingExercise.projectKey, - this.programmingExercise.solutionParticipation.buildPlanId, - ); - } - this.supportsAuxiliaryRepositories = - this.programmingLanguageFeatureService.getProgrammingLanguageFeature(programmingExercise.programmingLanguage).auxiliaryRepositoriesSupported ?? false; - this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); - this.localCIEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALCI); - this.irisEnabled = profileInfo.activeProfiles.includes(PROFILE_IRIS); - if (this.irisEnabled) { - this.irisSettingsService.getCombinedCourseSettings(this.courseId).subscribe((settings) => { - this.irisChatEnabled = settings?.irisChatSettings?.enabled ?? false; - this.exerciseDetailSections = this.getExerciseDetails(); - }); + this.templateAndSolutionParticipationSubscription = this.programmingExerciseService + .findWithTemplateAndSolutionParticipationAndLatestResults(programmingExercise.id!) + .subscribe((updatedProgrammingExercise) => { + this.programmingExercise = updatedProgrammingExercise.body!; + + this.setLatestCoveredLineRatio(); + this.loadingTemplateParticipationResults = false; + this.loadingSolutionParticipationResults = false; + this.profileInfoSubscription = this.profileService.getProfileInfo().subscribe(async (profileInfo) => { + if (profileInfo) { + if (this.programmingExercise.projectKey && this.programmingExercise.templateParticipation?.buildPlanId) { + this.programmingExercise.templateParticipation.buildPlanUrl = createBuildPlanUrl( + profileInfo.buildPlanURLTemplate, + this.programmingExercise.projectKey, + this.programmingExercise.templateParticipation.buildPlanId, + ); + } + if (this.programmingExercise.projectKey && this.programmingExercise.solutionParticipation?.buildPlanId) { + this.programmingExercise.solutionParticipation.buildPlanUrl = createBuildPlanUrl( + profileInfo.buildPlanURLTemplate, + this.programmingExercise.projectKey, + this.programmingExercise.solutionParticipation.buildPlanId, + ); + } + this.supportsAuxiliaryRepositories = + this.programmingLanguageFeatureService.getProgrammingLanguageFeature(programmingExercise.programmingLanguage).auxiliaryRepositoriesSupported ?? + false; + this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + this.localCIEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALCI); + this.irisEnabled = profileInfo.activeProfiles.includes(PROFILE_IRIS); + if (this.irisEnabled) { + this.irisSettingsSubscription = this.irisSettingsService.getCombinedCourseSettings(this.courseId).subscribe((settings) => { + this.irisChatEnabled = settings?.irisChatSettings?.enabled ?? false; + this.exerciseDetailSections = this.getExerciseDetails(); + }); + } } - } - this.exerciseDetailSections = this.getExerciseDetails(); - }); - - this.programmingExerciseSubmissionPolicyService.getSubmissionPolicyOfProgrammingExercise(exerciseId!).subscribe((submissionPolicy) => { - this.programmingExercise.submissionPolicy = submissionPolicy; - this.exerciseDetailSections = this.getExerciseDetails(); - }); + this.exerciseDetailSections = this.getExerciseDetails(); + }); + + this.submissionPolicySubscription = this.programmingExerciseSubmissionPolicyService + .getSubmissionPolicyOfProgrammingExercise(exerciseId!) + .subscribe((submissionPolicy) => { + this.programmingExercise.submissionPolicy = submissionPolicy; + this.exerciseDetailSections = this.getExerciseDetails(); + }); - this.loadGitDiffReport(); + this.loadGitDiffReport(); - // the build logs endpoint requires at least editor privileges - if (this.programmingExercise.isAtLeastEditor) { - this.programmingExerciseService - .getBuildLogStatistics(exerciseId!) - .subscribe((buildLogStatistics) => (this.programmingExercise.buildLogStatistics = buildLogStatistics)); - this.exerciseDetailSections = this.getExerciseDetails(); - } + // the build logs endpoint requires at least editor privileges + if (this.programmingExercise.isAtLeastEditor) { + this.buildLogsSubscription = this.programmingExerciseService + .getBuildLogStatistics(exerciseId!) + .subscribe((buildLogStatistics) => (this.programmingExercise.buildLogStatistics = buildLogStatistics)); + this.exerciseDetailSections = this.getExerciseDetails(); + } - this.setLatestCoveredLineRatio(); + this.setLatestCoveredLineRatio(); - this.checkAndAlertInconsistencies(); + this.checkAndAlertInconsistencies(); - this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature( - programmingExercise.programmingLanguage, - ).plagiarismCheckSupported; - this.exerciseDetailSections = this.getExerciseDetails(); - }); + this.plagiarismCheckSupported = this.programmingLanguageFeatureService.getProgrammingLanguageFeature( + programmingExercise.programmingLanguage, + ).plagiarismCheckSupported; + this.exerciseDetailSections = this.getExerciseDetails(); + }); - this.statisticsService.getExerciseStatistics(exerciseId!).subscribe((statistics: ExerciseManagementStatisticsDto) => { + this.exerciseStatisticsSubscription = this.statisticsService.getExerciseStatistics(exerciseId!).subscribe((statistics: ExerciseManagementStatisticsDto) => { this.doughnutStats = statistics; }); }); @@ -241,6 +254,13 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.dialogErrorSource.unsubscribe(); + this.activatedRouteSubscription?.unsubscribe(); + this.templateAndSolutionParticipationSubscription?.unsubscribe(); + this.profileInfoSubscription?.unsubscribe(); + this.irisSettingsSubscription?.unsubscribe(); + this.submissionPolicySubscription?.unsubscribe(); + this.buildLogsSubscription?.unsubscribe(); + this.exerciseStatisticsSubscription?.unsubscribe(); } getExerciseDetails(): DetailOverviewSection[] { @@ -375,6 +395,16 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { showOpenLink: !this.localVCEnabled, }, }, + exercise.isAtLeastEditor && + this.localCIEnabled && { + type: DetailType.ProgrammingCheckoutDirectories, + title: 'artemisApp.programmingExercise.checkoutDirectories', + data: { + exercise: exercise, + programmingLanguage: exercise.programmingLanguage, + isLocal: true, + }, + }, !this.localCIEnabled && { type: DetailType.Link, title: 'artemisApp.programmingExercise.templateBuildPlanId', diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts index c0c6469b93b1..e5710a37db11 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise.service.ts @@ -7,7 +7,7 @@ import { omit as _omit } from 'lodash-es'; import { createRequestOption } from 'app/shared/util/request.util'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; -import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; +import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming-exercise.model'; import { TemplateProgrammingExerciseParticipation } from 'app/entities/participation/template-programming-exercise-participation.model'; import { SolutionProgrammingExerciseParticipation } from 'app/entities/participation/solution-programming-exercise-participation.model'; import { TextPlagiarismResult } from 'app/exercises/shared/plagiarism/types/text/TextPlagiarismResult'; @@ -20,12 +20,13 @@ import { ProgrammingExerciseServerSideTask } from 'app/entities/hestia/programmi import { convertDateFromClient, convertDateFromServer } from 'app/utils/date.utils'; import { ExerciseHint } from 'app/entities/hestia/exercise-hint.model'; import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model'; -import { BuildLogStatisticsDTO } from 'app/exercises/programming/manage/build-log-statistics-dto'; +import { BuildLogStatisticsDTO } from 'app/entities/build-log-statistics-dto'; import { SortService } from 'app/shared/service/sort.service'; import { Result } from 'app/entities/result.model'; import { Participation } from 'app/entities/participation/participation.model'; import { PlagiarismResultDTO } from 'app/exercises/shared/plagiarism/types/PlagiarismResultDTO'; import { ImportOptions } from 'app/types/programming-exercises'; +import { CheckoutDirectoriesDto } from 'app/entities/checkout-directories-dto'; export type EntityResponseType = HttpResponse; export type EntityArrayResponseType = HttpResponse; @@ -673,4 +674,13 @@ export class ProgrammingExerciseService { .post(url, formData, { observe: 'response' }) .pipe(map((res: EntityResponseType) => this.processProgrammingExerciseEntityResponse(res))); } + + getCheckoutDirectoriesForProgrammingLanguage(programmingLanguage: ProgrammingLanguage, checkoutSolution: boolean): Observable { + return this.http.get(`${this.resourceUrl}/repository-checkout-directories`, { + params: { + programmingLanguage, + checkoutSolution: checkoutSolution, + }, + }); + } } diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component.html b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component.html deleted file mode 100644 index 6cbba474b951..000000000000 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component.html +++ /dev/null @@ -1,67 +0,0 @@ -@if (programmingExercise && programmingExercise.shortName) { -
-
-
-
- -
-
-
    -
  • - - {{ getCourseShortName()?.toLowerCase() }}{{ programmingExercise.shortName.toLowerCase() }}-exercise - - -
  • -
  • - - {{ getCourseShortName()?.toLowerCase() }}{{ programmingExercise.shortName.toLowerCase() }}-solution - - -
  • -
  • - - {{ getCourseShortName()?.toLowerCase() }}{{ programmingExercise.shortName.toLowerCase() }}-tests - - -
  • - @if (programmingExercise.auxiliaryRepositories?.length) { -
    - @for (auxiliaryRepository of programmingExercise.auxiliaryRepositories; track auxiliaryRepository) { -
    -
  • - - {{ getCourseShortName()?.toLowerCase() }}{{ programmingExercise.shortName.toLowerCase() }}-{{ auxiliaryRepository.name?.toLowerCase() }} - -
  • -
    - } -
    - } -
-
-
- @if (!isLocal) { -
-
- -
-
-
    -
  • - {{ getCourseShortName()?.toUpperCase() }}{{ programmingExercise.shortName.toUpperCase() }}-BASE - -
  • -
  • - {{ getCourseShortName()?.toUpperCase() }}{{ programmingExercise.shortName.toUpperCase() }}-SOLUTION - -
  • -
-
- } -
-} diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component.ts deleted file mode 100644 index 8eb2da99f4e5..000000000000 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { getCourseFromExercise } from 'app/entities/exercise.model'; -import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; - -@Component({ - selector: 'jhi-programming-exercise-plans-and-repositories-preview', - templateUrl: './programming-exercise-plans-and-repositories-preview.component.html', - styleUrls: ['../programming-exercise-form.scss'], -}) -export class ProgrammingExercisePlansAndRepositoriesPreviewComponent { - @Input() programmingExercise: ProgrammingExercise | null; - @Input() isLocal: boolean; - - getCourseShortName(): string | undefined { - if (!this.programmingExercise) { - return undefined; - } - return getCourseFromExercise(this.programmingExercise)?.shortName; - } -} diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts index 2af04b36d26e..764433e05c19 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts @@ -18,7 +18,7 @@ import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { ExerciseGroupService } from 'app/exam/manage/exercise-groups/exercise-group.service'; import { ProgrammingLanguageFeatureService } from 'app/exercises/programming/shared/service/programming-language-feature/programming-language-feature.service'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; -import { SHORT_NAME_PATTERN } from 'app/shared/constants/input.constants'; +import { EXERCISE_TITLE_NAME_PATTERN, EXERCISE_TITLE_NAME_REGEX, SHORT_NAME_PATTERN } from 'app/shared/constants/input.constants'; import { ExerciseCategory } from 'app/entities/exercise-category.model'; import { cloneDeep } from 'lodash-es'; import { ExerciseUpdateWarningService } from 'app/exercises/shared/exercise-update-warning/exercise-update-warning.service'; @@ -115,7 +115,6 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest // length of < 3 is also accepted in order to provide more accurate validation error messages readonly shortNamePattern = RegExp('(^(?![\\s\\S]))|^[a-zA-Z][a-zA-Z0-9]*$|' + SHORT_NAME_PATTERN); // must start with a letter and cannot contain special characters - titleNamePattern = '^[a-zA-Z0-9-_ ]+'; // must only contain alphanumeric characters, or whitespaces, or '_' or '-' exerciseCategories: ExerciseCategory[]; existingCategories: ExerciseCategory[]; @@ -205,15 +204,19 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest * used in the template to display warnings. */ refreshAuxiliaryRepositoryChecks() { + if (!this.programmingExercise?.auxiliaryRepositories) { + return; + } + let legalNameAndDirs = false; // Check that there are no duplicate names. const names = new Set(); - const auxReposWithName = this.programmingExercise.auxiliaryRepositories!.filter((auxiliaryRepository) => auxiliaryRepository.name); - auxReposWithName.forEach((auxiliaryRepository) => { + const auxReposWithName = this.programmingExercise.auxiliaryRepositories?.filter((auxiliaryRepository) => auxiliaryRepository.name); + auxReposWithName?.forEach((auxiliaryRepository) => { names.add(auxiliaryRepository.name); legalNameAndDirs ||= !this.invalidRepositoryNamePattern.test(auxiliaryRepository.name!); }); - this.auxiliaryRepositoryDuplicateNames = names.size !== auxReposWithName.length; + this.auxiliaryRepositoryDuplicateNames = names.size !== auxReposWithName?.length; // Check that there are no duplicate checkout directories const directories = new Set(); @@ -225,7 +228,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest this.auxiliaryRepositoryDuplicateDirectories = directories.size !== auxReposWithDirectory.length; // Check that there are no empty/incorrect repository names and directories - this.auxiliaryRepositoryNamedCorrectly = this.programmingExercise.auxiliaryRepositories!.length === auxReposWithName.length && !legalNameAndDirs; + this.auxiliaryRepositoryNamedCorrectly = this.programmingExercise.auxiliaryRepositories!.length === auxReposWithName?.length && !legalNameAndDirs; // Combining auxiliary variables to one to keep the template readable this.auxiliaryRepositoriesValid = this.auxiliaryRepositoryNamedCorrectly && !this.auxiliaryRepositoryDuplicateNames && !this.auxiliaryRepositoryDuplicateDirectories; @@ -838,7 +841,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest translateKey: 'artemisApp.exercise.form.title.undefined', translateValues: {}, }); - } else if (this.programmingExercise.title.match(this.titleNamePattern) === null || this.programmingExercise.title?.match(this.titleNamePattern)?.length === 0) { + } else if (!EXERCISE_TITLE_NAME_REGEX.test(this.programmingExercise.title)) { validationErrorReasons.push({ translateKey: 'artemisApp.exercise.form.title.pattern', translateValues: {}, @@ -1058,7 +1061,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest customBuildPlansSupported: this.customBuildPlansSupported, invalidDirectoryNamePattern: this.invalidDirectoryNamePattern, invalidRepositoryNamePattern: this.invalidRepositoryNamePattern, - titleNamePattern: this.titleNamePattern, + titleNamePattern: EXERCISE_TITLE_NAME_PATTERN, shortNamePattern: this.shortNamePattern, updateRepositoryName: this.updateRepositoryName, updateCheckoutDirectory: this.updateCheckoutDirectory, diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts index b2c9aeba59c9..d055eb081a39 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts @@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'; import { ProgrammingExerciseUpdateComponent } from 'app/exercises/programming/manage/update/programming-exercise-update.component'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { ProgrammingExercisePlansAndRepositoriesPreviewComponent } from 'app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component'; import { ArtemisTeamConfigFormGroupModule } from 'app/exercises/shared/team-config-form-group/team-config-form-group.module'; import { ArtemisDifficultyPickerModule } from 'app/exercises/shared/difficulty-picker/difficulty-picker.module'; import { ArtemisPresentationScoreModule } from 'app/exercises/shared/presentation-score/presentation-score.module'; @@ -31,6 +30,8 @@ import { AceEditorModule } from 'app/shared/markdown-editor/ace-editor/ace-edito import { ProgrammingExerciseCustomBuildPlanComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component'; import { ProgrammingExerciseDockerImageComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-docker-image/programming-exercise-docker-image.component'; import { FormsModule } from 'app/forms/forms.module'; +import { ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component'; +import { ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component'; @NgModule({ imports: [ @@ -54,6 +55,8 @@ import { FormsModule } from 'app/forms/forms.module'; ExerciseUpdatePlagiarismModule, AceEditorModule, FormsModule, + ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent, + ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent, ], declarations: [ ProgrammingExerciseUpdateComponent, @@ -65,10 +68,9 @@ import { FormsModule } from 'app/forms/forms.module'; ProgrammingExerciseLanguageComponent, ProgrammingExerciseGradingComponent, ProgrammingExerciseProblemComponent, - ProgrammingExercisePlansAndRepositoriesPreviewComponent, AddAuxiliaryRepositoryButtonComponent, RemoveAuxiliaryRepositoryButtonComponent, ], - exports: [ProgrammingExerciseUpdateComponent, ProgrammingExercisePlansAndRepositoriesPreviewComponent], + exports: [ProgrammingExerciseUpdateComponent], }) export class ArtemisProgrammingExerciseUpdateModule {} diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.html index acdf192cba22..e370449b38b4 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.html @@ -49,9 +49,14 @@
- +
- + @if (!programmingExerciseCreationConfig.isImportFromExistingExercise && programmingExerciseCreationConfig.auxiliaryRepositoriesSupported) {
@if (programmingExercise.auxiliaryRepositories && programmingExercise.auxiliaryRepositories.length > 0) { @@ -110,16 +115,16 @@ @if (programmingExerciseCreationConfig.auxiliaryRepositoryDuplicateNames || programmingExerciseCreationConfig.auxiliaryRepositoryDuplicateDirectories) { @if (programmingExerciseCreationConfig.auxiliaryRepositoryDuplicateNames) { - {{ 'artemisApp.programmingExercise.auxiliaryRepository.duplicateRepositoryNames' | artemisTranslate }} + } @if (programmingExerciseCreationConfig.auxiliaryRepositoryDuplicateDirectories) { - {{ 'artemisApp.programmingExercise.auxiliaryRepository.duplicateDirectoryNames' | artemisTranslate }} + } } @if (programmingExerciseCreationConfig && !isLocal) { - {{ 'artemisApp.programmingExercise.auxiliaryRepository.warning' | artemisTranslate }} + } +
    +
  • + {{ checkoutDirectories.exerciseCheckoutDirectory }} +
  • +
  • + {{ checkoutDirectories.solutionCheckoutDirectory }} +
  • +
  • + {{ checkoutDirectories.testCheckoutDirectory }} +
  • + @for (auxiliaryRepository of auxiliaryRepositories; track auxiliaryRepository) { +
  • + {{ ROOT_DIRECTORY_PATH }}{{ auxiliaryRepository.checkoutDirectory }} +
  • + } +
+} diff --git a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component.ts b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component.ts new file mode 100644 index 000000000000..4bcffecaa5af --- /dev/null +++ b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; +import { BuildPlanCheckoutDirectoriesDTO } from 'app/entities/build-plan-checkout-directories-dto'; +import { AuxiliaryRepository } from 'app/entities/programming-exercise-auxiliary-repository-model'; + +@Component({ + selector: 'jhi-programming-exercise-build-plan-checkout-directories', + templateUrl: './programming-exercise-build-plan-checkout-directories.component.html', + styleUrls: ['../../manage/programming-exercise-form.scss'], + standalone: true, +}) +export class ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent { + @Input() checkoutDirectories?: BuildPlanCheckoutDirectoriesDTO; + @Input() auxiliaryRepositories: AuxiliaryRepository[]; + + ROOT_DIRECTORY_PATH = '/'; +} diff --git a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.html b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.html new file mode 100644 index 000000000000..53b0300ab054 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.html @@ -0,0 +1,115 @@ +
+
+
+
+ +
+
+
    + + + + + @if (programmingExercise?.auxiliaryRepositories?.length) { + @for (auxiliaryRepository of programmingExercise!.auxiliaryRepositories; track auxiliaryRepository) { + + } + } +
+
+
+ @if (!isLocal) { +
+
+ +
+
+
    + + +
+
+ } @else if (checkoutDirectories) { +
+
+ + +
+ +
+
+
+ + +
+ +
+ } +
+ + +
  • + + {{ courseShortName }}{{ programmingExercise?.shortName }}{{ trailingRepoSlug }} + + @if (helpIconText) { + + } +
  • +
    diff --git a/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts new file mode 100644 index 000000000000..30021b146f48 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component.ts @@ -0,0 +1,72 @@ +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { getCourseFromExercise } from 'app/entities/exercise.model'; +import type { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming-exercise.model'; +import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; +import { Subscription } from 'rxjs'; +import type { CheckoutDirectoriesDto } from 'app/entities/checkout-directories-dto'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component'; + +@Component({ + selector: 'jhi-programming-exercise-repository-and-build-plan-details', + templateUrl: './programming-exercise-repository-and-build-plan-details.component.html', + styleUrls: ['../../manage/programming-exercise-form.scss'], + standalone: true, + imports: [ArtemisSharedComponentModule, ArtemisSharedCommonModule, ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent], +}) +export class ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent implements OnInit, OnChanges, OnDestroy { + @Input() programmingExercise: ProgrammingExercise; + @Input() programmingLanguage?: ProgrammingLanguage; + @Input() isLocal: boolean; + @Input() checkoutSolutionRepository?: boolean = true; + + constructor(private programmingExerciseService: ProgrammingExerciseService) {} + + checkoutDirectorySubscription?: Subscription; + + courseShortName?: string; + checkoutDirectories?: CheckoutDirectoriesDto; + + ngOnInit() { + this.updateCourseShortName(); + + if (this.isLocal) { + this.updateCheckoutDirectories(); + } + } + + ngOnChanges(changes: SimpleChanges) { + const isProgrammingLanguageUpdated = changes.programmingLanguage?.currentValue !== changes.programmingLanguage?.previousValue; + const isCheckoutSolutionRepositoryUpdated = changes.checkoutSolutionRepository?.currentValue !== changes.checkoutSolutionRepository?.previousValue; + if (this.isLocal && (isProgrammingLanguageUpdated || isCheckoutSolutionRepositoryUpdated)) { + this.updateCheckoutDirectories(); + } + } + + ngOnDestroy() { + this.checkoutDirectorySubscription?.unsubscribe(); + } + + private updateCheckoutDirectories() { + if (!this.programmingLanguage) { + return; + } + + this.checkoutDirectorySubscription?.unsubscribe(); // might be defined from previous method execution + + const CHECKOUT_SOLUTION_REPOSITORY_DEFAULT = true; + this.checkoutDirectorySubscription = this.programmingExerciseService + .getCheckoutDirectoriesForProgrammingLanguage(this.programmingLanguage, this.checkoutSolutionRepository ?? CHECKOUT_SOLUTION_REPOSITORY_DEFAULT) + .subscribe((checkoutDirectories) => { + this.checkoutDirectories = checkoutDirectories; + }); + } + + private updateCourseShortName() { + if (!this.programmingExercise) { + return; + } + this.courseShortName = getCourseFromExercise(this.programmingExercise)?.shortName; + } +} diff --git a/src/main/webapp/app/shared/constants/input.constants.ts b/src/main/webapp/app/shared/constants/input.constants.ts index 69817d0051be..8b97589a59d6 100644 --- a/src/main/webapp/app/shared/constants/input.constants.ts +++ b/src/main/webapp/app/shared/constants/input.constants.ts @@ -13,6 +13,9 @@ export const MAX_SUBMISSION_TEXT_LENGTH = 30 * 1000; export const MAX_QUIZ_SHORT_ANSWER_TEXT_LENGTH = 255; // Must be consistent with database column definition /** Short names must start with a letter and cannot contain special characters **/ export const SHORT_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9]{2,}$/; +/** Programming exercise titles must only contain alphanumeric characters, or whitespaces, or '_' or '-' **/ +export const EXERCISE_TITLE_NAME_PATTERN = '^[a-zA-Z0-9-_ ]+'; +export const EXERCISE_TITLE_NAME_REGEX = new RegExp(EXERCISE_TITLE_NAME_PATTERN); /** Prefixes must follow the login pattern **/ export const LOGIN_PATTERN = /^[_'.@A-Za-z0-9-]*$/; export const MAX_QUIZ_QUESTION_POINTS = 9999; diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 84a6929ab568..0d471f7a9ad5 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -434,17 +434,21 @@ "title": "Dein Repository ist gesperrt.", "tooltip": "Dein Repository ist gesperrt, weil die Einreichungsfrist abgelaufen ist oder du das Einreichungslimit erreicht hast. Du kannst weiterhin deinen Code einsehen, aber keine Änderungen vornehmen." }, + "checkoutDirectories": "Checkout-Verzeichnisse", "preview": { "label": "Vorschau", "tooltip": "Für jede Programmieraufgabe, werden verschieden Repositories und Build-Pläne erstellt. Links befinden sich deren generierte Namen basierend auf dem Kurznamen der Aufgabe", + "tooltipLocalVC": "Hier findest du die mit der Programmieraufgabe verknüpften Repositories. Rechts werden die jeweiligen Checkout-Verzeichnisse während der Build-Ausführung angezeigt.", "repositories": "Repositories", "buildPlans": "Build-Pläne", "templateRepoTooltip": "Das Repository, welches die Vorlage der Aufgabe beinhaltet", "solutionRepoTooltip": "Das Repository, welches die Lösung der Aufgabe beinhaltet", "testRepoTooltip": "Das Repository, welches alle Tests beinhaltet", "auxiliaryRepoTooltip": "{{description}}", - "templateBuildPlanTooltip": "Der Build-Plan der Vorlage. Lässt alle Tests über die Vorlage der Aufgabe laufen. Dient als Kontrollreferenz, für welche alle Tests fehlschlagen müssen", - "solutionBuildPlanTooltip": "Der Build-Plan der Lösung der Aufgabe. Lässt alle Tests über die Lösung der Aufgabe laufen. Dient als Kontrollreferenz, für welche alle Tests erfolgreich sein müssen" + "templateBuildPlanTooltip": "Der Build-Plan der Vorlage und Abgaben von Studierenden. Lässt alle Tests über die Vorlage/Studierenden-Abgabe, der Aufgabe laufen. Die Vorlage dient als Kontrollreferenz, für welche alle Tests fehlschlagen müssen.", + "solutionBuildPlanTooltip": "Der Build-Plan der Lösung der Aufgabe. Lässt alle Tests über die Lösung der Aufgabe laufen. Dient als Kontrollreferenz, für welche alle Tests erfolgreich sein müssen", + "submissionBuildPlan": "Abgabe-Build-Plan", + "solutionBuildPlan": "Lösungs-Build-Plan" }, "orion": { "openEditor": "Editor Öffnen" diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 9e87d39e5b3d..8fa4e5e20ed4 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -434,17 +434,21 @@ "title": "Your repository is locked.", "tooltip": "You are outside of the participation timeframe or you reached your submission limit, your repository is locked. You may still read the code but you may not make any changes to it." }, + "checkoutDirectories": "Checkout directories", "preview": { "label": "Preview", - "tooltip": "For every programming exercise, repositories and build plans will be generated. Here you can find their generated names based on the short name of the exercise (and course)", + "tooltip": "For every programming exercise, repositories and build plans will be generated. Here, you can find their generated names based on the short name of the exercise (and course)", + "tooltipLocalVC": "Here, you can find the repositories linked to the exercise and their respective checkout directories during the build execution.", "repositories": "Repositories", "buildPlans": "Build Plans", "templateRepoTooltip": "The repository which contains the template of the exercise", "solutionRepoTooltip": "The repository which contains the solution to the exercise", "testRepoTooltip": "The repository which contains all tests", "auxiliaryRepoTooltip": "{{description}}", - "templateBuildPlanTooltip": "The build plan of the template. Runs all tests on the exercise template. Used as a reference in which all tests must fail", - "solutionBuildPlanTooltip": "The build plan of the solution. Runs all tests on the exercise solution. Used as a reference in which all tests must succeed" + "templateBuildPlanTooltip": "The build plan of the template and student submissions. Runs all tests on the exercise template / student submission. The build for the exercise template is used as reference in which all tests must fail.", + "solutionBuildPlanTooltip": "The build plan of the solution. Runs all tests on the exercise solution. Used as a reference in which all tests must succeed.", + "submissionBuildPlan": "Submission build plan", + "solutionBuildPlan": "Solution build plan" }, "orion": { "openEditor": "Open Editor" diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programming/ContinuousIntegrationTestService.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ContinuousIntegrationTestService.java index db8f63346293..44172c3eae56 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programming/ContinuousIntegrationTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ContinuousIntegrationTestService.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.reset; +import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.charset.Charset; @@ -16,6 +17,7 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.participation.ParticipationUtilService; @@ -64,7 +66,7 @@ public void setup(String testPrefix, MockDelegate mockDelegate, ContinuousIntegr this.continuousIntegrationService = continuousIntegrationService; userUtilService.addUsers(testPrefix, 2, 0, 0, 1); - var course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise(); + Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise(); programmingExercise = (ProgrammingExercise) course.getExercises().iterator().next(); // init local repo @@ -74,7 +76,7 @@ public void setup(String testPrefix, MockDelegate mockDelegate, ContinuousIntegr localRepo.configureRepos("testLocalRepo", "testOriginRepo"); // add file to the repository folder Path filePath = Path.of(localRepo.localRepoFile + "/" + currentLocalFileName); - var file = Files.createFile(filePath).toFile(); + File file = Files.createFile(filePath).toFile(); // write content to the created file FileUtils.write(file, currentLocalFileContent, Charset.defaultCharset()); // add folder to the repository folder @@ -102,10 +104,6 @@ public ProgrammingExerciseStudentParticipation getParticipation() { return participation; } - public LocalRepository getLocalRepo() { - return localRepo; - } - public void testGetBuildStatusNotFound() throws Exception { mockDelegate.mockGetBuildPlan(participation.getProgrammingExercise().getProjectKey(), participation.getBuildPlanId(), false, false, false, false); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java index d3ea4e21bbb7..71d2206c52c4 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programming/ProgrammingExerciseLocalVCLocalCIIntegrationTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; @@ -38,6 +39,7 @@ import de.tum.in.www1.artemis.participation.ParticipationUtilService; import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCRepositoryUri; import de.tum.in.www1.artemis.util.LocalRepository; +import de.tum.in.www1.artemis.web.rest.dto.CheckoutDirectoriesDTO; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ProgrammingExerciseLocalVCLocalCIIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { @@ -89,7 +91,7 @@ void cleanupAll() { @BeforeEach void setup() throws Exception { - userUtilService.addUsers(TEST_PREFIX, 1, 1, 0, 1); + userUtilService.addUsers(TEST_PREFIX, 1, 1, 1, 1); course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise(); programmingExercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); @@ -262,4 +264,37 @@ void testImportProgrammingExercise() throws Exception { localVCLocalCITestService.testLatestSubmission(templateParticipation.getId(), null, 0, false); localVCLocalCITestService.testLatestSubmission(solutionParticipation.getId(), null, 13, false); } + + @Nested + class TestGetCheckoutDirectories { + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void testWithValidProgrammingLanguage() throws Exception { + CheckoutDirectoriesDTO checkoutDirectoryDTO = request.get("/api/programming-exercises/repository-checkout-directories?programmingLanguage=JAVA", HttpStatus.OK, + CheckoutDirectoriesDTO.class); + + assertThat(checkoutDirectoryDTO.submissionBuildPlanCheckoutDirectories().exerciseCheckoutDirectory()).isEqualTo("/assignment"); + assertThat(checkoutDirectoryDTO.submissionBuildPlanCheckoutDirectories().solutionCheckoutDirectory()).isNull(); + assertThat(checkoutDirectoryDTO.submissionBuildPlanCheckoutDirectories().testCheckoutDirectory()).isEqualTo("/"); + + // Verify solution build plan checkout directories + assertThat(checkoutDirectoryDTO.solutionBuildPlanCheckoutDirectories().exerciseCheckoutDirectory()).isEqualTo(null); + assertThat(checkoutDirectoryDTO.solutionBuildPlanCheckoutDirectories().solutionCheckoutDirectory()).isEqualTo("/assignment"); + assertThat(checkoutDirectoryDTO.solutionBuildPlanCheckoutDirectories().testCheckoutDirectory()).isEqualTo("/"); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void testWithNotSupportedProgrammingLanguage() throws Exception { + request.get("/api/programming-exercises/repository-checkout-directories?programmingLanguage=languageThatDoesNotExist", HttpStatus.BAD_REQUEST, + CheckoutDirectoriesDTO.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + void testAccessForbidden() throws Exception { + request.get("/api/programming-exercises/repository-checkout-directories?programmingLanguage=JAVA", HttpStatus.FORBIDDEN, CheckoutDirectoriesDTO.class); + } + } } diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java index 0e61033fa437..01ed24d0a60f 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalCIServiceTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -39,6 +40,7 @@ import de.tum.in.www1.artemis.service.connectors.localci.dto.BuildJobQueueItem; import de.tum.in.www1.artemis.service.connectors.localci.dto.JobTimingInfo; import de.tum.in.www1.artemis.service.connectors.localci.dto.RepositoryInfo; +import de.tum.in.www1.artemis.web.rest.dto.CheckoutDirectoriesDTO; class LocalCIServiceTest extends AbstractSpringIntegrationLocalCILocalVCTest { @@ -170,4 +172,53 @@ void testUnsupportedMethods() { assertThat(latestArtifactResponse.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); assertThat(latestArtifactResponse.getBody()).hasSize(0); } + + @Nested + class GetCheckoutDirectoriesTests { + + @Test + void getCheckoutDirectoriesForJava() { + CheckoutDirectoriesDTO checkoutDirectories = continuousIntegrationService.getCheckoutDirectories(ProgrammingLanguage.JAVA, true); + + // Verify submission build plan checkout directories + assertThat(checkoutDirectories.submissionBuildPlanCheckoutDirectories().exerciseCheckoutDirectory()).isEqualTo("/assignment"); + assertThat(checkoutDirectories.submissionBuildPlanCheckoutDirectories().solutionCheckoutDirectory()).isNull(); + assertThat(checkoutDirectories.submissionBuildPlanCheckoutDirectories().testCheckoutDirectory()).isEqualTo("/"); + + // Verify solution build plan checkout directories + assertThat(checkoutDirectories.solutionBuildPlanCheckoutDirectories().exerciseCheckoutDirectory()).isEqualTo(null); + assertThat(checkoutDirectories.solutionBuildPlanCheckoutDirectories().solutionCheckoutDirectory()).isEqualTo("/assignment"); + assertThat(checkoutDirectories.solutionBuildPlanCheckoutDirectories().testCheckoutDirectory()).isEqualTo("/"); + } + + @Test + void getCheckoutDirectoriesForOcaml() { + CheckoutDirectoriesDTO checkoutDirectories = continuousIntegrationService.getCheckoutDirectories(ProgrammingLanguage.OCAML, true); + + // Verify submission build plan checkout directories + assertThat(checkoutDirectories.submissionBuildPlanCheckoutDirectories().exerciseCheckoutDirectory()).isEqualTo("/assignment"); + assertThat(checkoutDirectories.submissionBuildPlanCheckoutDirectories().solutionCheckoutDirectory()).isEqualTo("/solution"); + assertThat(checkoutDirectories.submissionBuildPlanCheckoutDirectories().testCheckoutDirectory()).isEqualTo("/tests"); + + // Verify solution build plan checkout directories + assertThat(checkoutDirectories.solutionBuildPlanCheckoutDirectories().exerciseCheckoutDirectory()).isNull(); + assertThat(checkoutDirectories.solutionBuildPlanCheckoutDirectories().solutionCheckoutDirectory()).isEqualTo("/assignment"); + assertThat(checkoutDirectories.solutionBuildPlanCheckoutDirectories().testCheckoutDirectory()).isEqualTo("/tests"); + } + + @Test + void getCheckoutDirectoriesForOcamlWithoutCheckingOutSolution() { + CheckoutDirectoriesDTO checkoutDirectories = continuousIntegrationService.getCheckoutDirectories(ProgrammingLanguage.OCAML, false); + + // Verify submission build plan checkout directories + assertThat(checkoutDirectories.submissionBuildPlanCheckoutDirectories().exerciseCheckoutDirectory()).isEqualTo("/assignment"); + assertThat(checkoutDirectories.submissionBuildPlanCheckoutDirectories().solutionCheckoutDirectory()).isNull(); + assertThat(checkoutDirectories.submissionBuildPlanCheckoutDirectories().testCheckoutDirectory()).isEqualTo("/tests"); + + // Verify solution build plan checkout directories + assertThat(checkoutDirectories.solutionBuildPlanCheckoutDirectories().exerciseCheckoutDirectory()).isNull(); + assertThat(checkoutDirectories.solutionBuildPlanCheckoutDirectories().solutionCheckoutDirectory()).isEqualTo("/assignment"); + assertThat(checkoutDirectories.solutionBuildPlanCheckoutDirectories().testCheckoutDirectory()).isEqualTo("/tests"); + } + } } diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts index 18a788a09ab7..d92687f132e2 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-detail.component.spec.ts @@ -24,7 +24,7 @@ import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; import { MockProgrammingExerciseGradingService } from '../../helpers/mocks/service/mock-programming-exercise-grading.service'; import { ProgrammingExerciseGitDiffReport } from 'app/entities/hestia/programming-exercise-git-diff-report.model'; import { ProgrammingExerciseSolutionEntry } from 'app/entities/hestia/programming-exercise-solution-entry.model'; -import { BuildLogStatisticsDTO } from 'app/exercises/programming/manage/build-log-statistics-dto'; +import { BuildLogStatisticsDTO } from 'app/entities/build-log-statistics-dto'; import { TemplateProgrammingExerciseParticipation } from 'app/entities/participation/template-programming-exercise-participation.model'; import { SolutionProgrammingExerciseParticipation } from 'app/entities/participation/solution-programming-exercise-participation.model'; import { HttpResponse } from '@angular/common/http'; diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-repository-and-build-plan-details.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-repository-and-build-plan-details.component.spec.ts new file mode 100644 index 000000000000..ccc542ea3ec5 --- /dev/null +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-repository-and-build-plan-details.component.spec.ts @@ -0,0 +1,217 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; +import { CheckoutDirectoriesDto } from 'app/entities/checkout-directories-dto'; +import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming-exercise.model'; +import { HelpIconComponent } from 'app/shared/components/help-icon.component'; +import { MockComponent } from 'ng-mocks'; +import { Subscription, of } from 'rxjs'; +import { AuxiliaryRepository } from 'app/entities/programming-exercise-auxiliary-repository-model'; +import { SimpleChanges } from '@angular/core'; +import { ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component'; +import { ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MockProgrammingExerciseService } from '../../helpers/mocks/service/mock-programming-exercise.service'; + +describe('ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent', () => { + let component: ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent; + let fixture: ComponentFixture; + let programmingExerciseService: ProgrammingExerciseService; + + const CHECKOUT_DIRECTORY_PREVIEW_SUBMISSION_BUILD_PLAN = '#checkout-directory-preview-submission-build-plan'; + const CHECKOUT_DIRECTORY_PREVIEW_SOLUTION_BUILD_PLAN = '#checkout-directory-preview-solution-build-plan'; + + const JAVA_CHECKOUT_DIRECTORIES: CheckoutDirectoriesDto = { + submissionBuildPlanCheckoutDirectories: { + exerciseCheckoutDirectory: '/assignment', + testCheckoutDirectory: '', + }, + solutionBuildPlanCheckoutDirectories: { + solutionCheckoutDirectory: '/assignment', + testCheckoutDirectory: '', + }, + }; + + const OCAML_CHECKOUT_DIRECTORIES: CheckoutDirectoriesDto = { + submissionBuildPlanCheckoutDirectories: { + exerciseCheckoutDirectory: '/assignment', + solutionCheckoutDirectory: '/solution', + testCheckoutDirectory: 'tests', + }, + solutionBuildPlanCheckoutDirectories: { + solutionCheckoutDirectory: '/assignment', + testCheckoutDirectory: 'tests', + }, + }; + + const OCAML_CHECKOUT_DIRECTORIES_WITHOUT_SOLUTION_CHECKOUT: CheckoutDirectoriesDto = { + submissionBuildPlanCheckoutDirectories: { + exerciseCheckoutDirectory: '/assignment', + testCheckoutDirectory: 'tests', + }, + solutionBuildPlanCheckoutDirectories: { + solutionCheckoutDirectory: '/assignment', + testCheckoutDirectory: 'tests', + }, + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent, MockComponent(HelpIconComponent), ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent], + imports: [ArtemisSharedComponentModule, ArtemisSharedCommonModule], + providers: [ + { provide: ProgrammingExerciseService, useClass: MockProgrammingExerciseService }, + { provide: TranslateService, useClass: MockTranslateService }, + ], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent); + component = fixture.componentInstance; + programmingExerciseService = TestBed.inject(ProgrammingExerciseService); + + component.programmingLanguage = ProgrammingLanguage.C; + component.programmingExercise = { id: 1, shortName: 'shortName' } as ProgrammingExercise; + component.isLocal = true; + + jest.spyOn(programmingExerciseService, 'getCheckoutDirectoriesForProgrammingLanguage').mockImplementation( + (programmingLanguage: ProgrammingLanguage, checkoutSolution: boolean) => { + if (programmingLanguage === ProgrammingLanguage.JAVA) { + return of(JAVA_CHECKOUT_DIRECTORIES); + } + + if (programmingLanguage === ProgrammingLanguage.OCAML && !checkoutSolution) { + return of(OCAML_CHECKOUT_DIRECTORIES_WITHOUT_SOLUTION_CHECKOUT); + } + + return of(OCAML_CHECKOUT_DIRECTORIES); + }, + ); + }); + }); + + it('should display checkout directories when they exist', () => { + fixture.detectChanges(); + + const submissionPreviewElement = fixture.debugElement.nativeElement.querySelector(CHECKOUT_DIRECTORY_PREVIEW_SUBMISSION_BUILD_PLAN); + const solutionPreviewElement = fixture.debugElement.nativeElement.querySelector(CHECKOUT_DIRECTORY_PREVIEW_SOLUTION_BUILD_PLAN); + + expect(submissionPreviewElement).toBeTruthy(); + expect(submissionPreviewElement.textContent).toContain('/assignment'); + expect(submissionPreviewElement.textContent).toContain('/'); + expect(solutionPreviewElement).toBeTruthy(); + expect(solutionPreviewElement.textContent).toContain('/assignment'); + expect(solutionPreviewElement.textContent).toContain('/'); + }); + + it('should send request if localCI is used', () => { + jest.spyOn(programmingExerciseService, 'getCheckoutDirectoriesForProgrammingLanguage'); + + fixture.detectChanges(); + + expect(programmingExerciseService.getCheckoutDirectoriesForProgrammingLanguage).toHaveBeenCalled(); + }); + + it('should NOT send request if localCI is NOT used', () => { + component.isLocal = false; + const spy = jest.spyOn(programmingExerciseService, 'getCheckoutDirectoriesForProgrammingLanguage'); + + fixture.detectChanges(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should display checkoutDirectory preview if localCI is used', () => { + fixture.detectChanges(); + const submissionPreviewElement = fixture.debugElement.nativeElement.querySelector(CHECKOUT_DIRECTORY_PREVIEW_SUBMISSION_BUILD_PLAN); + const solutionPreviewElement = fixture.debugElement.nativeElement.querySelector(CHECKOUT_DIRECTORY_PREVIEW_SOLUTION_BUILD_PLAN); + expect(submissionPreviewElement).toBeTruthy(); + expect(solutionPreviewElement).toBeTruthy(); + }); + + it('should NOT display checkoutDirectory preview if localCI is NOT used', () => { + component.isLocal = false; + fixture.detectChanges(); + const submissionPreviewElement = fixture.debugElement.nativeElement.querySelector(CHECKOUT_DIRECTORY_PREVIEW_SUBMISSION_BUILD_PLAN); + const solutionPreviewElement = fixture.debugElement.nativeElement.querySelector(CHECKOUT_DIRECTORY_PREVIEW_SOLUTION_BUILD_PLAN); + expect(submissionPreviewElement).toBeFalsy(); + expect(solutionPreviewElement).toBeFalsy(); + }); + + it('should update auxiliary checkout repository directories', () => { + component.programmingExercise!.auxiliaryRepositories = [{ checkoutDirectory: 'assignment/sut' } as AuxiliaryRepository]; + + fixture.detectChanges(); + + const submissionPreviewElement = fixture.debugElement.nativeElement.querySelector(CHECKOUT_DIRECTORY_PREVIEW_SUBMISSION_BUILD_PLAN); + const solutionPreviewElement = fixture.debugElement.nativeElement.querySelector(CHECKOUT_DIRECTORY_PREVIEW_SOLUTION_BUILD_PLAN); + expect(submissionPreviewElement).toBeTruthy(); + expect(submissionPreviewElement.textContent).toContain('/assignment/sut'); // the slash for the root directory should have been added + expect(solutionPreviewElement).toBeTruthy(); + expect(solutionPreviewElement.textContent).toContain('/assignment/sut'); + }); + + it('should unsubscribe from programmingExerciseServiceSubscription on destroy', () => { + const subscription = new Subscription(); + jest.spyOn(subscription, 'unsubscribe'); + component.checkoutDirectorySubscription = subscription; + + component.ngOnDestroy(); + + expect(subscription.unsubscribe).toHaveBeenCalled(); + }); + + it('should update checkout directories when selectedProgrammingLanguage changes', () => { + jest.spyOn(programmingExerciseService, 'getCheckoutDirectoriesForProgrammingLanguage'); + + component.programmingLanguage = ProgrammingLanguage.OCAML; + component.ngOnChanges({ + programmingLanguage: { + previousValue: ProgrammingLanguage.JAVA, + currentValue: ProgrammingLanguage.OCAML, + }, + } as unknown as SimpleChanges); + + // assertion to check if ngOnChanges was executed properly and updated the checkout directories + expect(programmingExerciseService.getCheckoutDirectoriesForProgrammingLanguage).toHaveBeenCalledWith(ProgrammingLanguage.OCAML, true); + expect(component.checkoutDirectories?.submissionBuildPlanCheckoutDirectories?.solutionCheckoutDirectory).toBe('/solution'); // was null before with JAVA as programming language + }); + + it('should update checkout directories when checkoutSolution flag changes', () => { + jest.spyOn(programmingExerciseService, 'getCheckoutDirectoriesForProgrammingLanguage'); + + component.programmingLanguage = ProgrammingLanguage.OCAML; + component.checkoutSolutionRepository = false; + component.ngOnChanges({ + checkoutSolutionRepository: { + previousValue: true, + currentValue: false, + }, + } as unknown as SimpleChanges); + + // assertion to check if ngOnChanges was executed properly and updated the checkout directories + expect(programmingExerciseService.getCheckoutDirectoriesForProgrammingLanguage).toHaveBeenCalledWith(ProgrammingLanguage.OCAML, false); + // solution checkout directory was /solution before with OCaml as programming language and solution checkout allowed + expect(component.checkoutDirectories?.submissionBuildPlanCheckoutDirectories?.solutionCheckoutDirectory).toBeUndefined(); + }); + + it('should update auxiliary repository directories on changes', () => { + fixture.detectChanges(); + + component.programmingExercise!.auxiliaryRepositories = [{ checkoutDirectory: 'assignment/src' } as AuxiliaryRepository]; + component.ngOnChanges({ + programmingExercise: { + previousValue: { auxiliaryRepositories: [] }, + currentValue: { auxiliaryRepositories: [{ checkoutDirectory: 'assignment/src' } as AuxiliaryRepository] }, + }, + } as unknown as SimpleChanges); + + fixture.detectChanges(); + + const submissionPreviewElement = fixture.debugElement.nativeElement.querySelector(CHECKOUT_DIRECTORY_PREVIEW_SUBMISSION_BUILD_PLAN); + expect(submissionPreviewElement).toBeTruthy(); + expect(submissionPreviewElement.textContent).toContain('/assignment/src'); + }); +}); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts index 756832768cd3..f33778c9ca50 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts @@ -42,7 +42,6 @@ import { IncludedInOverallScorePickerComponent } from 'app/exercises/shared/incl import { CategorySelectorComponent } from 'app/shared/category-selector/category-selector.component'; import { AddAuxiliaryRepositoryButtonComponent } from 'app/exercises/programming/manage/update/add-auxiliary-repository-button.component'; import { TranslateDirective } from 'app/shared/language/translate.directive'; -import { ProgrammingExercisePlansAndRepositoriesPreviewComponent } from 'app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component'; import { TableEditableFieldComponent } from 'app/shared/table/table-editable-field.component'; import { RemoveKeysPipe } from 'app/shared/pipes/remove-keys.pipe'; import { SubmissionPolicyUpdateComponent } from 'app/exercises/shared/submission-policy/submission-policy-update.component'; @@ -68,6 +67,7 @@ import { AuxiliaryRepository } from 'app/entities/programming-exercise-auxiliary import { AlertService, AlertType } from 'app/core/util/alert.service'; import { FormStatusBarComponent } from 'app/forms/form-status-bar/form-status-bar.component'; import { FormFooterComponent } from 'app/forms/form-footer/form-footer.component'; +import { ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component'; describe('ProgrammingExerciseUpdateComponent', () => { const courseId = 1; @@ -96,7 +96,7 @@ describe('ProgrammingExerciseUpdateComponent', () => { SelectControlValueAccessor, NumberValueAccessor, MockComponent(HelpIconComponent), - MockComponent(ProgrammingExercisePlansAndRepositoriesPreviewComponent), + MockComponent(ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent), MockComponent(TableEditableFieldComponent), MockComponent(RemoveAuxiliaryRepositoryButtonComponent), MockComponent(CategorySelectorComponent), diff --git a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-information.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-information.component.spec.ts index c59bd4db78fb..30259859d720 100644 --- a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-information.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-information.component.spec.ts @@ -8,13 +8,13 @@ import { DefaultValueAccessor, NgModel } from '@angular/forms'; import { RemoveKeysPipe } from 'app/shared/pipes/remove-keys.pipe'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; import { HelpIconComponent } from 'app/shared/components/help-icon.component'; -import { ProgrammingExercisePlansAndRepositoriesPreviewComponent } from 'app/exercises/programming/manage/update/programming-exercise-plans-and-repositories-preview.component'; import { CategorySelectorComponent } from 'app/shared/category-selector/category-selector.component'; import { AddAuxiliaryRepositoryButtonComponent } from 'app/exercises/programming/manage/update/add-auxiliary-repository-button.component'; import { programmingExerciseCreationConfigMock } from './programming-exercise-creation-config-mock'; import { ExerciseTitleChannelNameComponent } from 'app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.component'; import { TableEditableFieldComponent } from 'app/shared/table/table-editable-field.component'; import { QueryList } from '@angular/core'; +import { ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component'; describe('ProgrammingExerciseInformationComponent', () => { let fixture: ComponentFixture; @@ -29,7 +29,7 @@ describe('ProgrammingExerciseInformationComponent', () => { NgModel, MockComponent(HelpIconComponent), MockComponent(ExerciseTitleChannelNameComponent), - MockComponent(ProgrammingExercisePlansAndRepositoriesPreviewComponent), + MockComponent(ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent), MockComponent(CategorySelectorComponent), MockComponent(AddAuxiliaryRepositoryButtonComponent), MockPipe(ArtemisTranslatePipe), diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts index e50b444b40bf..d897194809fd 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-programming-exercise.service.ts @@ -1,6 +1,7 @@ import { of } from 'rxjs'; import { ProgrammingExerciseInstructorRepositoryType } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { Participation } from 'app/entities/participation/participation.model'; +import { ProgrammingLanguage } from 'app/entities/programming-exercise.model'; export class MockProgrammingExerciseService { updateProblemStatement = (exerciseId: number, problemStatement: string) => of(); @@ -26,4 +27,5 @@ export class MockProgrammingExerciseService { generateStructureOracle = (exerciseId: number) => of({}); unlockAllRepositories = (exerciseId: number) => of({}); getDiffReportForCommits = (exerciseId: number, participationId: number, olderCommitHash: string, newerCommitHash: string, repositoryType: string) => of({}); + getCheckoutDirectoriesForProgrammingLanguage = (programmingLanguage: ProgrammingLanguage, checkoutSolution: boolean) => of(); }