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

Course project #33

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,132 changes: 437 additions & 695 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@angular/core": "^16.1.0",
"@angular/platform-browser": "^16.1.0",
"@angular/platform-browser-dynamic": "^16.1.0",
"@angular/router": "^16.1.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This project shouldn't concern it's self with the router.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's used by the sample

"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"uuid": "^9.0.0",
Expand Down
280 changes: 268 additions & 12 deletions projects/ngx-xapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,25 @@ The package contains the following entry-points:
@berry-cloud/ngx-xapi/model
@berry-cloud/ngx-xapi/client
@berry-cloud/ngx-xapi/profiles/cmi5
@berry-cloud/ngx-xapi/course
```

`@berry-cloud/ngx-xapi/model` contains the core types for xAPI. (Statement, Actor, Verb, etc.)
`@berry-cloud/ngx-xapi/client` contains utility methods for communicating with an LRS.
`@berry-cloud/ngx-xapi/client` contains utility functions for communicating with an LRS.
`@berry-cloud/ngx-xapi/profiles/cmi5` contains types and extensions for the cmi5 profile.
`@berry-cloud/ngx-xapi/course` contains utility functions for a tincan or cmi5 course player.

All of the exported types and methods from model and client can be accessed directly from `@berry-cloud/ngx-xapi` entry point too.
## Samples

See [BerryCloud/ngx-xapi GitHub repository](https://github.com/BerryCloud/ngx-xapi) for [Sample application](https://github.com/BerryCloud/ngx-xapi/tree/main/projects/samples)

It contains simple examples for the client and the course utilities.

## Configuration injection
## Client Utilities

If you plan to use the client methods, you must provide an `XapiConfig` to be injected into the `XapiClient`.
The client utilities can be accessed via the injectable `XapiClient` service.

If you plan to use this service, you must provide an `XapiConfig` to be injected into the `XapiClient`.
The HttpClientModule must also be imported.

For example:
Expand Down Expand Up @@ -116,11 +124,7 @@ function xapiConfigFactory(userService: UserService) {
export class AppModule {}
```

## Samples

See [BerryCloud/ngx-xapi GitHub repository](https://github.com/BerryCloud/ngx-xapi) for [Sample application](https://github.com/BerryCloud/ngx-xapi/tree/main/projects/samples)

## Post Statement
### Post Statement

```TypeScript
postPassedStatement() {
Expand All @@ -144,7 +148,7 @@ postPassedStatement() {
}
```

## Post State
### Post State

```TypeScript
postState(state: any) {
Expand Down Expand Up @@ -184,11 +188,263 @@ Example:
<h1 [innerHTML]="activity.definition.name | languageMap"></h1>
```

## Handling Responses
### Handling Responses

Most of the `XapiClient` utility methods return a `Observable<HttpResponse<T>>` object. Although in most cases `Observable<T>` would be enough, some important properties of the response can be gathered only from the response headers:
Most of the `XapiClient` utility functions return a `Observable<HttpResponse<T>>` object. Although in most cases `Observable<T>` would be enough, some important properties of the response can be gathered only from the response headers:

- ETag
- X-Experience-API-Consistent-Through

You can access the response object itself via `response.body`;

## Course Utilities

The course utilities can be accessed via the injectable `XapiCourseService`.
It contains useful utility functions for a tincan or cmi5 compatible course player.
You can turn an angular application into a tincan and/or cmi5 compatible course within minutes.

If you plan to use this service, you must provide an `Activity` object to be injected into the `XapiCourseService`.
This will be the activity object of the tincan/cmi5 course.

For example:

```TypeScript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { XAPI_ACTIVITY } from '@berry-cloud/ngx-xapi/course';
import { Activity } from '@berry-cloud/ngx-xapi/model';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
// import HttpClientModule after BrowserModule.
HttpClientModule,
AppRoutingModule,
],
providers: [
provide: XAPI_ACTIVITY,
useValue: {
id: 'https://berrycloud.co.uk/xapi/sample',
definition: {
type: "http://adlnet.gov/expapi/activities/course",
name: {
'en-US': 'BerryCloud Sample Course',
},
},
} as Activity,
],
bootstrap: [AppComponent],
})
export class AppModule {}
```

The `XapiCourseService` also automatically picks up the launch parameters from the URL when it is initialized. If the launch parameters come from a different source, or you want to hide the URL parameters before the service is initialized, you can provide this parameters via injection too:

```TypeScript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { XAPI_ACTIVITY, XAPI_LAUNCH, } from '@berry-cloud/ngx-xapi/course';
import { Launch } from '@berry-cloud/ngx-xapi/profiles/cmi5';
import { Activity } from '@berry-cloud/ngx-xapi/model';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
// import HttpClientModule after BrowserModule.
HttpClientModule,
AppRoutingModule,
],
providers: [
provide: XAPI_ACTIVITY,
useValue: {
id: 'https://berrycloud.co.uk/xapi/sample',
definition: {
name: {
'en-US': 'BerryCloud Sample Course',
},
},
} as Activity,
provide: XAPI_LAUNCH,
useValue: {
endpoint: 'https://mylrs.com/xapi';
actor: {
name: 'test user'
mbox: 'mailto:test@example.com';
};
registration: 'bc6c2d1e-6f5e-4023-83fd-01d89d5bfa32';
activityId: 'http://example.com/activity-from-launc-parameter';
auth: 'Basic *******';
fetch: 'https://mylms.com/token/12345678901234567890'
} as Launch,
],
bootstrap: [AppComponent],
})
export class AppModule {}
```

`XAPI_LAUNCH` can be a `Launch` or `Observable<Launch>`, so you can also provide it via a factory function which loads it asynchronously from a file or the URL.

The `XapiCourseService` can handle both the tincan `auth` parameter or the `cmi5` launch parameter. If the latter is provided it automatically fetches the auth token from the provided endpoint. In this case it also automatically loads the cmi5 launch-data, and will use it to decorate each further statements to be sent to the LRS. It also automatically sends the mandatory cmi5 `initialized` statement during initialization.

If the launch parameters are not provided and neither cannot be picked from the URL bar the `XapiCourseService` service still initializes itself, but also throws an `Error`.
This error is logged into the console, but does not cease the application. It must be handled manually if needed.
If it's ignored then the course will still run, but it will silently ignore any http requests to the LRS. (Get functions will return 404, put/post functions will return 200 or 204.) It can be useful for testing or when a course is launched locally and no need to store progress data.

If neither of the above initialization methods are suitable, you can create the `XapiCourseService` manually:

```
const courseService = new XapiCourseService(activity, launch);
```

The `XapiCourseService` uses the `XapiClient` internally, but extends its functionality with some useful convenience functions.
If these functions don't fit for your usecase, you can still get an `Observable<XapiClient | undefined>` via the `getXapiClient()` function. It returns `undefined` if the launch parameters were not provided or were deficient. See the `XapiClient` examples above.

## Sending a State

After the `XapiCourseService` was successfully initialized, you can send a state with the `putState` or `postState` functions. They have only one mandatory argument, the state to be sent. All parameters needed for the LRS request are filled or defaulted in by the `XapiCourseService`.

default stateId: `progress`
default content-type: `application/json`

If you want to use different `stateId` or send a state with any other properties, you can override any of the defaults by providing the `StateParams` and `StateOptions` arguments:

```TypeScript
this.courseService.putState(
state,
{
stateId: 'sample-state',
activityId: 'http://example.com/activities/sample-activity',
registration: '123456789-1234-1234-1234-123456789012',
agent: {
mbox: 'mailto:test@example.com',
},
},
{
contentType: 'application/json',
etag: '"123456789012345678901234567890123456789012"',
match: true
}
).subscribe( ... );
```

## Getting a State

You can use the `getState` function the same way as the above functions. Without any arguments it will try to get the state by the default parameters, but you can override any of them:

```TypeScript
this.xapiCourseService.getState(
{
stateId: 'sample-state',
activityId: 'http://example.com/activities/sample-activity',
registration: '123456789-1234-1234-1234-123456789012',
agent: {
mbox: 'mailto:test@example.com',
},
}
).subscribe( ... );
```

## Sending a Statement

You can send a default statement using the `postStatement` function:

```TypeScript
this.xapiCourseService.postStatement();
```

The default verb is `experienced`, the actor, object and registration properties are picked up from the launch parameters.
You can provide a `Partial<Statement>` argument to the `postStatement` function where you can override any of these parameters:

```TypeScript
this.courseService.postStatement(
{
verb: attempted,
object: {
id: 'http://example.com/activities/sample-activity',
definition: {
name: {
'en-US': 'Sample Activity',
},
},
},
actor: {
mbox: 'mailto:other@example.com',
},
context: {
registration: '00000000-0000-0000-0000-000000000000',
language: 'en-US',
extensions: {
'http://example.com/profiles/meetings/context/extensions/meeting-id':
'123456789',
'http://example.com/profiles/meetings/context/extensions/meeting-name':
'Example Meeting',
}
},
}
)
```

If the `XapiCourseService` was initialized as a cmi5 course, then the properties from cmi5 `contextTemplate` are also added to the `Statement`. The mandatory parameters from the `contextTemplate` cannot be overridden. (these are the sessionId extension and the contextActivities arrays)
If you want full control over the `Statement`, you can configure it via a callback function. The incoming `defaultStatement` argument contains the prefilled Statement, but you can override any or all of the properties. (Do it only if you **really know** what are you doing)

```TypeScript
this.courseService.postStatement(
(defaultStatement) =>
({
...defaultStatement,
verb: attempted,
context: {
contextActivities: {
grouping: [
{
id: 'http://example.com/activities/world-domination',
definition: {
name: {
'en-US': 'Nothing to see here',
},
},
},
],
parent: undefined
},
extensions: undefined
},
} as Statement)
);
```

## Convenience functions for sending Statements

You can use the following functions for sending the most common tincan or cmi5 statements. All of them can be used with an extra statement-template or a callback function argument, like the `postStatement` function above:

```TypeScript
sendCompletedStatement();
sendPassedStatement();
sendFailedStatement();
sendProgressedStatement(progressValue);
sendScoredStatement(scoreValue, scaledValue);
```

In the latter functions the progress and score values are merged into the statement after the callback function was used.

eg. sending a score for a test which has its own activity:

```TypeScript
// For an IQ test, the scaled score is not applicable, so we send undefined
this.courseService.sendScoredStatement(152, undefined, {
object: {
id: 'http://example.com/activities/iq-test',
definition: {
name: {
'en-US': 'IQ Test',
},
},
},
});
```
7 changes: 7 additions & 0 deletions projects/ngx-xapi/client/src/lib/state-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface StateOptions {
contentType: string;
etag?: string;
match?: boolean;
}

export type DeleteStateOptions = Omit<StateOptions, 'contentType'>;
11 changes: 6 additions & 5 deletions projects/ngx-xapi/client/src/lib/xapi-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
ActivityProfileParams,
ActivityProfilesParams,
} from './activity-profile-params';
import { DeleteStateOptions, StateOptions } from './state-options';

export interface XapiConfig {
endpoint: string;
Expand Down Expand Up @@ -110,7 +111,7 @@ export class XapiClient {
putState(
object: any,
stateParams: StateParams,
options: { contentType: string; etag?: string; match?: boolean }
options: StateOptions
): Observable<HttpResponse<object>> {
return this.config$.pipe(
mergeMap((config) => {
Expand All @@ -134,7 +135,7 @@ export class XapiClient {
postState(
object: any,
stateParams: StateParams,
options: { contentType: string; etag?: string; match?: boolean }
options: StateOptions
): Observable<HttpResponse<object>> {
return this.config$.pipe(
mergeMap((config) => {
Expand All @@ -159,7 +160,7 @@ export class XapiClient {
*/
deleteState(
stateParams: StateParams,
options: { etag?: string; match?: boolean }
options: DeleteStateOptions
): Observable<HttpResponse<object>> {
return this.config$.pipe(
mergeMap((config) => {
Expand All @@ -183,7 +184,7 @@ export class XapiClient {
*/
deleteStates(
statesParams: DeleteStatesParams,
options: { etag?: string; match?: boolean }
options: DeleteStateOptions
): Observable<HttpResponse<object>> {
return this.config$.pipe(
mergeMap((config) => {
Expand Down Expand Up @@ -699,7 +700,7 @@ export class XapiClient {
}

if (params.since) {
httpParams = httpParams.set('registration', params.since);
httpParams = httpParams.set('since', params.since);
}

if (params.stateId) {
Expand Down
1 change: 1 addition & 0 deletions projects/ngx-xapi/client/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export {
GetStatesParams,
DeleteStatesParams,
} from './lib/state-params';
export * from './lib/state-options';
export * from './lib/statements-params';
1 change: 1 addition & 0 deletions projects/ngx-xapi/course/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src/public-api';
Loading