Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
71f2eb9
added utils/helper/export.js file in the backend
melolw Jul 30, 2026
e0050f5
updated routes/export.js to the latest version I had
melolw Jul 30, 2026
cbf44b7
adds a different name for each download
melolw Jul 30, 2026
70959a4
added StepOptionsDocuments.vue for the doc-based export
melolw Jul 30, 2026
3d46271
added StepOptionsStudies.vue for the study-based export
melolw Jul 30, 2026
84929b5
updated with config view
melolw Jul 30, 2026
d2e38b7
updated ExportModal.vue
melolw Jul 30, 2026
c14978e
file naming error
melolw Jul 30, 2026
dd0a3c4
grades.json => scores.json
melolw Jul 30, 2026
64611c6
added deltaToHtml
melolw Jul 31, 2026
2085c4d
added delta to html dependency
melolw Jul 31, 2026
edd21cd
added ai score filtering for study export
melolw Aug 1, 2026
6fa0e01
added tagName along tagId in annotations.json
melolw Aug 2, 2026
34ac245
added export user behaviour
melolw Aug 2, 2026
e183f30
removed unused basicloading
melolw Aug 4, 2026
f9c80c7
resetting user selection when changing export/project
melolw Aug 5, 2026
f815db5
ajusted for multiple configurations in grade export
melolw Aug 5, 2026
a7a4d9e
added select all and unselect all buttons
melolw Aug 5, 2026
0df5ba8
added sessionId, userId and username to behaviour export
melolw Aug 5, 2026
a450787
added acceptStats for behaviour export
melolw Aug 5, 2026
39a99b8
merge conflict solved
melolw Aug 6, 2026
ef0d0e1
fixed confirm download step
melolw Aug 11, 2026
8470d76
changed grades to scores
melolw Aug 11, 2026
49546e8
added user roles into table for behaviour export
melolw Aug 11, 2026
273ffac
add roles column to user table, dropdown workflow filter, and inline …
melolw Aug 17, 2026
f47705f
change to correct logging
melolw Aug 21, 2026
a3803f3
split helper/export.js into 2
melolw Aug 21, 2026
2d86415
headers instead of empty string
melolw Aug 21, 2026
54587a7
add check project auth
melolw Aug 21, 2026
4b3b583
add label user behaviour
melolw Aug 21, 2026
8a4953c
change student name to full name
melolw Aug 24, 2026
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
348 changes: 346 additions & 2 deletions backend/package-lock.json

Large diffs are not rendered by default.

735 changes: 735 additions & 0 deletions backend/utils/helper/export.js
Comment thread
melolw marked this conversation as resolved.

Large diffs are not rendered by default.

1,190 changes: 672 additions & 518 deletions backend/webserver/routes/export.js

Large diffs are not rendered by default.

224 changes: 191 additions & 33 deletions frontend/src/components/dashboard/projects/ExportModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,24 @@
:fields="dataSelectionFields"
/>
</template>

<template #step-2>

<div v-if="dataSelection.exportType === 'reviewerList'">
<p>Exporting a list of all study sessions with hash:</p>
<p>
Total Studies: {{ studies.length }}<br>
Total Study Sessions: {{ studySessions.length }}
</p>
</div>
<div v-else-if="['submissions', 'grades'].includes(dataSelection.exportType)">
<StepSelectStudents
<div v-else-if="['submissions', 'grades', 'documents', 'studies', 'userBehaviour'].includes(dataSelection.exportType)">
<StepSelectUsers
v-if="dataSelection.projectId"
v-model="submissionSelection"
:project-id="dataSelection.projectId"
:project-id="dataSelection.projectId"
:export-type="dataSelection.exportType"
v-model="userSelection"
/>
<!-- We send the project ID and get the selected students back -->
<!-- We send the project ID and get the selected users back -->
</div>
<div v-else>
<p>Exporting all data</p>
Expand All @@ -50,30 +53,57 @@
</p>
</div>
</template>
<template
v-if="['submissions', 'grades'].includes(dataSelection.exportType)"
#step-3
>
<StepOptions
v-model:generate-aliases="generateAliases"
v-model:faker-seed="fakerSeed"
v-model:grade-format="gradeFormat"
v-model:merge-csv-files="mergeCsvFiles"
:show-grade-format="dataSelection.exportType === 'grades'"
/>
<!-- We get the info back if user wants to generate aliases and the seed that should be used for this -->


<template #step-3>
<div v-if="['submissions', 'grades'].includes(dataSelection.exportType)">
<StepOptions
v-model:generate-aliases="generateAliases"
v-model:faker-seed="fakerSeed"
v-model:grade-format="gradeFormat"
v-model:merge-csv-files="mergeCsvFiles"
:show-grade-format="dataSelection.exportType === 'grades'"
/>
<!-- We get the info back if user wants to generate aliases and the seed that should be used for this -->
</div>
<div v-else-if="['documents'].includes(dataSelection.exportType)">
<StepOptionsDocuments
v-model:selectedTypes="selectedDocumentTypes"
v-model:excludeNonConsentingEdits="excludeNonConsentingEdits"
v-model:excludeNonConsentingAnnotations="excludeNonConsentingAnnotations"
/>
<!-- We get the desired document types as well as if non consenting users' edits should be included -->
</div>
<div v-else-if="['studies'].includes(dataSelection.exportType)">
<StepOptionsStudies
:project-id="dataSelection.projectId"
v-model:selectedWorkflowIds="selectedWorkflowIds"
v-model:includeEmptyStudies="includeEmptyStudies"
v-model:includeDocumentFiles="includeStudyDocumentFiles"
v-model:includeGrades="includeStudyGrades"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The parent binds v-model:includeGrades, but this child declares the prop and emit as includeScores, so the "Include scores" switch never reaches ExportModal. includeStudyGrades stays true, so includeGrades: true is always posted at line 505 and scores are exported even with the switch off. Change the binding here to v-model:includeScores="includeStudyGrades" and leave the POST key at line 505 as includeGrades, which is what the backend reads.

v-model:includeAiScores="includeStudyIncludeAiScores"
v-model:excludeNonConsentingEdits="excludeNonConsentingEdits"
v-model:excludeNonConsentingAnnotations="excludeNonConsentingAnnotations"
/>
</div>
<div v-else-if="['userBehaviour'].includes(dataSelection.exportType)">
<StepOptionsUserBehaviour
v-model:outputFormat="behaviourOutputFormat"
v-model:fileFormat="behaviourFileFormat"
/>
</div>
</template>

<template
v-if="['submissions', 'grades'].includes(dataSelection.exportType)"
v-if="['submissions', 'grades', 'documents', 'studies', 'userBehaviour'].includes(dataSelection.exportType)"
#step-4
>
<StepConfirmDownload
v-if="['submissions', 'grades'].includes(dataSelection.exportType)"
<StepConfirmDownload
:wait="wait"
:generate-aliases="generateAliases"
:submission-selection="submissionSelection"
:user-selection="userSelection"
:export-type="dataSelection.exportType"
/>
<!-- We send the info the generateAliases because it is needed to show the warning talking about the mapping CSV -->
</template>
</StepperModal>
</template>
Expand All @@ -87,20 +117,23 @@ import JSZip from 'jszip';
import FileSaver from 'file-saver';
import Quill from "quill";
import {dbToDelta} from "editor-delta-conversion";
import StepSelectStudents from "@/components/dashboard/projects/export/StepSelectStudents.vue";
import StepSelectUsers from "@/components/dashboard/projects/export/StepSelectUsers.vue";
import StepOptions from "@/components/dashboard/projects/export/StepOptions.vue";
import StepOptionsDocuments from "@/components/dashboard/projects/export/StepOptionsDocuments.vue";
import StepOptionsStudies from "@/components/dashboard/projects/export/StepOptionsStudies.vue";
import StepOptionsUserBehaviour from "@/components/dashboard/projects/export/StepOptionsUserBehaviour.vue";
import StepConfirmDownload from "@/components/dashboard/projects/export/StepConfirmDownload.vue";
import getServerURL from "@/assets/serverUrl.js";


/**
* ProjectModal - modal component for adding and editing projects
*
* @author Dennis Zyska, Mélissa Loew, Linyin Huang
* @author Dennis Zyska, Mélissa Loew
*/
export default {
name: "ExportProjectModal",
components: { StepperModal, BasicForm, StepSelectStudents, StepOptions, StepConfirmDownload },
components: { StepperModal, BasicForm, StepSelectUsers, StepOptions, StepOptionsDocuments, StepOptionsStudies, StepOptionsUserBehaviour, StepConfirmDownload },
subscribeTable: [{
table: "document",
}, {
Expand All @@ -119,6 +152,14 @@ export default {
table: "tag_set",
}, {
table: "tag"
}, {
table: "document_data",
}, {
table: "study_step",
}, {
table: "configuration",
}, {
table: "workflow",
}
],
provide() {
Expand All @@ -135,37 +176,68 @@ export default {
filter: [],
wait: false,
// Data for Export Submissions
submissionSelection: [],
userSelection: [],
generateAliases:false,
fakerSeed: 846569412,
gradeFormat: "json",
mergeCsvFiles: false
mergeCsvFiles: false,
selectedDocumentTypes: [0, 1, 2, 4],
excludeNonConsentingEdits: false,
excludeNonConsentingAnnotations: false,
selectedWorkflowIds: [],
includeStudyDocumentFiles: true,
includeStudyGrades: true,
includeStudyIncludeAiScores: true,
includeEmptyStudies: true,
behaviourOutputFormat: 'single',
behaviourFileFormat: 'json',
};
},
computed: {
stepValid() {
if (["submissions", "grades"].includes(this.dataSelection.exportType)) {
return [
!!this.dataSelection.projectId && !!this.dataSelection.exportType, // must select a valid project and export type
this.submissionSelection.length > 0, // must select at least one student
this.userSelection.length > 0, // must select at least one student
true,
true
];
} else if (this.dataSelection.exportType === "documents") {
return [
!!this.dataSelection.projectId && !!this.dataSelection.exportType,
this.userSelection.length > 0,
this.selectedDocumentTypes.length > 0,
true,
];
} else if (this.dataSelection.exportType === 'studies') {
return [
!!this.dataSelection.projectId && !!this.dataSelection.exportType,
this.userSelection.length > 0,
this.selectedWorkflowIds.length > 0,
true,
];
} else if (this.dataSelection.exportType === 'userBehaviour') {
return [
!!this.dataSelection.projectId && !!this.dataSelection.exportType,
this.userSelection.length > 0,
true,
true,
];
}
return [
!!this.dataSelection.projectId && !!this.dataSelection.exportType,
true
];
},
steps() {
if (["submissions", "grades"].includes(this.dataSelection.exportType)) {
if (["submissions", "grades", "documents", "studies", "userBehaviour"].includes(this.dataSelection.exportType)) {
return [
{ title: "Settings" },
{ title: "Select Students" },
{ title: "Select Users" },
{ title: "Options" },
{ title: "Confirm Download" }
];
}
}
return [
{title: "Settings"},
{title: "Confirmation"}
Expand All @@ -191,6 +263,9 @@ export default {
{name: "Export a list of all reviewers", value: "reviewerList"},
{name: "Export submissions", value: "submissions"},
{name: "Export grades", value: "grades"},
{name: "Export documents", value:"documents"},
{name: "Export studies", value: "studies"},
...(this.$store.getters["auth/isAdmin"] ? [{name: "Export user behaviour", value: "userBehaviour"}] : []),
{name: "All", value: "all"},
],
required: true,
Expand Down Expand Up @@ -249,13 +324,35 @@ export default {
return this.$store.getters["table/project/getAll"];
},
},
watch: {
'dataSelection.exportType'() {
this.userSelection = [];
},
'dataSelection.projectId'() {
this.userSelection = [];
}
},
methods: {
open(projectId) {
this.dataSelection.projectId = projectId;
this.$refs.exportStepper.open();
},
hide() {
this.filter = [];
this.userSelection = [];
this.generateAliases = false;
this.fakerSeed = 846569412;
this.selectedDocumentTypes = [0, 1, 2, 4];
this.excludeNonConsentingEdits = false;
this.excludeNonConsentingAnnotations = false;
this.selectedWorkflowIds = [];
this.includeStudyDocumentFiles = true;
this.includeStudyGrades = true;
this.includeStudyIncludeAiScores = true;
this.includeEmptyStudies = true;
this.wait = false;
this.behaviourOutputFormat = 'single';
this.behaviourFileFormat = 'json';
},
downloadData() {
if (this.dataSelection.exportType === "reviewerList") {
Expand All @@ -264,6 +361,12 @@ export default {
this.downloadSubmissions();
} else if (this.dataSelection.exportType === "grades") {
this.downloadGrades();
} else if (this.dataSelection.exportType === "documents") {
this.downloadDocuments();
} else if (this.dataSelection.exportType === 'studies') {
this.downloadStudies();
} else if (this.dataSelection.exportType === 'userBehaviour') {
this.downloadUserBehaviour();
} else {
this.downloadAllData();
}
Expand Down Expand Up @@ -327,7 +430,7 @@ export default {
async downloadSubmissions() {
try {
// get the selected student's user ids
const selectedUserIds = this.submissionSelection.map(row => row.userId);
const selectedUserIds = this.userSelection.map(row => row.userId);

// call helper function to trigger the stream download
this.triggerStreamDownload({
Expand All @@ -346,7 +449,7 @@ export default {
},
async downloadGrades() {
try {
const selectedUserIds = this.submissionSelection.map(row => row.userId);
const selectedUserIds = this.userSelection.map(row => row.userId);
this.triggerStreamDownload({
projectId: this.dataSelection.projectId,
exportType: 'grades',
Expand All @@ -362,6 +465,61 @@ export default {
this.$toast.error("An error occurred starting the stream. Please try again.");
}
},
async downloadDocuments() {
try {
const selectedUserIds = this.userSelection.map(row => row.userId);
this.triggerStreamDownload({
projectId: this.dataSelection.projectId,
exportType: 'documents',
userIds: selectedUserIds,
documentTypes: this.selectedDocumentTypes,
excludeNonConsentingEdits: this.excludeNonConsentingEdits,
excludeNonConsentingAnnotations: this.excludeNonConsentingAnnotations
});

this.$refs.exportStepper.close();
} catch (error) {
console.error("Streaming error:", error);
this.$toast.error("An error occurred starting the stream. Please try again.");
}
},
async downloadStudies() {
try {
const selectedUserIds = this.userSelection.map(row => row.userId);
this.triggerStreamDownload({
projectId: this.dataSelection.projectId,
exportType: 'studies',
userIds: selectedUserIds,
workflowIds: this.selectedWorkflowIds,
includeEmptyStudies: this.includeEmptyStudies,
includeDocumentFiles: this.includeStudyDocumentFiles,
includeGrades: this.includeStudyGrades,
excludeNonConsentingEdits: this.excludeNonConsentingEdits,
excludeNonConsentingAnnotations: this.excludeNonConsentingAnnotations,
includeAiScores: this.includeStudyIncludeAiScores
});
this.$refs.exportStepper.close();
} catch (error) {
console.error("Streaming error:", error);
this.$toast.error("An error occurred starting the stream. Please try again.");
}
},
async downloadUserBehaviour() {
try {
const selectedUserIds = this.userSelection.map(row => row.userId);
this.triggerStreamDownload({
projectId: this.dataSelection.projectId,
exportType: 'userBehaviour',
userIds: selectedUserIds,
behaviourOutputFormat: this.behaviourOutputFormat,
behaviourFileFormat: this.behaviourFileFormat,
});
this.$refs.exportStepper.close();
} catch (error) {
console.error("Streaming error:", error);
this.$toast.error("An error occurred starting the stream. Please try again.");
}
},
async downloadAllData() {
this.wait = true;

Expand Down
Loading