Commit 4fe2f5bf authored by Robert Knight's avatar Robert Knight

Save context metadata provided by LMS app with new annotations

When the embedder frame (eg. our LMS app) provides context metadata to be saved
with annotations via the `annotationMetadata` config property, save that with
new annotations in the `metadata` field.

We add this information only when an annotation is created, and not on
subsequent edits, because the main use case in the LMS context is to record the
assignment where the annotation was created. If the user edits that annotation
subsequently in another assignment, we don't want it to be "moved" by updating
the `metadata` field.

Part of https://github.com/hypothesis/lms/issues/5698
parent 088ed0a3
...@@ -5,7 +5,7 @@ import type { ...@@ -5,7 +5,7 @@ import type {
Annotation, Annotation,
SavedAnnotation, SavedAnnotation,
} from '../../types/api'; } from '../../types/api';
import type { AnnotationEventType } from '../../types/config'; import type { AnnotationEventType, SidebarSettings } from '../../types/config';
import * as metadata from '../helpers/annotation-metadata'; import * as metadata from '../helpers/annotation-metadata';
import { import {
defaultPermissions, defaultPermissions,
...@@ -24,15 +24,18 @@ import type { APIService } from './api'; ...@@ -24,15 +24,18 @@ import type { APIService } from './api';
export class AnnotationsService { export class AnnotationsService {
private _activity: AnnotationActivityService; private _activity: AnnotationActivityService;
private _api: APIService; private _api: APIService;
private _settings: SidebarSettings;
private _store: SidebarStore; private _store: SidebarStore;
constructor( constructor(
annotationActivity: AnnotationActivityService, annotationActivity: AnnotationActivityService,
api: APIService, api: APIService,
settings: SidebarSettings,
store: SidebarStore, store: SidebarStore,
) { ) {
this._activity = annotationActivity; this._activity = annotationActivity;
this._api = api; this._api = api;
this._settings = settings;
this._store = store; this._store = store;
} }
...@@ -109,6 +112,12 @@ export class AnnotationsService { ...@@ -109,6 +112,12 @@ export class AnnotationsService {
if (metadata.isHighlight(annotation)) { if (metadata.isHighlight(annotation)) {
annotation.permissions = privatePermissions(userid); annotation.permissions = privatePermissions(userid);
} }
// Attach information about the current context (eg. LMS assignment).
if (this._settings.annotationMetadata) {
annotation.metadata = { ...this._settings.annotationMetadata };
}
return annotation; return annotation;
} }
......
...@@ -5,6 +5,7 @@ describe('AnnotationsService', () => { ...@@ -5,6 +5,7 @@ describe('AnnotationsService', () => {
let fakeAnnotationActivity; let fakeAnnotationActivity;
let fakeApi; let fakeApi;
let fakeMetadata; let fakeMetadata;
let fakeSettings;
let fakeStore; let fakeStore;
let fakeDefaultPermissions; let fakeDefaultPermissions;
...@@ -46,6 +47,8 @@ describe('AnnotationsService', () => { ...@@ -46,6 +47,8 @@ describe('AnnotationsService', () => {
isPublic: sinon.stub(), isPublic: sinon.stub(),
}; };
fakeSettings = {};
fakeStore = { fakeStore = {
addAnnotations: sinon.stub(), addAnnotations: sinon.stub(),
annotationSaveFinished: sinon.stub(), annotationSaveFinished: sinon.stub(),
...@@ -77,7 +80,12 @@ describe('AnnotationsService', () => { ...@@ -77,7 +80,12 @@ describe('AnnotationsService', () => {
}, },
}); });
svc = new AnnotationsService(fakeAnnotationActivity, fakeApi, fakeStore); svc = new AnnotationsService(
fakeAnnotationActivity,
fakeApi,
fakeSettings,
fakeStore,
);
}); });
afterEach(() => { afterEach(() => {
...@@ -118,6 +126,21 @@ describe('AnnotationsService', () => { ...@@ -118,6 +126,21 @@ describe('AnnotationsService', () => {
assert.equal(annotation.user, 'acct:foo@bar.com'); assert.equal(annotation.user, 'acct:foo@bar.com');
assert.isOk(annotation.$tag); assert.isOk(annotation.$tag);
assert.isString(annotation.$tag); assert.isString(annotation.$tag);
// `annotationMetadata` config not set, so this field should also not be set.
assert.isUndefined(annotation.metadata);
});
it('adds metadata from `annotationMetadata` setting to annotation', () => {
fakeStore.focusedGroupId.returns('mygroup');
fakeSettings.annotationMetadata = {
lms: { assignment_id: '1234' },
};
svc.create({}, now);
const annotation = getLastAddedAnnotation();
assert.deepEqual(annotation.metadata, fakeSettings.annotationMetadata);
}); });
describe('annotation permissions', () => { describe('annotation permissions', () => {
......
...@@ -213,6 +213,15 @@ export type APIAnnotationData = { ...@@ -213,6 +213,15 @@ export type APIAnnotationData = {
}; };
user_info?: UserInfo; user_info?: UserInfo;
/**
* An opaque object that contains metadata about the current context,
* provided by the embedder via the `annotationMetadata` config.
*
* The Hypothesis LMS app uses this field to attach information about the
* current assignment, course etc. to annotations.
*/
metadata?: object;
}; };
/** /**
......
...@@ -231,6 +231,15 @@ export type ConfigFromAnnotator = ConfigFromHost & { ...@@ -231,6 +231,15 @@ export type ConfigFromAnnotator = ConfigFromHost & {
* allow arbitrary web pages to set. * allow arbitrary web pages to set.
*/ */
export type ConfigFromEmbedder = ConfigFromHost & { export type ConfigFromEmbedder = ConfigFromHost & {
/**
* Metadata about the context which the client should store with new
* annotations in the `metadata` field.
*
* The Hypothesis LMS app uses this field to attach information about the
* current assignment, course etc. to annotations.
*/
annotationMetadata?: object;
/** /**
* Feature flags to enable. When a flag is listed here, it will be turned * Feature flags to enable. When a flag is listed here, it will be turned
* on even if disabled in the H user profile. * on even if disabled in the H user profile.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment