Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exam mode: Enable changing working time in exam edit screen #7284

Merged
merged 39 commits into from
Oct 14, 2023
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7b0bf9b
Enable time update in exam edit, add time extension tests
aplr Sep 28, 2023
3de5468
Add batching TODO
aplr Sep 28, 2023
eb6e411
add confirmation in exam edit form if dates change
reschandreas Sep 28, 2023
131af69
fix if condition
reschandreas Sep 28, 2023
6ac8ff4
add missing lines
reschandreas Sep 28, 2023
2996f72
Prettify code, make date inputs depend on each other, fix test errors…
aplr Sep 28, 2023
6538c4f
Fix client tests
aplr Sep 28, 2023
eb05970
Code cleanup & working time calc refactoring
aplr Sep 28, 2023
7890241
Remove console log
aplr Sep 28, 2023
b8476ac
Fix bugs in import client tests, adapt client tests to changes, major…
aplr Sep 29, 2023
2254b14
Merge branch 'develop' into feature/exam-mode/edit-working-time
reschandreas Sep 29, 2023
897c04f
Apply suggestions from code review
aplr Sep 29, 2023
7f29e48
Code style changes
aplr Sep 29, 2023
ba323d5
Merge remote-tracking branch 'origin/feature/exam-mode/edit-working-t…
aplr Sep 29, 2023
113d54a
Fix time picker start date behaviour
aplr Sep 29, 2023
15a0b5d
Revert working time check
aplr Sep 29, 2023
1dc0d2f
Fix issue w/ working time calculation
aplr Sep 29, 2023
66780de
Fix issue w/ working time calculation
aplr Sep 29, 2023
02f7ac6
Remove TODO
aplr Sep 29, 2023
0bc7fab
Fix date time picker tests
aplr Sep 29, 2023
f6c4d2b
Apply suggestions from code review
aplr Oct 1, 2023
bbcdee4
Merge branch 'develop' into feature/exam-mode/edit-working-time
aplr Oct 6, 2023
91482b5
Show working time change in dialog, extract working time change compo…
aplr Oct 6, 2023
e0514cb
Allow custom content for confirmation dialog, show working time chang…
aplr Oct 6, 2023
8779a85
Apply suggestions from code review
aplr Oct 6, 2023
f3b5d92
Fix tests
aplr Oct 6, 2023
835dbda
Fix translations
aplr Oct 6, 2023
e011c91
Fix translations
aplr Oct 9, 2023
6077be1
Fix course resolver issues
aplr Oct 10, 2023
3ca9f36
Merge branch 'develop' into feature/exam-mode/edit-working-time
Strohgelaender Oct 10, 2023
8080604
Update expression from code review
aplr Oct 10, 2023
2f6046a
PR feedback
aplr Oct 10, 2023
d360e6c
Use `isSame` instead of `diff` when comparing dates
aplr Oct 12, 2023
2ef28b4
Update translations
aplr Oct 12, 2023
5edf0b2
Merge branch 'develop' into feature/exam-mode/edit-working-time
aplr Oct 12, 2023
4d439dc
Merge branch 'develop' into feature/exam-mode/edit-working-time
krusche Oct 14, 2023
1eeb5de
Merge branch 'develop' into feature/exam-mode/edit-working-time
aplr Oct 14, 2023
72205fc
Fix tests
aplr Oct 14, 2023
81f2352
Fix tests
aplr Oct 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 23 additions & 14 deletions src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,54 +190,57 @@
@PutMapping("courses/{courseId}/exams")
@EnforceAtLeastInstructor
public ResponseEntity<Exam> updateExam(@PathVariable Long courseId, @RequestBody Exam updatedExam) throws URISyntaxException {
log.debug("REST request to update an exam : {}", updatedExam);

if (updatedExam.getId() == null) {
return createExam(courseId, updatedExam);
}

checkForExamConflictsElseThrow(courseId, updatedExam);

examAccessService.checkCourseAndExamAccessForInstructorElseThrow(courseId, updatedExam.getId());

// Make sure that the original references are preserved.
Exam originalExam = examRepository.findByIdElseThrow(updatedExam.getId());
var originalExamDuration = originalExam.getDuration();

// The Exam Mode cannot be changed after creation -> Compare request with version in the database
if (updatedExam.isTestExam() != originalExam.isTestExam()) {
throw new ConflictException("The Exam Mode cannot be changed after creation", ENTITY_NAME, "examModeMismatch");
}

// NOTE: Make sure that all references are preserved here
updatedExam.setExerciseGroups(originalExam.getExerciseGroups());
updatedExam.setStudentExams(originalExam.getStudentExams());
updatedExam.setExamUsers(originalExam.getExamUsers());

Channel updatedChannel = channelService.updateExamChannel(originalExam, updatedExam);

Exam savedExam = examRepository.save(updatedExam);

// NOTE: We have to get exercises and groups as we need them for re-scheduling
Exam examWithExercises = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(savedExam.getId());

// We can't test dates for equality as the dates retrieved from the database lose precision. Also use instant to take timezones into account
Comparator<ZonedDateTime> comparator = Comparator.comparing(date -> date.truncatedTo(ChronoUnit.SECONDS).toInstant());
if (comparator.compare(originalExam.getVisibleDate(), updatedExam.getVisibleDate()) != 0
|| comparator.compare(originalExam.getStartDate(), updatedExam.getStartDate()) != 0) {
// get all exercises
Exam examWithExercises = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(savedExam.getId());
// for all programming exercises in the exam, send their ids for scheduling
examWithExercises.getExerciseGroups().stream().flatMap(group -> group.getExercises().stream()).filter(ProgrammingExercise.class::isInstance).map(Exercise::getId)
.forEach(instanceMessageSendService::sendProgrammingExerciseSchedule);
}

if (comparator.compare(originalExam.getEndDate(), updatedExam.getEndDate()) != 0) {
// get all exercises
Exam examWithExercises = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(savedExam.getId());
examService.scheduleModelingExercises(examWithExercises);
// NOTE: if the end date was changed, we need to update student exams and re-schedule exercises
if (!originalExam.getEndDate().equals(savedExam.getEndDate())) {
int workingTimeChange = savedExam.getDuration() - originalExamDuration;
updateStudentExamsAndRescheduleExercises(examWithExercises, originalExamDuration, workingTimeChange);
}

if (updatedChannel != null) {
savedExam.setChannelName(updatedExam.getChannelName());
}

return ResponseEntity.ok(savedExam);

Check warning on line 243 in src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java

View check run for this annotation

Teamscale / teamscale-findings

src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java#L193-L243

This method is a bit lengthy [0]. Consider shortening it, e.g. by extracting code blocks into separate methods. [0] https://teamscale.io/findings.html#details/GitHub-ls1intum-Artemis?t=feature%2Fexam-mode%2Fedit-working-time%3AHEAD&id=C1AF4737B08B652EE2DD9A4E4899755D
}

/**
Expand All @@ -259,9 +262,7 @@
throw new BadRequestException();
}

var now = now();

// We have to get exercise groups as `scheduleModelingExercises` needs them
// NOTE: We have to get exercise groups as `scheduleModelingExercises` needs them
Exam exam = examService.findByIdWithExerciseGroupsAndExercisesElseThrow(examId);
var originalExamDuration = exam.getDuration();

Expand All @@ -270,10 +271,18 @@
exam.setWorkingTime(exam.getWorkingTime() + workingTimeChange);
examRepository.save(exam);

// 2. Re-calculate the working times of all student exams
updateStudentExamsAndRescheduleExercises(exam, originalExamDuration, workingTimeChange);

return ResponseEntity.ok(exam);
}

private void updateStudentExamsAndRescheduleExercises(Exam exam, Integer originalExamDuration, Integer workingTimeChange) {
var now = now();

User instructor = userRepository.getUser();

// 2. Re-calculate the working times of all student exams
var studentExams = studentExamRepository.findByExamId(examId);
var studentExams = studentExamRepository.findByExamId(exam.getId());
for (var studentExam : studentExams) {
Integer originalStudentWorkingTime = studentExam.getWorkingTime();
int originalTimeExtension = originalStudentWorkingTime - originalExamDuration;
Expand All @@ -288,7 +297,9 @@
int adjustedWorkingTime = Math.max(newNormalWorkingTime + timeAdjustment, 0);
studentExam.setWorkingTime(adjustedWorkingTime);
}
// TODO: probably batch these updates?

Check warning on line 300 in src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java

View check run for this annotation

Teamscale / teamscale-findings

src/main/java/de/tum/in/www1/artemis/web/rest/ExamResource.java#L300

TODO: probably batch these updates? https://teamscale.io/findings.html#details/GitHub-ls1intum-Artemis?t=feature%2Fexam-mode%2Fedit-working-time%3AHEAD&id=F4AC8E1D0A69DF83039E9004B7EF52E1
aplr marked this conversation as resolved.
Show resolved Hide resolved
var savedStudentExam = studentExamRepository.save(studentExam);

// NOTE: if the exam is already visible, notify the student about the working time change
if (now.isAfter(exam.getVisibleDate())) {
examLiveEventsService.createAndSendWorkingTimeUpdateEvent(savedStudentExam, savedStudentExam.getWorkingTime(), originalStudentWorkingTime, true, instructor);
Expand All @@ -300,12 +311,10 @@
instanceMessageSendService.sendRescheduleAllStudentExams(exam.getId());
}

// NOTE: potentially re-schedule clustering of modeling submissions (in case Compass is active)
if (now.isBefore(examDateService.getLatestIndividualExamEndDate(exam))) {
// potentially re-schedule clustering of modeling submissions (in case Compass is active)
examService.scheduleModelingExercises(exam);
}

return ResponseEntity.ok(exam);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
import { Observable, filter, map, of } from 'rxjs';
import { HttpResponse } from '@angular/common/http';
import { StudentExamWithGradeDTO } from 'app/exam/exam-scores/exam-score-dtos.model';
import { Course } from 'app/entities/course.model';
import { CourseManagementService } from 'app/course/manage/course-management.service';
import { catchError } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class CourseResolve implements Resolve<Course | null> {
constructor(private courseManagementService: CourseManagementService) {}

resolve(route: ActivatedRouteSnapshot): Observable<Course | null> {
const courseId = route.params['courseId'];

if (courseId) {
return this.courseManagementService.find(courseId).pipe(
map((response) => response.body),
catchError(() => of(null)),
);
}

return of(null);
}
}

@Injectable({ providedIn: 'root' })
export class ExamResolve implements Resolve<Exam> {
Expand Down
5 changes: 4 additions & 1 deletion src/main/webapp/app/exam/manage/exam-management.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { OrionTutorAssessmentComponent } from 'app/orion/assessment/orion-tutor-
import { isOrion } from 'app/shared/orion/orion';
import { FileUploadExerciseManagementResolve } from 'app/exercises/file-upload/manage/file-upload-exercise-management-resolve.service';
import { ModelingExerciseResolver } from 'app/exercises/modeling/manage/modeling-exercise-resolver.service';
import { ExamResolve, ExerciseGroupResolve, StudentExamResolve } from 'app/exam/manage/exam-management-resolve.service';
import { CourseResolve, ExamResolve, ExerciseGroupResolve, StudentExamResolve } from 'app/exam/manage/exam-management-resolve.service';
import { BonusComponent } from 'app/grading-system/bonus/bonus.component';
import { SuspiciousBehaviorComponent } from 'app/exam/manage/suspicious-behavior/suspicious-behavior.component';
import { SuspiciousSessionsOverviewComponent } from 'app/exam/manage/suspicious-behavior/suspicious-sessions-overview/suspicious-sessions-overview.component';
Expand All @@ -73,6 +73,7 @@ export const examManagementRoute: Routes = [
component: ExamUpdateComponent,
resolve: {
exam: ExamResolve,
course: CourseResolve,
aplr marked this conversation as resolved.
Show resolved Hide resolved
},
data: {
authorities: [Authority.INSTRUCTOR, Authority.ADMIN],
Expand All @@ -85,6 +86,7 @@ export const examManagementRoute: Routes = [
component: ExamUpdateComponent,
resolve: {
exam: ExamResolve,
course: CourseResolve,
},
data: {
authorities: [Authority.INSTRUCTOR, Authority.ADMIN],
Expand Down Expand Up @@ -113,6 +115,7 @@ export const examManagementRoute: Routes = [
component: ExamUpdateComponent,
resolve: {
exam: ExamResolve,
course: CourseResolve,
},
data: {
authorities: [Authority.INSTRUCTOR, Authority.ADMIN],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,14 @@ <h4 class="modal-title">
name="workingTimeSeconds"
durationLabelText="artemisApp.examManagement.editWorkingTime.label"
[(ngModel)]="workingTimeSeconds"
[allowNegative]="true"
[showRelative]="false"
[disabled]="isLoading"
/>
</div>

<p
class="mb-0"
*ngIf="absoluteWorkingTimeDuration"
jhiTranslate="artemisApp.examManagement.editWorkingTime.duration"
[translateValues]="{ duration: absoluteWorkingTimeDuration }"
></p>
<div class="bg-light rounded-2 p-3 mt-3">
<jhi-working-time-change *ngIf="newWorkingTime && oldWorkingTime" [newWorkingTime]="newWorkingTime" [oldWorkingTime]="oldWorkingTime" />
</div>

<hr />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { HttpResponse } from '@angular/common/http';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { faBan, faCheck, faSpinner } from '@fortawesome/free-solid-svg-icons';

import { Exam } from 'app/entities/exam.model';
import { ExamManagementService } from 'app/exam/manage/exam-management.service';
import { normalWorkingTime } from 'app/exam/participate/exam.utils';
import dayjs from 'dayjs/esm';
import { examWorkingTime } from 'app/exam/participate/exam.utils';

@Component({
selector: 'jhi-edit-working-time-dialog',
Expand All @@ -27,11 +26,12 @@ export class ExamEditWorkingTimeDialogComponent {

workingTimeSeconds = 0;

get absoluteWorkingTimeDuration() {
const currentWorkingTimeSeconds = normalWorkingTime(this.exam);
if (!currentWorkingTimeSeconds) return undefined;
const duration = dayjs.duration(currentWorkingTimeSeconds + this.workingTimeSeconds, 'seconds');
return [duration.asHours() >= 1 ? `${Math.floor(duration.asHours())} h` : null, duration.format('m [min] s [s]')].filter(Boolean).join(' ');
get oldWorkingTime() {
return examWorkingTime(this.exam);
}

get newWorkingTime() {
return this.oldWorkingTime ? this.oldWorkingTime + this.workingTimeSeconds : undefined;
}

constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import dayjs from 'dayjs/esm';
import { Subscription, from } from 'rxjs';

import { Exam } from 'app/entities/exam.model';
import { ExamEditWorkingTimeDialogComponent } from 'app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time-dialog.component';
import { AlertService } from 'app/core/util/alert.service';
import { ExamEditWorkingTimeDialogComponent } from './exam-edit-working-time-dialog.component';

@Component({
selector: 'jhi-exam-edit-working-time',
Expand Down
35 changes: 25 additions & 10 deletions src/main/webapp/app/exam/manage/exams/exam-update.component.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="row justify-content-center">
<div class="col">
<form name="editForm" role="form" #editForm="ngForm" (ngSubmit)="save()">
<form name="editForm" role="form" #editForm="ngForm" (ngSubmit)="handleSubmit()">
<div *ngIf="!exam.id; else edit" class="d-flex align-items-center">
<h4 *ngIf="!isImport">{{ 'artemisApp.examManagement.createExam' | artemisTranslate }}</h4>
<h4 *ngIf="isImport">{{ 'artemisApp.examManagement.importExam' | artemisTranslate }}</h4>
Expand Down Expand Up @@ -56,45 +56,61 @@ <h5 class="pb-1" jhiTranslate="artemisApp.examManagement.sections.configuration"
<hr />
<!-- Section exam conduction (release dates and working time) -->
<h5 class="pb-1" jhiTranslate="artemisApp.examManagement.sections.conduction">Exam Conduction</h5>
<div *ngIf="isOngoingExam" class="mb-3">
<span class="d-inline-flex" ngbTooltip="{{ 'artemisApp.examManagement.dateChange.message' | artemisTranslate }}">
<fa-icon [icon]="faExclamationTriangle" class="text-warning"></fa-icon>
<span class="fw-bold mx-1" jhiTranslate="artemisApp.examManagement.dateChange.label">
Changing the times of an active exam will disrupt the workflow of the students!
</span>
</span>
</div>
<ng-template #workingTimeConfirmationContent>
<div class="bg-light rounded-2 p-3">
<jhi-working-time-change *ngIf="oldWorkingTime && newWorkingTime" [oldWorkingTime]="oldWorkingTime" [newWorkingTime]="newWorkingTime" />
</div>
</ng-template>
<div class="row mb-3">
<div class="col-sm-4">
<jhi-date-time-picker
labelName="{{ 'artemisApp.examManagement.visibleDate' | artemisTranslate }}"
labelTooltip="{{ 'artemisApp.examManagement.visibleDateTooltip' | artemisTranslate }}"
[(ngModel)]="exam.visibleDate"
[error]="!isValidVisibleDate"
[startAt]="exam.visibleDate"
name="visibleDate"
id="visibleDate"
></jhi-date-time-picker>
/>
</div>
<div class="col-sm-4">
<jhi-date-time-picker
labelName="{{ 'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.startDate' | artemisTranslate }}"
labelTooltip="{{ 'artemisApp.examManagement.startDateTooltip' | artemisTranslate }}"
[(ngModel)]="exam.startDate"
(valueChange)="updateExamWorkingTime()"
[error]="!isValidStartDate"
(valueChange)="calculateMaxWorkingTime()"
[startAt]="exam.startDate || exam.visibleDate"
name="startDate"
id="startDate"
></jhi-date-time-picker>
/>
</div>
<div class="col-sm-4">
<jhi-date-time-picker
labelName="{{ 'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.endDate' | artemisTranslate }}"
labelTooltip="{{ 'artemisApp.examManagement.endDateTooltip' | artemisTranslate }}"
[(ngModel)]="exam.endDate"
(valueChange)="updateExamWorkingTime()"
[error]="!isValidEndDate"
(valueChange)="calculateMaxWorkingTime()"
[startAt]="exam.endDate || exam.startDate"
name="endDate"
id="endDate"
></jhi-date-time-picker>
/>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-6">
<label class="form-check-label" for="workingTimeInMinutes" jhiTranslate="artemisApp.examManagement.workingTime">Working Time</label>
<jhi-help-icon [text]="'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.workingTimeTooltip'"></jhi-help-icon>
<input *ngIf="!exam.testExam" readonly type="text" class="form-control" [value]="calculateWorkingTime" />
aplr marked this conversation as resolved.
Show resolved Hide resolved
<input *ngIf="!exam.testExam" readonly type="text" class="form-control" [value]="workingTimeInMinutes" />
<input
*ngIf="exam.testExam"
id="workingTimeInMinutes"
Expand All @@ -104,7 +120,6 @@ <h5 class="pb-1" jhiTranslate="artemisApp.examManagement.sections.conduction">Ex
[customMin]="1"
[customMax]="maxWorkingTimeInMinutes"
[(ngModel)]="workingTimeInMinutes"
(change)="convertWorkingTimeFromMinutesToSeconds($event)"
/>
</div>
<div class="col-sm-6">
Expand Down Expand Up @@ -230,7 +245,7 @@ <h5 class="pb-1" jhiTranslate="artemisApp.examManagement.sections.text">Exam Tex
<jhi-markdown-editor id="confirmationEndText" class="markdown-editor" [(markdown)]="exam.confirmationEndText" [enableResize]="true"></jhi-markdown-editor>
</div>
<div>
<button type="button" class="btn btn-secondary" (click)="previousState()">
<button type="button" class="btn btn-secondary" (click)="resetToPreviousState()">
<fa-icon [icon]="faBan"></fa-icon>&nbsp;<span jhiTranslate="entity.action.cancel"> Cancel</span>
</button>
<button id="save-exam" type="submit" [disabled]="editForm.form.invalid || !isValidConfiguration || isSaving" class="btn btn-primary">
Expand All @@ -239,7 +254,7 @@ <h5 class="pb-1" jhiTranslate="artemisApp.examManagement.sections.text">Exam Tex
<ng-template #renderedWarning>
<div>{{ 'artemisApp.examManagement.reviewDatesInvalidExplanation' | artemisTranslate }}</div>
</ng-template>
<span *ngIf="!isValidExamStudentReviewEnd" class="badge bg-warning" [ngbTooltip]="renderedWarning" tooltip-placement="right auto">
<span *ngIf="!isValidExamStudentReviewEnd" class="badge bg-warning" [ngbTooltip]="renderedWarning" placement="right auto">
<span jhiTranslate="artemisApp.quizExercise.edit.warning"></span>
</span>
</div>
Expand Down
Loading
Loading