Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
65 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
88fcad9
fix include scores switch not reaching ExportModal
melolw Sep 7, 2026
201fb41
remove unused BasicButton import
melolw Sep 7, 2026
d46b1cd
extract study/document/user-behaviour export processors into helper
melolw Sep 7, 2026
451a8ff
add export processors helper
melolw Sep 7, 2026
49559c4
reject empty workflowIds on studies export, add missing JSDoc
melolw Sep 7, 2026
d0dc244
merge duplicate docblocks on buildGradeRecords
melolw Sep 7, 2026
a47f0a3
add getSortedStudySteps, matching getSortedWorkflowSteps
melolw Sep 7, 2026
ef5459a
fix: use boolean acceptDataSharing instead of Yes/No strings in expor…
melolw Sep 7, 2026
10a94df
Merge branch 'dev' into feat-331-data-export
melolw Sep 7, 2026
9f79413
i18n: translate summary sentence and per-row list, fix returnObjects bug
melolw Sep 7, 2026
e4a3749
i18n: translate document options labels, switch to bg-body-tertiary
melolw Sep 7, 2026
4593c17
i18n: translate study options labels, replace raw button with BasicBu…
melolw Sep 7, 2026
c735388
i18n: translate user behaviour options labels
melolw Sep 7, 2026
03fda52
i18n: translate table columns and headings, fix acceptStatsSharing bo…
melolw Sep 7, 2026
8494998
i18n: add selectAll/unselectAll keys
melolw Sep 7, 2026
3ff77dc
i18n: add export options/select-users keys, remove keys orphaned by S…
melolw Sep 7, 2026
25b3620
i18n: add selectAll/unselectAll keys
melolw Sep 7, 2026
870fa57
i18n: add export options/select-users keys, remove keys orphaned by S…
melolw Sep 7, 2026
8de1147
i18n: return export validation errors as i18n keys instead of plain text
melolw Sep 7, 2026
17bc66a
i18n: send export route errors as {message: key} JSON, hide raw archi…
melolw Sep 7, 2026
75988d5
i18n: add export api error message keys
melolw Sep 7, 2026
1693e15
i18n: add export api error message keys
melolw Sep 7, 2026
2131086
fix: replace all name occurrences in zip anonymization, allow seed 0;…
melolw Sep 7, 2026
6fa6cf5
feat: anonymize zip documents in documents/studies exports when alias…
melolw Sep 7, 2026
a7e9790
feat: pass alias settings through to documents export
melolw Sep 7, 2026
0394fbe
feat: expose generate-aliases option for documents/studies export types
melolw Sep 7, 2026
dc6c71e
fix: guard workflowIds parsing against malformed JSON
melolw Sep 7, 2026
630b48f
fix: derive grades CSV header from union of all rows' columns
melolw Sep 7, 2026
72682d8
fix: don't silently drop study steps orphaned by a soft-deleted inter…
melolw Sep 7, 2026
5f4f470
utils/modules/i18n/en/dashboard.json
melolw Sep 7, 2026
ba689db
fix: warn on the correct consent flag for userBehaviour export declines
melolw Sep 7, 2026
d6ab47d
fix: only default-select all workflows once, not on every recompute
melolw Sep 7, 2026
983e377
fix: exclude soft-deleted documents from studies export step files
melolw Sep 7, 2026
f76b724
perf: cache anonymized ZIP buffers by hash across review sessions
melolw Sep 7, 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"
Comment thread
melolw marked this conversation as resolved.
Outdated
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