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