Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • j.michal/grady
1 result
Show changes
Commits on Source (27)
Showing
with 234 additions and 269 deletions
......@@ -7,8 +7,8 @@ stages:
- staging
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
CONTAINER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
DOCKER_DRIVER: overlay2
# ========================== Build Testing section =========================== #
build_test_env:
......@@ -39,6 +39,7 @@ build_frontend:
artifacts:
paths:
- frontend/dist
- frontend/node_modules/
expire_in: 20 minutes
cache:
key: "$CI_JOB_NAME"
......@@ -78,7 +79,7 @@ test_flake8:
<<: *test_definition_virtualenv
stage: test
script:
- flake8 --exclude=migrations core util
- flake8 --exclude=migrations core util functional_tests
# ----------------------------- Frontend subsection -------------------------- #
.test_template_frontend: &test_definition_frontend
......@@ -102,6 +103,17 @@ test_frontend:
- python manage.py collectstatic --no-input
- HEADLESS_TESTS=True pytest --ds=grady.settings.test functional_tests
test_frontend_unit:
image: node:carbon
stage: test
dependencies:
- build_frontend
tags:
- docker
script:
- cd frontend/
- yarn test:unit
# =========================== Build Image section ============================ #
build_backend:
......@@ -114,9 +126,12 @@ build_backend:
DOCKER_DRIVER: overlay2
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build -t $IMAGE_TAG .
- docker tag $IMAGE_TAG $IMAGE_TAG-$CI_COMMIT_SHA
- docker push $IMAGE_TAG
- docker pull "$CONTAINER_IMAGE-base" || true
- docker build --cache-from "$CONTAINER_IMAGE-base" -t "$CONTAINER_IMAGE-base" --target node .
- docker pull $CONTAINER_IMAGE || true
- docker build --cache-from $CONTAINER_IMAGE --cache-from "$CONTAINER_IMAGE-base" -t $CONTAINER_IMAGE .
- docker push "$CONTAINER_IMAGE-base"
- docker push $CONTAINER_IMAGE
tags:
- docker
......@@ -143,6 +158,7 @@ pages:
image: docker:latest
only:
- master
when: manual
before_script:
- apk add --update py-pip && pip install docker-compose
tags:
......
......@@ -30,6 +30,13 @@ install:
test:
DJANGO_SETTINGS_MODULE=grady.settings pytest
teste2e:
cd frontend && yarn build && cp dist/index.html ../core/templates && cd .. && python util/format_index.py && python manage.py collectstatic --no-input && HEADLESS_TESTS=$(headless) pytest --ds=grady.settings $(path); git checkout core/templates/index.html
teste2e-nc:
cp frontend/dist/index.html ./core/templates && python util/format_index.py && python manage.py collectstatic --no-input && HEADLESS_TESTS=$(headless) pytest --ds=grady.settings $(path); git checkout core/templates/index.html
coverage:
DJANGO_SETTINGS_MODULE=grady.settings pytest --cov
coverage html
......
......@@ -193,7 +193,7 @@ You need the following:
generated by [hektor](https://gitlab.gwdg.de/j.michal/hektor)
2. A .csv file where the columns are: id, name, score, (file suffix). No
suffix defaults to .c
Supported suffixes: .c , .java , .hs , .s (for mips)
Supported suffixes: .c , .java , .hs , .s or .asm (for mips)
Important: The name values must be the same as the ones that are contained in
the export file file from 1.
Example:
......@@ -204,16 +204,16 @@ You need the following:
a03, Gamma Ray, 20
```
3. A path to a directory with sample solutions named
<id>-lsg.c (same id as in 2.)
<id>.c (same id as in 2.)
4. A path to a directory containing HTML files with an accurate
description of the task. File name pattern has to be: <id>.html (same id as in 2.)
```commandline
$ tree -L 2
.
├── code-lsg
│ ├── a01-lsg.c
│ ├── a02-lsg.c
│ └── a03-lsg.c
│ ├── a01.c
│ ├── a02.java
│ └── a03.hs
└── html
├── a01.html
├── a02.html
......
......@@ -47,9 +47,10 @@ def get_annotated_tutor_list() -> QuerySet:
def get_random_element_from_queryset(queryset):
length = queryset.count()
qs_elements = queryset.all()
length = len(qs_elements)
index = secrets.choice(range(length))
return queryset.all()[index]
return qs_elements[index]
class ExamType(models.Model):
......@@ -662,7 +663,7 @@ class SubmissionSubscription(models.Model):
def get_available_in_stage(self) -> int:
try:
return self._get_available_submissions_in_subscription_stage().count() # noqa
except (SubscriptionTemporarilyEnded, SubscriptionEnded) as err:
except (SubscriptionTemporarilyEnded, SubscriptionEnded):
return 0
@transaction.atomic
......
......@@ -155,7 +155,7 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
try:
score = data.get('score')
submission = data.get('of_submission')
except KeyError as err:
except KeyError:
raise serializers.ValidationError(
'You need a score and a submission.')
......
......@@ -3,7 +3,8 @@ from rest_framework import serializers
from core.models import StudentInfo
from core.serializers import DynamicFieldsModelSerializer, ExamSerializer
from core.serializers.submission import (SubmissionListSerializer,
SubmissionNoTextFieldsSerializer)
SubmissionNoTextFieldsSerializer,
SubmissionNoTypeSerializer)
class StudentInfoSerializer(DynamicFieldsModelSerializer):
......@@ -42,3 +43,24 @@ class StudentInfoForListViewSerializer(DynamicFieldsModelSerializer):
'matrikel_no',
'passes_exam',
'is_active')
class StudentExportSerializer(DynamicFieldsModelSerializer):
name = serializers.ReadOnlyField(source='user.fullname')
user = serializers.ReadOnlyField(source='user.username')
user_pk = serializers.ReadOnlyField(source='user.pk')
exam = serializers.ReadOnlyField(source='exam.pk')
is_active = serializers.BooleanField(source='user.is_active')
submissions = SubmissionNoTypeSerializer(many=True)
class Meta:
model = StudentInfo
fields = ('pk',
'name',
'user',
'user_pk',
'exam',
'submissions',
'matrikel_no',
'passes_exam',
'is_active')
......@@ -10,13 +10,15 @@ def make_data():
'module_reference': 'Test Exam 01',
'total_score': 100,
'pass_score': 60,
'pass_only': True
}],
'submission_types': [
{
'name': '01. Sort',
'full_score': 35,
'description': 'Very complicated',
'solution': 'Trivial!'
'solution': 'Trivial!',
'programming_language': 'Haskell',
},
{
'name': '02. Shuffle',
......@@ -29,6 +31,9 @@ def make_data():
{'username': 'student01', 'exam': 'Test Exam 01'},
{'username': 'student02', 'exam': 'Test Exam 01'}
],
'tutors': [{
'username': 'tutor01'
}],
'reviewers': [
{'username': 'reviewer'}
],
......@@ -73,6 +78,86 @@ def make_data():
)
class ExportInstanceTest(APITestCase):
@classmethod
def setUpTestData(cls):
cls.data = make_data()
def setUp(self):
self.client = APIClient()
self.client.force_login(user=self.data['reviewers'][0])
self.response = self.client.get('/api/instance/export/')
def test_can_access(self):
self.assertEqual(status.HTTP_200_OK, self.response.status_code)
def test_data_is_correct(self):
instance = self.response.json()
# examTypes fields
self.assertIn('examTypes', instance)
self.assertIn('pk', instance['examTypes'][0])
self.assertEqual('Test Exam 01', instance['examTypes'][0]['moduleReference'])
self.assertEqual(100, instance['examTypes'][0]['totalScore'])
self.assertEqual(60, instance['examTypes'][0]['passScore'])
self.assertEqual(True, instance['examTypes'][0]['passOnly'])
# submissionTypes fields
self.assertIn('submissionTypes', instance)
self.assertEquals(2, len(instance['submissionTypes']))
self.assertIn('pk', instance['submissionTypes'][0])
self.assertEqual('01. Sort', instance['submissionTypes'][0]['name'])
self.assertEqual(35, instance['submissionTypes'][0]['fullScore'])
self.assertEqual('Very complicated', instance['submissionTypes'][0]['description'])
self.assertEqual('Trivial!', instance['submissionTypes'][0]['solution'])
self.assertEqual('Haskell', instance['submissionTypes'][0]['programmingLanguage'])
# students fields
self.assertIn('students', instance)
self.assertEqual(2, len(instance['students']))
self.assertIn('pk', instance['students'][0])
self.assertIn('userPk', instance['students'][0])
self.assertIn('exam', instance['students'][0])
self.assertEqual('student01', instance['students'][1]['user'])
self.assertLess(0, len(instance['students'][1]['submissions']))
# students[submissions] nested
self.assertIn('submissions', instance['students'][1])
self.assertLess(0, len(instance['students'][1]['submissions']))
self.assertIn('pk', instance['students'][1]['submissions'][0])
self.assertIn('function blabl', instance['students'][1]['submissions'][0]['text'])
self.assertIn('type', instance['students'][1]['submissions'][0])
self.assertIn('tests', instance['students'][1]['submissions'][0])
# students[submissions][feedback] nested
submissions = instance['students'][1]['submissions']
self.assertIn('feedback', submissions[0])
self.assertLess(0, len(submissions[0]['feedback']))
self.assertEqual(5, submissions[0]['feedback']['score'])
self.assertEqual(True, submissions[0]['feedback']['isFinal'])
self.assertIn('created', submissions[0]['feedback'])
# students[submissions][feedback][feedbackLines] nested
feedback = instance['students'][1]['submissions'][0]['feedback']
self.assertIn('feedbackLines', feedback)
self.assertLess(0, len(feedback['feedbackLines']))
self.assertIn('1', feedback['feedbackLines'])
self.assertIn('pk', feedback['feedbackLines']['1'][0])
self.assertEqual('This is very bad!', feedback['feedbackLines']['1'][0]['text'])
self.assertEqual('reviewer', feedback['feedbackLines']['1'][0]['ofTutor'])
# reviewers fields
self.assertIn('reviewers', instance)
self.assertLess(0, len(instance['reviewers']))
self.assertIn('pk', instance['reviewers'][0])
self.assertEqual('reviewer', instance['reviewers'][0]['username'])
# tutors fields
self.assertIn('tutors', instance)
self.assertLess(0, len(instance['tutors']))
self.assertEqual('tutor01', instance['tutors'][0]['username'])
class ExportJSONTest(APITestCase):
@classmethod
def setUpTestData(cls):
......
......@@ -43,7 +43,7 @@ class SubmissionSubscriptionRandomTest(APITestCase):
self.submission_02.delete()
try:
self.subscription.get_or_create_work_assignment()
except SubscriptionEnded as err:
except SubscriptionEnded:
self.assertFalse(False)
else:
self.assertTrue(False)
......@@ -59,7 +59,7 @@ class SubmissionSubscriptionRandomTest(APITestCase):
feedback_stage=SubmissionSubscription.FEEDBACK_VALIDATION)
try:
validation.get_or_create_work_assignment()
except SubscriptionTemporarilyEnded as err:
except SubscriptionTemporarilyEnded:
self.assertTrue(True)
else:
self.assertTrue(False)
......
......@@ -9,19 +9,19 @@ from core import views
# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register('student', views.StudentReviewerApiViewSet,
base_name='student')
basename='student')
router.register('examtype', views.ExamApiViewSet)
router.register('feedback', views.FeedbackApiView)
router.register('feedback-comment', views.FeedbackCommentApiView)
router.register('submission', views.SubmissionViewSet,
base_name='submission')
basename='submission')
router.register('submissiontype', views.SubmissionTypeApiView)
router.register('tutor', views.TutorApiViewSet, base_name='tutor')
router.register('tutor', views.TutorApiViewSet, basename='tutor')
router.register('subscription', views.SubscriptionApiViewSet,
base_name='subscription')
basename='subscription')
router.register('assignment', views.AssignmentApiViewSet)
router.register('statistics', views.StatisticsEndpoint, base_name='statistics')
router.register('user', views.UserAccountViewSet, base_name='user')
router.register('statistics', views.StatisticsEndpoint, basename='statistics')
router.register('user', views.UserAccountViewSet, basename='user')
schema_view = get_schema_view(
openapi.Info(
......@@ -46,6 +46,7 @@ regular_views_urlpatterns = [
path('jwt-time-delta/',
views.get_jwt_expiration_delta,
name='jwt-time-delta'),
path('instance/export/', views.InstanceExport.as_view(), name="instance-export"),
path('export/json/', views.StudentJSONExport.as_view(), name='export-json'),
re_path(r'swagger(?P<format>\.json|\.yaml)$',
schema_view.without_ui(cache_timeout=0), name='schema-json'),
......
from .feedback import FeedbackApiView, FeedbackCommentApiView # noqa
from .subscription import SubscriptionApiViewSet, AssignmentApiViewSet # noqa
from .common_views import * # noqa
from .export import StudentJSONExport # noqa
from .export import StudentJSONExport, InstanceExport # noqa
......@@ -10,7 +10,7 @@ import django.contrib.auth.password_validation as validators
from django.core import exceptions
from rest_framework import generics, mixins, status, viewsets
from rest_framework.decorators import (api_view, list_route, throttle_classes,
from rest_framework.decorators import (api_view, throttle_classes,
action)
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import AllowAny
......@@ -78,12 +78,12 @@ class StudentReviewerApiViewSet(viewsets.ReadOnlyModelViewSet):
user.is_active = active
user.save()
@list_route(methods=['post'])
@action(detail=False, methods=['post'])
def deactivate(self, request):
self._set_students_active(False)
return Response(status=status.HTTP_200_OK)
@list_route(methods=['post'])
@action(detail=False, methods=['post'])
def activate(self, request):
self._set_students_active(True)
return Response(status=status.HTTP_200_OK)
......@@ -111,7 +111,7 @@ class TutorApiViewSet(
.prefetch_related('subscriptions__assignments')
serializer_class = TutorSerializer
@list_route(methods=['post'], permission_classes=[AllowAny])
@action(detail=False, methods=['post'], permission_classes=[AllowAny])
@throttle_classes([AnonRateThrottle])
def register(self, request):
serializer = self.get_serializer(data=request.data)
......@@ -132,9 +132,11 @@ class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet):
class StatisticsEndpoint(viewsets.ViewSet):
def list(self, request, *args, **kwargs):
first_sub_type = models.SubmissionType.objects.first()
return Response({
'submissions_per_type':
models.SubmissionType.objects.first().submissions.count(),
first_sub_type.submissions.count() if first_sub_type is not None else 0,
'submissions_per_student':
models.SubmissionType.objects.count(),
......
......@@ -3,8 +3,12 @@ from rest_framework.views import APIView
import xkcdpass.xkcd_password as xp
from core.models import StudentInfo, UserAccount
from core.models import StudentInfo, UserAccount, ExamType, SubmissionType
from core.permissions import IsReviewer
from core.serializers.common_serializers import SubmissionTypeSerializer, \
ExamSerializer, UserAccountSerializer
from core.serializers.student import StudentExportSerializer
from core.serializers.tutor import TutorSerializer
words = xp.generate_wordlist(wordfile=xp.locate_wordfile(), min_length=5, max_length=8)
......@@ -42,3 +46,24 @@ class StudentJSONExport(APIView):
} for student
in StudentInfo.get_annotated_score_submission_list()]
return Response(content)
class InstanceExport(APIView):
permission_classes = (IsReviewer, )
def get(self, request):
exam_types_serializer = ExamSerializer(ExamType.objects.all(), many=True)
submission_types_serializer = SubmissionTypeSerializer(
SubmissionType.objects.all(), many=True)
tutors_serializer = TutorSerializer(UserAccount.tutors.with_feedback_count(), many=True)
reviewer_serializer = UserAccountSerializer(UserAccount.get_reviewers(), many=True)
student_serializer = StudentExportSerializer(StudentInfo.objects.all(), many=True)
content = {
"examTypes": exam_types_serializer.data,
"submissionTypes": submission_types_serializer.data,
"students": student_serializer.data,
"tutors": tutors_serializer.data,
"reviewers": reviewer_serializer.data
}
return Response(content)
......@@ -97,7 +97,7 @@ class AssignmentApiViewSet(
@permission_classes((IsReviewer,))
def list(self, *args, **kwargs):
super().list(*args, **kwargs)
return super().list(*args, **kwargs)
@action(detail=False, permission_classes=(IsReviewer,), methods=['get', 'delete'])
def active(self, request):
......
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"description": "Vue.js frontend for Grady",
"author": "robinwilliam.hundt <robinwilliam.hundt@stud.uni-goettingen.de>",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e"
"test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit --require mock-local-storage"
},
"dependencies": {
"axios": "^0.18.0",
......@@ -28,18 +28,22 @@
"devDependencies": {
"@types/chai": "^4.1.0",
"@types/highlight.js": "^9.12.3",
"@types/mocha": "^5.2.4",
"@types/mocha": "^5.2.5",
"@types/nightwatch": "^0.9.8",
"@vue/cli-plugin-e2e-nightwatch": "^3.0.0-rc.10",
"@types/sinon": "^7.0.2",
"@vue/cli-plugin-eslint": "^3.0.0-rc.10",
"@vue/cli-plugin-typescript": "^3.0.0-rc.10",
"@vue/cli-plugin-unit-mocha": "^3.0.0-rc.10",
"@vue/cli-service": "^3.0.0-rc.10",
"@vue/cli-plugin-typescript": "^3.2.0",
"@vue/cli-plugin-unit-mocha": "^3.2.0",
"@vue/cli-service": "^3.2.0",
"@vue/eslint-config-standard": "^3.0.0-rc.10",
"@vue/eslint-config-typescript": "^3.0.0-rc.10",
"@vue/test-utils": "^1.0.0-beta.20",
"chai": "^4.1.2",
"typescript": "^3.0.0",
"@vue/eslint-config-typescript": "^3.2.0",
"@vue/test-utils": "^1.0.0-beta.27",
"chai": "^4.2.0",
"mocha": "^5.2.0",
"mocha-webpack": "^1.1.0",
"mock-local-storage": "^1.1.8",
"sinon": "^7.2.2",
"typescript": "^3.2.2",
"vue-template-compiler": "^2.5.16"
}
}
......@@ -30,4 +30,7 @@ export default {
a {
text-decoration: none;
}
span {
tab-size: 4;
}
</style>
......@@ -25,7 +25,7 @@ let ax: AxiosInstance = axios.create({
baseURL: getInstanceBaseUrl()
})
{
let token = sessionStorage.getItem('token')
let token = window.sessionStorage.getItem('token')
if (token) {
ax.defaults.headers['Authorization'] = `JWT ${token}`
}
......@@ -221,4 +221,16 @@ export async function fetchStudentExportData (options: StudentExportOptions): Pr
return (await ax.post('/api/export/json/', options)).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 {
students: {
name: string,
matrikelNo: string
}[]
}
export async function fetchInstanceExportData (): Promise<InstanceExportData> {
return (await ax.get('/api/instance/export')).data
}
export default ax
......@@ -4,7 +4,7 @@
max-width="30%"
v-model="logoutDialog"
>
<v-card>
<v-card id="logout-dialog">
<v-card-title class="headline">
You'll be logged out!
</v-card-title>
......@@ -15,10 +15,12 @@
</v-card-text>
<v-card-actions>
<v-btn flat color="grey lighten-0"
id="logout-btn"
@click="logout"
>Logout now</v-btn>
<v-spacer/>
<v-btn flat color="blue darken-2"
id="continue-btn"
@click="continueWork"
>Continue</v-btn>
</v-card-actions>
......@@ -70,17 +72,17 @@ export default {
},
mounted () {
this.timer = setInterval(() => {
const timeToLogOutDialog = Math.min(600 * 1e3,
const timeDialogAppearsBeforeLogout = Math.min(600 * 1e3,
this.jwtTimeDelta ? this.jwtTimeDelta * 0.3 : Infinity)
if (this.$route.name !== 'login' && this.$store.getters.isLoggedIn) {
if (this.$route.name !== 'login' && Authentication.isLoggedIn) {
if (Date.now() > this.lastTokenRefreshTry + this.jwtTimeDelta) {
this.logoutDialog = false
actions.logout("You've been logged out due to inactivity.")
} else if (Date.now() + timeToLogOutDialog > this.lastTokenRefreshTry + this.jwtTimeDelta) {
} else if (Date.now() + timeDialogAppearsBeforeLogout > this.lastTokenRefreshTry + this.jwtTimeDelta) {
this.logoutDialog = true
}
}
}, 5 * 1e3)
}, 1 * 1e3)
},
beforeDestroy () {
clearInterval(this.timer)
......
......@@ -74,11 +74,11 @@
<v-btn slot="activator" color="cyan" style="text-transform: none">
{{ userRole }} | {{ username }} <v-icon>arrow_drop_down</v-icon>
</v-btn>
<user-options/>
<user-options class="mt-1"/>
</v-menu>
</div>
<v-btn color="blue darken-1" @click.native="logout">Logout</v-btn>
<slot name="toolbar-right"></slot>
<v-btn color="blue darken-1" id="logout" @click.native="logout">Logout</v-btn>
</v-toolbar>
</div>
</template>
......
<template>
<v-card class="py-2">
<v-card class="py-2" id="correction-statistics">
<v-card-title>
<span class="title">Statistics</span>
</v-card-title>
......
<template>
<div>
<v-tooltip bottom>
<v-btn :color="exportColor"
slot="activator"
@click="showDialog"
>
export
<v-icon>file_download</v-icon>
</v-btn>
<span v-if="corrected">All submissions have been corrected!</span>
<span v-else>UNCORRECTED submissions left! Export will be incomplete.</span>
</v-tooltip>
<v-dialog v-model="exportDialog" max-width="30vw">
<v-card>
<v-card-title class="title">
Student Data Export
</v-card-title>
<v-card-text>
<div v-if="!mapFileLoaded">
If you select a mapping file, the anonymized data
will be mapped back automatically and locally on your machine.
<v-layout row align-center>
<file-select v-model="mapFile" display-text="Select map file" class="ma-3"/>
<span>Without the mapping, the data will still be obfuscated.</span>
</v-layout>
</div>
<v-layout row>
<v-flex xs4>
<v-tooltip top>
<v-checkbox
label="Set passwords"
v-model="setPasswords"
slot="activator"
/>
<span>Setting this will cause all student passwords
to be reset upon export. The new passwords will be contained in the
export file.
</span>
</v-tooltip>
</v-flex>
<v-flex xs3 offset-xs1>
<v-select
label="Export file format"
:items="availableExportTypes"
v-model="exportType"
/>
</v-flex>
</v-layout>
<v-card-actions>
<v-btn
flat color="blue lighten-2"
@click="exportDialog = false"
>close</v-btn>
<v-spacer/>
<v-btn flat outline @click="getExportFile"
>{{mapFile || mapFileLoaded ? 'Download and apply mapping' : 'Download without mapping'}}</v-btn>
</v-card-actions>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script lang="ts">
import {Vue, Component, Mixins} from 'vue-property-decorator'
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'
}
@Component({
components: { FileSelect }
})
export default class DataExport extends Mixins(parseCSVMapMixin) {
exportDialog = false
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) => {
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 DOMException("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
}
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
}
createDownloadPopup (content: string | StudentExportItem[], fileType: ExportType) {
const blobProperties: BlobPropertyBag = {}
if (fileType === ExportType.JSON) {
blobProperties.type = 'application/json'
content = JSON.stringify(content)
} else {
blobProperties.type = 'text/csv'
}
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)
}
}
}
</script>
<style scoped>
</style>