diff --git a/core/tests/test_access_rights.py b/core/tests/test_access_rights.py index 6e172626ff78f4374547336ebd2399e9c0461b09..9c22248b907e106d7a78448fb011655a04b5bb45 100644 --- a/core/tests/test_access_rights.py +++ b/core/tests/test_access_rights.py @@ -4,7 +4,7 @@ from rest_framework.test import (APIRequestFactory, APITestCase, force_authenticate) from core.views import (ExamApiViewSet, StudentReviewerApiViewSet, - StudentSelfApiViewSet, TutorApiViewSet) + StudentSelfApiView, TutorApiViewSet) from util.factories import GradyUserFactory @@ -21,8 +21,8 @@ class AccessRightsOfStudentAPIViewTests(APITestCase): self.student = self.user_factory.make_student() self.tutor = self.user_factory.make_tutor() self.reviewer = self.user_factory.make_reviewer() - self.request = self.factory.get(reverse('student_page-list')) - self.view = StudentSelfApiViewSet.as_view({'get': 'retrieve'}) + self.request = self.factory.get(reverse('student-page')) + self.view = StudentSelfApiView.as_view() def test_unauthenticated_access_denied(self): response = self.view(self.request) diff --git a/core/tests/test_student_page.py b/core/tests/test_student_page.py index 930163ad8c94ebb906210976e06f6de8253c3b14..099c54cd6d54d4bb4b910d4feee6180395274433 100644 --- a/core/tests/test_student_page.py +++ b/core/tests/test_student_page.py @@ -4,7 +4,7 @@ from rest_framework.test import (APIRequestFactory, APITestCase, from core.models import Reviewer, SubmissionType from core.tests import data_factories -from core.views import StudentSelfApiViewSet +from core.views import StudentSelfApiView class StudentPageTests(APITestCase): @@ -19,8 +19,8 @@ class StudentPageTests(APITestCase): self.student = self.submission.student self.reviewer = Reviewer.objects.create( user=data_factories.make_user(username='reviewer')) - self.request = self.factory.get(reverse('student_page-list')) - self.view = StudentSelfApiViewSet.as_view({'get': 'retrieve'}) + self.request = self.factory.get(reverse('student-page')) + self.view = StudentSelfApiView.as_view() force_authenticate(self.request, user=self.student.user) self.response = self.view(self.request) diff --git a/core/urls.py b/core/urls.py index 845578227821504d6eb67111ecf6ea7c33593140..7511c18649e358f4af225e18f74a4f1fa267faff 100644 --- a/core/urls.py +++ b/core/urls.py @@ -12,11 +12,15 @@ router.register(r'student', views.StudentReviewerApiViewSet) router.register(r'examtype', views.ExamApiViewSet) router.register(r'submissiontype', views.SubmissionTypeApiView) router.register(r'tutor', views.TutorApiViewSet) -router.register(r'student-page', views.StudentSelfApiViewSet, - base_name='student_page') + +# regular views that are not viewsets +regular_views_urlpatterns = [ + url(r'student-page', views.StudentSelfApiView.as_view(), name='student-page') +] urlpatterns = [ url(r'^api/', include(router.urls)), + url(r'^api/', include(regular_views_urlpatterns)), url(r'^api-token-auth/', obtain_jwt_token), url(r'^api-token-refresh', refresh_jwt_token), url(r'^$', TemplateView.as_view(template_name='index.html')), diff --git a/core/views.py b/core/views.py index 563331875203a5312d98bbc74e4e5e5afa15ee40..ed932990555d678923b716a80c204fde4ebaf4ce 100644 --- a/core/views.py +++ b/core/views.py @@ -1,7 +1,7 @@ """ All API views that are used to retrieve data from the database. They can be categorized by the permissions they require. All views require a user to be authenticated and most are only accessible by one user group """ -from rest_framework import mixins, viewsets +from rest_framework import mixins, viewsets, generics from core.models import ExamType, Student, SubmissionType, Tutor from core.permissions import IsReviewer, IsStudent @@ -10,10 +10,9 @@ from core.serializers import (ExamSerializer, StudentSerializer, SubmissionTypeSerializer, TutorSerializer) -class StudentSelfApiViewSet(viewsets.ReadOnlyModelViewSet): +class StudentSelfApiView(generics.RetrieveAPIView): """ Gets all data that belongs to one student """ permission_classes = (IsStudent,) - queryset = Student.objects.all() serializer_class = StudentSerializer def get_object(self) -> Student: diff --git a/frontend/package.json b/frontend/package.json index 9a8b2be5838dca92a043717b4fb8e678d35a8756..df857580b0d73ecbe6c26b4116cac54a204a78b6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "axios": "^0.17.0", + "material-design-icons": "^3.0.1", "vue": "^2.5.2", "vue-router": "^3.0.1", "vuetify": "^0.17.3", diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png deleted file mode 100644 index f3d2503fc2a44b5053b0837ebea6e87a2d339a43..0000000000000000000000000000000000000000 Binary files a/frontend/src/assets/logo.png and /dev/null differ diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue index fddc17eecc103aa5bac87342fbd1e78fb165eca7..92e3a906c3de047889f2560f8a810089dd0d161a 100644 --- a/frontend/src/components/Login.vue +++ b/frontend/src/components/Login.vue @@ -49,7 +49,7 @@ methods: { submit () { this.$store.dispatch('getJWTToken', this.credentials).then(response => { - this.$router.push('/reviewer/') + this.$router.push('/student/') }).catch(_ => { this.error = this.$store.state.error }) diff --git a/frontend/src/components/base/BaseLayout.vue b/frontend/src/components/base/BaseLayout.vue new file mode 100644 index 0000000000000000000000000000000000000000..26d45fe79899e68ffbed62e44b68447a2a0205ff --- /dev/null +++ b/frontend/src/components/base/BaseLayout.vue @@ -0,0 +1,87 @@ +<template> + <div> + <v-navigation-drawer + fixed + clipped + app + v-model="drawer" + > + <v-toolbar flat> + <v-list> + <v-list-tile> + <v-list-tile-title class="title"> + {{ examInstance }} + </v-list-tile-title> + </v-list-tile> + </v-list> + </v-toolbar> + <v-list dense> + <v-list-tile v-for="(item, i) in navItems" :key="i" :to="item.route"> + {{ item.name }} + </v-list-tile> + </v-list> + </v-navigation-drawer> + <v-toolbar + app + clipped-left + fixed + dark + color="indigo darken-4" + class="grady-toolbar" + > + <v-toolbar-title> + <v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon> + <v-avatar> + <img src="../../assets/brand.png"> + </v-avatar> + </v-toolbar-title> + <span class="pl-2 grady-speak">{{ gradySpeak }}</span> + <div class="toolbar-content"> + <span>{{ userRole }} | {{ username }}</span> + </div> + <v-btn color="blue darken-1" to="/" @click.native="logout">Logout</v-btn> + </v-toolbar> + <v-content> + <slot></slot> + </v-content> + </div> +</template> + +<script> + import { mapActions, mapGetters, mapState } from 'vuex' + export default { + name: 'base-layout', + data () { + return { + drawer: true + } + }, + props: ['navItems'], + computed: { + ...mapGetters([ + 'gradySpeak' + ]), + ...mapState([ + 'examInstance', + 'username', + 'userRole' + ]) + }, + methods: { + ...mapActions([ + 'logout' + ]) + } + } +</script> + +<style scoped> + .toolbar-content { + margin-left: auto; + } + + .grady-toolbar { + font-weight: bold; + } + +</style> diff --git a/frontend/src/components/student/StudentLayout.vue b/frontend/src/components/student/StudentLayout.vue new file mode 100644 index 0000000000000000000000000000000000000000..105f55c30fcc3e4fedb75b4c262c1ca9cf54ecef --- /dev/null +++ b/frontend/src/components/student/StudentLayout.vue @@ -0,0 +1,25 @@ +<template> + <base-layout + :navItems="navItems" + > + <slot></slot> + </base-layout> +</template> + +<script> + import BaseLayout from '../base/BaseLayout' + export default { + components: {BaseLayout}, + name: 'student-layout', + data () { + return { + navItems: [ + { + name: 'Login', + route: '/' + } + ] + } + } + } +</script> diff --git a/frontend/src/components/student/StudentNav.vue b/frontend/src/components/student/StudentNav.vue deleted file mode 100644 index 3b2484e9d207c8f3e87b8ff9d67eaaec93a1e4d9..0000000000000000000000000000000000000000 --- a/frontend/src/components/student/StudentNav.vue +++ /dev/null @@ -1,38 +0,0 @@ -<template> - <v-navbar toggleable="md" type="light" variant="light"> - <v-navbar-toggle target="nav_collapse"></v-navbar-toggle> - - <v-navbar-brand> - <img src="../../assets/brand.png" width="30" class="d-inline-block align-top"> - Grady - </v-navbar-brand> - - <v-collapse is-nav id="nav_collapse"> - - <v-navbar-nav id="nav-left"> - <v-nav-item class="active" href="#">Results</v-nav-item> - <v-nav-item href="#">Statistics</v-nav-item> - </v-navbar-nav> - - <!-- Right aligned nav items --> - <v-navbar-nav class="ml-auto"> - <v-nav-item>{{ this.$store.state.username }}</v-nav-item> - <router-link to="/"> - <v-button class="btn-dark" @click="logout()" >Signout</v-button> - </router-link> - </v-navbar-nav> - </v-collapse> - </v-navbar> -</template> - - -<script> - export default { - name: 'grady-nav', - methods: { - logout () { - this.$store.dispatch('logout') - } - } - } -</script> diff --git a/frontend/src/components/student/StudentPage.vue b/frontend/src/components/student/StudentPage.vue index b1e9249692fde918bbbfb6f48106301ed3fb62ef..686550e29f57f3f254c265fc81631b434c394ff5 100644 --- a/frontend/src/components/student/StudentPage.vue +++ b/frontend/src/components/student/StudentPage.vue @@ -1,25 +1,24 @@ <template> - <div> - <grady-nav></grady-nav> - <div class="container-fluid"> - <div class="row justify-content-center my-3"> - <div class="col-md-3"> - <h2 class="my-5">Exam Overview</h2> + <student-layout> + <v-container fluid> + <v-layout justify center> + <v-flex md3> + <h2>Exam Overview</h2> <exam-information v-if="doneLoading" :exam="exam"></exam-information> - </div> - <div class="col-md-6 offset-md-1" v-if="doneLoading"> - <h2 class="my-5">Submissions of {{ this.studentData.name }}</h2> + </v-flex> + <v-flex md7 offset-md1 v-if="doneLoading"> + <h2>Submissions of {{ this.studentData.name }}</h2> <submission-list :submissions="submissions"></submission-list> - </div> - </div> - </div> - </div> + </v-flex> + </v-layout> + </v-container> + </student-layout> </template> <script> import ax from '@/store/api' - import GradyNav from './StudentNav.vue' + import StudentLayout from './StudentLayout.vue' import SubmissionList from './SubmissionList.vue' import ExamInformation from './ExamInformation.vue' @@ -27,7 +26,7 @@ components: { ExamInformation, SubmissionList, - GradyNav}, + StudentLayout}, name: 'student-page', data () { return { @@ -37,7 +36,7 @@ }, created: function () { this.doneLoading = false - ax.get('api/student/').then(response => { + ax.get('api/student-page/').then(response => { this.studentData = response.data this.doneLoading = true }) diff --git a/frontend/src/components/student/SubmissionDetail.vue b/frontend/src/components/student/SubmissionDetail.vue new file mode 100644 index 0000000000000000000000000000000000000000..56f6dc744d9169b5681c63b9091a1b673c2c5852 --- /dev/null +++ b/frontend/src/components/student/SubmissionDetail.vue @@ -0,0 +1,10 @@ +<template> + <p>Test</p> +</template> + + +<script> + export default { + name: 'submission-detail' + } +</script> diff --git a/frontend/src/components/student/SubmissionList.vue b/frontend/src/components/student/SubmissionList.vue index c39e745fc152cca06a50428cc0f9fe595b858230..6d0238b118813d39a12f97f97266b7485323839d 100644 --- a/frontend/src/components/student/SubmissionList.vue +++ b/frontend/src/components/student/SubmissionList.vue @@ -1,9 +1,19 @@ <template> <div class="row my-2 justify-content-center"> - <b-table hover :items="submissions" :fields="fields"></b-table> - <div class="alert alert-info"> - You reached <b>{{ sumScore }}</b> of <b>{{ sumFullScore }}</b> possible points( {{ pointRatio }}% ). - </div> + <v-data-table + hide-actions + :headers="headers" + :items="submissions" + > + <template slot="items" slot-scope="props"> + <td>{{ props.item.type }}</td> + <td class="text-xs-right">{{ props.item.score }}</td> + <td class="text-xs-right">{{ props.item.full_score }}</td> + </template> + </v-data-table> + <v-alert color="info" value="true"> + You reached <b>{{ sumScore }}</b> of <b>{{ sumFullScore }}</b> possible points ( {{ pointRatio }}% ). + </v-alert> </div> </template> @@ -13,6 +23,22 @@ name: 'submission-list', data () { return { + headers: [ + { + text: 'Task', + align: 'left', + value: 'type' + }, + { + text: 'Score', + value: 'score' + }, + { + text: 'Maximum Score', + value: 'full_score' + } + ], + fields: [ { key: 'type', sortable: true }, { key: 'score', label: 'Score', sortable: true }, diff --git a/frontend/src/main.js b/frontend/src/main.js index 997422b82770d3b595a39127ea98f36215f9c98c..9a43b0eea0680622375f3caf543207d03cbc4a1e 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -7,6 +7,7 @@ import store from './store/store' import Vuetify from 'vuetify' import 'vuetify/dist/vuetify.min.css' +import 'material-design-icons/iconfont/material-icons.css' Vue.use(Vuetify) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index efb8236906d71e2dc99b1f2f5453851413f6988a..3e5f67cc59bb4ecb70ffb8ba3a9a94e3c2e4862b 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -2,8 +2,10 @@ import Vue from 'vue' import Router from 'vue-router' import Login from '@/components/Login' import StudentPage from '@/components/student/StudentPage' +import SubmissionDetail from '@/components/student/SubmissionDetail' import ReviewerPage from '@/components/reviewer/ReviewerPage' import StudentListOverview from '@/components/reviewer/StudentListOverview' +import BaseLayout from '@/components/base/BaseLayout' Vue.use(Router) @@ -17,7 +19,14 @@ export default new Router({ { path: '/student/', name: 'student-page', - component: StudentPage + component: StudentPage, + children: [ + { + path: 'submission:id/', + component: SubmissionDetail + } + ] + }, { path: '/reviewer/', @@ -28,6 +37,11 @@ export default new Router({ path: 'reviewer/student-overview/', name: 'student-overview', component: StudentListOverview + }, + { + path: '/base/', + name: 'base-layout', + component: BaseLayout } ] }) diff --git a/frontend/src/store/grady_speak.js b/frontend/src/store/grady_speak.js new file mode 100644 index 0000000000000000000000000000000000000000..6ae7abba064d4c3f07922ad004073bea8baacbfc --- /dev/null +++ b/frontend/src/store/grady_speak.js @@ -0,0 +1,25 @@ +const gradySays = [ + 'Now let\'s see if we can improve this with a little water, sir.', + 'Won\'t keep you a moment, sir.', + 'Grady, sir. Delbert Grady.', + 'Yes, sir.', + 'That\'s right, sir.', + 'Why no, sir. I don\'t believe so.', + 'Ah ha, it\'s coming off now, sir.', + 'Why no, sir. I don\'t believe so.', + 'Yes, sir. I have a wife and two daughters, sir.', + 'Oh, they\'re somewhere around. I\'m not quite sure at the moment, sir.', + 'That\'s strange, sir. I don\'t have any recollection of that at all.', + 'I\'m sorry to differ with you, sir, but you are the caretaker.', + 'You have always been the caretaker, I should know, sir.', + 'I\'ve always been here.', + 'Indeed, he is, Mr. Torrance. A very willful boy. ', + 'A rather naughty boy, if I may be so bold, sir.', + 'Perhaps they need a good talking to, if you don\'t mind my saying so. Perhaps a bit more.', + 'My girls, sir, they didn\'t care for the Overlook at first.', + 'One of them actually stole a packet of matches and tried to burn it down.', + 'But I corrected them, sir.', + 'And when my wife tried to prevent me from doing my duty... I corrected her.' +] + +export default gradySays diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 3eb032bf8effaf4fb5e4e2e69237d1c74fb588b7..9766bf89d1f78a8004278f201912618921eba887 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -2,26 +2,37 @@ import Vuex from 'vuex' import Vue from 'vue' import ax from './api' +import gradySays from './grady_speak' + Vue.use(Vuex) const store = new Vuex.Store({ state: { token: '', loggedIn: false, - username: '', - error: '' + username: 'username', + userRole: 'Student', + error: '', + examInstance: 'B.Inf 1301 Kohorte 2' + }, + getters: { + gradySpeak: state => { + return gradySays[Math.floor(Math.random() * gradySays.length)] + } }, mutations: { 'API_FAIL': function (state, error) { state.error = error }, - 'LOGIN': function (state, creds) { - state.token = creds.token + 'SET_JWT_TOKEN': function (state, token) { + state.token = token + ax.defaults.headers.common['Authorization'] = 'JWT ' + token + }, + 'LOGIN': function (state, username) { state.loggedIn = true - state.username = creds.username + state.username = username }, 'LOGOUT': function (state) { - state.token = '' state.loggedIn = false } }, @@ -29,10 +40,8 @@ const store = new Vuex.Store({ async getJWTToken (context, credentials) { try { const response = await ax.post('api-token-auth/', credentials) - context.commit('LOGIN', { - token: response.data.token, - username: credentials.username - }) + context.commit('LOGIN', credentials.username) + context.commit('SET_JWT_TOKEN', response.data.token) } catch (error) { if (error.response) { const errorMsg = 'Unable to log in with provided credentials.' @@ -47,6 +56,7 @@ const store = new Vuex.Store({ }, logout (store) { store.commit('LOGOUT') + store.commit('SET_JWT_TOKEN', '') } } }) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 7fac4471d3aef38b2a829c2ace79c724663298f8..e4ccbb2e378208009ccbf7e2e92e305fef76116a 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3704,6 +3704,10 @@ map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" +material-design-icons@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf" + math-expression-evaluator@^1.2.14: version "1.2.17" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"