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 12 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
92 changes: 69 additions & 23 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 @@ -12,7 +12,12 @@
import java.security.Principal;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
aplr marked this conversation as resolved.
Show resolved Hide resolved
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import javax.validation.constraints.NotNull;
import javax.ws.rs.BadRequestException;
Expand All @@ -25,32 +30,63 @@
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.*;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import de.tum.in.www1.artemis.config.Constants;
import de.tum.in.www1.artemis.domain.*;
import de.tum.in.www1.artemis.domain.Course;
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.domain.ProgrammingExercise;
import de.tum.in.www1.artemis.domain.Submission;
import de.tum.in.www1.artemis.domain.User;
import de.tum.in.www1.artemis.domain.exam.Exam;
import de.tum.in.www1.artemis.domain.exam.ExerciseGroup;
import de.tum.in.www1.artemis.domain.exam.StudentExam;
import de.tum.in.www1.artemis.domain.metis.conversation.Channel;
import de.tum.in.www1.artemis.domain.participation.TutorParticipation;
import de.tum.in.www1.artemis.repository.*;
import de.tum.in.www1.artemis.repository.CourseRepository;
import de.tum.in.www1.artemis.repository.CustomAuditEventRepository;
import de.tum.in.www1.artemis.repository.ExamRepository;
import de.tum.in.www1.artemis.repository.ExerciseRepository;
import de.tum.in.www1.artemis.repository.StudentExamRepository;
import de.tum.in.www1.artemis.repository.TutorParticipationRepository;
import de.tum.in.www1.artemis.repository.UserRepository;
import de.tum.in.www1.artemis.repository.metis.conversation.ChannelRepository;
import de.tum.in.www1.artemis.security.Role;
import de.tum.in.www1.artemis.security.annotations.*;
import de.tum.in.www1.artemis.service.*;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor;
import de.tum.in.www1.artemis.service.AssessmentDashboardService;
import de.tum.in.www1.artemis.service.AuthorizationCheckService;
import de.tum.in.www1.artemis.service.ProfileService;
import de.tum.in.www1.artemis.service.SubmissionService;
import de.tum.in.www1.artemis.service.dto.StudentDTO;
import de.tum.in.www1.artemis.service.exam.*;
import de.tum.in.www1.artemis.service.exam.ExamAccessService;
import de.tum.in.www1.artemis.service.exam.ExamDateService;
import de.tum.in.www1.artemis.service.exam.ExamDeletionService;
import de.tum.in.www1.artemis.service.exam.ExamImportService;
import de.tum.in.www1.artemis.service.exam.ExamLiveEventsService;
import de.tum.in.www1.artemis.service.exam.ExamRegistrationService;
import de.tum.in.www1.artemis.service.exam.ExamService;
import de.tum.in.www1.artemis.service.exam.ExamSessionService;
import de.tum.in.www1.artemis.service.feature.Feature;
import de.tum.in.www1.artemis.service.feature.FeatureToggle;
import de.tum.in.www1.artemis.service.messaging.InstanceMessageSendService;
import de.tum.in.www1.artemis.service.metis.conversation.ChannelService;
import de.tum.in.www1.artemis.service.util.TimeLogUtil;
import de.tum.in.www1.artemis.web.rest.dto.*;
import de.tum.in.www1.artemis.web.rest.dto.examevent.ExamWideAnnouncementEventDTO;
import de.tum.in.www1.artemis.web.rest.errors.*;
import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenAlertException;
import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException;
import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException;
import de.tum.in.www1.artemis.web.rest.errors.ConflictException;
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;
import de.tum.in.www1.artemis.web.rest.util.HeaderUtil;
import io.swagger.annotations.ApiParam;
import tech.jhipster.web.util.PaginationUtil;
Expand Down Expand Up @@ -191,6 +227,7 @@ public ResponseEntity<Exam> createExam(@PathVariable Long courseId, @RequestBody
@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);
}
Expand All @@ -216,21 +253,23 @@ public ResponseEntity<Exam> updateExam(@PathVariable Long courseId, @RequestBody

Exam savedExam = examRepository.save(updatedExam);

// NOTE: We have to get exercise groups as we need them for re-scheduling
aplr marked this conversation as resolved.
Show resolved Hide resolved
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())) {
// TODO: check if there are any problems with that - i.e. TZ issues, only changing working time in UI, ...
aplr marked this conversation as resolved.
Show resolved Hide resolved
int workingTimeChange = savedExam.getDuration() - originalExam.getDuration();
aplr marked this conversation as resolved.
Show resolved Hide resolved
updateStudentExamsAndRescheduleExercises(examWithExercises, workingTimeChange);
}

if (updatedChannel != null) {
Expand Down Expand Up @@ -259,21 +298,28 @@ public ResponseEntity<Exam> updateExamWorkingTime(@PathVariable Long courseId, @
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();

// 1. Update the end date & working time of the exam
exam.setEndDate(exam.getEndDate().plusSeconds(workingTimeChange));
exam.setWorkingTime(exam.getWorkingTime() + workingTimeChange);
examRepository.save(exam);

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

return ResponseEntity.ok(exam);
}

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

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 +334,9 @@ public ResponseEntity<Exam> updateExamWorkingTime(@PathVariable Long courseId, @
int adjustedWorkingTime = Math.max(newNormalWorkingTime + timeAdjustment, 0);
studentExam.setWorkingTime(adjustedWorkingTime);
}
// TODO: probably batch these updates?
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 +348,10 @@ public ResponseEntity<Exam> updateExamWorkingTime(@PathVariable Long courseId, @
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,26 @@ 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';

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

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

if (courseId) {
return this.courseManagementService.find(courseId).pipe(
filter((response) => response.ok),
map((response) => response.body!),
);
}

return of(new Course());
aplr marked this conversation as resolved.
Show resolved Hide resolved
}
}

@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
@@ -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,6 +56,14 @@ <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="row mb-3">
<div class="col d-flex">
<fa-icon [icon]="faExclamationTriangle" class="text-warning" ngbTooltip="{{ 'artemisApp.examManagement.dateChange.warning' | artemisTranslate }}"></fa-icon>
<h6 class="mt-1 mx-1" jhiTranslate="artemisApp.examManagement.dateChange.title">
Changing the times of an active exam will disrupt the workflow of the students!
</h6>
aplr marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4">
<jhi-date-time-picker
Expand All @@ -72,8 +80,9 @@ <h5 class="pb-1" jhiTranslate="artemisApp.examManagement.sections.conduction">Ex
labelName="{{ 'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.startDate' | artemisTranslate }}"
labelTooltip="{{ 'artemisApp.examManagement.startDateTooltip' | artemisTranslate }}"
[(ngModel)]="exam.startDate"
(valueChange)="calculateWorkingTime()"
[error]="!isValidStartDate"
(valueChange)="calculateMaxWorkingTime()"
[startAt]="exam.visibleDate"
name="startDate"
id="startDate"
></jhi-date-time-picker>
Expand All @@ -83,8 +92,9 @@ <h5 class="pb-1" jhiTranslate="artemisApp.examManagement.sections.conduction">Ex
labelName="{{ 'artemisApp.examManagement' + (exam.testExam ? '.testExam' : '') + '.endDate' | artemisTranslate }}"
labelTooltip="{{ 'artemisApp.examManagement.endDateTooltip' | artemisTranslate }}"
[(ngModel)]="exam.endDate"
(valueChange)="calculateWorkingTime()"
[error]="!isValidEndDate"
(valueChange)="calculateMaxWorkingTime()"
[startAt]="exam.startDate"
name="endDate"
id="endDate"
></jhi-date-time-picker>
Expand All @@ -94,7 +104,7 @@ <h5 class="pb-1" jhiTranslate="artemisApp.examManagement.sections.conduction">Ex
<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 +114,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 +239,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 +248,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