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: Fix problems with highlighting of the differences in the problem statement #8461

Merged
merged 25 commits into from
Jun 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6f7fadb
fix problems with highlighting of differences
coolchock May 9, 2024
4fa380f
Merge branch 'refs/heads/develop' into bugfix/exam-mode/highlighting-…
coolchock May 9, 2024
8fb09e2
update highlighter and submission components
coolchock May 10, 2024
ed85b8d
remove redundant else block and check for undefined
coolchock May 10, 2024
ee42419
update problem statement
coolchock May 10, 2024
99df37b
rebuild if statement
coolchock May 11, 2024
d2804f3
adjust tests
coolchock May 11, 2024
e42d578
adjust client tests
coolchock May 11, 2024
76a3ad3
fix server test
coolchock May 11, 2024
98e79b8
Merge branch 'refs/heads/develop' into bugfix/exam-mode/highlighting-…
coolchock May 12, 2024
ce037c9
Merge branch 'refs/heads/develop' into bugfix/exam-mode/highlighting-…
coolchock May 29, 2024
ca0aee8
Merge branch 'refs/heads/develop' into highlighting-of-differences
coolchock Jun 21, 2024
fa3d437
Merge branch 'refs/heads/develop' into highlighting-of-differences
coolchock Jun 23, 2024
ea909ed
use innerHTML in feedback component
coolchock Jun 23, 2024
fc4f4db
use ADD_TAGS instead of ALLOWED_TAGS
coolchock Jun 23, 2024
97cda57
rebuild instruction component
coolchock Jun 23, 2024
f282182
rebuild extension
coolchock Jun 23, 2024
83aef81
refactor the code
coolchock Jun 23, 2024
60b41dd
adjust tests
coolchock Jun 23, 2024
0c2e70c
prettier
coolchock Jun 23, 2024
63120b3
fix tests
coolchock Jun 23, 2024
edae665
Merge branch 'develop' into bugfix/exam-mode/highlighting-of-differences
pzdr7 Jun 26, 2024
64fa425
Merge branch 'refs/heads/develop' into bugfix/exam-mode/highlighting-…
coolchock Jun 30, 2024
efeabfc
revert changes for test case names and ids
coolchock Jun 30, 2024
9231c0e
revert changes for test case names and ids
coolchock Jun 30, 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
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"dompurify": "3.1.5",
"export-to-csv": "1.3.0",
"franc-min": "6.2.0",
"html-diff-ts": "1.4.2",
"interactjs": "1.10.27",
"ismobilejs-es5": "0.0.1",
"js-video-url-parser": "0.5.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
<button
id="highlightDiffButton"
class="btn py-0 px-2 mt-0 mb-0 ms-2"
[hidden]="previousProblemStatementUpdate === undefined || isHidden"
(click)="toggleHighlightedProblemStatement($event)"
>
<button id="highlightDiffButton" class="btn py-0 px-2 mt-0 mb-0 ms-2" [hidden]="isHidden" (click)="toggleHighlightedProblemStatement($event)">
{{
showHighlightedDifferences ? ('artemisApp.exam.problemStatementUpdate.showNew' | artemisTranslate) : ('artemisApp.exam.problemStatementUpdate.showDiff' | artemisTranslate)
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angu
import { Subscription } from 'rxjs';
import { ExamExerciseUpdateService } from 'app/exam/manage/exam-exercise-update.service';
import { Exercise, ExerciseType } from 'app/entities/exercise.model';
import { Diff, DiffMatchPatch, DiffOperation } from 'diff-match-patch-typescript';
import { htmlForMarkdown } from 'app/shared/util/markdown.conversion.util';
import diff from 'html-diff-ts';

@Component({
selector: 'jhi-exam-exercise-update-highlighter',
Expand All @@ -12,11 +13,12 @@ import { Diff, DiffMatchPatch, DiffOperation } from 'diff-match-patch-typescript
export class ExamExerciseUpdateHighlighterComponent implements OnInit, OnDestroy {
subscriptionToLiveExamExerciseUpdates: Subscription;
themeSubscription: Subscription;
previousProblemStatementUpdate?: string;
updatedProblemStatementWithHighlightedDifferences: string;
updatedProblemStatementHTML: string;
updatedProblemStatementWithHighlightedDifferencesHTML: string;
outdatedProblemStatement: string;
updatedProblemStatement: string;
showHighlightedDifferences = true;
isHidden = false;
isHidden = true;
@Input() exercise: Exercise;

@Output() problemStatementUpdateEvent: EventEmitter<string> = new EventEmitter<string>();
Expand All @@ -27,10 +29,6 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit, OnDestroy
this.subscriptionToLiveExamExerciseUpdates = this.examExerciseUpdateService.currentExerciseIdAndProblemStatement.subscribe((update) => {
if (update) {
this.updateExerciseProblemStatementById(update.exerciseId, update.problemStatement);
this.isHidden = false;
} else {
// No update so hide the component
this.isHidden = true;
}
});
}
Expand All @@ -47,13 +45,14 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit, OnDestroy
toggleHighlightedProblemStatement(event: MouseEvent): void {
// prevents the jhi-resizeable-container from collapsing the right panel on a button click
event.stopPropagation();
let problemStatementToEmit;
if (this.showHighlightedDifferences) {
this.exercise.problemStatement = this.updatedProblemStatement;
problemStatementToEmit = this.updatedProblemStatementHTML;
} else {
this.exercise.problemStatement = this.updatedProblemStatementWithHighlightedDifferences;
problemStatementToEmit = this.updatedProblemStatementWithHighlightedDifferencesHTML;
}
this.showHighlightedDifferences = !this.showHighlightedDifferences;
this.problemStatementUpdateEvent.emit(this.exercise.problemStatement);
this.problemStatementUpdateEvent.emit(problemStatementToEmit);
}

/**
Expand All @@ -64,109 +63,27 @@ export class ExamExerciseUpdateHighlighterComponent implements OnInit, OnDestroy
*/
updateExerciseProblemStatementById(exerciseId: number, updatedProblemStatement: string) {
if (updatedProblemStatement != undefined && exerciseId === this.exercise.id) {
this.outdatedProblemStatement = this.exercise.problemStatement!;
this.updatedProblemStatement = updatedProblemStatement;
this.exercise.problemStatement = this.highlightProblemStatementDifferences();
this.exercise.problemStatement = updatedProblemStatement;
this.showHighlightedDifferences = true;
// Highlighting of the changes in the problem statement of a programming exercise id handled
// in ProgrammingExerciseInstructionComponent
if (this.exercise.type !== ExerciseType.PROGRAMMING) {
this.highlightProblemStatementDifferences();
}
this.isHidden = false;
this.problemStatementUpdateEvent.emit(this.updatedProblemStatementWithHighlightedDifferencesHTML);
}
this.problemStatementUpdateEvent.emit(this.exercise.problemStatement);
}

/**
* Computes the difference between the old and new (updated) problem statement and displays this difference.
*/
highlightProblemStatementDifferences() {
if (!this.updatedProblemStatement) {
return;
}

this.showHighlightedDifferences = true;

// creates the diffMatchPatch library object to be able to modify strings
const dmp = new DiffMatchPatch();
let outdatedProblemStatement: string;

// checks if first update i.e. no highlight
if (!this.previousProblemStatementUpdate) {
outdatedProblemStatement = this.exercise.problemStatement!;
// else use previousProblemStatementUpdate as new outdatedProblemStatement to avoid inserted HTML elements
} else {
outdatedProblemStatement = this.previousProblemStatementUpdate;
}

this.previousProblemStatementUpdate = this.updatedProblemStatement;
let removedDiagrams: string[] = [];
let diff: Diff[];
if (this.exercise.type === ExerciseType.PROGRAMMING) {
const updatedProblemStatementAndRemovedDiagrams = this.removeAnyPlantUmlDiagramsInProblemStatement(this.updatedProblemStatement);
const outdatedProblemStatementAndRemovedDiagrams = this.removeAnyPlantUmlDiagramsInProblemStatement(outdatedProblemStatement);
const updatedProblemStatementWithoutDiagrams = updatedProblemStatementAndRemovedDiagrams.problemStatementWithoutPlantUmlDiagrams;
const outdatedProblemStatementWithoutDiagrams = outdatedProblemStatementAndRemovedDiagrams.problemStatementWithoutPlantUmlDiagrams;
removedDiagrams = updatedProblemStatementAndRemovedDiagrams.removedDiagrams;
diff = dmp.diff_main(outdatedProblemStatementWithoutDiagrams!, updatedProblemStatementWithoutDiagrams);
} else {
diff = dmp.diff_main(outdatedProblemStatement!, this.updatedProblemStatement);
}
// finds the initial difference then cleans the text with added html & css elements
dmp.diff_cleanupEfficiency(diff);
this.updatedProblemStatementWithHighlightedDifferences = this.diffPrettyHtml(diff);

if (this.exercise.type === ExerciseType.PROGRAMMING) {
this.addPlantUmlToProblemStatementWithDiffHighlightAgain(removedDiagrams);
}
return this.updatedProblemStatementWithHighlightedDifferences;
}

private addPlantUmlToProblemStatementWithDiffHighlightAgain(removedDiagrams: string[]) {
removedDiagrams.forEach((text) => {
this.updatedProblemStatementWithHighlightedDifferences = this.updatedProblemStatementWithHighlightedDifferences.replace('@startuml', '@startuml\n' + text + '\n');
});
}

private removeAnyPlantUmlDiagramsInProblemStatement(problemStatement: string): {
problemStatementWithoutPlantUmlDiagrams: string;
removedDiagrams: string[];
} {
// Regular expression to match content between @startuml and @enduml
const plantUmlSequenceRegex = /@startuml([\s\S]*?)@enduml/g;
const removedDiagrams: string[] = [];
const problemStatementWithoutPlantUmlDiagrams = problemStatement.replace(plantUmlSequenceRegex, (match, content) => {
removedDiagrams.push(content);
// we have to keep the markers, otherwise we cannot add the diagrams back later
return '@startuml\n@enduml';
});
return {
problemStatementWithoutPlantUmlDiagrams,
removedDiagrams,
};
}

/**
* Convert a diff array into a pretty HTML report.
* Keeps markdown styling intact (not like the original method)
* Modified diff_prettHtml() method from DiffMatchPatch
* The original library method is intended to be modified
* for more info: https://www.npmjs.com/package/diff-match-patch,
* https://github.com/google/diff-match-patch/blob/master/javascript/diff_match_patch_uncompressed.js
*
* @param diffs Array of diff tuples. (from DiffMatchPatch)
* @return the HTML representation as string with markdown intact.
*/
private diffPrettyHtml(diffs: Diff[]): string {
const html: any[] = [];
diffs.forEach((diff: Diff, index: number) => {
const op = diffs[index][0]; // Operation (insert, delete, equal)
const text = diffs[index][1]; // Text of change.
switch (op) {
case DiffOperation.DIFF_INSERT:
html[index] = '<ins class="bg-success" ">' + text + '</ins>';
break;
case DiffOperation.DIFF_DELETE:
html[index] = '<del class="bg-danger">' + text + '</del>';
break;
case DiffOperation.DIFF_EQUAL:
html[index] = text;
break;
}
});
return html.join('');
const outdatedProblemStatementHTML = htmlForMarkdown(this.outdatedProblemStatement);
const updatedProblemStatementHTML = htmlForMarkdown(this.updatedProblemStatement);
this.updatedProblemStatementHTML = updatedProblemStatementHTML;
this.updatedProblemStatementWithHighlightedDifferencesHTML = diff(outdatedProblemStatementHTML, updatedProblemStatementHTML);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ <h6>
@if (exercise) {
<jhi-exam-exercise-update-highlighter [exercise]="exercise" (problemStatementUpdateEvent)="updateProblemStatement($event)" />
}
@if (exercise!.problemStatement) {
@if (problemStatementHtml) {
<p class="mb-3 markdown-preview">
<span [innerHTML]="exercise!.problemStatement! | htmlForMarkdown"></span>
<span [innerHTML]="problemStatementHtml"></span>
</p>
}
</ng-container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Exercise, ExerciseType, IncludedInOverallScore } from 'app/entities/exe
import { Submission } from 'app/entities/submission.model';
import { faListAlt } from '@fortawesome/free-regular-svg-icons';
import { SubmissionVersion } from 'app/entities/submission-version.model';
import { htmlForMarkdown } from 'app/shared/util/markdown.conversion.util';

@Component({
selector: 'jhi-file-upload-submission-exam',
Expand All @@ -35,6 +36,7 @@ export class FileUploadExamSubmissionComponent extends ExamSubmissionComponent i
studentSubmission: FileUploadSubmission;
@Input()
exercise: FileUploadExercise;
problemStatementHtml: string;

submittedFileName: string;
submittedFileExtension: string;
Expand Down Expand Up @@ -69,15 +71,16 @@ export class FileUploadExamSubmissionComponent extends ExamSubmissionComponent i
*/
ngOnInit() {
// show submission answers in UI
this.problemStatementHtml = htmlForMarkdown(this.exercise?.problemStatement);
this.updateViewFromSubmission();
}

/**
* Updates the problem statement of the currently loaded file upload exercise which is part of the user's student exam.
* @param newProblemStatement is the updated problem statement that should be displayed to the user.
* Updates the problem statement html of the currently loaded file upload exercise which is part of the user's student exam.
* @param newProblemStatementHtml is the updated problem statement html that should be displayed to the user.
*/
updateProblemStatement(newProblemStatement: string): void {
this.exercise.problemStatement = newProblemStatement;
updateProblemStatement(newProblemStatementHtml: string): void {
this.problemStatementHtml = newProblemStatementHtml;
this.changeDetectorReference.detectChanges();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ <h3 class="text-align-left fw-normal">
@if (exercise) {
<jhi-exam-exercise-update-highlighter [exercise]="exercise" (problemStatementUpdateEvent)="updateProblemStatement($event)" />
}
@if (exercise!.problemStatement) {
@if (problemStatementHtml) {
<p class="mb-3 markdown-preview">
<span [innerHTML]="exercise!.problemStatement! | htmlForMarkdown"></span>
<span [innerHTML]="problemStatementHtml"></span>
</p>
}
</ng-container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Submission } from 'app/entities/submission.model';
import { Exercise, ExerciseType, IncludedInOverallScore } from 'app/entities/exercise.model';
import { faListAlt } from '@fortawesome/free-regular-svg-icons';
import { SubmissionVersion } from 'app/entities/submission-version.model';
import { htmlForMarkdown } from 'app/shared/util/markdown.conversion.util';

@Component({
selector: 'jhi-modeling-submission-exam',
Expand All @@ -27,6 +28,7 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp
// IMPORTANT: this reference must be contained in this.studentParticipation.submissions[0] otherwise the parent component will not be able to react to changes
@Input()
studentSubmission: ModelingSubmission;
problemStatementHtml: string;

@Input()
exercise: ModelingExercise;
Expand All @@ -45,15 +47,16 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp

ngOnInit(): void {
// show submission answers in UI
this.problemStatementHtml = htmlForMarkdown(this.exercise?.problemStatement);
this.updateViewFromSubmission();
}

/**
* Updates the problem statement of the currently loaded modeling exercise which is part of the user's student exam.
* @param newProblemStatement is the updated problem statement that should be displayed to the user.
* Updates the problem statement html of the currently loaded modeling exercise which is part of the user's student exam.
* @param newProblemStatementHtml is the updated problem statement html that should be displayed to the user.
*/
updateProblemStatement(newProblemStatement: string): void {
this.exercise.problemStatement = newProblemStatement;
updateProblemStatement(newProblemStatementHtml: string): void {
this.problemStatementHtml = newProblemStatementHtml;
this.changeDetectorReference.detectChanges();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ <h3 class="text-align-left fw-normal">
@if (exercise) {
<jhi-exam-exercise-update-highlighter [exercise]="exercise" (problemStatementUpdateEvent)="updateProblemStatement($event)" />
}
@if (exercise.problemStatement) {
@if (problemStatementHtml) {
<p class="markdown-preview mb-3">
<span [innerHTML]="exercise.problemStatement! | htmlForMarkdown"></span>
<span [innerHTML]="problemStatementHtml"></span>
</p>
}
</ng-container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Submission } from 'app/entities/submission.model';
import { faListAlt } from '@fortawesome/free-regular-svg-icons';
import { MAX_SUBMISSION_TEXT_LENGTH } from 'app/shared/constants/input.constants';
import { SubmissionVersion } from 'app/entities/submission-version.model';
import { htmlForMarkdown } from 'app/shared/util/markdown.conversion.util';

@Component({
selector: 'jhi-text-editor-exam',
Expand All @@ -30,6 +31,7 @@ export class TextExamSubmissionComponent extends ExamSubmissionComponent impleme

// answer represents the view state
answer: string;
problemStatementHtml: string;
private textEditorInput = new Subject<string>();

// Icons
Expand All @@ -45,6 +47,7 @@ export class TextExamSubmissionComponent extends ExamSubmissionComponent impleme

ngOnInit(): void {
// show submission answers in UI
this.problemStatementHtml = htmlForMarkdown(this.exercise?.problemStatement);
this.updateViewFromSubmission();
}

Expand All @@ -56,8 +59,8 @@ export class TextExamSubmissionComponent extends ExamSubmissionComponent impleme
return this.exercise;
}

updateProblemStatement(newProblemStatement: string): void {
this.exercise.problemStatement = newProblemStatement;
updateProblemStatement(newProblemStatementHtml: string): void {
this.problemStatementHtml = newProblemStatementHtml;
this.changeDetectorReference.detectChanges();
}

Expand Down
Loading
Loading