From ac4c2c1f170f9573eacc81b49246cbb7a58b93fa Mon Sep 17 00:00:00 2001 From: Florian Glombik <63976129+florian-glombik@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:14:35 +0100 Subject: [PATCH] Programming exercises: Add simple mode to create and edit view (#9283) --- .../dto/CourseExistingExerciseDetailsDTO.java | 9 + .../aet/artemis/core/web/CourseResource.java | 34 ++ .../manage/detail/course-detail.component.ts | 2 +- .../app/exam/manage/exam-management.module.ts | 1 - .../manage/exams/exam-update.component.html | 3 +- .../manage/exams/exam-update.component.ts | 18 +- ...d-auxiliary-repository-button.component.ts | 8 +- ...programming-exercise-update.component.html | 32 +- .../programming-exercise-update.component.ts | 191 ++++++++--- .../programming-exercise-update.helper.ts | 97 ++++++ .../programming-exercise-update.module.ts | 20 +- .../switch-edit-mode-button.component.html | 9 + .../switch-edit-mode-button.component.ts | 25 ++ ...ramming-exercise-difficulty.component.html | 6 + ...ogramming-exercise-difficulty.component.ts | 15 + ...rogramming-exercise-grading.component.html | 139 ++++++++ .../programming-exercise-grading.component.ts | 19 +- ...amming-exercise-information.component.html | 122 ++++--- ...amming-exercise-information.component.scss | 0 ...gramming-exercise-information.component.ts | 318 ++++++++++++++++++ ...ogramming-exercise-language.component.html | 240 +++++++++++++ ...programming-exercise-language.component.ts | 10 +- .../programming-exercise-mode.component.html} | 83 ++--- .../programming-exercise-mode.component.ts} | 24 +- ...rogramming-exercise-problem.component.html | 58 ++++ .../programming-exercise-problem.component.ts | 23 +- ...rogramming-exercise-grading.component.html | 127 ------- ...gramming-exercise-information.component.ts | 157 --------- ...ogramming-exercise-language.component.html | 228 ------------- ...rogramming-exercise-problem.component.html | 50 --- ...gramming-exercise-lifecycle.component.html | 91 ++--- ...rogramming-exercise-lifecycle.component.ts | 27 +- ...exercise-title-channel-name.component.html | 2 + .../exercise-title-channel-name.component.ts | 29 +- .../shared/exercise/exercise.service.ts | 22 ++ .../form-footer/form-footer.component.html | 23 +- .../form-footer/form-footer.component.scss | 2 +- .../form-footer/form-footer.component.ts | 24 +- .../form-status-bar.component.html | 8 +- .../form-status-bar.component.ts | 5 +- src/main/webapp/app/forms/forms.module.ts | 3 +- .../category-selector.component.ts | 27 +- .../shared/components/help-icon.component.ts | 11 +- .../app/shared/constants/input.constants.ts | 21 ++ .../title-channel-name.component.html | 43 ++- .../title-channel-name.component.ts | 48 ++- .../title-channel-name.module.ts | 4 +- src/main/webapp/app/shared/util/utils.ts | 4 + ...tom-not-included-in-validator.directive.ts | 30 ++ src/main/webapp/i18n/de/exercise.json | 9 +- .../webapp/i18n/de/programmingExercise.json | 13 +- src/main/webapp/i18n/en/exercise.json | 9 +- .../webapp/i18n/en/programmingExercise.json | 13 +- .../artemis/core/util/CourseTestService.java | 12 + .../artemis/core/util/CourseUtilService.java | 13 + .../CourseGitlabJenkinsIntegrationTest.java | 13 + .../util/ProgrammingExerciseUtilService.java | 10 +- .../exam/exam-update.component.spec.ts | 9 +- ...rcise-title-channel-name.component.spec.ts | 76 ++++- .../forms/form-status-bar.component.spec.ts | 8 +- ...mming-exercise-lifecycle.component.spec.ts | 26 +- ...gramming-exercise-update.component.spec.ts | 242 +++++++++++-- ...iliary-repository-button.component.spec.ts | 45 +++ ...ramming-exercise-grading.component.spec.ts | 98 +++++- ...ing-exercise-information.component.spec.ts | 79 ++++- ...amming-exercise-language.component.spec.ts | 12 +- ...ogramming-exercise-mode.component.spec.ts} | 28 +- ...ramming-exercise-problem.component.spec.ts | 7 +- .../switch-edit-mode-button.component.spec.ts | 42 +++ .../form/title-channel-name.component.spec.ts | 7 +- .../javascript/spec/util/shared/utils.spec.ts | 9 + .../e2e/exam/ExamManagement.spec.ts | 1 + .../exam/test-exam/TestExamManagement.spec.ts | 1 + .../ProgrammingExerciseManagement.spec.ts | 2 + .../ProgrammingExerciseCreationPage.ts | 4 + 75 files changed, 2292 insertions(+), 988 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/dto/CourseExistingExerciseDetailsDTO.java create mode 100644 src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.helper.ts create mode 100644 src/main/webapp/app/exercises/programming/manage/update/switch-edit-mode-button/switch-edit-mode-button.component.html create mode 100644 src/main/webapp/app/exercises/programming/manage/update/switch-edit-mode-button/switch-edit-mode-button.component.ts create mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/difficulty/programming-exercise-difficulty.component.html create mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/difficulty/programming-exercise-difficulty.component.ts create mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/grading/programming-exercise-grading.component.html rename src/main/webapp/app/exercises/programming/manage/update/update-components/{ => grading}/programming-exercise-grading.component.ts (91%) rename src/main/webapp/app/exercises/programming/manage/update/update-components/{ => information}/programming-exercise-information.component.html (71%) rename src/main/webapp/app/exercises/programming/manage/update/update-components/{ => information}/programming-exercise-information.component.scss (100%) create mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/information/programming-exercise-information.component.ts create mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.html rename src/main/webapp/app/exercises/programming/manage/update/update-components/{ => language}/programming-exercise-language.component.ts (92%) rename src/main/webapp/app/exercises/programming/manage/update/update-components/{programming-exercise-difficulty.component.html => mode/programming-exercise-mode.component.html} (60%) rename src/main/webapp/app/exercises/programming/manage/update/update-components/{programming-exercise-difficulty.component.ts => mode/programming-exercise-mode.component.ts} (61%) create mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/problem/programming-exercise-problem.component.html rename src/main/webapp/app/exercises/programming/manage/update/update-components/{ => problem}/programming-exercise-problem.component.ts (59%) delete mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-grading.component.html delete mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html delete mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-problem.component.html create mode 100644 src/main/webapp/app/shared/validators/custom-not-included-in-validator.directive.ts create mode 100644 src/test/javascript/spec/component/programming-exercise/update-components/add-auxiliary-repository-button.component.spec.ts rename src/test/javascript/spec/component/programming-exercise/update-components/{programming-exercise-difficulty.component.spec.ts => programming-exercise-mode.component.spec.ts} (74%) create mode 100644 src/test/javascript/spec/component/programming-exercise/update-components/switch-edit-mode-button.component.spec.ts diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseExistingExerciseDetailsDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseExistingExerciseDetailsDTO.java new file mode 100644 index 000000000000..cae411dfb905 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/CourseExistingExerciseDetailsDTO.java @@ -0,0 +1,9 @@ +package de.tum.cit.aet.artemis.core.dto; + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record CourseExistingExerciseDetailsDTO(Set exerciseTitles, Set shortNames) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index c1eac3ba3a66..16b7dd554842 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -72,6 +72,7 @@ import de.tum.cit.aet.artemis.core.config.Constants; import de.tum.cit.aet.artemis.core.domain.Course; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.core.dto.CourseExistingExerciseDetailsDTO; import de.tum.cit.aet.artemis.core.dto.CourseForArchiveDTO; import de.tum.cit.aet.artemis.core.dto.CourseForDashboardDTO; import de.tum.cit.aet.artemis.core.dto.CourseForImportDTO; @@ -97,6 +98,7 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; +import de.tum.cit.aet.artemis.core.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse; import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService; import de.tum.cit.aet.artemis.core.service.CourseService; import de.tum.cit.aet.artemis.core.service.FilePathService; @@ -107,6 +109,7 @@ import de.tum.cit.aet.artemis.exam.repository.ExamRepository; import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.domain.ExerciseMode; +import de.tum.cit.aet.artemis.exercise.domain.ExerciseType; import de.tum.cit.aet.artemis.exercise.domain.Submission; import de.tum.cit.aet.artemis.exercise.domain.Team; import de.tum.cit.aet.artemis.exercise.domain.participation.Participant; @@ -1473,4 +1476,35 @@ public ResponseEntity getNumberOfAllowedComplaintsInCourse(@PathVariable L long unacceptedComplaints = complaintService.countUnacceptedComplaintsByParticipantAndCourseId(participant, courseId); return ResponseEntity.ok(Math.max(complaintService.getMaxComplaintsPerParticipant(course, participant) - unacceptedComplaints, 0)); } + + /** + * GET courses/{courseId}/existing-exercise-details: Get the exercise names (and shortNames for {@link ExerciseType#PROGRAMMING} exercises) + * of all exercises with the given type in the given course. + * + * @param courseId of the course for which all exercise names should be fetched + * @param exerciseType for which the details should be fetched, as the name of an exercise only needs to be unique for each exercise type + * @return {@link CourseExistingExerciseDetailsDTO} with the exerciseNames (and already used shortNames if a {@link ExerciseType#PROGRAMMING} exercise is requested) + */ + @GetMapping("courses/{courseId}/existing-exercise-details") + @EnforceAtLeastEditorInCourse + public ResponseEntity getExistingExerciseDetails(@PathVariable Long courseId, @RequestParam String exerciseType) { + log.debug("REST request to get details of existing exercises in course : {}", courseId); + Course course = courseRepository.findByIdWithEagerExercisesElseThrow(courseId); + + Set alreadyTakenExerciseNames = new HashSet<>(); + Set alreadyTakenShortNames = new HashSet<>(); + + boolean includeShortNames = exerciseType.equals(ExerciseType.PROGRAMMING.toString()); + + course.getExercises().forEach((exercise -> { + if (exercise.getType().equals(exerciseType)) { + alreadyTakenExerciseNames.add(exercise.getTitle()); + if (includeShortNames && exercise.getShortName() != null) { + alreadyTakenShortNames.add(exercise.getShortName()); + } + } + })); + + return ResponseEntity.ok(new CourseExistingExerciseDetailsDTO(alreadyTakenExerciseNames, alreadyTakenShortNames)); + } } diff --git a/src/main/webapp/app/course/manage/detail/course-detail.component.ts b/src/main/webapp/app/course/manage/detail/course-detail.component.ts index 29858e79b334..e75bc32beab8 100644 --- a/src/main/webapp/app/course/manage/detail/course-detail.component.ts +++ b/src/main/webapp/app/course/manage/detail/course-detail.component.ts @@ -367,7 +367,7 @@ export class CourseDetailComponent implements OnInit, OnDestroy { if (this.paramSub) { this.paramSub.unsubscribe(); } - this.eventManager.destroy(this.eventSubscriber); + this.eventManager?.destroy(this.eventSubscriber); } /** diff --git a/src/main/webapp/app/exam/manage/exam-management.module.ts b/src/main/webapp/app/exam/manage/exam-management.module.ts index b9a9829ab1fa..ddc813cc21f6 100644 --- a/src/main/webapp/app/exam/manage/exam-management.module.ts +++ b/src/main/webapp/app/exam/manage/exam-management.module.ts @@ -88,7 +88,6 @@ const ENTITY_STATES = [...examManagementState]; ArtemisMarkdownEditorModule, NgxDatatableModule, ArtemisDataTableModule, - ArtemisTextExerciseModule, ArtemisFileUploadExerciseManagementModule, ArtemisProgrammingExerciseManagementModule, ArtemisQuizManagementModule, diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.html b/src/main/webapp/app/exam/manage/exams/exam-update.component.html index 780e07a92c40..57ebf71c3272 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.html @@ -5,8 +5,7 @@
@if (!isImport) {

- } - @if (isImport) { + } @else {

} diff --git a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts index ea4d3a510ec4..610798664c76 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-update.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-update.component.ts @@ -24,28 +24,27 @@ import { examWorkingTime, normalWorkingTime } from 'app/exam/participate/exam.ut templateUrl: './exam-update.component.html', }) export class ExamUpdateComponent implements OnInit, OnDestroy { + protected readonly faSave = faSave; + protected readonly faBan = faBan; + protected readonly faExclamationTriangle = faExclamationTriangle; + protected readonly documentationType: DocumentationType = 'Exams'; + exam: Exam; course: Course; isSaving: boolean; isImport = false; isImportInSameCourse = false; - hideChannelNameInput = false; + hideChannelNameInput = false; private originalStartDate?: dayjs.Dayjs; + private originalEndDate?: dayjs.Dayjs; private componentActive = true; - // Link to the component enabling the selection of exercise groups and exercises for import @ViewChild(ExamExerciseImportComponent) examExerciseImportComponent: ExamExerciseImportComponent; - @ViewChild('workingTimeConfirmationContent') public workingTimeConfirmationContent: TemplateRef; - - readonly documentationType: DocumentationType = 'Exams'; - // Icons - faSave = faSave; - faBan = faBan; - faExclamationTriangle = faExclamationTriangle; + @ViewChild('workingTimeConfirmationContent') public workingTimeConfirmationContent: TemplateRef; constructor( private route: ActivatedRoute, @@ -471,6 +470,7 @@ export class ExamUpdateComponent implements OnInit, OnDestroy { dayjs(this.exam.exampleSolutionPublicationDate).isBefore(this.exam.endDate || null) ); } + /** * Default exam start text, which can be edited by instructors in the text editor */ diff --git a/src/main/webapp/app/exercises/programming/manage/update/add-auxiliary-repository-button.component.ts b/src/main/webapp/app/exercises/programming/manage/update/add-auxiliary-repository-button.component.ts index 06d5d6940c83..e913910c0b29 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/add-auxiliary-repository-button.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/add-auxiliary-repository-button.component.ts @@ -17,16 +17,14 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'; `, }) export class AddAuxiliaryRepositoryButtonComponent { - ButtonType = ButtonType; - ButtonSize = ButtonSize; + protected readonly ButtonType = ButtonType; + protected readonly ButtonSize = ButtonSize; + protected readonly faPlus = faPlus; @Input() programmingExercise: ProgrammingExercise; @Output() onRefresh: EventEmitter = new EventEmitter(); - // Icons - faPlus = faPlus; - /** * Adds a new auxiliary repository, which is displayed as a new row, to the respective programming exercise and activates the angular change detection. */ diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.html b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.html index 6dcae2b857a3..4092b38225b2 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.html @@ -9,7 +9,7 @@

- +

- + +
+ } +
- -

@@ -40,18 +54,22 @@

- @if (!isExamMode) { + @if (isEditFieldDisplayedRecord().plagiarismControl && !isExamMode) { }

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 8b1a5ef9529a..5bdde2d8c65c 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 @@ -1,5 +1,5 @@ import { ActivatedRoute, Params } from '@angular/router'; -import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild, computed, effect, signal } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AlertService, AlertType } from 'app/core/util/alert.service'; import { ProgrammingExerciseBuildConfig } from 'app/entities/programming/programming-exercise-build.config'; @@ -10,22 +10,29 @@ import { ProgrammingExerciseService } from '../services/programming-exercise.ser import { FileService } from 'app/shared/http/file.service'; import { TranslateService } from '@ngx-translate/core'; import { switchMap, tap } from 'rxjs/operators'; -import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; -import { AssessmentType } from 'app/entities/assessment-type.model'; import { Exercise, IncludedInOverallScore, ValidationReason } from 'app/entities/exercise.model'; 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 { EXERCISE_TITLE_NAME_PATTERN, EXERCISE_TITLE_NAME_REGEX, SHORT_NAME_PATTERN } from 'app/shared/constants/input.constants'; +import { + APP_NAME_PATTERN_FOR_SWIFT, + EXERCISE_TITLE_NAME_PATTERN, + EXERCISE_TITLE_NAME_REGEX, + INVALID_DIRECTORY_NAME_PATTERN, + INVALID_REPOSITORY_NAME_PATTERN, + MAX_PENALTY_PATTERN, + PACKAGE_NAME_PATTERN_FOR_JAVA_BLACKBOX, + PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN, + PROGRAMMING_EXERCISE_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'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { AuxiliaryRepository } from 'app/entities/programming/programming-exercise-auxiliary-repository-model'; import { SubmissionPolicyType } from 'app/entities/submission-policy.model'; -import { faExclamationCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { ModePickerOption } from 'app/exercises/shared/mode-picker/mode-picker.component'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; import { ProgrammingExerciseCreationConfig } from 'app/exercises/programming/manage/update/programming-exercise-creation-config'; @@ -33,12 +40,15 @@ import { loadCourseExerciseCategories } from 'app/exercises/shared/course-exerci import { PROFILE_AEOLUS, PROFILE_LOCALCI, PROFILE_THEIA } from 'app/app.constants'; import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.service'; import { FormSectionStatus } from 'app/forms/form-status-bar/form-status-bar.component'; -import { ProgrammingExerciseInformationComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-information.component'; -import { ProgrammingExerciseDifficultyComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component'; -import { ProgrammingExerciseLanguageComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-language.component'; -import { ProgrammingExerciseGradingComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-grading.component'; +import { ProgrammingExerciseInformationComponent } from 'app/exercises/programming/manage/update/update-components/information/programming-exercise-information.component'; +import { ProgrammingExerciseModeComponent } from 'app/exercises/programming/manage/update/update-components/mode/programming-exercise-mode.component'; +import { ProgrammingExerciseLanguageComponent } from 'app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component'; +import { ProgrammingExerciseGradingComponent } from 'app/exercises/programming/manage/update/update-components/grading/programming-exercise-grading.component'; import { ExerciseUpdatePlagiarismComponent } from 'app/exercises/shared/plagiarism/exercise-update-plagiarism/exercise-update-plagiarism.component'; import { ImportOptions } from 'app/types/programming-exercises'; +import { IS_DISPLAYED_IN_SIMPLE_MODE, ProgrammingExerciseInputField } from 'app/exercises/programming/manage/update/programming-exercise-update.helper'; + +export const LOCAL_STORAGE_KEY_IS_SIMPLE_MODE = 'isSimpleMode'; @Component({ selector: 'jhi-programming-exercise-update', @@ -46,17 +56,40 @@ import { ImportOptions } from 'app/types/programming-exercises'; styleUrls: ['../programming-exercise-form.scss'], }) export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDestroy, OnInit { + protected readonly documentationType: DocumentationType = 'Programming'; + protected readonly maxPenaltyPattern = MAX_PENALTY_PATTERN; + protected readonly packageNamePatternForJavaBlackbox = PACKAGE_NAME_PATTERN_FOR_JAVA_BLACKBOX; + protected readonly invalidRepositoryNamePattern = INVALID_REPOSITORY_NAME_PATTERN; + protected readonly invalidDirectoryNamePattern = INVALID_DIRECTORY_NAME_PATTERN; + protected readonly shortNamePattern = PROGRAMMING_EXERCISE_SHORT_NAME_PATTERN; + readonly packageNamePatternForJavaKotlin = PACKAGE_NAME_PATTERN_FOR_JAVA_KOTLIN; + readonly appNamePatternForSwift = APP_NAME_PATTERN_FOR_SWIFT; + @ViewChild(ProgrammingExerciseInformationComponent) exerciseInfoComponent?: ProgrammingExerciseInformationComponent; - @ViewChild(ProgrammingExerciseDifficultyComponent) exerciseDifficultyComponent?: ProgrammingExerciseDifficultyComponent; + @ViewChild(ProgrammingExerciseModeComponent) exerciseDifficultyComponent?: ProgrammingExerciseModeComponent; @ViewChild(ProgrammingExerciseLanguageComponent) exerciseLanguageComponent?: ProgrammingExerciseLanguageComponent; @ViewChild(ProgrammingExerciseGradingComponent) exerciseGradingComponent?: ProgrammingExerciseGradingComponent; @ViewChild(ExerciseUpdatePlagiarismComponent) exercisePlagiarismComponent?: ExerciseUpdatePlagiarismComponent; - readonly IncludedInOverallScore = IncludedInOverallScore; - readonly FeatureToggle = FeatureToggle; - readonly ProgrammingLanguage = ProgrammingLanguage; - readonly ProjectType = ProjectType; - readonly documentationType: DocumentationType = 'Programming'; + packageNamePattern = ''; + isSimpleMode = signal(true); + isAuxiliaryRepositoryInputValid = signal(true); + + isEditFieldDisplayedRecord = computed(() => { + const inputFieldEditModeMapping = IS_DISPLAYED_IN_SIMPLE_MODE; + + const isEditFieldDisplayedMapping: Record = {} as Record; + Object.keys(inputFieldEditModeMapping).forEach((key) => { + let isDisplayed = true; + if (this.isSimpleMode() && !(this.isImportFromFile || this.isImportFromExistingExercise)) { + isDisplayed = inputFieldEditModeMapping[key as ProgrammingExerciseInputField]; + } + + isEditFieldDisplayedMapping[key as ProgrammingExerciseInputField] = isDisplayed; + }); + + return isEditFieldDisplayedMapping; + }); private translationBasePath = 'artemisApp.programmingExercise.'; @@ -72,6 +105,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest isImportFromExistingExercise: boolean; isImportFromFile: boolean; isEdit: boolean; + isCreate: boolean; isExamMode: boolean; isLocal: boolean; hasUnsavedChanges = false; @@ -85,40 +119,16 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest notificationText?: string; courseId: number; - AssessmentType = AssessmentType; rerenderSubject = new Subject(); // This is used to revert the select if the user cancels to override the new selected programming language. private selectedProgrammingLanguageValue: ProgrammingLanguage; // This is used to revert the select if the user cancels to override the new selected project type. private selectedProjectTypeValue?: ProjectType; - maxPenaltyPattern = '^([0-9]|([1-9][0-9])|100)$'; - // Java package name Regex according to Java 14 JLS (https://docs.oracle.com/javase/specs/jls/se14/html/jls-7.html#jls-7.4.1), - // with the restriction to a-z,A-Z,_ as "Java letter" and 0-9 as digits due to JavaScript/Browser Unicode character class limitations - packageNamePatternForJavaKotlin = - '^(?!.*(?:\\.|^)(?:abstract|continue|for|new|switch|assert|default|if|package|synchronized|boolean|do|goto|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while|_|true|false|null)(?:\\.|$))[A-Z_a-z][0-9A-Z_a-z]*(?:\\.[A-Z_a-z][0-9A-Z_a-z]*)*$'; - // No dots allowed for the blackbox project type, because the folder naming works slightly different here. - packageNamePatternForJavaBlackbox = - '^(?!.*(?:\\.|^)(?:abstract|continue|for|new|switch|assert|default|if|package|synchronized|boolean|do|goto|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while|_|true|false|null)(?:\\.|$))[A-Z_a-z][0-9A-Z_a-z]*$'; - // Swift package name Regex derived from (https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID412), - // with the restriction to a-z,A-Z as "Swift letter" and 0-9 as digits where no separators are allowed - appNamePatternForSwift = - '^(?!(?:associatedtype|class|deinit|enum|extension|fileprivate|func|import|init|inout|internal|let|open|operator|private|protocol|public|rethrows|static|struct|subscript|typealias|var|break|case|continue|default|defer|do|else|fallthrough|for|guard|if|in|repeat|return|switch|where|while|as|Any|catch|false|is|nil|super|self|Self|throw|throws|true|try|_|[sS]wift)$)[A-Za-z][0-9A-Za-z]*$'; - packageNamePattern = ''; - - // Auxiliary Repository names must only include words or '-' characters. - invalidRepositoryNamePattern = RegExp('^(?!(solution|exercise|tests|auxiliary)\\b)\\b(\\w|-)+$'); - - // Auxiliary Repository checkout directories must be valid directory paths. Those must only include words, - // '-' or '/' characters. - invalidDirectoryNamePattern = RegExp('^[\\w-]+(/[\\w-]+)*$'); - - // 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 exerciseCategories: ExerciseCategory[]; existingCategories: ExerciseCategory[]; - formStatusSections: FormSectionStatus[]; + formStatusSections = signal([]); inputFieldSubscriptions: (Subscription | undefined)[] = []; @@ -133,7 +143,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest public sequentialTestRunsAllowed = false; public testwiseCoverageAnalysisSupported = false; public auxiliaryRepositoriesSupported = false; - public auxiliaryRepositoriesValid = true; + auxiliaryRepositoriesValid = signal(true); public customBuildPlansSupported: string = ''; public theiaEnabled = false; @@ -152,10 +162,6 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest public modePickerOptions?: ModePickerOption[] = []; - // Icons - faQuestionCircle = faQuestionCircle; - faExclamationCircle = faExclamationCircle; - constructor( private programmingExerciseService: ProgrammingExerciseService, private modalService: NgbModal, @@ -171,7 +177,29 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest private programmingLanguageFeatureService: ProgrammingLanguageFeatureService, private navigationUtilService: ArtemisNavigationUtilService, private aeolusService: AeolusService, - ) {} + ) { + effect( + function updateStatusBarSectionsWhenEditModeChanges() { + if (this.isSimpleMode()) { + this.calculateFormStatusSections(); + } + }.bind(this), + { allowSignalWrites: true }, + ); + + effect( + function initializeEditMode() { + const editModeRetrievedFromLocalStorage = localStorage.getItem(LOCAL_STORAGE_KEY_IS_SIMPLE_MODE); + if (editModeRetrievedFromLocalStorage) { + this.isSimpleMode.set(editModeRetrievedFromLocalStorage === 'true'); + } else { + const DEFAULT_EDIT_MODE_IS_SIMPLE_MODE = true; + this.isSimpleMode.set(DEFAULT_EDIT_MODE_IS_SIMPLE_MODE); + } + }.bind(this), + { allowSignalWrites: true }, + ); + } /** * Updates the name of the editedAuxiliaryRepository. @@ -231,7 +259,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest 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; + this.auxiliaryRepositoriesValid.set(this.auxiliaryRepositoryNamedCorrectly && !this.auxiliaryRepositoryDuplicateNames && !this.auxiliaryRepositoryDuplicateDirectories); } /** @@ -244,7 +272,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest this.selectedProgrammingLanguageValue = language; const programmingLanguageFeature = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(language); - this.packageNameRequired = programmingLanguageFeature.packageNameRequired; + this.packageNameRequired = programmingLanguageFeature?.packageNameRequired; this.staticCodeAnalysisAllowed = programmingLanguageFeature.staticCodeAnalysis; this.checkoutSolutionRepositoryAllowed = programmingLanguageFeature.checkoutSolutionRepositoryAllowed; this.sequentialTestRunsAllowed = programmingLanguageFeature.sequentialTestRuns; @@ -412,6 +440,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest this.isImportFromExistingExercise = segments.some((segment) => segment.path === 'import'); this.isImportFromFile = segments.some((segment) => segment.path === 'import-from-file'); this.isEdit = segments.some((segment) => segment.path === 'edit'); + this.isCreate = segments.some((segment) => segment.path === 'new'); }), switchMap(() => this.activatedRoute.params), tap((params) => { @@ -498,26 +527,65 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest } calculateFormStatusSections() { - this.formStatusSections = [ + const updatedFormStatusSections = [ { title: 'artemisApp.programmingExercise.wizardMode.detailedSteps.generalInfoStepTitle', valid: this.exerciseInfoComponent?.formValid ?? false, }, { title: 'artemisApp.programmingExercise.wizardMode.detailedSteps.difficultyStepTitle', - valid: (this.exerciseDifficultyComponent?.teamConfigComponent.formValid && this.validIdeSelection()) ?? false, + valid: (this.exerciseDifficultyComponent?.teamConfigComponent?.formValid && this.validIdeSelection()) ?? false, }, { title: 'artemisApp.programmingExercise.wizardMode.detailedSteps.languageStepTitle', valid: (this.exerciseLanguageComponent?.formValid && this.validOnlineIdeSelection()) ?? false, }, - { title: 'artemisApp.programmingExercise.wizardMode.detailedSteps.problemStepTitle', valid: true, empty: !this.programmingExercise.problemStatement }, + { + title: 'artemisApp.programmingExercise.wizardMode.detailedSteps.problemStepTitle', + valid: true, + empty: !this.programmingExercise.problemStatement, + }, { title: 'artemisApp.programmingExercise.wizardMode.detailedSteps.gradingStepTitle', - valid: Boolean(this.exerciseGradingComponent?.formValid && (this.isExamMode || this.exercisePlagiarismComponent?.formValid)), + valid: Boolean( + this.exerciseGradingComponent?.formValid && + (this.isExamMode || !this.isEditFieldDisplayedRecord().plagiarismControl || this.exercisePlagiarismComponent?.formValid), + ), empty: this.exerciseGradingComponent?.formEmpty, }, ]; + + if (this.isSimpleMode()) { + // the mode section would only contain the difficulty in the simple mode, + // which is why the difficulty is moved to the general section instead + const MODE_SECTION_INDEX = 1; + updatedFormStatusSections.splice(MODE_SECTION_INDEX, MODE_SECTION_INDEX); + } + + this.formStatusSections.set(updatedFormStatusSections); + } + + /** + * Depending on the build environment not all project types might be supported. Per default the project type is currently set to {@link ProjectType.GRADLE_GRADLE}. + * This is also the case, even if {@link ProjectType.GRADLE_GRADLE} is not supported by the build environment. + * + * This method is called to ensure that a valid project type is selected from the simple mode, if the project type cannot be determined automatically, an error message is + * displayed to the user that indicates that the advanced mode should be used to define the project type. + */ + private determineProjectTypeIfNotSelectedAndInSimpleMode() { + if (this.isSimpleMode() && this.isCreate && this.projectTypes) { + const selectedProjectType = this.programmingExercise.projectType; + const isInvalidProjectTypeSelected = selectedProjectType === undefined || !this.projectTypes.includes(selectedProjectType); + if (isInvalidProjectTypeSelected) { + if (this.projectTypes.includes(ProjectType.PLAIN_GRADLE)) { + this.programmingExercise.projectType = ProjectType.PLAIN_GRADLE; + } else if (this.projectTypes.includes(ProjectType.PLAIN_MAVEN)) { + this.programmingExercise.projectType = ProjectType.PLAIN_MAVEN; + } else { + this.alertService.addErrorAlert('Could not automatically determine project type', 'artemisApp.exercise.errors.projectTypeCouldNotBeDetermined'); + } + } + } } private defineSupportedProgrammingLanguages() { @@ -597,6 +665,8 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest save() { const ref = this.popupService.checkExerciseBeforeUpdate(this.programmingExercise, this.backupExercise, this.isExamMode); + this.determineProjectTypeIfNotSelectedAndInSimpleMode(); + if (!this.modalService.hasOpenModals()) { this.saveExercise(); } else { @@ -811,6 +881,11 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest } } + switchEditMode = () => { + this.isSimpleMode.update((isSimpleMode) => !isSimpleMode); + localStorage.setItem(LOCAL_STORAGE_KEY_IS_SIMPLE_MODE, JSON.stringify(this.isSimpleMode())); + }; + /** * Change the selected programming language for the current exercise. If there are unsaved changes, the user * will see a confirmation dialog about switching to a new template @@ -893,6 +968,11 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest translateKey: 'artemisApp.exercise.form.title.pattern', translateValues: {}, }); + } else if (this.exerciseInfoComponent?.titleComponent?.titleChannelNameComponent?.field_title?.control?.errors?.disallowedValue) { + validationErrorReasons.push({ + translateKey: 'artemisApp.exercise.form.title.disallowedValue', + translateValues: {}, + }); } } @@ -911,6 +991,11 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest translateKey: 'artemisApp.exercise.form.shortName.undefined', translateValues: {}, }); + } else if (this.exerciseInfoComponent?.shortNameField()?.control?.errors?.disallowedValue) { + validationErrorReasons.push({ + translateKey: 'artemisApp.exercise.form.title.disallowedValue', + translateValues: {}, + }); } else { if (this.programmingExercise.shortName.length < 3) { validationErrorReasons.push({ @@ -1062,7 +1147,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest } private validateExerciseAuxiliaryRepositories(validationErrorReasons: ValidationReason[]): void { - if (!this.auxiliaryRepositoriesValid) { + if (!this.auxiliaryRepositoriesValid()) { validationErrorReasons.push({ translateKey: 'artemisApp.programmingExercise.auxiliaryRepository.error', translateValues: {}, diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.helper.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.helper.ts new file mode 100644 index 000000000000..fc81642785d7 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.helper.ts @@ -0,0 +1,97 @@ +export enum ProgrammingExerciseInputField { + // General section + TITLE = 'title', + CHANNEL_NAME = 'channelName', + SHORT_NAME = 'shortName', + EDIT_REPOSITORIES_CHECKOUT_PATH = 'editRepositoriesCheckoutPath', + ADD_AUXILIARY_REPOSITORY = 'addAuxiliaryRepository', + CATEGORIES = 'categories', + + // Mode section + DIFFICULTY = 'difficulty', + PARTICIPATION_MODE = 'participationMode', + ALLOW_OFFLINE_IDE = 'allowOfflineIde', + ALLOW_ONLINE_CODE_EDITOR = 'allowOnlineCodeEditor', + ALLOW_ONLINE_IDE = 'allowOnlineIde', + + // Language section + PROGRAMMING_LANGUAGE = 'programmingLanguage', + PROJECT_TYPE = 'projectType', + WITH_EXEMPLARY_DEPENDENCY = 'withExemplaryDependency', + PACKAGE_NAME = 'packageName', + ENABLE_STATIC_CODE_ANALYSIS = 'enableStaticCodeAnalysis', + SEQUENTIAL_TEST_RUNS = 'sequentialTestRuns', + CUSTOMIZE_BUILD_SCRIPT = 'customizeBuildScript', + + // Problem section + PROBLEM_STATEMENT = 'problemStatement', + LINKED_COMPETENCIES = 'linkedCompetencies', + + // Grading section + INCLUDE_EXERCISE_IN_COURSE_SCORE_CALCULATION = 'includeExerciseInCourseScoreCalculation', + POINTS = 'points', + BONUS_POINTS = 'bonusPoints', + SUBMISSION_POLICY = 'submissionPolicy', + TIMELINE = 'timeline', + RELEASE_DATE = 'releaseDate', + START_DATE = 'startDate', + DUE_DATE = 'dueDate', + RUN_TESTS_AFTER_DUE_DATE = 'runTestsAfterDueDate', + ASSESSMENT_DUE_DATE = 'assessmentDueDate', + EXAMPLE_SOLUTION_PUBLICATION_DATE = 'exampleSolutionPublicationDate', + COMPLAINT_ON_AUTOMATIC_ASSESSMENT = 'complaintOnAutomaticAssessment', + MANUAL_FEEDBACK_REQUESTS = 'manualFeedbackRequests', + SHOW_TEST_NAMES_TO_STUDENTS = 'showTestNamesToStudents', + INCLUDE_TESTS_INTO_EXAMPLE_SOLUTION = 'includeTestsIntoExampleSolution', + ASSESSMENT_INSTRUCTIONS = 'assessmentInstructions', + PRESENTATION_SCORE = 'presentationScore', + PLAGIARISM_CONTROL = 'plagiarismControl', +} + +export type InputFieldEditModeMapping = Record; + +export const IS_DISPLAYED_IN_SIMPLE_MODE: InputFieldEditModeMapping = { + // General section + title: true, + channelName: false, + shortName: false, + editRepositoriesCheckoutPath: false, + addAuxiliaryRepository: false, + categories: true, + // Mode section + difficulty: true, + participationMode: false, + allowOfflineIde: false, + allowOnlineCodeEditor: false, + allowOnlineIde: false, // refers to theia + // Language section + programmingLanguage: true, + projectType: false, + withExemplaryDependency: false, + packageName: true, + enableStaticCodeAnalysis: false, + sequentialTestRuns: false, + customizeBuildScript: false, + // Problem section + problemStatement: true, + linkedCompetencies: false, + // Grading section + includeExerciseInCourseScoreCalculation: true, + points: true, + bonusPoints: true, + submissionPolicy: false, + timeline: true, + releaseDate: true, + startDate: false, + dueDate: true, + runTestsAfterDueDate: false, + assessmentDueDate: true, + exampleSolutionPublicationDate: false, + complaintOnAutomaticAssessment: false, + manualFeedbackRequests: false, + showTestNamesToStudents: false, + includeTestsIntoExampleSolution: false, + assessmentInstructions: true, + presentationScore: false, + plagiarismControl: false, +}; 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 5185968f143f..fb2cd4485455 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 @@ -17,11 +17,11 @@ import { ArtemisTableModule } from 'app/shared/table/table.module'; import { RemoveAuxiliaryRepositoryButtonComponent } from 'app/exercises/programming/manage/update/remove-auxiliary-repository-button.component'; import { SubmissionPolicyUpdateModule } from 'app/exercises/shared/submission-policy/submission-policy-update.module'; import { ArtemisModePickerModule } from 'app/exercises/shared/mode-picker/mode-picker.module'; -import { ProgrammingExerciseInformationComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-information.component'; -import { ProgrammingExerciseDifficultyComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component'; -import { ProgrammingExerciseLanguageComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-language.component'; -import { ProgrammingExerciseGradingComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-grading.component'; -import { ProgrammingExerciseProblemComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-problem.component'; +import { ProgrammingExerciseInformationComponent } from 'app/exercises/programming/manage/update/update-components/information/programming-exercise-information.component'; +import { ProgrammingExerciseModeComponent } from 'app/exercises/programming/manage/update/update-components/mode/programming-exercise-mode.component'; +import { ProgrammingExerciseLanguageComponent } from 'app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component'; +import { ProgrammingExerciseGradingComponent } from 'app/exercises/programming/manage/update/update-components/grading/programming-exercise-grading.component'; +import { ProgrammingExerciseProblemComponent } from 'app/exercises/programming/manage/update/update-components/problem/programming-exercise-problem.component'; import { ExerciseTitleChannelNameModule } from 'app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.module'; import { ExerciseUpdateNotificationModule } from 'app/exercises/shared/exercise-update-notification/exercise-update-notification.module'; import { ExerciseUpdatePlagiarismModule } from 'app/exercises/shared/plagiarism/exercise-update-plagiarism/exercise-update-plagiarism.module'; @@ -34,6 +34,10 @@ import { ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent } from 'app/e import { ProgrammingExerciseTheiaComponent } from 'app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component'; import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; import { ProgrammingExerciseEditCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component'; +import { ProgrammingExerciseDifficultyComponent } from 'app/exercises/programming/manage/update/update-components/difficulty/programming-exercise-difficulty.component'; +import { SwitchEditModeButtonComponent } from 'app/exercises/programming/manage/update/switch-edit-mode-button/switch-edit-mode-button.component'; +import { TitleChannelNameModule } from 'app/shared/form/title-channel-name/title-channel-name.module'; +import { CustomNotIncludedInValidatorDirective } from 'app/shared/validators/custom-not-included-in-validator.directive'; @NgModule({ imports: [ @@ -61,11 +65,15 @@ import { ProgrammingExerciseEditCheckoutDirectoriesComponent } from 'app/exercis MonacoEditorComponent, ProgrammingExerciseTheiaComponent, ProgrammingExerciseEditCheckoutDirectoriesComponent, + ProgrammingExerciseDifficultyComponent, + SwitchEditModeButtonComponent, + TitleChannelNameModule, + CustomNotIncludedInValidatorDirective, ], declarations: [ ProgrammingExerciseUpdateComponent, ProgrammingExerciseInformationComponent, - ProgrammingExerciseDifficultyComponent, + ProgrammingExerciseModeComponent, ProgrammingExerciseCustomAeolusBuildPlanComponent, ProgrammingExerciseCustomBuildPlanComponent, ProgrammingExerciseBuildConfigurationComponent, diff --git a/src/main/webapp/app/exercises/programming/manage/update/switch-edit-mode-button/switch-edit-mode-button.component.html b/src/main/webapp/app/exercises/programming/manage/update/switch-edit-mode-button/switch-edit-mode-button.component.html new file mode 100644 index 000000000000..516e01e1c5b5 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/switch-edit-mode-button/switch-edit-mode-button.component.html @@ -0,0 +1,9 @@ + diff --git a/src/main/webapp/app/exercises/programming/manage/update/switch-edit-mode-button/switch-edit-mode-button.component.ts b/src/main/webapp/app/exercises/programming/manage/update/switch-edit-mode-button/switch-edit-mode-button.component.ts new file mode 100644 index 000000000000..2487314232f4 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/switch-edit-mode-button/switch-edit-mode-button.component.ts @@ -0,0 +1,25 @@ +import { Component, input, output } from '@angular/core'; +import { faHandshakeAngle } from '@fortawesome/free-solid-svg-icons'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; + +@Component({ + selector: 'jhi-switch-edit-mode-button', + standalone: true, + templateUrl: './switch-edit-mode-button.component.html', + imports: [ArtemisSharedCommonModule, ArtemisSharedComponentModule], +}) +export class SwitchEditModeButtonComponent { + protected readonly faHandShakeAngle = faHandshakeAngle; + protected readonly ButtonType = ButtonType; + + switchEditMode = output(); + isSimpleMode = input.required(); + buttonSize = input(ButtonSize.MEDIUM); + disabled = input(true); + + protected toggleEditMode(): void { + this.switchEditMode.emit(); + } +} diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/difficulty/programming-exercise-difficulty.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/difficulty/programming-exercise-difficulty.component.html new file mode 100644 index 000000000000..44060a2565ed --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/difficulty/programming-exercise-difficulty.component.html @@ -0,0 +1,6 @@ +
+ +
+ +
+
diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/difficulty/programming-exercise-difficulty.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/difficulty/programming-exercise-difficulty.component.ts new file mode 100644 index 000000000000..1fb82744ccf8 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/difficulty/programming-exercise-difficulty.component.ts @@ -0,0 +1,15 @@ +import { Component, input } from '@angular/core'; +import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; +import { ArtemisDifficultyPickerModule } from 'app/exercises/shared/difficulty-picker/difficulty-picker.module'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; + +@Component({ + selector: 'jhi-programming-exercise-difficulty', + standalone: true, + templateUrl: './programming-exercise-difficulty.component.html', + styleUrls: ['../../../programming-exercise-form.scss'], + imports: [ArtemisDifficultyPickerModule, TranslateDirective], +}) +export class ProgrammingExerciseDifficultyComponent { + programmingExercise = input.required(); +} diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/grading/programming-exercise-grading.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/grading/programming-exercise-grading.component.html new file mode 100644 index 000000000000..815ed56c435c --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/grading/programming-exercise-grading.component.html @@ -0,0 +1,139 @@ +
+

+

+ @if (isEditFieldDisplayedRecord().includeExerciseInCourseScoreCalculation) { +
+ +
+ +
+
+ } +
+ @if (isEditFieldDisplayedRecord().points) { +
+
+ + + @if (maxScore?.invalid && (maxScore?.dirty || maxScore?.touched) && maxScore?.errors) { +
+ } +
+
+ } + @if (isEditFieldDisplayedRecord().bonusPoints) { +
+
+ + + @if (bonusPoints?.invalid && (bonusPoints?.dirty || bonusPoints?.touched) && bonusPoints?.errors) { +
+ } +
+
+ } +
+ @if (programmingExercise.staticCodeAnalysisEnabled) { +
+ + +
+
+ % +
+ +
+ @for (error of maxPenalty.errors! | keyvalue; track error) { + @if (maxPenalty.invalid && (maxPenalty.dirty || maxPenalty.touched)) { +
+
+
+ } + } +
+ } + @if (isEditFieldDisplayedRecord().submissionPolicy) { +
+ + @if (programmingExerciseCreationConfig.isEdit) { + + + + + + + } +
+ } + @if (isEditFieldDisplayedRecord().timeline) { +
+
+ + +
+ +
+ } + @if (isEditFieldDisplayedRecord().assessmentInstructions && programmingExercise.assessmentType === AssessmentType.SEMI_AUTOMATIC) { +
+ + +
+ } + @if (isEditFieldDisplayedRecord().presentationScore) { + + } + @if (programmingExerciseCreationConfig.showSummary) { +
{{ getGradingSummary() }}
+ } +
diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-grading.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/grading/programming-exercise-grading.component.ts similarity index 91% rename from src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-grading.component.ts rename to src/main/webapp/app/exercises/programming/manage/update/update-components/grading/programming-exercise-grading.component.ts index 5a5c6a17c6d3..09c4b8a3ef60 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-grading.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/grading/programming-exercise-grading.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, Input, OnDestroy, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, Input, OnDestroy, ViewChild, input, signal } from '@angular/core'; import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { SubmissionPolicyType } from 'app/entities/submission-policy.model'; @@ -11,22 +11,24 @@ import { NgModel } from '@angular/forms'; import { SubmissionPolicyUpdateComponent } from 'app/exercises/shared/submission-policy/submission-policy-update.component'; import { ProgrammingExerciseLifecycleComponent } from 'app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component'; import { ImportOptions } from 'app/types/programming-exercises'; +import { ProgrammingExerciseInputField } from 'app/exercises/programming/manage/update/programming-exercise-update.helper'; @Component({ selector: 'jhi-programming-exercise-grading', templateUrl: './programming-exercise-grading.component.html', - styleUrls: ['../../programming-exercise-form.scss'], + styleUrls: ['../../../programming-exercise-form.scss'], }) export class ProgrammingExerciseGradingComponent implements AfterViewInit, OnDestroy { - readonly IncludedInOverallScore = IncludedInOverallScore; - readonly AssessmentType = AssessmentType; - readonly faQuestionCircle = faQuestionCircle; + protected readonly IncludedInOverallScore = IncludedInOverallScore; + protected readonly AssessmentType = AssessmentType; + protected readonly faQuestionCircle = faQuestionCircle; private translationBasePath = 'artemisApp.programmingExercise.wizardMode.gradingLabels.'; @Input() programmingExercise: ProgrammingExercise; @Input() programmingExerciseCreationConfig: ProgrammingExerciseCreationConfig; @Input() importOptions: ImportOptions; + isEditFieldDisplayedRecord = input.required>(); @ViewChild(SubmissionPolicyUpdateComponent) submissionPolicyUpdateComponent?: SubmissionPolicyUpdateComponent; @ViewChild(ProgrammingExerciseLifecycleComponent) lifecycleComponent?: ProgrammingExerciseLifecycleComponent; @@ -34,6 +36,8 @@ export class ProgrammingExerciseGradingComponent implements AfterViewInit, OnDes @ViewChild('bonusPoints') bonusPointsField?: NgModel; @ViewChild('maxPenalty') maxPenaltyField?: NgModel; + formValidSignal = signal(false); + formValid: boolean; formEmpty: boolean; formValidChanges = new Subject(); @@ -60,13 +64,16 @@ export class ProgrammingExerciseGradingComponent implements AfterViewInit, OnDes } calculateFormStatus() { - this.formValid = Boolean( + const newFormValidValue = Boolean( this.maxScoreField?.valid && this.bonusPointsField?.valid && (this.maxPenaltyField?.valid || !this.programmingExercise.staticCodeAnalysisEnabled) && !this.submissionPolicyUpdateComponent?.invalid && this.lifecycleComponent?.formValid, ); + + this.formValidSignal.set(newFormValidValue); + this.formValid = newFormValidValue; this.formEmpty = this.lifecycleComponent?.formEmpty ?? false; this.formValidChanges.next(this.formValid); } 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/information/programming-exercise-information.component.html similarity index 71% rename from src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.html rename to src/main/webapp/app/exercises/programming/manage/update/update-components/information/programming-exercise-information.component.html index acdc4cf0b5e0..79658f67ec1d 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/information/programming-exercise-information.component.html @@ -10,60 +10,67 @@ -
-
- - -
- - @for (error of shortName.errors! | keyvalue | removekeys: ['required']; track error) { - @if (shortName.invalid && (shortName.dirty || shortName.touched)) { -
-
-
+ @if (isEditFieldDisplayedRecord().shortName) { +
+
+ + +
+ + @for (error of shortName.errors! | keyvalue | removekeys: ['required']; track error.key) { + @if (shortName.invalid && (shortName.dirty || shortName.touched)) { +
+
+
+ } } - } -
- @if (programmingExercise.shortName && !shortName.invalid) { +
+ } + @if (programmingExercise().shortName && isShortNameFieldValid()) {
- +
- @if (editRepositoryCheckoutPath && isLocal && !isImport && !programmingExerciseCreationConfig.isEdit) { + @if (editRepositoryCheckoutPath && isLocal() && !isImport() && !programmingExerciseCreationConfig.isEdit && isEditFieldDisplayedRecord().editRepositoriesCheckoutPath) { - @if (programmingExercise.auxiliaryRepositories && programmingExercise.auxiliaryRepositories.length > 0) { + @if ( + programmingExercise().auxiliaryRepositories && + programmingExercise().auxiliaryRepositories!.length > 0 && + isEditFieldDisplayedRecord().addAuxiliaryRepository + ) { @@ -117,7 +128,7 @@ } - @if (programmingExerciseCreationConfig && !isLocal) { + @if (programmingExerciseCreationConfig && !isLocal()) { } - @if (isLocal && !isImport && !programmingExerciseCreationConfig.isEdit) { + @if (isLocal() && !isImport() && !programmingExerciseCreationConfig.isEdit && isEditFieldDisplayedRecord().editRepositoriesCheckoutPath) { } - - + @if (isEditFieldDisplayedRecord().addAuxiliaryRepository) { + + + }
} @@ -170,7 +183,7 @@ type="checkbox" name="recreateBuildPlans" id="field_recreateBuildPlans" - [(ngModel)]="importOptions.recreateBuildPlans" + [(ngModel)]="importOptions().recreateBuildPlans" (change)="programmingExerciseCreationConfig.recreateBuildPlanOrUpdateTemplateChange()" /> @@ -180,8 +193,8 @@ } @if ( programmingExerciseCreationConfig.isImportFromExistingExercise && - programmingExercise.projectType !== ProjectType.PLAIN_GRADLE && - programmingExercise.projectType !== ProjectType.GRADLE_GRADLE + programmingExercise().projectType !== ProjectType.PLAIN_GRADLE && + programmingExercise().projectType !== ProjectType.GRADLE_GRADLE ) {
} - @if (!programmingExerciseCreationConfig.isExamMode) { + @if (!programmingExerciseCreationConfig.isExamMode && isEditFieldDisplayedRecord().categories) {
@@ -213,4 +226,7 @@ />
} + @if (isSimpleMode()) { + + } diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.scss b/src/main/webapp/app/exercises/programming/manage/update/update-components/information/programming-exercise-information.component.scss similarity index 100% rename from src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-information.component.scss rename to src/main/webapp/app/exercises/programming/manage/update/update-components/information/programming-exercise-information.component.scss diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/information/programming-exercise-information.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/information/programming-exercise-information.component.ts new file mode 100644 index 000000000000..78158c26e1ce --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/information/programming-exercise-information.component.ts @@ -0,0 +1,318 @@ +import { + AfterViewInit, + Component, + Input, + OnChanges, + OnDestroy, + QueryList, + SimpleChanges, + ViewChild, + ViewChildren, + effect, + inject, + input, + model, + signal, + viewChild, +} from '@angular/core'; +import { NgModel } from '@angular/forms'; +import { ProgrammingExercise, ProjectType } from 'app/entities/programming/programming-exercise.model'; +import { ProgrammingExerciseCreationConfig } from 'app/exercises/programming/manage/update/programming-exercise-creation-config'; +import { ExerciseTitleChannelNameComponent } from 'app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.component'; +import { Subject, Subscription } from 'rxjs'; +import { TableEditableFieldComponent } from 'app/shared/table/table-editable-field.component'; +import { every } from 'lodash-es'; +import { ImportOptions } from 'app/types/programming-exercises'; +import { ProgrammingExerciseInputField } from 'app/exercises/programming/manage/update/programming-exercise-update.helper'; +import { removeSpecialCharacters } from 'app/shared/util/utils'; +import { CourseExistingExerciseDetailsType, ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { AlertService } from 'app/core/util/alert.service'; +import { ExerciseType } from 'app/entities/exercise.model'; +import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; +import { faPlus } from '@fortawesome/free-solid-svg-icons'; +import { ProgrammingExerciseEditCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-edit-checkout-directories/programming-exercise-edit-checkout-directories.component'; +import { BuildPlanCheckoutDirectoriesDTO } from 'app/entities/programming/build-plan-checkout-directories-dto'; + +const MAXIMUM_TRIES_TO_GENERATE_UNIQUE_SHORT_NAME = 200; + +@Component({ + selector: 'jhi-programming-exercise-info', + templateUrl: './programming-exercise-information.component.html', + styleUrls: ['../../../programming-exercise-form.scss', 'programming-exercise-information.component.scss'], +}) +export class ProgrammingExerciseInformationComponent implements AfterViewInit, OnChanges, OnDestroy { + protected readonly ProjectType = ProjectType; + protected readonly ButtonType = ButtonType; + protected readonly ButtonSize = ButtonSize; + protected readonly faPlus = faPlus; + + @Input({ required: true }) programmingExerciseCreationConfig: ProgrammingExerciseCreationConfig; + isImport = input.required(); + isExamMode = input.required(); + programmingExercise = input.required(); + isLocal = input.required(); + importOptions = input.required(); + isSimpleMode = input.required(); + isEditFieldDisplayedRecord = input.required>(); + courseId = input(); + isAuxiliaryRepositoryInputValid = model.required(); + + exerciseTitleChannelComponent = viewChild('titleChannelNameComponent'); + @ViewChildren(TableEditableFieldComponent) tableEditableFields?: QueryList; + + shortNameField = viewChild('shortName'); + @ViewChild('checkoutSolutionRepository') checkoutSolutionRepositoryField?: NgModel; + @ViewChild('recreateBuildPlans') recreateBuildPlansField?: NgModel; + @ViewChild('updateTemplateFiles') updateTemplateFilesField?: NgModel; + @ViewChild('titleChannelNameComponent') titleComponent?: ExerciseTitleChannelNameComponent; + @ViewChild(ProgrammingExerciseEditCheckoutDirectoriesComponent) programmingExerciseEditCheckoutDirectories?: ProgrammingExerciseEditCheckoutDirectoriesComponent; + + private readonly exerciseService: ExerciseService = inject(ExerciseService); + private readonly alertService: AlertService = inject(AlertService); + + isShortNameFieldValid = signal(false); + isShortNameFromAdvancedMode = signal(false); + + formValid: boolean; + formValidChanges = new Subject(); + + inputFieldSubscriptions: (Subscription | undefined)[] = []; + + alreadyUsedExerciseNames = signal>(new Set()); + alreadyUsedShortNames = signal>(new Set()); + + exerciseTitle = signal(undefined); + + editRepositoryCheckoutPath: boolean = false; + submissionBuildPlanCheckoutRepositories: BuildPlanCheckoutDirectoriesDTO; + + constructor() { + effect( + () => { + this.defineShortNameOnEditModeChangeIfNotDefinedInAdvancedMode(); + }, + { allowSignalWrites: true }, + ); + + effect( + () => { + this.generateShortNameWhenInSimpleMode(); + }, + { allowSignalWrites: true }, + ); + + effect(() => { + this.registerInputFieldsWhenChildComponentsAreReady(); + }); + + effect(() => { + this.fetchAndInitializeTakenTitlesAndShortNames(); + }); + } + + ngAfterViewInit() { + this.registerInputFields(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.programmingExercise) { + this.exerciseTitle.set(this.programmingExercise().title); + } + } + + ngOnDestroy(): void { + for (const subscription of this.inputFieldSubscriptions) { + subscription?.unsubscribe(); + } + } + + registerInputFields() { + this.inputFieldSubscriptions.forEach((subscription) => subscription?.unsubscribe()); + + this.inputFieldSubscriptions.push(this.exerciseTitleChannelComponent()?.titleChannelNameComponent?.formValidChanges.subscribe(() => this.calculateFormValid())); + this.inputFieldSubscriptions.push(this.shortNameField()?.valueChanges?.subscribe(() => this.calculateFormValid())); + this.inputFieldSubscriptions.push(this.checkoutSolutionRepositoryField?.valueChanges?.subscribe(() => this.calculateFormValid())); + this.inputFieldSubscriptions.push(this.recreateBuildPlansField?.valueChanges?.subscribe(() => this.calculateFormValid())); + this.inputFieldSubscriptions.push(this.updateTemplateFilesField?.valueChanges?.subscribe(() => this.calculateFormValid())); + this.inputFieldSubscriptions.push(this.programmingExerciseEditCheckoutDirectories?.formValidChanges.subscribe(() => this.calculateFormValid())); + this.tableEditableFields?.changes.subscribe((fields: QueryList) => { + fields.toArray().forEach((field) => this.inputFieldSubscriptions.push(field.editingInput.valueChanges?.subscribe(() => this.calculateFormValid()))); + }); + + this.titleComponent?.titleChannelNameComponent?.field_title?.valueChanges?.subscribe((newTitle: string) => { + if (this.isSimpleMode()) { + this.updateShortName(newTitle); + } + }); + + this.shortNameField()?.valueChanges?.subscribe(() => { + this.updateIsShortNameValid(); + }); + } + + updateShortName(newTitle: string) { + this.exerciseTitle.set(newTitle); + } + + calculateFormValid() { + const isCheckoutSolutionRepositoryValid = this.isCheckoutSolutionRepositoryValid(); + const isRecreateBuildPlansValid = this.isRecreateBuildPlansValid(); + const isUpdateTemplateFilesValid = this.isUpdateTemplateFilesValid(); + const areAuxiliaryRepositoriesValid = this.areAuxiliaryRepositoriesValid(); + const areCheckoutPathsValid = this.areCheckoutPathsValid(); + this.formValid = Boolean( + this.exerciseTitleChannelComponent()?.titleChannelNameComponent?.formValidSignal() && + this.getIsShortNameFieldValid() && + isCheckoutSolutionRepositoryValid && + isRecreateBuildPlansValid && + isUpdateTemplateFilesValid && + areAuxiliaryRepositoriesValid && + areCheckoutPathsValid, + ); + this.formValidChanges.next(this.formValid); + } + + areAuxiliaryRepositoriesValid(): boolean { + const areAuxiliaryRepositoriesValid = + (every( + this.tableEditableFields?.map((field) => field.editingInput.valid), + Boolean, + ) && + !this.programmingExerciseCreationConfig.auxiliaryRepositoryDuplicateDirectories && + !this.programmingExerciseCreationConfig.auxiliaryRepositoryDuplicateNames) || + !this.programmingExercise().auxiliaryRepositories?.length; + + const isAuxRepoEditingPossibleInCurrentEditMode = !this.isSimpleMode() || this.isEditFieldDisplayedRecord().addAuxiliaryRepository; + if (isAuxRepoEditingPossibleInCurrentEditMode) { + // if editing is not possible the field will not be displayed and validity checks will evaluate to true, + // even if the actual current setting is invalid + this.isAuxiliaryRepositoryInputValid.set(areAuxiliaryRepositoriesValid); + } + return areAuxiliaryRepositoriesValid; + } + + isUpdateTemplateFilesValid(): boolean { + return ( + this.updateTemplateFilesField?.valid || + !this.programmingExerciseCreationConfig.isImportFromExistingExercise || + this.programmingExercise().projectType === ProjectType.PLAIN_GRADLE || + this.programmingExercise().projectType === ProjectType.GRADLE_GRADLE + ); + } + + isRecreateBuildPlansValid(): boolean { + return this.recreateBuildPlansField?.valid || !this.programmingExerciseCreationConfig.isImportFromExistingExercise; + } + + isCheckoutSolutionRepositoryValid(): boolean { + return Boolean( + this.checkoutSolutionRepositoryField?.valid || + this.programmingExercise().id || + !this.programmingExercise().programmingLanguage || + !this.programmingExerciseCreationConfig.checkoutSolutionRepositoryAllowed, + ); + } + + areCheckoutPathsValid(): boolean { + return Boolean( + !this.programmingExerciseEditCheckoutDirectories || + (this.programmingExerciseEditCheckoutDirectories.formValid && + this.programmingExerciseEditCheckoutDirectories.areValuesUnique([ + this.programmingExercise().buildConfig?.assignmentCheckoutPath, + this.programmingExercise().buildConfig?.testCheckoutPath, + this.programmingExercise().buildConfig?.solutionCheckoutPath, + ])), + ); + } + + toggleEditRepositoryCheckoutPath() { + this.editRepositoryCheckoutPath = !this.editRepositoryCheckoutPath; + } + + updateSubmissionBuildPlanCheckoutDirectories(buildPlanCheckoutDirectoriesDTO: BuildPlanCheckoutDirectoriesDTO) { + this.submissionBuildPlanCheckoutRepositories = buildPlanCheckoutDirectoriesDTO; + } + + onAssigmentRepositoryCheckoutPathChange(event: string) { + this.programmingExercise().buildConfig!.assignmentCheckoutPath = event; + // We need to create a new object to trigger the change detection + this.programmingExercise().buildConfig = { ...this.programmingExercise().buildConfig }; + } + + onTestRepositoryCheckoutPathChange(event: string) { + this.programmingExercise().buildConfig!.testCheckoutPath = event; + // We need to create a new object to trigger the change detection + this.programmingExercise().buildConfig = { ...this.programmingExercise().buildConfig }; + } + + onSolutionRepositoryCheckoutPathChange(event: string) { + this.programmingExercise().buildConfig!.solutionCheckoutPath = event; + // We need to create a new object to trigger the change detection + this.programmingExercise().buildConfig = { ...this.programmingExercise().buildConfig }; + } + + private registerInputFieldsWhenChildComponentsAreReady() { + this.shortNameField(); // triggers effect + this.registerInputFields(); + } + + private fetchAndInitializeTakenTitlesAndShortNames() { + const courseId = this.courseId() ?? this.programmingExercise().course?.id; + if (courseId) { + this.exerciseService.getExistingExerciseDetailsInCourse(courseId, ExerciseType.PROGRAMMING).subscribe((exerciseDetails: CourseExistingExerciseDetailsType) => { + this.alreadyUsedExerciseNames.set(exerciseDetails.exerciseTitles ?? new Set()); + this.alreadyUsedShortNames.set(exerciseDetails.shortNames ?? new Set()); + }); + } + } + + private generateShortNameWhenInSimpleMode() { + const shouldNotGenerateShortName = !this.isSimpleMode() || this.programmingExerciseCreationConfig.isEdit; + if (shouldNotGenerateShortName) { + this.isShortNameFromAdvancedMode.set(this.isShortNameFieldValid()); + return; + } + let newShortName = this.exerciseTitle() ?? this.programmingExercise().title; + if (this.isImport() || this.isShortNameFromAdvancedMode()) { + newShortName = this.programmingExercise().shortName; + } + + if (newShortName && newShortName.length > 3) { + const sanitizedShortName = removeSpecialCharacters(newShortName ?? ''); + // noinspection UnnecessaryLocalVariableJS: not inlined because the variable name improves readability + const uniqueShortName = this.ensureShortNameIsUnique(sanitizedShortName); + this.programmingExercise().shortName = uniqueShortName; + } + + this.updateIsShortNameValid(); + } + + private defineShortNameOnEditModeChangeIfNotDefinedInAdvancedMode() { + if (this.isSimpleMode()) { + this.updateIsShortNameValid(); + this.calculateFormValid(); + } + } + + private updateIsShortNameValid() { + this.isShortNameFieldValid.set(this.getIsShortNameFieldValid()); + } + + private getIsShortNameFieldValid() { + return this.shortNameField() === undefined || this.shortNameField()?.control?.status === 'VALID' || this.shortNameField()?.control?.status === 'DISABLED'; + } + + private ensureShortNameIsUnique(shortName: string): string { + let newShortName = shortName; + let counter = 1; + while (this.alreadyUsedShortNames().has(newShortName)) { + if (counter > MAXIMUM_TRIES_TO_GENERATE_UNIQUE_SHORT_NAME) { + this.alertService.error('artemisApp.error.shortNameGenerationFailed'); + break; + } + newShortName = `${shortName}${counter}`; + counter++; + } + return newShortName; + } +} diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.html new file mode 100644 index 000000000000..7b60308b7091 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.html @@ -0,0 +1,240 @@ +
+

+

+ @if (isEditFieldDisplayedRecord().programmingLanguage) { +
+ + +
+ } + @if (programmingExercise.programmingLanguage && programmingExerciseCreationConfig.projectTypes?.length && programmingExerciseCreationConfig.modePickerOptions) { +
+ @if (isEditFieldDisplayedRecord().projectType) { + + + } + @if ( + isEditFieldDisplayedRecord().withExemplaryDependency && + !programmingExerciseCreationConfig.isImportFromExistingExercise && + !programmingExerciseCreationConfig.isImportFromFile && + !programmingExercise.id && + programmingExercise.programmingLanguage === ProgrammingLanguage.JAVA && + programmingExercise.projectType !== ProjectType.MAVEN_BLACKBOX + ) { +
+ + + +
+ } +
+ } + @if (isEditFieldDisplayedRecord().packageName) { + @if (programmingExercise.programmingLanguage === ProgrammingLanguage.EMPTY) { +
+

+ + +

+
    +
  1. +
  2. +

    +
      +
    1. +
    2. +
    3. +
    +
  3. +
  4. +
  5. +
  6. +
+
+ } + @if (programmingExercise.programmingLanguage && programmingExerciseCreationConfig.packageNameRequired && programmingExercise.projectType !== ProjectType.XCODE) { +
+ + + @for (e of packageName.errors! | keyvalue | removekeys: ['required']; track e) { + @if (packageName.invalid && (packageName.dirty || packageName.touched)) { +
+ @if (programmingExercise.projectType === ProjectType.MAVEN_BLACKBOX) { +
+ } + @if (programmingExercise.projectType !== ProjectType.MAVEN_BLACKBOX) { +
+ } +
+ } + } +
+ } + @if (programmingExercise.programmingLanguage === ProgrammingLanguage.SWIFT && programmingExercise.projectType === ProjectType.XCODE) { +
+ + + @for (e of packageName.errors! | keyvalue | removekeys: ['required']; track e) { + @if (packageName.invalid && (packageName.dirty || packageName.touched)) { +
+
+
+ } + } +
+ } + } + + + @if (programmingExercise.allowOnlineIde && programmingExercise.programmingLanguage) { + + + } + + + @if (isEditFieldDisplayedRecord().enableStaticCodeAnalysis && programmingExercise.programmingLanguage && programmingExerciseCreationConfig.staticCodeAnalysisAllowed) { +
+
+ + + +
+
+ } + + @if (isEditFieldDisplayedRecord().sequentialTestRuns && programmingExerciseCreationConfig.sequentialTestRunsAllowed) { +
+
+ + + +
+
+ } + + @if (programmingExerciseCreationConfig.testwiseCoverageAnalysisSupported) { +
+ + + +
+ } + @if (!programmingExercise.id && programmingExercise.programmingLanguage && programmingExerciseCreationConfig.checkoutSolutionRepositoryAllowed) { +
+ +
+ } + @if (isEditFieldDisplayedRecord().customizeBuildScript) { + @if (programmingExerciseCreationConfig.customBuildPlansSupported === PROFILE_LOCALCI) { + + } @else if (programmingExerciseCreationConfig.customBuildPlansSupported === PROFILE_AEOLUS) { + + } + } +
diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.ts similarity index 92% rename from src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts rename to src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.ts index 7514543ec6e9..5f9bae769c84 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/language/programming-exercise-language.component.ts @@ -1,4 +1,4 @@ -import { AfterViewChecked, AfterViewInit, Component, EventEmitter, Input, OnDestroy, ViewChild } from '@angular/core'; +import { AfterViewChecked, AfterViewInit, Component, EventEmitter, Input, OnDestroy, ViewChild, input } from '@angular/core'; import { ProgrammingExercise, ProgrammingLanguage, ProjectType } from 'app/entities/programming/programming-exercise.model'; import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { ProgrammingExerciseCreationConfig } from 'app/exercises/programming/manage/update/programming-exercise-creation-config'; @@ -8,18 +8,20 @@ import { Subject, Subscription } from 'rxjs'; import { ProgrammingExerciseCustomAeolusBuildPlanComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component'; import { ProgrammingExerciseCustomBuildPlanComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component'; import { ProgrammingExerciseTheiaComponent } from 'app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component'; +import { ProgrammingExerciseInputField } from 'app/exercises/programming/manage/update/programming-exercise-update.helper'; @Component({ selector: 'jhi-programming-exercise-language', templateUrl: './programming-exercise-language.component.html', - styleUrls: ['../../programming-exercise-form.scss'], + styleUrls: ['../../../programming-exercise-form.scss'], }) export class ProgrammingExerciseLanguageComponent implements AfterViewChecked, AfterViewInit, OnDestroy { readonly ProgrammingLanguage = ProgrammingLanguage; readonly ProjectType = ProjectType; - @Input() programmingExercise: ProgrammingExercise; - @Input() programmingExerciseCreationConfig: ProgrammingExerciseCreationConfig; + @Input({ required: true }) programmingExercise: ProgrammingExercise; + @Input({ required: true }) programmingExerciseCreationConfig: ProgrammingExerciseCreationConfig; + isEditFieldDisplayedRecord = input.required>(); @ViewChild('select') selectLanguageField: NgModel; @ViewChild('packageName') packageNameField?: NgModel; diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/mode/programming-exercise-mode.component.html similarity index 60% rename from src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.html rename to src/main/webapp/app/exercises/programming/manage/update/update-components/mode/programming-exercise-mode.component.html index 59cc94f7c1ba..aa2b6df8bc7d 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/mode/programming-exercise-mode.component.html @@ -4,50 +4,51 @@ id="artemisApp.programmingExercise.wizardMode.detailedSteps.difficultyStepTitle" >

-
- -
- + @if (isEditFieldDisplayedRecord().difficulty) { + + } + @if (isEditFieldDisplayedRecord().participationMode) { +
+
-
+ }
- -
-
-
- +
+ } + @if (ProjectType.XCODE !== programmingExerciseCreationConfig.selectedProjectType && isEditFieldDisplayedRecord().allowOnlineCodeEditor) {