diff --git a/frontend/src/components/export/DataExport.vue b/frontend/src/components/export/DataExport.vue index 4bed957d5d887366c03bd0b9235ccb3dea2bd02e..8fd23f3174eba2cb1134519b56419c2da0fd2212 100644 --- a/frontend/src/components/export/DataExport.vue +++ b/frontend/src/components/export/DataExport.vue @@ -42,7 +42,7 @@ @click="exportDialog = false" >close</v-btn> <v-spacer/> - <v-btn id="export-data-download-btn" flat outline @click="getExportFile" + <v-btn id="export-data-download-btn" flat outline @click="getExportFile('data')" >{{mapFile || mapFileLoaded ? 'Download and apply mapping' : 'Download without mapping'}}</v-btn> </v-card-actions> </v-card-text> @@ -56,105 +56,40 @@ import { getters } from '@/store/getters' import ax, { StudentExportItem, fetchStudentExportData } from '@/api' import FileSelect from '@/components/util/FileSelect.vue' import { mutations as mut } from '@/store/mutations' -import { parseCSVMapMixin } from '@/components/mixins/mixins' - -enum ExportType { - JSON = 'JSON', - CSV = 'CSV' -} +import { exportMixin, ExportType } from '@/components/mixins/mixins' @Component({ components: { FileSelect } }) -export default class DataExport extends Mixins(parseCSVMapMixin) { +export default class DataExport extends Mixins(exportMixin) { exportDialog = true mapFile: File | null = null setPasswords = false exportType = ExportType.CSV - get corrected () { return getters.corrected } get studentMap () { return getters.state.studentMap } - get mapFileLoaded () { - return Object.keys(getters.state.studentMap).length > 0 - } - get exportColor () { - return this.corrected ? 'green darken-1' : 'red lighten-1' - } - get availableExportTypes (): ExportType[] { - return Object.values(ExportType) - } - - showDialog () { - this.exportDialog = true - } - - readMapFileAndCommit () { - const fileReader = new FileReader() - return new Promise((resolve, reject) => { - // @ts-ignore - fileReader.onload = event => { - // @ts-ignore typings of EventTarget seem to be wrong - const studentMap = this.parseCSVMap(event.target.result) - mut.SET_STUDENT_MAP(studentMap) - resolve() - } - fileReader.onerror = () => { - fileReader.abort() - reject(new Error('Problem parsing input file.')) - } - - if (!this.mapFile) { - reject(new Error('Can only call' + - ' readMapFileAndCommit when mapFile is not undefined')) - } else { - fileReader.readAsText(this.mapFile) - } - }) - } applyMapping (studentExport: StudentExportItem[]) { - return studentExport.map(student => { - return { - ...student, - Matrikel: this.studentMap[student.Matrikel].matrikelNo, - Name: this.studentMap[student.Matrikel].name - } - }) - } - - jsonToCSV (studentExport: StudentExportItem[], delimeter = ';') { - let headerLine = Object.keys(studentExport[0]).reduce((acc: string, curr) => { - if (curr === 'Scores') { - return acc - } - return acc ? `${acc};${curr}` : `${curr}` - }, '') - headerLine += Object.values(studentExport[0].Scores) - .reduce((acc: string, curr) => { - return `${acc};${curr.type}` - }, '') - - const lines = studentExport.map(student => { - const normalFields = Object.values(student).reduce((acc: string, curr): string => { - // skip the Scores field - if (typeof curr === 'object') { - return acc + console.log(studentExport) + studentExport.forEach(student => { + if (this.studentMap[student.Matrikel]) { + student = { + ...student, + Matrikel: this.studentMap[student.Matrikel].matrikelNo, + Name: this.studentMap[student.Matrikel].name } - return acc ? `${acc};${curr}` : `${curr}` - }, '') - - const scoreFields = Object.values(student.Scores).reduce((acc: string, curr) => { - return `${acc};${curr.score}` - }, '') - return normalFields + scoreFields + } else { + this.$notify({ + title: `Unknown student: ${student.Name}`, + text: `Student ${student.Name} is missing in mapping file`, + type: 'error', + duration: -1 + }) + } }) - - return headerLine + lines.reduce((acc, curr) => { - return `${acc}\n${curr}` - }, '') + '\n' // add trailing newline } - createDownloadPopup (content: string | StudentExportItem[], fileType: ExportType) { + createDownloadPopup (content: string | StudentExportItem[], fileType: ExportType): void { const blobProperties: BlobPropertyBag = {} if (fileType === ExportType.JSON) { blobProperties.type = 'application/json' @@ -165,39 +100,6 @@ export default class DataExport extends Mixins(parseCSVMapMixin) { const blobData = new Blob([<string> content], blobProperties) window.open(window.URL.createObjectURL(blobData)) } - - optionalConvertAndCreatePopup (studentData: StudentExportItem[]) { - const convertedData = this.exportType === ExportType.CSV - ? this.jsonToCSV(studentData) : studentData - - this.createDownloadPopup(convertedData, this.exportType) - } - - async getMappedExportFile (studentData: StudentExportItem[]) { - if (!this.mapFile && !this.mapFileLoaded) { - throw new Error('Either mapFile must be selected or already loaded ' + - 'to call getMappedExportFile') - } - if (this.mapFile) { - await this.readMapFileAndCommit() - } - const mappedData = this.applyMapping(studentData) - this.optionalConvertAndCreatePopup(mappedData) - } - - async getExportFile () { - const studentData = await fetchStudentExportData({ setPasswords: this.setPasswords }) - - if (this.mapFile || this.mapFileLoaded) { - this.getMappedExportFile(studentData) - } else { - this.optionalConvertAndCreatePopup(studentData) - } - } - - hide () { - this.$emit('hide') - } } </script> diff --git a/frontend/src/components/export/InstanceExport.vue b/frontend/src/components/export/InstanceExport.vue index 91c63ba69a5a05e6fb508026c64a223729b74bf1..681e0e6b9958f92c2c2db814a320b2362d90ed1a 100644 --- a/frontend/src/components/export/InstanceExport.vue +++ b/frontend/src/components/export/InstanceExport.vue @@ -20,7 +20,7 @@ @click="exportDialog = false" >close</v-btn> <v-spacer/> - <v-btn id="instance-export-dl" flat outline @click="getExportFile" + <v-btn id="instance-export-dl" flat outline @click="getExportFile('instance')" >{{mapFile || mapFileLoaded ? 'Download and apply mapping' : 'Download without mapping'}}</v-btn> </v-card-actions> </v-card-text> @@ -34,50 +34,17 @@ import { getters } from '@/store/getters' import ax, { StudentExportItem, fetchStudentExportData, fetchInstanceExportData, InstanceExportData } from '@/api' import FileSelect from '@/components/util/FileSelect.vue' import { mutations as mut } from '@/store/mutations' -import { parseCSVMapMixin } from '@/components/mixins/mixins' +import { exportMixin, ExportType } from '@/components/mixins/mixins' @Component({ components: { FileSelect } }) -export default class DataExport extends Mixins(parseCSVMapMixin) { +export default class DataExport extends exportMixin { exportDialog = true mapFile: File | null = null + exportType = ExportType.JSON // instance export is only available as JSON - get corrected () { return getters.corrected } get studentMap () { return getters.state.studentMap } - get mapFileLoaded () { - return Object.keys(getters.state.studentMap).length > 0 - } - get exportColor () { - return this.corrected ? 'green darken-1' : 'red lighten-1' - } - - showDialog () { - this.exportDialog = true - } - - readMapFileAndCommit () { - const fileReader = new FileReader() - return new Promise((resolve, reject) => { - fileReader.onload = event => { - // @ts-ignore typings of EventTarget seem to be wrong - const studentMap = this.parseCSVMap(event.target.result) - mut.SET_STUDENT_MAP(studentMap) - resolve() - } - fileReader.onerror = () => { - fileReader.abort() - reject(new Error('Problem parsing input file.')) - } - - if (!this.mapFile) { - reject(new Error('Can only call' + - ' readMapFileAndCommit when mapFile is not undefined')) - } else { - fileReader.readAsText(this.mapFile) - } - }) - } applyMapping (instanceExport: InstanceExportData) { instanceExport.students.forEach(student => { @@ -96,39 +63,13 @@ export default class DataExport extends Mixins(parseCSVMapMixin) { }) } - createDownloadPopup (content: string | InstanceExportData) { + createDownloadPopup (content: string | InstanceExportData): void { const blobProperties: BlobPropertyBag = {} blobProperties.type = 'application/json' content = JSON.stringify(content) const blobData = new Blob([<string> content], blobProperties) window.open(window.URL.createObjectURL(blobData)) } - - async getMappedExportFile (studentData: InstanceExportData) { - if (!this.mapFile && !this.mapFileLoaded) { - throw new Error('Either mapFile must be selected or already loaded ' + - 'to call getMappedExportFile') - } - if (this.mapFile) { - await this.readMapFileAndCommit() - } - this.applyMapping(studentData) - this.createDownloadPopup(studentData) - } - - async getExportFile () { - const instanceData = await fetchInstanceExportData() - - if (this.mapFile || this.mapFileLoaded) { - this.getMappedExportFile(instanceData) - } else { - this.createDownloadPopup(instanceData) - } - } - - hide () { - this.$emit('hide') - } } </script> diff --git a/frontend/src/components/mixins/mixins.ts b/frontend/src/components/mixins/mixins.ts index caae87fff23b45ef41a8dcdd20a0135233e541bb..972371d08fd6f147cd4264f6836e1b75413c655a 100644 --- a/frontend/src/components/mixins/mixins.ts +++ b/frontend/src/components/mixins/mixins.ts @@ -1,7 +1,28 @@ import { Vue, Component } from 'vue-property-decorator' +import { fetchStudentExportData, StudentExportItem, InstanceExportData, fetchInstanceExportData } from '@/api' +import { getters } from '@/store/getters' +import { mutations as mut } from '@/store/mutations' + +export enum ExportType { + JSON = 'JSON', + CSV = 'CSV' +} @Component -export class parseCSVMapMixin extends Vue { +export class exportMixin extends Vue { + exportDialog = true + mapFile: File | null = null + setPasswords = false + exportType = ExportType.CSV + + get mapFileLoaded () { + return Object.keys(getters.state.studentMap).length > 0 + } + + get availableExportTypes (): ExportType[] { + return Object.values(ExportType) + } + parseCSVMap (csvMap: string) { let lines = csvMap.split('\n') lines.shift() // drop the first line since it contains only headings @@ -14,4 +35,107 @@ export class parseCSVMapMixin extends Vue { return acc }, {}) } + + async getExportFile (type: string) { + let studentData + if (type === 'data') { + studentData = await fetchStudentExportData({ setPasswords: this.setPasswords }) + } else if (type === 'instance') { + studentData = await fetchInstanceExportData() + } else { + throw new Error('Unsupported export type') + } + + if (this.mapFile || this.mapFileLoaded) { + this.getMappedExportFile(studentData) + } else { + this.optionalConvertAndCreatePopup(studentData) + } + } + + jsonToCSV (studentExport: StudentExportItem[], delimeter = ';') { + let headerLine = Object.keys(studentExport[0]).reduce((acc: string, curr) => { + if (curr === 'Scores') { + return acc + } + return acc ? `${acc};${curr}` : `${curr}` + }, '') + headerLine += Object.values(studentExport[0].Scores) + .reduce((acc: string, curr) => { + return `${acc};${curr.type}` + }, '') + + const lines = studentExport.map(student => { + const normalFields = Object.values(student).reduce((acc: string, curr): string => { + // skip the Scores field + if (typeof curr === 'object') { + return acc + } + return acc ? `${acc};${curr}` : `${curr}` + }, '') + + const scoreFields = Object.values(student.Scores).reduce((acc: string, curr) => { + return `${acc};${curr.score}` + }, '') + return normalFields + scoreFields + }) + + return headerLine + lines.reduce((acc, curr) => { + return `${acc}\n${curr}` + }, '') + '\n' // add trailing newline + } + + optionalConvertAndCreatePopup(studentData: StudentExportItem[] | InstanceExportData) { + const convertedData = this.exportType === ExportType.CSV + ? this.jsonToCSV(studentData as StudentExportItem[]) : studentData + // we have a cast here because only student export may be converted to csv + + this.createDownloadPopup(convertedData, this.exportType) + } + + async getMappedExportFile (studentData: StudentExportItem[] | InstanceExportData) { + if (!this.mapFile && !this.mapFileLoaded) { + throw new Error('Either mapFile must be selected or already loaded ' + + 'to call getMappedExportFile') + } + if (this.mapFile) { + await this.readMapFileAndCommit() + } + this.applyMapping(studentData) + this.optionalConvertAndCreatePopup(studentData) + } + + readMapFileAndCommit () { + const fileReader = new FileReader() + return new Promise((resolve, reject) => { + fileReader.onload = event => { + // @ts-ignore typings of EventTarget seem to be wrong + const studentMap = this.parseCSVMap(event.target.result) + mut.SET_STUDENT_MAP(studentMap) + resolve() + } + fileReader.onerror = () => { + fileReader.abort() + reject(new Error('Problem parsing input file.')) + } + + if (!this.mapFile) { + reject(new Error('Can only call' + + ' readMapFileAndCommit when mapFile is not undefined')) + } else { + fileReader.readAsText(this.mapFile) + } + }) + } + + hide () { + this.$emit('hide') + } + + showDialog () { + this.exportDialog = true + } + + applyMapping (exportData: StudentExportItem[] | InstanceExportData): void { throw new Error("Not implemented.") } + createDownloadPopup (content: string | StudentExportItem[] | InstanceExportData, fileType: ExportType): void { throw new Error("Not implemented.") } }