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: Allow instructors to view unsubmitted exams and adjust summary layout #7588

Merged
merged 71 commits into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from 70 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
eb1a7e4
Prevent user from viewing unsubmitted exams
Nov 13, 2023
6bb4c41
Fixing path to translation
Nov 19, 2023
7d9f4f9
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Nov 20, 2023
332621a
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Nov 25, 2023
93aeeaf
Reverting UI changes that prevent the instructor to view an unsubmitt…
Nov 25, 2023
1d8355e
use isTestRun from loaded exam
Nov 25, 2023
b50c2f9
Improving code quality and allowing instructor to always view the gra…
Nov 25, 2023
4805e30
Make sure error message is no longer displayed
Nov 25, 2023
e610187
Adding indication in client to show that the exam was not submitted
Nov 25, 2023
f90f347
Moving general information info to the left in summary and changing p…
Nov 25, 2023
5f09586
Moving results table to the left and reducing whitespace
Nov 25, 2023
c9a88df
Fixing style of displayed grade
Nov 25, 2023
2c7081b
Displaying grading scale to the right
Nov 26, 2023
de59a94
Fixing responsiveness of grading keys
Nov 26, 2023
34a9b9f
Fixing position of grade display
Nov 26, 2023
4d060d8
Adding fallback calculation for achieved percentage
Nov 26, 2023
b2b9843
Fixing total percentage display
Nov 26, 2023
4a5e6ef
Adding javadoc
Nov 26, 2023
0b131a2
Adding fallback implementation for points aggregation
Nov 26, 2023
ba46c38
Prevent the titles of columns to wrap
Nov 26, 2023
b6ae4b7
Removing inline styles
Nov 26, 2023
718ff56
Fixing header structure a bit
Nov 26, 2023
8d62de4
Using bootstrap media queries instead of rebuilding them
Nov 26, 2023
23cb545
Revert changes in utils
Nov 26, 2023
259eb97
Instead of checking test run check for instructor
Nov 26, 2023
5a40945
Fixing teamscale test
Nov 26, 2023
9be9f19
Tests: adding test for fallback implementation of percentage implemen…
Nov 26, 2023
3e6ef4f
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Nov 27, 2023
44d44f1
Fixing server style issues
Nov 27, 2023
5eaa776
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Nov 28, 2023
b28aa48
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Dec 7, 2023
422f4aa
Incorporate code review, using redice instead of for each
florian-glombik Dec 7, 2023
545c09e
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Dec 10, 2023
79e99ab
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Dec 11, 2023
48fc602
Increase timeout, as test must now wait until re-configuration is pos…
florian-glombik Dec 11, 2023
2f09c6e
Move collapsed state to shared component
florian-glombik Dec 11, 2023
2bb73ea
Pass toggleCollapsed properly
florian-glombik Dec 11, 2023
ca51f0c
Renaming files and fixing turning icon
florian-glombik Dec 11, 2023
325d2ba
Remove not needed import
florian-glombik Dec 11, 2023
e1abac5
Add logic for collapsing sections
florian-glombik Dec 11, 2023
58d01c5
Make grading table section collapsible
florian-glombik Dec 11, 2023
7cf53c4
Adding grading key section
florian-glombik Dec 11, 2023
e6b301a
Adding bonus grading key section
florian-glombik Dec 11, 2023
d7b708e
Allow collapse action on the complete header
florian-glombik Dec 11, 2023
130dba9
Reduce the used with of the gradings keys
florian-glombik Dec 11, 2023
4fd2de8
Adjusting translations
florian-glombik Dec 11, 2023
4031f66
Adding margin to left
florian-glombik Dec 11, 2023
e896a59
Improve responsiveness
florian-glombik Dec 11, 2023
67041fa
Remove not needed css
florian-glombik Dec 11, 2023
d13be3f
Unify font usage
florian-glombik Dec 11, 2023
999fb1d
Center the grade details vertically
florian-glombik Dec 11, 2023
b743466
Fixing client tests
florian-glombik Dec 11, 2023
5742e55
Remove unused code
florian-glombik Dec 11, 2023
c18793b
Fixint client test
florian-glombik Dec 11, 2023
c3d8b43
Fixing E2E tests by increasing timeouts
florian-glombik Dec 11, 2023
aecc46b
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Dec 11, 2023
ec0047f
Reduce E2E test flakyness by increasing timeout
florian-glombik Dec 12, 2023
5525928
message
Dec 25, 2023
5f17d51
fixing merge mistakes
florian-glombik Dec 25, 2023
7affb22
fixing control flow
florian-glombik Dec 25, 2023
5e26e0b
fixing formatting
florian-glombik Dec 25, 2023
35a58d9
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Dec 29, 2023
f0ab4eb
adding logs
florian-glombik Dec 29, 2023
f5392f2
fixing displaying table and removing logs
florian-glombik Dec 29, 2023
eb48e24
fixing spelling of collapsible-content class
florian-glombik Dec 29, 2023
92e2196
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Dec 29, 2023
08328ab
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Dec 29, 2023
0aae373
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Dec 29, 2023
8b8c445
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Jan 2, 2024
2eaac3f
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Jan 6, 2024
0089ca7
Merge branch 'develop' into bugfix/exam/prevent-displaying-unsubmitte…
florian-glombik Jan 7, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import de.tum.in.www1.artemis.domain.iris.settings.*;
import de.tum.in.www1.artemis.domain.iris.settings.IrisCourseSettings;
import de.tum.in.www1.artemis.domain.iris.settings.IrisExerciseSettings;
import de.tum.in.www1.artemis.domain.iris.settings.IrisGlobalSettings;
import de.tum.in.www1.artemis.domain.iris.settings.IrisSettings;
import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException;

/**
Expand Down Expand Up @@ -38,6 +41,12 @@ default IrisGlobalSettings findGlobalSettingsElseThrow() {
""")
Optional<IrisCourseSettings> findCourseSettings(Long courseId);

/**
* Retrieves Iris exercise settings for a given exercise ID.
*
* @param exerciseId for which settings are to be retrieved.
* @return An Optional containing IrisExerciseSettings if found, otherwise empty.
*/
@Query("""
SELECT irisSettings
FROM IrisExerciseSettings irisSettings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,8 @@ private Map<Long, BonusSourceResultDTO> calculateExamScoresAsBonusSource(Long ex
StudentExam studentExam = studentExamRepository.findWithExercisesByUserIdAndExamId(targetUser.getId(), examId, IS_TEST_RUN)
.orElseThrow(() -> new EntityNotFoundException("No student exam found for examId " + examId + " and userId " + studentId));

StudentExamWithGradeDTO studentExamWithGradeDTO = getStudentExamGradesForSummaryAsStudent(targetUser, studentExam, IS_TEST_RUN);
StudentExamWithGradeDTO studentExamWithGradeDTO = getStudentExamGradesForSummary(targetUser, studentExam,
authorizationCheckService.isAtLeastInstructorInCourse(studentExam.getExam().getCourse(), targetUser));
var studentResult = studentExamWithGradeDTO.studentResult();
return Map.of(studentId, new BonusSourceResultDTO(studentResult.overallPointsAchieved(), studentResult.mostSeverePlagiarismVerdict(), null, null,
Boolean.TRUE.equals(studentResult.submitted())));
florian-glombik marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -443,20 +444,22 @@ private Map<Long, BonusSourceResultDTO> calculateExamScoresAsBonusSource(Long ex
* <p>
* See {@link StudentExamWithGradeDTO} for more explanation.
*
* @param targetUser the user who submitted the studentExam
* @param studentExam the student exam to be evaluated
* @param isTestRun set if an instructor executes a test run
* @param targetUser the user who submitted the studentExam
* @param studentExam the student exam to be evaluated
* @param accessingUserIsAtLeastInstructor is passed to decide the access (e.g. for test runs access will be needed regardless of submission or published dates)
* @return the student exam result with points and grade
*/
public StudentExamWithGradeDTO getStudentExamGradesForSummaryAsStudent(User targetUser, StudentExam studentExam, boolean isTestRun) {
public StudentExamWithGradeDTO getStudentExamGradesForSummary(User targetUser, StudentExam studentExam, boolean accessingUserIsAtLeastInstructor) {

loadQuizExercisesForStudentExam(studentExam);

boolean accessToSummaryAlwaysAllowed = studentExam.isTestRun() || accessingUserIsAtLeastInstructor;

// check that the studentExam has been submitted, otherwise /student-exams/conduction should be used
if (!Boolean.TRUE.equals(studentExam.isSubmitted()) && !isTestRun) {
if (!Boolean.TRUE.equals(studentExam.isSubmitted()) && !accessToSummaryAlwaysAllowed) {
throw new AccessForbiddenException(NOT_ALLOWED_TO_ACCESS_THE_GRADE_SUMMARY + "which was NOT submitted!");
}
if (!studentExam.areResultsPublishedYet() && !isTestRun) {
if (!studentExam.areResultsPublishedYet() && !accessToSummaryAlwaysAllowed) {
throw new AccessForbiddenException(NOT_ALLOWED_TO_ACCESS_THE_GRADE_SUMMARY + "before the release date of results");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,12 +537,8 @@ public ResponseEntity<StudentExamWithGradeDTO> getStudentExamGradesForSummary(@P
if (!isAtLeastInstructor && !currentUser.getId().equals(targetUser.getId())) {
throw new AccessForbiddenException("Current user cannot access grade info for target user");
}
boolean nonInstructorSetsTestRunToTrue = !isAtLeastInstructor && isTestRun;
if (nonInstructorSetsTestRunToTrue) {
throw new AccessForbiddenException("Test runs are only accessible for instructors");
}

StudentExamWithGradeDTO studentExamWithGradeDTO = examService.getStudentExamGradesForSummaryAsStudent(targetUser, studentExam, isTestRun);
StudentExamWithGradeDTO studentExamWithGradeDTO = examService.getStudentExamGradesForSummary(targetUser, studentExam, isAtLeastInstructor);

log.info("getStudentExamGradesForSummary done in {}ms for {} exercises for target user {} by caller user {}", System.currentTimeMillis() - start,
studentExam.getExercises().size(), targetUser.getLogin(), currentUser.getLogin());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ <h3 id="general-information-title">
{{ 'artemisApp.exam.examSummary.generalInformation' | artemisTranslate }}
</h3>
}
<div class="mt-3 mb-4 text-center">
<div class="ml-4 mt-3 mb-4">
<table class="table table-borderless mx-auto d-block">
<tbody>
@if (isTestExam) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
table {
width: fit-content;
}

th,
td {
text-align: start;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="question card">
<div class="card-header d-flex align-items-center justify-content-between" (click)="toggleCollapse()">
<ng-content select=".header"></ng-content>
<button class="btn rotate-icon" [class.rotated]="!isCardContentCollapsed" [attr.aria-expanded]="isCardContentCollapsed" [attr.aria-controls]="isCardContentCollapsed">
<fa-icon size="2x" [icon]="faAngleRight"></fa-icon>
</button>
</div>

<div class="card-body question-card-body" [ngbCollapse]="isCardContentCollapsed">
<ng-content select=".collapsible-content"></ng-content>
</div>
</div>
florian-glombik marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Component, Input } from '@angular/core';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';

@Component({
selector: 'jhi-collapsible-card',
templateUrl: './collapsible-card.component.html',
styleUrls: [
'../../../course/manage/course-exercise-card.component.scss',
'../../../exercises/quiz/shared/quiz.scss',
'exam-result-summary.component.scss',
'collapsible-card.component.scss',
],
})
export class CollapsibleCardComponent {
@Input() isCardContentCollapsed: boolean;
@Input() toggleCollapse: () => void;

faAngleRight = faAngleRight;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ <h2>
</button>
</h2>
</div>
@if (studentExam && !studentExam.submitted) {
<div class="alert alert-danger text-center">
<strong>
{{ 'artemisApp.exam.examSummary.youAreViewingAnUnsubmittedExam' | artemisTranslate }}
</strong>
</div>
}
@if (studentExam?.exam) {
<jhi-exam-general-information
[exam]="studentExam.exam!"
Expand Down Expand Up @@ -38,9 +45,11 @@ <h3>
</h3>
@for (exercise of studentExam?.exercises; track exercise; let i = $index) {
<div [id]="'exercise-' + exercise.id">
<div class="question card">
<jhi-result-summary-exercise-card-header [index]="i" [exercise]="exercise" [exerciseInfo]="exerciseInfos[exercise.id!]" [resultsPublished]="resultsArePublished" />
<div class="card-body question-card-body" [ngbCollapse]="exerciseInfos[exercise.id!].isCollapsed">
<jhi-collapsible-card [isCardContentCollapsed]="exerciseInfos[exercise.id!].isCollapsed" [toggleCollapse]="toggleCollapseExercise(exerciseInfos[exercise.id!])">
<div class="header">
<jhi-result-summary-exercise-card-header [index]="i" [exercise]="exercise" [exerciseInfo]="exerciseInfos[exercise.id!]" [resultsPublished]="resultsArePublished" />
</div>
<div class="collapsible-content">
<div class="clearfix">
<span class="exercise-buttons">
@if (plagiarismCaseInfos[exercise.id!]) {
Expand Down Expand Up @@ -183,7 +192,7 @@ <h3>
}
}
</div>
</div>
</jhi-collapsible-card>
</div>
}
<button class="btn btn-light mx-auto d-block" (click)="scrollToOverviewOrTop()" id="back-to-overview-button">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { captureException } from '@sentry/angular-ivy';
import { AlertService } from 'app/core/util/alert.service';
import { ProgrammingExercise } from 'app/entities/programming-exercise.model';
import { isExamResultPublished } from 'app/exam/participate/exam.utils';
import { Course } from 'app/entities/course.model';

export type ResultSummaryExerciseInfo = {
icon: IconProp;
Expand Down Expand Up @@ -345,7 +346,7 @@ export class ExamResultSummaryComponent implements OnInit {
icon: getIcon(exercise.type),
isCollapsed: false,
achievedPoints: this.getPointsByExerciseIdFromExam(exercise.id, studentExamWithGrade),
achievedPercentage: this.getAchievedPercentageByExerciseId(exercise.id),
achievedPercentage: this.getAchievedPercentageByExerciseId(exercise.id, studentExamWithGrade),
colorClass: textColorClass,
resultIconClass: resultIconClass,

Expand Down Expand Up @@ -406,23 +407,46 @@ export class ExamResultSummaryComponent implements OnInit {
this.exerciseInfos[exerciseId].displayExampleSolution = !this.exerciseInfos[exerciseId].displayExampleSolution;
}

getAchievedPercentageByExerciseId(exerciseId?: number): number | undefined {
const result = this.getExerciseResultByExerciseId(exerciseId);
if (result === undefined) {
return undefined;
private calculateAchievedPercentageFromScoreAndMaxPoints(achievedPoints?: number, maxScore?: number, course?: Course) {
const canCalculatePercentage = maxScore !== undefined && achievedPoints !== undefined;
if (canCalculatePercentage) {
return roundScorePercentSpecifiedByCourseSettings(achievedPoints! / maxScore, course);
}

const course = this.studentExamGradeInfoDTO.studentExam?.exam?.course;
return undefined;
}

private getAchievedPercentageFromResult(result: ExerciseResult, course?: Course) {
if (result.achievedScore !== undefined) {
return roundScorePercentSpecifiedByCourseSettings(result.achievedScore / 100, course);
}

const canCalculatePercentage = result.maxScore && result.achievedPoints !== undefined;
if (canCalculatePercentage) {
return roundScorePercentSpecifiedByCourseSettings(result.achievedPoints! / result.maxScore, course);
return this.calculateAchievedPercentageFromScoreAndMaxPoints(result.achievedPoints, result.maxScore, course);
}

/**
* This should only be needed when unsubmitted exercises are viewed, otherwise the results should be set
*/
private getAchievedPercentageFromExamResults(exerciseId?: number, studentExamWithGrade?: StudentExamWithGradeDTO | undefined, course?: Course) {
if (exerciseId === undefined) {
return undefined;
}

return undefined;
const maxPoints = studentExamWithGrade?.studentExam?.exercises?.find((exercise) => exercise.id === exerciseId)?.maxPoints;
const achievedPoints = this.getPointsByExerciseIdFromExam(exerciseId, studentExamWithGrade);

return this.calculateAchievedPercentageFromScoreAndMaxPoints(achievedPoints, maxPoints, course);
}

getAchievedPercentageByExerciseId(exerciseId?: number, studentExamWithGrade?: StudentExamWithGradeDTO | undefined): number | undefined {
const result = this.getExerciseResultByExerciseId(exerciseId);
const course = this.studentExamGradeInfoDTO?.studentExam?.exam?.course;

if (result === undefined) {
return this.getAchievedPercentageFromExamResults(exerciseId, studentExamWithGrade, course);
}

return this.getAchievedPercentageFromResult(result, course);
}

getTextColorAndIconClassByExercise(exercise: Exercise) {
Expand All @@ -439,5 +463,9 @@ export class ExamResultSummaryComponent implements OnInit {
};
}

toggleCollapseExercise(exerciseInfo: ResultSummaryExerciseInfo) {
return () => (exerciseInfo!.isCollapsed = !exerciseInfo!.isCollapsed);
}

protected readonly getIcon = getIcon;
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { ArtemisModelingParticipationModule } from 'app/exercises/modeling/parti
import { ArtemisTextParticipationModule } from 'app/exercises/text/participate/text-participation.module';
import { ArtemisFileUploadParticipationModule } from 'app/exercises/file-upload/participate/file-upload-participation.module';
import { ArtemisFeedbackModule } from 'app/exercises/shared/feedback/feedback.module';
import { CollapsibleCardComponent } from 'app/exam/participate/summary/collapsible-card.component';

@NgModule({
imports: [
Expand Down Expand Up @@ -69,6 +70,7 @@ import { ArtemisFeedbackModule } from 'app/exercises/shared/feedback/feedback.mo
ExamResultSummaryExerciseCardHeaderComponent,
TestRunRibbonComponent,
ExampleSolutionComponent,
CollapsibleCardComponent,
],
exports: [ExamResultSummaryComponent, ExamGeneralInformationComponent, TestRunRibbonComponent],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="card-header d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<div class="d-flex align-items-center">
<h5 class="mb-0" [id]="'exercise-group-title-' + exercise.id">
#{{ index + 1 }} &nbsp;
Expand Down Expand Up @@ -31,14 +31,4 @@ <h5 class="mb-0" [id]="'exercise-group-title-' + exercise.id">
<span class="badge bg-danger" jhiTranslate="artemisApp.assessment.assessmentIllegalSubmission"> Warning: You are viewing an illegal submission. </span>
</div>
}
<button
id="{{ 'toggleCollapseExerciseButton-' + exercise.id! }}"
class="btn rotate-icon"
[class.rotated]="!exerciseInfo?.isCollapsed"
(click)="toggleCollapseExercise()"
[attr.aria-expanded]="exerciseInfo?.isCollapsed"
[attr.aria-controls]="exerciseInfo?.isCollapsed"
>
<fa-icon size="2x" [icon]="faAngleRight"></fa-icon>
</button>
</div>
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
import { Component, Input } from '@angular/core';
import { Exercise } from 'app/entities/exercise.model';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { ResultSummaryExerciseInfo } from 'app/exam/participate/summary/exam-result-summary.component';
import { SubmissionType } from 'app/entities/submission.model';

@Component({
selector: 'jhi-result-summary-exercise-card-header',
templateUrl: './exam-result-summary-exercise-card-header.component.html',
styleUrls: ['./exam-result-summary-exercise-card-header.component.scss'],
})
export class ExamResultSummaryExerciseCardHeaderComponent {
@Input() index: number;
@Input() exercise: Exercise;
@Input() exerciseInfo?: ResultSummaryExerciseInfo;
@Input() resultsPublished: boolean;

faAngleRight = faAngleRight;

toggleCollapseExercise() {
this.exerciseInfo!.isCollapsed = !this.exerciseInfo!.isCollapsed;
}

readonly SUBMISSION_TYPE_ILLEGAL = SubmissionType.ILLEGAL;
}
Loading
Loading