Skip to content
Snippets Groups Projects
Commit 91f057be authored by robinwilliam.hundt's avatar robinwilliam.hundt
Browse files

Merge remote-tracking branch 'origin/69-general-layout-navigation' into...

Merge remote-tracking branch 'origin/69-general-layout-navigation' into 39-a-new-student-overview-page-with-vue-js
parents c5fe33d6 e4be7981
No related branches found
No related tags found
1 merge request!23Resolve "Logout of tutors after inactivity"
Showing
with 248 additions and 81 deletions
......@@ -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)
......
......@@ -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)
......
......@@ -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')),
......
""" 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:
......
......@@ -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",
......
frontend/src/assets/logo.png

6.69 KiB

......@@ -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
})
......
<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>
<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>
<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>
<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
})
......
<template>
<p>Test</p>
</template>
<script>
export default {
name: 'submission-detail'
}
</script>
<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 },
......
......@@ -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)
......
......@@ -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
}
]
})
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
......@@ -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', '')
}
}
})
......
......@@ -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"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment