From 05086310b547920ad4a2c487d64e1b8d83b5c19d Mon Sep 17 00:00:00 2001 From: "robinwilliam.hundt" <robinwilliam.hundt@stud.uni-goettingen.de> Date: Wed, 9 Oct 2019 19:00:25 +0200 Subject: [PATCH] Added frontend part for import ui --- frontend/src/api.ts | 4 + frontend/src/components/BaseLayout.vue | 2 +- frontend/src/components/ImportDialog.vue | 91 +++++++++++++++++++ frontend/src/components/UserOptions.vue | 16 +++- frontend/src/components/util/FileSelect.vue | 3 +- frontend/src/pages/StartPageSelector.vue | 3 +- .../src/pages/reviewer/ReviewerStartPage.vue | 48 +++++++++- functional_tests/data/hektor.json | 34 +++++++ functional_tests/test_import.py | 54 +++++++++++ 9 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/ImportDialog.vue create mode 100644 functional_tests/data/hektor.json create mode 100644 functional_tests/test_import.py diff --git a/frontend/src/api.ts b/frontend/src/api.ts index c13a4fef..c616cf07 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -256,6 +256,10 @@ export async function fetchStudentExportData (options: StudentExportOptions): Pr return (await ax.post('/api/export/json/', options)).data } +export async function importData (data: Object): Promise<AxiosResponse<void>> { + return ax.post('/api/import/', data) +} + // Note, this interface does not represent all of the returned data, // but only the fields which have to be transformed for deanonymisation export interface InstanceExportData { diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index ae32a6af..789b03e5 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -71,7 +71,7 @@ <slot name="toolbar-center"/> <div class="toolbar-content"> <v-menu bottom offset-y v-if="!isStudent"> - <v-btn slot="activator" color="cyan" style="text-transform: none"> + <v-btn id="user-options" slot="activator" color="cyan" style="text-transform: none"> {{ userRole }} | {{ username }} <v-icon>arrow_drop_down</v-icon> </v-btn> <user-options class="mt-1" v-if="!isStudent"/> diff --git a/frontend/src/components/ImportDialog.vue b/frontend/src/components/ImportDialog.vue new file mode 100644 index 00000000..1f88e5cd --- /dev/null +++ b/frontend/src/components/ImportDialog.vue @@ -0,0 +1,91 @@ +<template> + <v-dialog v-model="show" width="30%"> + <v-card> + <v-card-title class="title">Import data</v-card-title> + <v-card-text> + <p> + You can use this component to import data into Grady. + You can use + <a + href="https://gitlab.gwdg.de/grady-corp/rusty-hektor" + target="_blank" + >rusty-hektor</a> to convert + and pseudonomize ILIAS output. + </p> + <file-select v-model="hektorFile" display-text="Select json file" /> + </v-card-text> + <v-card-actions> + <v-btn @click="submitData" :loading="loading" id="submit-import">Import</v-btn> + <v-btn @click="$emit('hide')" color="red">Cancel</v-btn> + </v-card-actions> + </v-card> + </v-dialog> +</template> + +<script> + import FileSelect from "@/components/util/FileSelect.vue"; + import { importData } from "@/api"; + + export default { + name: "ImportDialog", + components: { + FileSelect + }, + data: () => { + return { + show: true, + loading: false, + hektorFile: null + }; + }, + methods: { + async submitData() { + this.loading = true + let data; + try { + data = await this.readFile(); + data = JSON.parse(data) + } catch (error) { + this.$notify({ + type: 'error', + title: 'Error reading import file', + text: error.message + }) + this.loading = false + return + } + + try { + await importData(data) + this.$emit('imported') + this.$notify({ + title: 'Successfully imported data. Please log out and in again.', + type: 'success' + }) + } finally { + this.loading = false + } + }, + readFile() { + const fileReader = new FileReader(); + return new Promise((resolve, reject) => { + fileReader.onload = event => { + resolve(event.target.result); + }; + fileReader.onerror = () => { + fileReader.abort(); + reject(new Error("Problem parsing input file.")); + }; + fileReader.readAsText(this.hektorFile) + }); + } + }, + watch: { + show(val) { + if (!val) { + this.$emit("hide"); + } + } + } + }; +</script> diff --git a/frontend/src/components/UserOptions.vue b/frontend/src/components/UserOptions.vue index 465288e3..db59c490 100644 --- a/frontend/src/components/UserOptions.vue +++ b/frontend/src/components/UserOptions.vue @@ -5,6 +5,7 @@ <v-list-tile v-if="opt.condition()" @click="opt.action" + :id="opt.id" :key="i" > {{opt.display}} @@ -17,11 +18,12 @@ <script> import PasswordChangeDialog from '@/components/PasswordChangeDialog' +import ImportDialog from '@/components/ImportDialog' import { Authentication } from '@/store/modules/authentication' import { deleteAllActiveAssignments } from '@/api' export default { name: 'UserOptions', - components: { PasswordChangeDialog }, + components: { PasswordChangeDialog, ImportDialog }, data () { return { displayComponent: null, @@ -29,12 +31,20 @@ export default { { display: 'Change password', action: () => { this.displayComponent = PasswordChangeDialog }, - condition: () => !Authentication.isStudent + condition: () => !Authentication.isStudent, + id: "change-password-list-tile" }, { display: 'Free all reserved submissions', action: deleteAllActiveAssignments, - condition: () => Authentication.isReviewer + condition: () => Authentication.isReviewer, + id: "free-assignments-list-tile" + }, + { + display: 'Import data', + action: () => { this.displayComponent = ImportDialog }, + condition: () => Authentication.isReviewer, + id: "import-data-list-tile" } ] } diff --git a/frontend/src/components/util/FileSelect.vue b/frontend/src/components/util/FileSelect.vue index 1bf8b055..8e5334d9 100644 --- a/frontend/src/components/util/FileSelect.vue +++ b/frontend/src/components/util/FileSelect.vue @@ -4,7 +4,7 @@ <span v-if="value">Selected: {{value.name}}</span> <span v-else>{{displayText}}</span> </div> - <input type="file" @change="handleFileChange"/> + <input id="file-input" type="file" @change="handleFileChange"/> </label> </template> @@ -17,7 +17,6 @@ export default { default: 'Select File' } }, - methods: { handleFileChange (e) { this.$emit('input', e.target.files[0]) diff --git a/frontend/src/pages/StartPageSelector.vue b/frontend/src/pages/StartPageSelector.vue index 4cb6a3c2..397111a8 100644 --- a/frontend/src/pages/StartPageSelector.vue +++ b/frontend/src/pages/StartPageSelector.vue @@ -1,6 +1,5 @@ <template> - <component :is="startPage"> - </component> + <component :is="startPage"/> </template> <script> diff --git a/frontend/src/pages/reviewer/ReviewerStartPage.vue b/frontend/src/pages/reviewer/ReviewerStartPage.vue index 860c602f..ca74f4e8 100644 --- a/frontend/src/pages/reviewer/ReviewerStartPage.vue +++ b/frontend/src/pages/reviewer/ReviewerStartPage.vue @@ -1,5 +1,5 @@ <template> - <div> + <div v-if="dataLoaded"> <v-layout row wrap> <v-flex lg5 md9 xs12> <correction-statistics class="ma-4"></correction-statistics> @@ -10,22 +10,66 @@ </v-layout> <SubmissionTypesOverview class="ma-4"/> </div> + <v-layout v-else justify-center class="mt-4 pt-4"> + <import-dialog + v-if="showImportDialog" + @hide="showImportDialog = false" + @imported="importDone" + /> + <v-card class="import-card"> + <v-card-title class="title"> + Import data + </v-card-title> + <v-card-text> + It looks like this instance doesn't contain any data. + Would you like to import some? + </v-card-text> + <v-card-actions class="justify-center"> + <v-btn @click="showImportDialog = true" class="info">Import data</v-btn> + </v-card-actions> + </v-card> + </v-layout> </template> <script> import CorrectionStatistics from '@/components/CorrectionStatistics' +import ImportDialog from '@/components/ImportDialog' import SubscriptionList from '@/components/subscriptions/SubscriptionList' import SubmissionTypesOverview from '@/components/submission_type/SubmissionTypesOverview' +import { getters } from '../../store/getters' +import { Subscriptions } from '../../store/modules/subscriptions' export default { components: { + ImportDialog, SubmissionTypesOverview, SubscriptionList, CorrectionStatistics }, - name: 'reviewer-start-page' + name: 'reviewer-start-page', + data: () => { + return { + showImportDialog: false, + dataImported: false + } + }, + computed: { + dataLoaded () { + return Object.keys(getters.state.submissionTypes).length !== 0 || this.dataImported + } + }, + methods: { + importDone() { + this.dataImported = true + Subscriptions.RESET_STATE() + } + } } </script> <style scoped> +.import-card { + width: 30%; +} + </style> diff --git a/functional_tests/data/hektor.json b/functional_tests/data/hektor.json new file mode 100644 index 00000000..19ecbb0a --- /dev/null +++ b/functional_tests/data/hektor.json @@ -0,0 +1,34 @@ +{ + "meta": { + "version": "3.0.0" + }, + "module": { + "module_reference": "B.Inf.1801", + "total_score": 50, + "pass_score": 25, + "pass_only": false + }, + "submission_types": [ + { + "name": "Eine Bibliothek für Permutationen (I1-ID: l120mlc005h0)", + "full_score": 50, + "description": "A <b>description</b>!", + "solution": "Blub", + "programming_language": "java" + } + ], + "students": [ + { + "fullname": "Test, User", + "identifier": "20000000", + "username": "TU20000000", + "submissions": [ + { + "code": "234;", + "type": "Eine Bibliothek für Permutationen (I1-ID: l120mlc005h0)", + "tests": {} + } + ] + } + ] +} diff --git a/functional_tests/test_import.py b/functional_tests/test_import.py new file mode 100644 index 00000000..58e89030 --- /dev/null +++ b/functional_tests/test_import.py @@ -0,0 +1,54 @@ +import os +from django.test import LiveServerTestCase +from selenium import webdriver +from selenium.webdriver.support.ui import WebDriverWait + +from core import models +from functional_tests.util import (login, create_browser, query_returns_object, + reset_browser_after_test) +from util import factory_boys as fact + + +JSON_EXPORT_FILE = os.path.join(os.path.dirname(__file__), 'data/hektor.json') + + +class TestImport(LiveServerTestCase): + browser: webdriver.Firefox = None + username = None + password = None + role = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.browser = create_browser() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls.browser.quit() + + def setUp(self): + super().setUp() + self.username = 'rev' + self.password = 'p' + fact.UserAccountFactory( + username=self.username, + password=self.password, + role=models.UserAccount.REVIEWER + ) + + def tearDown(self): + reset_browser_after_test(self.browser, self.live_server_url) + + def _login(self): + login(self.browser, self.live_server_url, self.username, self.password) + + def test_reviewer_can_import_data(self): + self._login() + self.browser.find_element_by_id("user-options").click() + self.browser.find_element_by_id("import-data-list-tile").click() + file_input = self.browser.find_element_by_id("file-input") + file_input.send_keys(JSON_EXPORT_FILE) + self.browser.find_element_by_id("submit-import").click() + WebDriverWait(self.browser, 20).until(query_returns_object(models.SubmissionType)) -- GitLab