Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.addColumn('submission', 'previousSubmissionId', {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'submission',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
});
},

async down (queryInterface, Sequelize) {
await queryInterface.removeColumn('submission', 'previousSubmissionId');
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
'use strict';

const workflows = [
{
name: "Revision Workflow",
description: "Two-step annotation workflow: review previous submission, then annotate with assessment",
steps: [
{
stepType: 1,
allowBackward: false,
workflowStepDocument: null,
configuration: {
settings: {
fields: [
{
key: "configurationId",
label: "Assessment Configuration File:",
type: "select",
required: true,
options: {
table: "configuration",
name: "name",
value: "id",
filter: [
{ key: "type", value: 0 },
{ key: "deleted", value: false }
]
},
help: "Select the configuration file for assessment sidebar."
},
{
key: "forcedAssessment",
label: "Forced Assessment",
type: "switch",
required: false,
default: false,
help: "If enabled, users must save a score and justification for every criterion before they can proceed."
},
{
key: "showAllDocumentAnnotations",
label: "Show all document Annotations",
type: "switch",
required: false,
default: true,
help: "If enabled, all document annotations will be shown to the reviewer."
}
],
},
readOnlyComponents: ["annotator", "assessment"],
placeholders: false
}
},
{
stepType: 1,
allowBackward: true,
workflowStepDocument: null,
configuration: {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should add to the configuration that the assessment from the previous step should be loaded, maybe sometimes this is not needed

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You mean like we add a check that the previous assessment is loaded why do wee need that though?

settings: {
fields: [
{
key: "configurationId",
label: "Assessment Configuration File:",
type: "select",
required: true,
options: {
table: "configuration",
name: "name",
value: "id",
filter: [
{ key: "type", value: 0 },
{ key: "deleted", value: false }
]
},
help: "Select the configuration file for assessment sidebar."
},
{
key: "forcedAssessment",
label: "Forced Assessment",
type: "switch",
required: false,
default: false,
help: "If enabled, users must save a score and justification for every criterion before they can proceed."
}
],
},
previousAssessmentData: 1,
readOnlyComponents: [],
placeholders: false
}
},
],
}
];

module.exports = {
async up(queryInterface, Sequelize) {
// Insert workflows
const workflowInsertions = await queryInterface.bulkInsert(
'workflow',
workflows.map(w => ({
name: w.name,
description: w.description,
createdAt: new Date(),
updatedAt: new Date()
})),
{ returning: true }
);

const workflowMap = {};
workflowInsertions.forEach((w, index) => {
workflowMap[workflows[index].name] = w.id;
});

// Insert workflow steps
for (const workflow of workflows) {
const workflowId = workflowMap[workflow.name];
let previousStepId = null;
const stepMap = {};
const innerStepMap = {};

for (let innerStepId = 1; innerStepId <= workflow.steps.length; innerStepId++) {
const step = workflow.steps[innerStepId - 1];

const stepInsertion = await queryInterface.bulkInsert(
'workflow_step',
[{
workflowId: workflowId,
stepType: step.stepType,
workflowStepPrevious: previousStepId,
allowBackward: step.allowBackward,
workflowStepDocument: null,
configuration: JSON.stringify(step.configuration || {}),
createdAt: new Date(),
updatedAt: new Date()
}],
{ returning: true }
);

const dbStepId = stepInsertion[0].id;
stepMap[innerStepId] = dbStepId;
innerStepMap[innerStepId] = dbStepId;
previousStepId = dbStepId;
}

// Update workflowStepDocument with correct references
for (let innerStepId = 1; innerStepId <= workflow.steps.length; innerStepId++) {
const step = workflow.steps[innerStepId - 1];

if (step.workflowStepDocument !== null) {
await queryInterface.bulkUpdate(
'workflow_step',
{ workflowStepDocument: innerStepMap[step.workflowStepDocument] },
{ id: innerStepMap[innerStepId] }
);
}
}
}
},

async down(queryInterface, Sequelize) {
const workflowNames = workflows.map(w => w.name);

// First get workflow IDs
const workflowRecords = await queryInterface.sequelize.query(
`SELECT id, name FROM workflow WHERE name IN (:names)`,
{
replacements: { names: workflowNames },
type: queryInterface.sequelize.QueryTypes.SELECT
}
);

const workflowIds = workflowRecords.map(w => w.id);

if (workflowIds.length > 0) {
await queryInterface.bulkDelete('workflow_step', {
workflowId: {
[Sequelize.Op.in]: workflowIds
}
}, {});

await queryInterface.bulkDelete('workflow', {
name: {
[Sequelize.Op.in]: workflowNames
}
}, {});
}
}
};
4 changes: 3 additions & 1 deletion backend/db/models/document_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,12 @@ module.exports = (sequelize, DataTypes) => {
const newDataEntries = originalDataEntries.map(entry => ({
...entry,
documentId: duplicateDocumentId, // Set to the new duplicated document ID
createdAt: new Date(),
updatedAt: new Date(),
studySessionId: null,
studyStepId: null,
id: undefined
id: undefined,
deletedAt: undefined,
}));
await this.bulkCreate(newDataEntries, {transaction});
}
Expand Down
51 changes: 48 additions & 3 deletions backend/db/models/submission.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,50 @@ module.exports = (sequelize, DataTypes) => {

return affectedCount;
}
/**
* Get the most recent submission for a user and project, traversing back through parent submissions
*
* @param {number} userId Id of the user
* @param {number} projectId Id of the project
* @param {boolean} root Whether to return the root submission
* @param {Object} transaction Database transaction object
* @returns {Object|null} The most recent submission or null if none found
*/
static async getParentSubmission(userId, projectId, returnRoot, transaction = {}) {
let submission = await Submission.findOne({
where: {
userId,
projectId,
deleted: false,
},
order: [['createdAt', 'DESC']],
raw: true
});
if(returnRoot && submission && submission.parentSubmissionId) {
submission = await this.getRootSubmission(submission);
}
return submission ? submission : null;
}

/**
* Get the root submission from a given submission by traversing back through parent submissions
*
* @param {Object} submission The starting submission object
* @returns {Object} The root submission object
*/
static async getRootSubmission(submission) {
let currentSubmission = submission;
while (currentSubmission.parentSubmissionId) {
currentSubmission = await Submission.findOne({
where: {
id: currentSubmission.parentSubmissionId,
deleted: false,
},
raw: true,
});
}
return currentSubmission ? currentSubmission : null;
}

/**
* Copy a submission and all its associated documents
Expand All @@ -76,11 +120,11 @@ module.exports = (sequelize, DataTypes) => {
* @param {number} createdByUserId - The ID of the user creating the copy
* @param {Object} submissionOverrides - Overrides for the submission (e.g., hideInFrontend)
* @param {Object} documentOverrides - Overrides for documents (e.g., studySessionId, studyStepId)
* @param {Object} includes - Additional includes for document duplication (eg: {studySessionId: 1}) in case we require cenrtain extra files
* @param {Object} filters - Additional filters for document duplication (eg: {studySessionId: 1}) in case we require certain extra files
* @param {Object} options - Database options including transaction
* @returns {Promise<Object>} Object containing copied submission and documents
*/
static async copySubmission(originalSubmissionId, createdByUserId, submissionOverrides = {}, documentOverrides = {}, includes= {}, options = {}) {
static async copySubmission(originalSubmissionId, createdByUserId, submissionOverrides = {}, documentOverrides = {}, filters= {}, options = {}) {
const transaction = options.transaction;

// Get the original submission
Expand Down Expand Up @@ -129,7 +173,7 @@ module.exports = (sequelize, DataTypes) => {
const copiedDoc = await sequelize.models.document.duplicateDocument(
originalDoc.id,
mergedDocumentOverrides,
includes,
filters,
{transaction}
);
copiedDocuments.push(copiedDoc);
Expand Down Expand Up @@ -192,6 +236,7 @@ module.exports = (sequelize, DataTypes) => {
createdByUserId: DataTypes.INTEGER,
projectId: DataTypes.INTEGER,
parentSubmissionId: DataTypes.INTEGER,
previousSubmissionId: DataTypes.INTEGER,
extId: DataTypes.INTEGER,
group: DataTypes.INTEGER,
additionalSettings: DataTypes.JSONB,
Expand Down
Loading