Skip to content

Commit

Permalink
feat: enable reviewing, passing, deleting test attempts and add test …
Browse files Browse the repository at this point in the history
…attempt model and service
  • Loading branch information
satikaj committed Jun 2, 2024
1 parent 8a7e108 commit 40a7366
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 22 deletions.
3 changes: 3 additions & 0 deletions src/app/api/models/doubtfire-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export * from './task-similarity';
export * from './tii-action';
export * from './scorm-datamodel';
export * from './scorm-player-context';
export * from './test-attempt';
export * from './task-comment/scorm-comment';

// Users -- are students or staff
export * from './user/user';
Expand All @@ -58,3 +60,4 @@ export * from '../services/teaching-period-break.service';
export * from '../services/learning-outcome.service';
export * from '../services/group-set.service';
export * from '../services/task-similarity.service';
export * from '../services/test-attempt.service';
9 changes: 9 additions & 0 deletions src/app/api/models/task-comment/scorm-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {Task, TaskComment, TestAttempt} from '../doubtfire-model';

export class ScormComment extends TaskComment {
testAttempt: TestAttempt;

constructor(task: Task) {
super(task);
}
}
20 changes: 20 additions & 0 deletions src/app/api/models/test-attempt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {Entity} from 'ngx-entity-service';
import {Task} from './doubtfire-model';

export class TestAttempt extends Entity {
id: number;
attemptNumber: number;
terminated: boolean;
completionStatus: boolean;
successStatus: boolean;
scoreScaled: number;
cmiDatamodel: string;
attemptedTime: Date;

task: Task;

constructor(task: Task) {
super();
this.task = task;
}
}
29 changes: 29 additions & 0 deletions src/app/api/services/scorm-adapter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export class ScormAdapterService {
this.context.mode = mode;
}

set testAttemptId(testAttemptId: number) {
this.context.attemptId = testAttemptId;
}

get state() {
return this.context.state;
}
Expand All @@ -53,6 +57,31 @@ export class ScormAdapterService {
break;
}

if (this.context.mode === 'review') {
this.xhr.open('GET', `${API_URL}/test_attempts/${this.context.attemptId}/review`, false);

this.xhr.onload = () => {
if (this.xhr.status >= 200 && this.xhr.status < 400) {
console.log('Retrieved the attempt.');
} else if (this.xhr.status == 404) {
console.log('Not found.');
noTestFound = true;
} else {
console.error('Error saving DataModel:', this.xhr.responseText);
}
};

this.xhr.send();
console.log(this.xhr.responseText);

const reviewSession = JSON.parse(this.xhr.responseText);
this.dataModel.restore(reviewSession.cmi_datamodel);
console.log(this.dataModel.dump());

this.context.state = 'Initialized';
return 'true';
}

// TODO: move this part into the player component
this.xhr.open(
'GET',
Expand Down
22 changes: 19 additions & 3 deletions src/app/api/services/task-comment.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Task, TaskComment, UserService } from 'src/app/api/models/doubtfire-model';
import { ScormComment, Task, TaskComment, TestAttemptService, UserService } from 'src/app/api/models/doubtfire-model';
import { EventEmitter, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
Expand Down Expand Up @@ -32,7 +32,8 @@ export class TaskCommentService extends CachedEntityService<TaskComment> {
httpClient: HttpClient,
private emojiService: EmojiService,
private userService: UserService,
private downloader: FileDownloaderService
private downloader: FileDownloaderService,
private testAttemptService: TestAttemptService,
) {
super(httpClient, API_URL);

Expand Down Expand Up @@ -85,7 +86,20 @@ export class TaskCommentService extends CachedEntityService<TaskComment> {
'status',
'numberOfPrompts',
'timeDiscussionComplete',
'timeDiscussionStarted'
'timeDiscussionStarted',

// Scorm Comments
{
keys: 'testAttempt',
toEntityFn: (data: object, key: string, comment: ScormComment) => {
const testAttempt = this.testAttemptService.cache.getOrCreate(
data[key].id,
testAttemptService,
data[key],
);
return testAttempt;
},
},
);

this.mapping.addJsonKey(
Expand All @@ -103,6 +117,8 @@ export class TaskCommentService extends CachedEntityService<TaskComment> {
return new DiscussionComment(other);
case 'extension':
return new ExtensionComment(other);
case 'scorm':
return new ScormComment(other);
default:
return new TaskComment(other);
}
Expand Down
103 changes: 103 additions & 0 deletions src/app/api/services/test-attempt.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {Injectable} from '@angular/core';
import {CachedEntityService} from 'ngx-entity-service';
import API_URL from 'src/app/config/constants/apiURL';
import {Task, TestAttempt} from 'src/app/api/models/doubtfire-model';
import {Observable} from 'rxjs';
import {AppInjector} from 'src/app/app-injector';
import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants';
import {AlertService} from 'src/app/common/services/alert.service';
import {HttpClient} from '@angular/common/http';

@Injectable()
export class TestAttemptService extends CachedEntityService<TestAttempt> {
protected readonly endpointFormat = 'test_attempts/:id:';
protected readonly forTaskEndpoint =
'/projects/:project_id:/task_definition_id/:task_def_id:/test_attempts';
protected readonly latestCompletedEndpoint =
this.forTaskEndpoint + '/latest?completed=:completed:';

constructor(httpClient: HttpClient) {
super(httpClient, API_URL);

this.mapping.addKeys(
'id',
'attemptNumber',
'terminated',
'completionStatus',
'successStatus',
'scoreScaled',
'cmiDatamodel',
'attemptedTime',
);
}

public override createInstanceFrom(_json: object, constructorParams: Task): TestAttempt {
return new TestAttempt(constructorParams);
}

public getAttemptsForTask(task: Task): Observable<TestAttempt[]> {
return this.query(
{
project_id: task.project.id,
task_def_id: task.taskDefId,
},
{
endpointFormat: this.forTaskEndpoint,
constructorParams: task,
},
);
}

public getLatestCompletedAttempt(task: Task): Observable<TestAttempt> {
return this.get(
{
project_id: task.project.id,
task_def_id: task.taskDefId,
completed: true,
},
{
endpointFormat: this.latestCompletedEndpoint,
constructorParams: task,
},
);
}

public overrideSuccessStatus(testAttemptId: number, successStatus: boolean): void {
const http = AppInjector.get(HttpClient);

http
.patch(
`${AppInjector.get(DoubtfireConstants).API_URL}/test_attempts/${testAttemptId}?success_status=${successStatus}`,
{},
)
.subscribe({
next: (_data) => {
(AppInjector.get(AlertService) as AlertService).success(
'Attempt pass status successfully overridden.',
6000,
);
},
error: (message) => {
(AppInjector.get(AlertService) as AlertService).error(message, 6000);
},
});
}

public deleteAttempt(testAttemptId: number): void {
const http = AppInjector.get(HttpClient);

http
.delete(`${AppInjector.get(DoubtfireConstants).API_URL}/test_attempts/${testAttemptId}`, {})
.subscribe({
next: (_data) => {
(AppInjector.get(AlertService) as AlertService).success(
'Attempt successfully deleted.',
6000,
);
},
error: (message) => {
(AppInjector.get(AlertService) as AlertService).error(message, 6000);
},
});
}
}
11 changes: 9 additions & 2 deletions src/app/common/scorm-player/scorm-player.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export class ScormPlayerComponent implements OnInit {
@Input()
mode: 'browse' | 'normal' | 'review';

@Input()
testAttemptId: number;

iframeSrc: SafeResourceUrl;

constructor(
Expand All @@ -41,9 +44,13 @@ export class ScormPlayerComponent implements OnInit {
this.globalState.setView(ViewType.OTHER);
this.globalState.hideHeader();

this.scormAdapter.projectId = this.projectId;
this.scormAdapter.taskDefId = this.taskDefId;
this.scormAdapter.mode = this.mode;
if (this.mode === 'normal') {
this.scormAdapter.projectId = this.projectId;
this.scormAdapter.taskDefId = this.taskDefId;
} else if (this.mode === 'review') {
this.scormAdapter.testAttemptId = this.testAttemptId;
}

window.API_1484_11 = {
Initialize: () => this.scormAdapter.Initialize(),
Expand Down
2 changes: 2 additions & 0 deletions src/app/doubtfire-angular.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component
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';

@NgModule({
// Components we declare
Expand Down Expand Up @@ -406,6 +407,7 @@ import {TaskScormCardComponent} from './projects/states/dashboard/directives/tas
IsActiveUnitRole,
CreateNewUnitModal,
ScormAdapterService,
TestAttemptService,
provideLottieOptions({
player: () => player,
}),
Expand Down
38 changes: 32 additions & 6 deletions src/app/doubtfire.states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,18 +296,18 @@ const ViewAllUnits: NgHybridStateDeclaration = {
/**
* Define the SCORM Player state.
*/
const ScormPlayerState: NgHybridStateDeclaration = {
name: 'scorm-player',
url: '/projects/:project_id/task_def_id/:task_definition_id/scorm-player/:mode',
const ScormPlayerNormalState: NgHybridStateDeclaration = {
name: 'scorm-player-normal',
url: '/projects/:project_id/task_def_id/:task_definition_id/scorm-player/normal',
resolve: {
projectId: function ($stateParams) {
return $stateParams.project_id;
},
taskDefId: function ($stateParams) {
return $stateParams.task_definition_id;
},
mode: function ($stateParams) {
return $stateParams.mode;
mode: function () {
return 'normal';
},
},
views: {
Expand All @@ -321,6 +321,31 @@ const ScormPlayerState: NgHybridStateDeclaration = {
},
};

const ScormPlayerReviewState: NgHybridStateDeclaration = {
name: 'scorm-player-review',
url: '/task_def_id/:task_definition_id/scorm-player/review/:test_attempt_id',
resolve: {
taskDefId: function ($stateParams) {
return $stateParams.task_definition_id;
},
testAttemptId: function ($stateParams) {
return $stateParams.test_attempt_id;
},
mode: function () {
return 'review';
},
},
views: {
main: {
component: ScormPlayerComponent,
},
},
data: {
pageTitle: 'Review Knowledge Check',
roleWhitelist: ['Student', 'Tutor', 'Convenor', 'Admin'],
},
};

/**
* Export the list of states we have created in angular
*/
Expand All @@ -336,5 +361,6 @@ export const doubtfireStates = [
ViewAllProjectsState,
ViewAllUnits,
AdministerUnits,
ScormPlayerState,
ScormPlayerNormalState,
ScormPlayerReviewState,
];
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<div fxLayout="row" fxLayoutAlign="space-evenly center">
<div fxLayout="column" fxLayoutAlign="space-around center">
<div class="flex-row justify-evenly items-center">
<div class="flex-column justify-around items-center">
<div>
<hr class="hr-text" data-content="Knowledge Check Summary" />
<div class="markdown-to-html" [innerHTML]="comment.text"></div>
<button mat-button (click)="reviewScormTest()" [hidden]="!task.definition.scormAllowReview">Review test</button>
<br/>
<div class="flex-row">
<button mat-button (click)="reviewScormTest()" [hidden]="!task.definition.scormAllowReview">Review test</button>
<button mat-button (click)="passScormAttempt()" [hidden]="!canOverridePass">Pass attempt</button>
<button mat-button (click)="deleteScormAttempt()" [hidden]="!user.isStaff">Delete attempt</button>
</div>
<hr class="hr-fade" />
</div>
</div>
Expand Down
Loading

0 comments on commit 40a7366

Please sign in to comment.