Assessments, shared assessments, and their resources and tags

Assessments

Assessments usually start as some client code and server configuration to perform a new kind of assessment as part of a mobile study. Along with that code comes a good deal of documentation, validation information, customization details, and the like. All of this information together defines an assessment, which can then be shared across apps.

One particularly important part of an assessment for client applications is the configuration, described in a separate API design document.

Assessments API

Method

Path

Description

Method

Path

Description

GET

/v1/assessments

List the assessments in an app context. Should be paged and can be filtered by tags.

POST

/v1/assessments

Create a new assessment (returns the assessment with the new root assessment configuration node).

GET

/v1/assessments/{guid}
/v1/assessments/identifier:{identifier}

Get an assessment by GUID (does not require a revision number), or by identifier (returning the most recent revision of the assessment).

GET

/v1/assessments/identifier:{identifier}/revisions

Get a paged list of the revisions of this assessment.

GET

/v1/assessments/identifier:{identifier}/revisions/{revision}

Get an assessment by identifier and revision. You do not need to use this API if you know the revision’s GUID.

POST

/v1/assessments/{guid}

Update an assessment (does not update the assessment configuration).

POST

/v1/assessments/{guid}/publish

Copies the assessment, its configuration, and its documents into the shared assessments library (see below)

Model Objects

Similar to our accounts system, the DAO will work with a HibernateAssessment implementation that abstracts away a few details of the persisted object from the Assessment as it is expressed in the service and client layers. This conversion happens in our DAO layer so it’s not visible in our domain model.

The HibernateAssessment model:

@Entity @Table(name = "Assessments") public class HibernateAssessment { public static HibernateAssessment create(Assessment dto, String appId) { // copies from Assessment to HibernateAssessment } @Id private String guid; private String appId; private String identifier; private int revision = 1; private String title; private String summary; private String validationStatus; private String normingStatus; // OperatingSystem string private String osName; // For imported assessments, they retain a link to the assessment they // were copied from. The origin assessment will be in the shared // assessment library. Every revision has a unique GUID to make it // easier to query for these. private String originGuid; // Substudy will be refactored into organization with a system-wide, unique // identifier of "appId:substudyId". In local studies it remains identical // to the substudy ID, but in the shared context, the compound key must be // used. private String ownerId; @ManyToMany(cascade = { MERGE, PERSIST }, fetch = EAGER) @JoinTable(name = "AssessmentTags", joinColumns = { @JoinColumn(name = "assessmentGuid") }, inverseJoinColumns = { @JoinColumn(name = "tagValue")} ) private Set<Tag> tags; @Convert(converter = StringToStringSetMapConverter.class) private Map<String,Set<String>> customizationFields; @Convert(converter = DateTimeToLongAttributeConverter.class) private DateTime createdOn; @Convert(converter = DateTimeToLongAttributeConverter.class) private DateTime modifiedOn; private boolean deleted; @Version private long version; // getters and setters ommitted }

The Assessment simplifies the tag collection and hides the appId:

public class Assessment implements BridgeEntity { public static Assessment create(HibernateAssessment assessment) { // converts HibernateAssessment to Assessment } public static Assessment copy(Assessment assessment) { return create(HibernateAssessment.create(assessment, null)); } private String guid; private String identifier; private int revision = 1; private String ownerId; private String title; private String summary; private String osName; private String originGuid; private String validationStatus; private String normingStatus; private Set<String> tags; private Map<String, Set<String>> customizationFields; private DateTime createdOn; private DateTime modifiedOn; private boolean deleted; private long version; // getters and setters ommitted }

The tag implementation for assessments is not specific to assessments and can be used for other resources (such as protocols or studies). Tags can be differentiated using a namespace convention of namespace : tag, and the tags API will explicitly return tags grouped by these namespaces.

@Entity @Table(name = "Tags") public final class Tag { @Id @NaturalId private String value; public Tag() {} public Tag(String value) { this.value = value; } // getters and setters ommitted }

Services

There is one service that handles both shared and local assessments. Many APIs allow you to work either with a GUID, or with an identifier/revision pair, as scientists are likely to have the latter while tools and internal code will find it easier to work with the former. These methods should never be called with an appId = shared, as this library of shared assessments has different access rules (which are enforced from the AssessmentController and SharedAssessmentController):

The assessment configuration service is covered in a separate document.

Assessment Configuration Customization

An assessment can provide a map of the fields in its configuration that a study author can change without invalidating an assessment as an instance of a shared assessment. Shared assessments would define this map and it would constrain how study designers could modify a cloned assessment configuration (using a separate API defined in the configuration design document). Critically, the fields being modified should not change the data that is output from an assessment from the documented behavior of the assessment. In addition, assessment authors will have to take care that the changes don’t invalidate the assessment’s scientific validation. Changing the name of a UI control probably has no impact on an assessment; changing the wording of a survey question is problematic.

The map in the Assessment maps a configuration node GUID to a list of fields in the configuration node that are editable. Fields in the metadata map of the node are prefixed with the string “metadata:”. The /v1/assessments/{guid}/customize API will check this configuration and only allow these fields to be edited in the JSON that is submitted via the API (without breaking the referential integrity to the original shared assessment):

Example

{
"gIeVMw1JnrW-lBz0D6rxFt4pjNM": [
"title",
"metadata:next_button_label",
"metadata:previous_button_label"
],
"qxcSET8W0mKBk6pF-7SvUO1_Qic": [
"title",
"metadata:next_button_label",
"metadata:previous_button_label",
"forChild"
]
}

This JSON creates a map that allows two nodes in the assessment configuration to have some editable fields. Both nodes allow the designer to change some custom metadata about back/next buttons, while the first allows for the title to be altered and the second allows for the “forChild” flag to be set (on an assessment node that should have this property as a result of being a strictly typed property).

In implementation, it is may be easier to modify these fields as JSON, or else we’ll have to use Java reflection/JavaBean APIs.

SQL

Shared Assessments

Shared Assessments API

Method

Path

Description

Method

Path

Description

GET

/v1/sharedassessments

List the shared assessments available on Bridge (public API). Should be paged and filterable by tag.

GET

/v1/sharedassessments/{guid}
/v1/sharedassessments/identifier:{identifier}

Get a shared assessment, or the most recent revision of an assessment by its identifier (public API)

POST

/v1/sharedassessments/{guid}

Updated a shared assessment (the caller must be in the organization that owns the shared assessment. While this is good for typos, corrections, additions, fundamental changes should be made alongside other changes through the import/publish workflow. Cannot change the assessment configuration tree (there’s no API for this once an assessment is shared).

GET

/v1/sharedassessments/{guid}/revisions

Get all revisions of this assessment

GET

/v1/sharedassessments/{identifier}/revisions/{revision}

Get a shared assessment by identifier and revision. You do not need to specify a revision if you use a GUID.

POST

/v1/sharedassessments/{guid}/import

Import the assessment, its configuration, and its documents into the caller’s current study. It will be linked to the shared assessment. A new owning organization must be specified in the call.

Publication/Import

Local assessments. Assessments are globally visible in an app context and can be used in any protocol in an app context, regardless of the organization that owns the protocol. One organization “owns” the assessment and only users from that organization can edit, delete, or publish that assessment (ownership is specified on create, and must indicate one of the organizations the caller is associated with).

If a local assessment configuration includes locked nodes from other assessments, and it is updated, those nodes are copied on write. Reusing configuration in a new assessment will not change the configuration of an assessment that is shown to be derived from a shared assessment. If an assessment which is shown to be derived from a shared assessment is updated, it loses its association to the original shared assessment and through the copy-on-write algorithm, gains an entirely new copy of the configuration tree that is editable (though its identifier does not change).

Publication of a local assessment. A full copy of an assessment, its documents, and its configuration is made in the shared assessment library. The organizational relationship is maintained. Members of that organization can call a shared assessment API to update the assessment or its documents, but not the configuration. This is intended to allow improvements to the documentation that would not invalidate the shared module as used by others (the configuration is immutable at this point). Finally, the original assessment is updated to lock its configuration tree and show its derivation from the newly shared assessment, just as if it had been imported from the shared assessment library to begin with.

The steps are as follows:

  1. The shared module library is checked for the identifier;

  2. If the identifier is unique, a full copy is made in the shared library at revision 1;

  3. If the identifier is not unique, the organizational memberships of the caller are checked against the owning organization of the shared assessment (only members of the owning organization can update or create a new revision of their shared assessment). If successful, a copy is made in the shared library at revision N+1 where N is the largest revision number for this identifier;

  4. All copies of the assessment, configuration, documents, and Files are given new GUIDs;

  5. The original assessment is updated to show it was derived from this new shared module, and all its configuration nodes are locked (it should be identical to an assessment imported from this shared module).

Shared assessments are publicly available (no authentication is needed for read-only access to these assessments). Owners of the shared assessment can edit the assessment metadata through the API, but not the configuration tree. To work on the next version of an assessment, authors must import it into an app context first.

Import of a shared assessment. The caller imports the assessment into their own app context. The owning organization is updated to one of the organizations the importing caller is associated to. The assessment is given a link to the original shared assessment (via the originGUID field). The configuration is “locked” such that an update of any configuration that includes these nodes will cause a copy-on-write. The steps are as follows:

  1. The local context is checked for the identifier;

  2. If the identifier is unique, a full copy is made in the local context at revision 1;

  3. Otherwise, a copy is made in the local context at revision N+1 where N is the largest revision number for this identifier;

  4. All copies of the assessment, configuration, documents, and Files are given new GUIDs, and the assessment is assigned an owning organization associated to the caller.

Assessment Resources

The detailed documentation of an assessment and its use will not be in the metadata record for the assessment, but in an associated set of external resources. At least as exposed in the API, the external resource is not specific to assessments and could be used elsewhere in our system. These resources are essentially annotated links to other forms of documentation, code, data repositories, etc. (this could include Bridge hosted files as well, if need be). The properties of the external resource are the basic properties of the Dublin Core Metadata set.

These resources are associated to a stream of assessment revisions, so they are linked through the assessment’s identifier. Each resource can provide specific details about which revisions of the assessment the resource applies to, and we can do some basic tracking as to whether or not a resource has been created/updated since a new revision was introduced into the system. These criteria can be used to filter the resources as well.

API

Method

Path

Description

Method

Path

Description

GET

/v1/assessments/identifier:{identifier}/resources

Get a paged list of the resources. In addition to paging parameters (offsetBy, pageSize), you can filter by minRevision for the minimum supported revision, maxRevision for the maximum supported revision, and by category for one or more categories of resources.

POST

/v1/assessments/identifier:{identifier}/resources

Create a new resource

GET

/v1/assessments/identifier:{identifier}/resources/{guid}

Get a single resource (not sure if this is useful as the entire record will be returned in the list above)

POST

/v1/assessments/identifier:{identifier}/resources/{guid}

Update an existing resource

GET

/v1/sharedassessments/identifier:{identifier}/resources

Get a paged list of the resources. In addition to paging parameters (offsetBy, pageSize), you can filter by minRevision for the minimum supported revision, maxRevision for the maximum supported revision, and by category for a one or more categories of resource.

POST

/v1/sharedassessments/identifier:{identifier}/resources

Create a new resource

GET

/v1/sharedassessments/identifier:{identifier}/resources/{guid}

Get a single resource (not sure if this is useful as the entire record will be returned in the list above)

POST

/v1/sharedassessments/identifier:{identifier}/resources/{guid}

Update an existing resource

POST

/v1/assessments/identifier:{identifier/resources/publish

Publish one or more resources for the assessment to the shared assessment library (the assessment itself must already have been published to the shared library). Same security restriction apply to resources that apply to the assessment.

POST

/v1/sharedassessments/identifier:{identifier}/resources/import

Import one or more resources for an assessment to the local app context. The assessment must have been imported first. Same security restriction apply to resources that apply to the assessment.

Java

External resources are metadata about a resource for the assessment:

And the Hibernate managed object:

Services

A service to manage the documents associates the documents with an assessment. This service is only called from the assessment service after the user’s access to the assessment itself has been verified (hence there isn’t a need to carry over the appId):

SQL

minRevision, maxRevision, modifiedAtRevision. Each document can indicate its applicability to the stream of assessments (minRevision and maxRevision are both optional):

minRevision-: is similar to the @since annotation in Java. The document was introduced with this revision of the assessment.

-maxRevision : is similar to deprecation at a specific revision. The document is no longer applicable to newer revisions of the assessment.

minRevision-maxRevision should be self-explanatory (if the values are equivalent to one another, this can indicate that the resource is just specifically about that revision, such as release notes).

If the latest revision of the assessment is the same as the modifiedAtRevision value, the resource can be considered up-to-date. Any time a resource is created or updated, this field is set to the latest assessment’s revision, so any edit of the assessment resource should indicate it can be considered up-to-date, at least in some perfunctory sense.

Publication and Import behavior

After an assessment has been published at least once to the shared library, the resources associated with that assessment can be published to the shared library as well. If the resource has already been published, the new publication will update the version in the shared library (undeleting it if necessary).

After a shared assessment has been imported to an app context, you would typically refer to the documentation for the assessment in the shared library. However, you can also import some or all of the shared assessment’s resources. If they have already been imported, they are updated to the current versions in the shared library.

Tags

Early designs for the assessments catalog show a faceted search functionality that is being implemented using tags. Because it’s useful for a UI to know all the tags in the system (as well as all the available categories of assessments), a public API to retrieve these would be useful.

Method

Path

Description

Method

Path

Description

GET

/v1/tags

A public endpoint that returns an object listing all the tags by their category (see below; right now that is “category” for assessment categories and “tag” for the more generic tagging mechanism).

POST

/v1/tags

Add a tag with a simple POST body: { “value”: “newTagValue” }

DELETE

/v1/tags/{tagValue}

For tests, we should clean up autogenerated test tags, so provide an API for that. This should fail if the tag is in use by any entity.

The object returned would look something like this:

Because tagging might be used for other entities, the tag category might need to be more specifically “assessment.category” and “assessment.tag”.

There is no API to add, edit, or remove these tags (this is just done as a consequence of updating tagged entities). We could add a label to tags for the UI; if we want to hold open that option, each of the strings in the arrays above should be a JSON representation of the Tag object, so we can add fields like a label at a later time.