Skip to content

Commit

Permalink
feat: enable students to request extra scorm attempt
Browse files Browse the repository at this point in the history
  • Loading branch information
satikaj committed Jun 5, 2024
1 parent 58c24c3 commit d904ffd
Show file tree
Hide file tree
Showing 17 changed files with 402 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/app/api/models/doubtfire-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export * from './scorm-datamodel';
export * from './scorm-player-context';
export * from './test-attempt';
export * from './task-comment/scorm-comment';
export * from './task-comment/scorm-extension-comment';

// Users -- are students or staff
export * from './user/user';
Expand Down
38 changes: 38 additions & 0 deletions src/app/api/models/task-comment/scorm-extension-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {Observable} from 'rxjs';
import {tap} from 'rxjs/operators';
import {AppInjector} from 'src/app/app-injector';
import {TaskCommentService} from '../../services/task-comment.service';
import {TaskComment, Task} from '../doubtfire-model';

export class ScormExtensionComment extends TaskComment {
assessed: boolean;
granted: boolean;
dateAssessed: Date;
taskScormExtensions: number;

constructor(task: Task) {
super(task);
}

private assessScormExtension(): Observable<TaskComment> {
const tcs: TaskCommentService = AppInjector.get(TaskCommentService);
return tcs.assessScormExtension(this).pipe(
tap((tc: TaskComment) => {
const scormExtension: ScormExtensionComment = tc as ScormExtensionComment;

const task = tc.task;
task.scormExtensions = scormExtension.taskScormExtensions;
}),
);
}

public deny(): Observable<TaskComment> {
this.granted = false;
return this.assessScormExtension();
}

public grant(): Observable<TaskComment> {
this.granted = true;
return this.assessScormExtension();
}
}
1 change: 1 addition & 0 deletions src/app/api/models/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class Task extends Entity {
status: TaskStatusEnum = 'not_started';
dueDate: Date;
extensions: number;
scormExtensions: number;
submissionDate: Date;
completionDate: Date;
timesAssessed: number;
Expand Down
43 changes: 43 additions & 0 deletions src/app/api/services/task-comment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import API_URL from 'src/app/config/constants/apiURL';
import { EmojiService } from 'src/app/common/services/emoji.service';
import { MappingFunctions } from './mapping-fn';
import { FileDownloaderService } from 'src/app/common/file-downloader/file-downloader.service';
import { ScormExtensionComment } from '../models/task-comment/scorm-extension-comment';

@Injectable()
export class TaskCommentService extends CachedEntityService<TaskComment> {
Expand All @@ -22,6 +23,10 @@ export class TaskCommentService extends CachedEntityService<TaskComment> {
'projects/:projectId:/task_def_id/:taskDefinitionId:/assess_extension/:id:';
private readonly requestExtensionEndpointFormat =
'projects/:projectId:/task_def_id/:taskDefinitionId:/request_extension';
private readonly scormExtensionGrantEndpointFormat =
'projects/:projectId:/task_def_id/:taskDefinitionId:/assess_scorm_extension/:id:';
private readonly scormRequestExtensionEndpointFormat =
'projects/:projectId:/task_def_id/:taskDefinitionId:/request_scorm_extension';
private readonly discussionCommentReplyEndpointFormat = "/projects/:project_id:/task_def_id/:task_definition_id:/comments/:task_comment_id:/discussion_comment/reply";
private readonly getDiscussionCommentPromptEndpointFormat = "/projects/:project_id:/task_def_id/:task_definition_id:/comments/:task_comment_id:/discussion_comment/prompt_number/:prompt_number:";

Expand Down Expand Up @@ -100,6 +105,9 @@ export class TaskCommentService extends CachedEntityService<TaskComment> {
return testAttempt;
},
},

// Scorm Extension Comments
['taskScormExtensions', 'scorm_extensions']
);

this.mapping.addJsonKey(
Expand All @@ -119,6 +127,8 @@ export class TaskCommentService extends CachedEntityService<TaskComment> {
return new ExtensionComment(other);
case 'scorm':
return new ScormComment(other);
case 'scorm_extension':
return new ScormExtensionComment(other);
default:
return new TaskComment(other);
}
Expand Down Expand Up @@ -218,6 +228,39 @@ export class TaskCommentService extends CachedEntityService<TaskComment> {
);
}

public assessScormExtension(extension: ScormExtensionComment): Observable<TaskComment> {
const opts: RequestOptions<TaskComment> = {
endpointFormat: this.scormExtensionGrantEndpointFormat,
entity: extension,
};

return super.update(
{
id: extension.id,
projectId: extension.project.id,
taskDefinitionId: extension.task.definition.id,
},
opts,
);
}

public requestScormExtension(reason: string, task: any): Observable<TaskComment> {
const opts: RequestOptions<TaskComment> = {
endpointFormat: this.scormRequestExtensionEndpointFormat,
body: {
comment: reason,
},
cache: task.commentCache,
};
return super.create(
{
projectId: task.project.id,
taskDefinitionId: task.definition.id,
},
opts,
);
}

public postDiscussionReply(comment: TaskComment, replyAudio: Blob): Observable<TaskComment>{
const form = new FormData();
const pathIds = {
Expand Down
1 change: 1 addition & 0 deletions src/app/api/services/task.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class TaskService extends CachedEntityService<Task> {
toEntityFn: MappingFunctions.mapDateToEndOfDay,
},
'extensions',
'scormExtensions',
{
keys: 'submissionDate',
toEntityFn: MappingFunctions.mapDateToDay,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<h3 mat-dialog-title>Extra attempt request</h3>
<div mat-dialog-content [formGroup]="extensionData">
<p class="w-full">
Please explain why you require an extra attempt for this knowledge check, the teaching team will
assess the request shortly.
</p>

<mat-form-field style="width: 100%" appearance="outline">
<mat-label>Reason</mat-label>
<textarea
type="text"
matInput
[formControl]="extensionData.controls.extensionReason"
[errorStateMatcher]="matcher"
placeholder="Reason"
></textarea>
<mat-hint align="end"
>{{ extensionData.controls.extensionReason.value.length }} / {{ reasonMaxLength }}</mat-hint
>
@if (extensionData.controls.extensionReason.hasError('required')) {
<mat-error>You must enter a reason</mat-error>
}
@if (extensionData.controls.extensionReason.hasError('minlength')) {
<mat-error>The reason must be at least {{ reasonMinLength }} characters long</mat-error>
}
@if (extensionData.controls.extensionReason.hasError('maxlength')) {
<mat-error>The reason must be less than {{ reasonMaxLength }} characters long</mat-error>
}
</mat-form-field>
</div>

<div mat-dialog-actions style="float: right">
<button mat-dialog-close mat-stroked-button color="warn">Cancel</button>
<button
mat-dialog-close
style="margin-right: 10px"
mat-stroked-button
color="primary"
mat-button
[disabled]="!extensionData.valid"
(click)="submitApplication()"
>
Request extra attempt
</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {Component, Inject, LOCALE_ID} from '@angular/core';
import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import {TaskComment, TaskCommentService, Task} from 'src/app/api/models/doubtfire-model';
import {AppInjector} from 'src/app/app-injector';
import {FormControl, Validators, FormGroup, FormGroupDirective, NgForm} from '@angular/forms';
import {ErrorStateMatcher} from '@angular/material/core';
import {AlertService} from '../../services/alert.service';

/** Error when invalid control is dirty, touched, or submitted. */
export class ReasonErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
}
}

@Component({
selector: 'f-scorm-extension-modal',
templateUrl: './scorm-extension-modal.component.html',
})
export class ScormExtensionModalComponent {
protected reasonMinLength: number = 15;
protected reasonMaxLength: number = 256;
constructor(
public dialogRef: MatDialogRef<ScormExtensionModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: {task: Task; afterApplication?: () => void},
private alerts: AlertService,
) {}

matcher = new ReasonErrorStateMatcher();
currentLocale = AppInjector.get(LOCALE_ID);
extensionData = new FormGroup({
extensionReason: new FormControl('', [
Validators.required,
Validators.minLength(this.reasonMinLength),
Validators.maxLength(this.reasonMaxLength),
]),
});

private scrollCommentsDown(): void {
setTimeout(() => {
const objDiv = document.querySelector('div.comments-body');
// let wrappedResult = angular.element(objDiv);
objDiv.scrollTop = objDiv.scrollHeight;
}, 50);
}

submitApplication() {
const tcs: TaskCommentService = AppInjector.get(TaskCommentService);
tcs
.requestScormExtension(this.extensionData.controls.extensionReason.value, this.data.task)
.subscribe({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
next: ((tc: TaskComment) => {
this.alerts.success('Extra attempt requested.', 2000);
this.scrollCommentsDown();
if (typeof this.data.afterApplication === 'function') {
this.data.afterApplication();
}
}).bind(this),
error: ((response: never) => {
this.alerts.error('Error requesting extra attempt ' + response);
console.log(response);
}).bind(this),
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Injectable} from '@angular/core';
import {Task} from 'src/app/api/models/task';
import {MatDialogRef, MatDialog} from '@angular/material/dialog';
import {ScormExtensionModalComponent} from './scorm-extension-modal.component';

@Injectable({
providedIn: 'root',
})
export class ScormExtensionModalService {
constructor(public dialog: MatDialog) {}

public show(task: Task, afterApplication?: any) {
let dialogRef: MatDialogRef<ScormExtensionModalComponent, any>;

dialogRef = this.dialog.open(ScormExtensionModalComponent, {
data: {
task,
afterApplication,
},
});

dialogRef.afterOpened().subscribe((result: any) => {});

dialogRef.afterClosed().subscribe((result: any) => {});
}
}
4 changes: 4 additions & 0 deletions src/app/doubtfire-angular.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ import {ScormAdapterService} from './api/services/scorm-adapter.service';
import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/scorm-comment.component';
import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component';
import {TestAttemptService} from './api/services/test-attempt.service';
import {ScormExtensionCommentComponent} from './tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component';
import {ScormExtensionModalComponent} from './common/modals/scorm-extension-modal/scorm-extension-modal.component';

@NgModule({
// Components we declare
Expand Down Expand Up @@ -335,6 +337,8 @@ import {TestAttemptService} from './api/services/test-attempt.service';
ScormPlayerComponent,
ScormCommentComponent,
TaskScormCardComponent,
ScormExtensionCommentComponent,
ScormExtensionModalComponent,
],
// Services we provide
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
</button>
<button
mat-stroked-button
(click)="requestMoreAttempts()"
[hidden]="task.definition.scormAttemptLimit === 0"
(click)="requestExtraAttempt()"
[hidden]="task.definition.scormAttemptLimit === 0 || attemptsLeft !== 0"
>
Request more attempts
Request extra attempt
</button>
</mat-card-actions>
</mat-card>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
import {Task} from 'src/app/api/models/doubtfire-model';
import {ScormExtensionModalService} from 'src/app/common/modals/scorm-extension-modal/scorm-extension-modal.service';

@Component({
selector: 'f-task-scorm-card',
Expand All @@ -10,6 +11,8 @@ export class TaskScormCardComponent implements OnChanges {
@Input() task: Task;
attemptsLeft: number;

constructor(private extensions: ScormExtensionModalService) {}

ngOnChanges(changes: SimpleChanges) {
if (changes.task && changes.task.currentValue) {
this.attemptsLeft = undefined;
Expand All @@ -22,7 +25,7 @@ export class TaskScormCardComponent implements OnChanges {
this.task.fetchTestAttempts().subscribe((attempts) => {
let count = attempts.length;
if (count > 0 && attempts[0].terminated === false) count--;
this.attemptsLeft = this.task.definition.scormAttemptLimit - count;
this.attemptsLeft = this.task.definition.scormAttemptLimit + this.task.scormExtensions - count;
});
}
}
Expand All @@ -34,5 +37,9 @@ export class TaskScormCardComponent implements OnChanges {
);
}

requestMoreAttempts(): void {}
requestExtraAttempt(): void {
this.extensions.show(this.task, () => {
this.task.refresh();
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<div class="flex flex-row justify-evenly items-center">
@if (comment.assessed) {
<div class="flex flex-col justify-around items-center">
<hr class="hr-text" [attr.data-content]="message" />
<p class="fade-text"><strong> reason:</strong> {{ comment.text }}</p>
</div>
}

@if (!comment.assessed) {
<div class="flex flex-col justify-around items-center">
<hr class="hr-fade" />
<p>
{{ message }} <br />
<strong> reason:</strong> {{ comment.text }}
</p>
@if (isNotStudent) {
<div class="flex flex-row justify-evenly items-center">
<button
(click)="grantExtension()"
mat-flat-button
color="primary"
style="background-color: #43a047"
>
Grant
</button>
<button (click)="denyExtension()" mat-flat-button color="warn">Deny</button>
</div>
}
<hr class="hr-fade" />
</div>
}
</div>
Loading

0 comments on commit d904ffd

Please sign in to comment.