From 8de036ae70f41cd3f482c57b4b32eacb25c85072 Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 7 Mar 2024 10:41:51 -0500 Subject: [PATCH 1/5] Add process for differentiating between training and normal video entries --- packages/server/src/config/configuration.ts | 3 ++- packages/server/src/entry/models/entry.model.ts | 2 +- .../server/src/entry/services/entry.service.ts | 5 +++-- .../entry/services/upload-session.service.ts | 3 ++- .../src/tag/services/video-field.service.ts | 17 +++++++++++++---- .../src/tag/transformers/field-transformer.ts | 3 ++- .../tag/transformers/video-field-transformer.ts | 7 +++++-- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/server/src/config/configuration.ts b/packages/server/src/config/configuration.ts index 5710d440..82b28685 100644 --- a/packages/server/src/config/configuration.ts +++ b/packages/server/src/config/configuration.ts @@ -34,6 +34,7 @@ export default () => ({ tag: { videoFieldFolder: process.env.TAG_VIDEO_FIELD_FOLDER || 'video-fields', videoRecordFileType: 'webm', - videoUploadExpiration: process.env.TAG_VIDEO_UPLOAD_EXPIRATION || 15 * 60 * 1000 // 15 minutes + videoUploadExpiration: process.env.TAG_VIDEO_UPLOAD_EXPIRATION || 15 * 60 * 1000, // 15 minutes + trainingPrefix: process.env.TAG_TRAINING_PREFIX || 'training' } }); diff --git a/packages/server/src/entry/models/entry.model.ts b/packages/server/src/entry/models/entry.model.ts index dd6282a7..d4c9228b 100644 --- a/packages/server/src/entry/models/entry.model.ts +++ b/packages/server/src/entry/models/entry.model.ts @@ -32,7 +32,7 @@ export class Entry { // TODO: Add GraphQL reference back to dataset object @Prop() - @Field(() => ID) + @Field(() => ID, { nullable: true }) dataset: string; // TODO: Add GraphQL reference back to user object diff --git a/packages/server/src/entry/services/entry.service.ts b/packages/server/src/entry/services/entry.service.ts index 618d02e5..ed294f23 100644 --- a/packages/server/src/entry/services/entry.service.ts +++ b/packages/server/src/entry/services/entry.service.ts @@ -25,10 +25,11 @@ export class EntryService { return this.entryModel.findOne({ _id: entryID }); } - async create(entryCreate: EntryCreate, dataset: Dataset, user: TokenPayload): Promise { + async create(entryCreate: EntryCreate, dataset: Dataset, user: TokenPayload, isTraining: boolean): Promise { + // Make the entry, note that training entries are not associated with a dataset return this.entryModel.create({ ...entryCreate, - dataset: dataset._id, + dataset: isTraining ? undefined : dataset._id, organization: dataset.organization, recordedInSignLab: false, dateCreated: new Date(), diff --git a/packages/server/src/entry/services/upload-session.service.ts b/packages/server/src/entry/services/upload-session.service.ts index f45db9fa..ae726bde 100644 --- a/packages/server/src/entry/services/upload-session.service.ts +++ b/packages/server/src/entry/services/upload-session.service.ts @@ -94,7 +94,8 @@ export class UploadSessionService { meta: entryUpload.metadata }, dataset, - user + user, + false ); // Move the entry to the dataset diff --git a/packages/server/src/tag/services/video-field.service.ts b/packages/server/src/tag/services/video-field.service.ts index 09af6206..c6c4b7ae 100644 --- a/packages/server/src/tag/services/video-field.service.ts +++ b/packages/server/src/tag/services/video-field.service.ts @@ -11,6 +11,7 @@ import { Entry } from '../../entry/models/entry.model'; import { EntryService } from '../../entry/services/entry.service'; import { DatasetPipe } from '../../dataset/pipes/dataset.pipe'; import { TokenPayload } from '../../jwt/token.dto'; +import { Dataset } from '../../dataset/dataset.model'; @Injectable() export class VideoFieldService { @@ -19,6 +20,7 @@ export class VideoFieldService { private readonly bucketName = this.configService.getOrThrow('gcp.storage.bucket'); private readonly bucket: Bucket = this.storage.bucket(this.bucketName); private readonly expiration = this.configService.getOrThrow('tag.videoUploadExpiration'); + private readonly trainingPrefix = this.configService.getOrThrow('tag.trainingPrefix'); constructor( @InjectModel(VideoField.name) private readonly videoFieldModel: Model, @@ -72,13 +74,14 @@ export class VideoFieldService { * Move the video itself to the permanent storage location and create the * cooresponding entry. */ - async markComplete(videoFieldID: string, datasetID: string, user: TokenPayload): Promise { + async markComplete(videoFieldID: string, datasetID: string, user: TokenPayload, tag: Tag): Promise { const videoField = await this.videoFieldModel.findById(videoFieldID); if (!videoField) { throw new BadRequestException(`Video field ${videoFieldID} not found`); } - const dataset = await this.datasetPipe.transform(datasetID); + // The dataset that the entry would be associated with + const dataset: Dataset = await this.datasetPipe.transform(datasetID); // Make the entry const entry = await this.entryService.create( @@ -88,12 +91,18 @@ export class VideoFieldService { meta: {} }, dataset, - user + user, + tag.training ); + // Where to move the entry video + let newLocation = `${dataset.bucketPrefix}/${entry._id}.webm`; + if (tag.training) { + newLocation = `${this.trainingPrefix}/${dataset.organization}/${tag.study}/${entry._id}.webm`; + } + // Move the video to the permanent location const source = this.bucket.file(videoField.bucketLocation); - const newLocation = `${dataset.bucketPrefix}/${entry._id}.webm`; await source.move(newLocation); await this.entryService.setBucketLocation(entry, newLocation); entry.bucketLocation = newLocation; diff --git a/packages/server/src/tag/transformers/field-transformer.ts b/packages/server/src/tag/transformers/field-transformer.ts index 905e2981..d9badcd2 100644 --- a/packages/server/src/tag/transformers/field-transformer.ts +++ b/packages/server/src/tag/transformers/field-transformer.ts @@ -1,5 +1,6 @@ import { JsonSchema, UISchemaElement } from '@jsonforms/core'; import { TokenPayload } from '../../jwt/token.dto'; +import { Tag } from '../models/tag.model'; /** * A field transformer handles converting and operating on fields of a tag. @@ -7,7 +8,7 @@ import { TokenPayload } from '../../jwt/token.dto'; * and ensuring that the data meets any additional formatting requirements. */ export interface FieldTransformer { - transformField(field: any, uischema: UISchemaElement, schema: JsonSchema, user: TokenPayload): Promise; + transformField(tag: Tag, uischema: UISchemaElement, schema: JsonSchema, user: TokenPayload): Promise; } /** diff --git a/packages/server/src/tag/transformers/video-field-transformer.ts b/packages/server/src/tag/transformers/video-field-transformer.ts index 6b40997b..5a959f57 100644 --- a/packages/server/src/tag/transformers/video-field-transformer.ts +++ b/packages/server/src/tag/transformers/video-field-transformer.ts @@ -3,13 +3,14 @@ import { FieldTransformer } from './field-transformer'; import { JsonSchema, UISchemaElement } from '@jsonforms/core'; import { VideoFieldService } from '../services/video-field.service'; import { TokenPayload } from '../../jwt/token.dto'; +import { Tag } from '../models/tag.model'; @Injectable() export class VideoFieldTransformer implements FieldTransformer { constructor(private readonly videoFieldService: VideoFieldService) {} async transformField( - data: string[], + tag: Tag, uischema: UISchemaElement, _schema: JsonSchema, user: TokenPayload @@ -19,9 +20,11 @@ export class VideoFieldTransformer implements FieldTransformer { throw new BadRequestException('Dataset ID not provided'); } + const data: string[] = tag.data; + const videoFields = await Promise.all( data.map(async (videoFieldId) => { - const entry = await this.videoFieldService.markComplete(videoFieldId, datasetID, user); + const entry = await this.videoFieldService.markComplete(videoFieldId, datasetID, user, tag); return entry._id; }) ); From c5bd0feefc9fd683e6daaed6b7a2a8c738c24973 Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 7 Mar 2024 11:03:12 -0500 Subject: [PATCH 2/5] Fix issue with lack of access to correct portion of tag data --- packages/server/src/tag/resolvers/tag.resolver.ts | 1 + packages/server/src/tag/services/tag-transformer.service.ts | 5 +++-- packages/server/src/tag/services/tag.service.ts | 2 +- packages/server/src/tag/transformers/field-transformer.ts | 2 +- .../server/src/tag/transformers/video-field-transformer.ts | 3 +-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/server/src/tag/resolvers/tag.resolver.ts b/packages/server/src/tag/resolvers/tag.resolver.ts index 257c3f4b..fe1f8024 100644 --- a/packages/server/src/tag/resolvers/tag.resolver.ts +++ b/packages/server/src/tag/resolvers/tag.resolver.ts @@ -63,6 +63,7 @@ export class TagResolver { ): Promise { // TODO: Add user context and verify the correct user has completed the tag const study = await this.studyPipe.transform(tag.study); + tag.data = data; await this.tagService.complete(tag, data, study, user); return true; } diff --git a/packages/server/src/tag/services/tag-transformer.service.ts b/packages/server/src/tag/services/tag-transformer.service.ts index a77ba154..eb9a0162 100644 --- a/packages/server/src/tag/services/tag-transformer.service.ts +++ b/packages/server/src/tag/services/tag-transformer.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Study } from '../../study/study.model'; import { FieldTransformerFactory } from '../transformers/field-transformer-factory'; import { TokenPayload } from '../../jwt/token.dto'; +import { Tag } from '../../tag/models/tag.model'; @Injectable() export class TagTransformer { @@ -11,7 +12,7 @@ export class TagTransformer { * Transforms the tag data. Takes in the whole tag and produces the modified * tag data. */ - async transformTagData(data: any, study: Study, user: TokenPayload): Promise { + async transformTagData(tag: Tag, data: any, study: Study, user: TokenPayload): Promise { const transformedData: { [property: string]: any } = {}; const schema = study.tagSchema.dataSchema; @@ -36,7 +37,7 @@ export class TagTransformer { // Apply the transformation if present, otherwise just return the data const transformed = transformer - ? await transformer.transformField(data[field], fieldUiSchema, fieldSchema, user) + ? await transformer.transformField(tag, data[field], fieldUiSchema, fieldSchema, user) : data[field]; transformedData[field] = transformed; } diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index 274ffc77..d4df52aa 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -188,7 +188,7 @@ export class TagService { } // Handle any transformations - const transformed = await this.tagTransformService.transformTagData(data, study, user); + const transformed = await this.tagTransformService.transformTagData(tag, data, study, user); // Save the tag information and mark the tag as complete await this.tagModel.findOneAndUpdate({ _id: tag._id }, { $set: { data: transformed, complete: true } }); diff --git a/packages/server/src/tag/transformers/field-transformer.ts b/packages/server/src/tag/transformers/field-transformer.ts index d9badcd2..129db94e 100644 --- a/packages/server/src/tag/transformers/field-transformer.ts +++ b/packages/server/src/tag/transformers/field-transformer.ts @@ -8,7 +8,7 @@ import { Tag } from '../models/tag.model'; * and ensuring that the data meets any additional formatting requirements. */ export interface FieldTransformer { - transformField(tag: Tag, uischema: UISchemaElement, schema: JsonSchema, user: TokenPayload): Promise; + transformField(tag: Tag, data: any, uischema: UISchemaElement, schema: JsonSchema, user: TokenPayload): Promise; } /** diff --git a/packages/server/src/tag/transformers/video-field-transformer.ts b/packages/server/src/tag/transformers/video-field-transformer.ts index 5a959f57..a130f996 100644 --- a/packages/server/src/tag/transformers/video-field-transformer.ts +++ b/packages/server/src/tag/transformers/video-field-transformer.ts @@ -11,6 +11,7 @@ export class VideoFieldTransformer implements FieldTransformer { async transformField( tag: Tag, + data: string[], uischema: UISchemaElement, _schema: JsonSchema, user: TokenPayload @@ -20,8 +21,6 @@ export class VideoFieldTransformer implements FieldTransformer { throw new BadRequestException('Dataset ID not provided'); } - const data: string[] = tag.data; - const videoFields = await Promise.all( data.map(async (videoFieldId) => { const entry = await this.videoFieldService.markComplete(videoFieldId, datasetID, user, tag); From 4a378c80ff4e6020591b76ce63bd6f482aafad7a Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 7 Mar 2024 11:22:09 -0500 Subject: [PATCH 3/5] Working ability to separate training and normal tagging --- packages/client/src/graphql/tag/tag.graphql | 2 -- packages/client/src/graphql/tag/tag.ts | 6 ++---- packages/server/src/entry/models/entry.model.ts | 6 +++++- packages/server/src/entry/services/entry.service.ts | 7 ++++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index af863dca..c90e51a2 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -53,7 +53,6 @@ query getTags($study: ID!) { organization entryID contentType - dataset creator dateCreated meta @@ -73,7 +72,6 @@ query getTrainingTags($study: ID!, $user: String!) { organization entryID contentType - dataset creator dateCreated meta diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index e36489ed..007e0cb1 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -67,7 +67,7 @@ export type GetTagsQueryVariables = Types.Exact<{ }>; -export type GetTagsQuery = { __typename?: 'Query', getTags: Array<{ __typename?: 'Tag', _id: string, data?: any | null, complete: boolean, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number } }> }; +export type GetTagsQuery = { __typename?: 'Query', getTags: Array<{ __typename?: 'Tag', _id: string, data?: any | null, complete: boolean, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number } }> }; export type GetTrainingTagsQueryVariables = Types.Exact<{ study: Types.Scalars['ID']['input']; @@ -75,7 +75,7 @@ export type GetTrainingTagsQueryVariables = Types.Exact<{ }>; -export type GetTrainingTagsQuery = { __typename?: 'Query', getTrainingTags: Array<{ __typename?: 'Tag', _id: string, data?: any | null, complete: boolean, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number } }> }; +export type GetTrainingTagsQuery = { __typename?: 'Query', getTrainingTags: Array<{ __typename?: 'Tag', _id: string, data?: any | null, complete: boolean, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number } }> }; export const CreateTagsDocument = gql` @@ -333,7 +333,6 @@ export const GetTagsDocument = gql` organization entryID contentType - dataset creator dateCreated meta @@ -382,7 +381,6 @@ export const GetTrainingTagsDocument = gql` organization entryID contentType - dataset creator dateCreated meta diff --git a/packages/server/src/entry/models/entry.model.ts b/packages/server/src/entry/models/entry.model.ts index d4c9228b..4643e6dc 100644 --- a/packages/server/src/entry/models/entry.model.ts +++ b/packages/server/src/entry/models/entry.model.ts @@ -32,7 +32,7 @@ export class Entry { // TODO: Add GraphQL reference back to dataset object @Prop() - @Field(() => ID, { nullable: true }) + @Field(() => ID) dataset: string; // TODO: Add GraphQL reference back to user object @@ -51,6 +51,10 @@ export class Entry { @Prop({ required: false }) signedURLExpiration: Date; + @Prop() + @Field() + isTraining: boolean; + // TODO: Add creator field } diff --git a/packages/server/src/entry/services/entry.service.ts b/packages/server/src/entry/services/entry.service.ts index ed294f23..343369b5 100644 --- a/packages/server/src/entry/services/entry.service.ts +++ b/packages/server/src/entry/services/entry.service.ts @@ -29,11 +29,12 @@ export class EntryService { // Make the entry, note that training entries are not associated with a dataset return this.entryModel.create({ ...entryCreate, - dataset: isTraining ? undefined : dataset._id, + dataset: dataset._id, organization: dataset.organization, recordedInSignLab: false, dateCreated: new Date(), - creator: user.user_id + creator: user.user_id, + isTraining }); } @@ -42,7 +43,7 @@ export class EntryService { } async findForDataset(dataset: Dataset): Promise { - return this.entryModel.find({ dataset: dataset._id.toString() }); + return this.entryModel.find({ dataset: dataset._id.toString(), isTraining: false }); } async exists(entryID: string, dataset: Dataset): Promise { From 68a8cd354d729b89ac5ecefa6ae641f8416dc323 Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 7 Mar 2024 11:39:02 -0500 Subject: [PATCH 4/5] Ensure that the tags retrived are not training tags --- packages/server/src/tag/services/tag.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index d4df52aa..a3859e87 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -218,7 +218,7 @@ export class TagService { } async getTags(study: Study): Promise { - return this.tagModel.find({ study: study._id }); + return this.tagModel.find({ study: study._id, training: false }); } private async getIncomplete(study: Study, user: string): Promise { From 32f8e72b622fa97802690ba85fb690fb5af138be Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 7 Mar 2024 11:41:19 -0500 Subject: [PATCH 5/5] Ensure the correct tags are returned --- packages/server/src/tag/services/tag.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index a3859e87..4a205f6e 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -146,7 +146,7 @@ export class TagService { await this.tagModel.db.transaction(async (): Promise => { const searchResult = await this.tagModel.aggregate([ // Only search on tags that are enabled for the current study - { $match: { enabled: true, study: study._id.toString() } }, + { $match: { enabled: true, study: study._id.toString(), training: false } }, // Grab tags that are unassigned (user field doesn't exist) or have been completed by the user { $match: { $or: [{ user: { $exists: false } }, { user: { $eq: user } }] } }, // Group by the entrys and expand tags