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

First step towards REST + Vue Student Page

Backend
- Added isStudent permission
- Added serializers apropriate for StudentPage
- Added api-endpoint urls
- Added views regarding StudentPage
- Migrated the Django Session Authentication to djangorestframework-jwt authentication

Frontend
- Migrated from vue-resource to axios because of better documentation & features
- Added Login Component
- Added First Part of StudentPage containing overview of exam and subissions

Closes #41
References #39
parent 13d8e4fb
No related branches found
No related tags found
2 merge requests!15Refactor,!13Student page
Pipeline #
Showing
with 343 additions and 120 deletions
from rest_framework import permissions
from core.custom_annotations import in_groups
from core.models import Student, Submission, Feedback
from core.models import Student
class StudentRequestOwnData(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if in_groups(request.user, ['Students']):
student = request.user.student
if isinstance(obj, Student):
return student == obj
elif isinstance(obj, Submission):
return student == obj.student
elif isinstance(obj, Feedback):
return student == obj.of_submission.student
return False
class IsStudent(permissions.BasePermission):
def has_permission(self, request, view):
user = request.user
return user.is_authenticated() and isinstance(user.get_associated_user(), Student)
from django.contrib.auth.models import User
from rest_framework import serializers
from core.models import Student, Submission, Feedback
from core.models import Student, Submission, Feedback, ExamType
class ExamSerializer(serializers.ModelSerializer):
class Meta:
model = ExamType
fields = ('module_reference', 'total_score', 'pass_score', 'pass_only',)
class FeedbackSerializer(serializers.ModelSerializer):
......@@ -11,15 +15,20 @@ class FeedbackSerializer(serializers.ModelSerializer):
class SubmissionSerializer(serializers.ModelSerializer):
feedback = FeedbackSerializer()
feedback = serializers.ReadOnlyField(source='feedback.text')
score = serializers.ReadOnlyField(source='feedback.score')
type = serializers.ReadOnlyField(source='type.name')
full_score = serializers.ReadOnlyField(source='type.full_score')
class Meta:
model = Submission
fields = ('seen_by_student', 'text', 'type', 'student', 'feedback')
fields = ('type', 'text', 'feedback', 'score', 'full_score')
class StudentSerializer(serializers.ModelSerializer):
name = serializers.ReadOnlyField(source='user.fullname')
user = serializers.ReadOnlyField(source='user.username')
exam = ExamSerializer()
submissions = SubmissionSerializer(many=True)
class Meta:
......
from django.conf.urls import url
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from rest_framework_jwt.views import obtain_jwt_token
from core import views
......@@ -27,8 +28,10 @@ urlpatterns = [
url(r'^csv/$', views.export_csv, name='export'),
url(r'^api/student/$', views.StudentApiView.as_view()),
url(r'^api/submission/$', views.SubmissionApiView.as_view()),
url(r'^api/feedback/$', views.FeedbackApiView.as_view()),
url(r'^api/student/submission/(?P<pk>[0-9]+)$', views.SubmissionApiView.as_view()),
url(r'^api/student/submission/(?P<pk>[0-9]+)/feedback/$', views.FeedbackApiView.as_view()),
url(r'^api-token-auth/', obtain_jwt_token)
]
urlpatterns += staticfiles_urlpatterns()
from core.models import Student, Submission, Feedback
from core.serializers import SubmissionSerializer, StudentSerializer, FeedbackSerializer
from rest_framework.generics import RetrieveAPIView
from core.permissions import IsStudent
class StudentApiView(RetrieveAPIView):
permission_classes = (IsStudent,)
def get_object(self):
return self.request.user.student
serializer_class = StudentSerializer
class SubmissionApiView(RetrieveAPIView):
def get_object(self):
permission_classes = (IsStudent,)
def get_queryset(self):
return self.request.user.student.submissions
serializer_class = SubmissionSerializer
class FeedbackApiView(RetrieveAPIView):
permission_classes = (IsStudent,)
def get_queryset(self):
return [submission.feedback for submission in self.request.user.submissions]
serializer_class = FeedbackSerializer
class StudentPageView(RetrieveAPIView):
queryset = Student.objects.all()
......@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import datetime
from django.contrib.messages import constants as messages
......@@ -143,3 +144,19 @@ AUTH_PASSWORD_VALIDATORS = []
CORS_ORIGIN_WHITELIST = (
'localhost:8080'
)
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=600),
}
Django~=1.11.3
django-extensions~=1.7.7
djangorestframework~=3.6.3
djangorestframework-jwt~=1.11.0
django_compressor~=2.1.1
gunicorn~=19.7.0
psycopg2~=2.7.1
......
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view/>
</div>
</template>
<script>
export default {
name: 'app'
name: 'app',
components: {
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
File moved
<template>
<span>{{msg}}</span>
<div>
<span>{{msg}}</span>
<span>{{token}}</span>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
import axios from 'axios'
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App',
token: ''
}
},
created: function () {
axios.post('http://localhost:8000/api-token-auth/', {
username: 'robin',
password: 'p'
}).then(token => {
this.token = token.data.token
})
}
}
}
</script>
<template>
<div class="mx-auto col-md-4 col-xl-2" id="login">
<img src="../assets/brand.png"/>
<h2>Log in</h2>
<p>Log in to your account to grade stuff!</p>
<div class="aler alert-danger" v-if="error">
<p>{{ error }}</p>
</div>
<div class="form-group">
<input
type="text"
class="form-control"
placeholder="Enter your username"
v-model="credentials.username"
/>
</div>
<div class="form-group" @keyup.enter="submit()">
<input
type="password"
class="form-control"
placeholder="Enter your password"
v-model="credentials.password"
/>
</div>
<button class="btn btn-primary" @click="submit()">Access</button>
</div>
</template>
<script>
export default {
name: 'grady-login',
data () {
return {
credentials: {
username: '',
password: ''
},
error: ''
}
},
methods: {
submit () {
const credentials = {
username: this.credentials.username,
password: this.credentials.password
}
this.$store.dispatch('getToken', credentials).then(response => {
this.$router.push('/student/')
})
}
}
}
</script>
<style scoped>
#login {
text-align: center;
margin-top: 10%;
}
</style>
<template>
</template>
<script>
</script>
<style>
</style>
<template>
<table class="table table-info rounded">
<tbody>
<tr>
<th>Modul</th>
<td>{{ exam.module_reference }}</td>
</tr>
<tr v-if="!exam.pass_only">
<th>Pass score</th>
<td>{{ exam.pass_score }}</td>
</tr>
<tr v-else>
<th>Pass only!</th>
</tr>
<tr>
<th>Total score</th>
<td>{{ exam.total_score }}</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
name: 'exam-information',
props: ['exam']
}
</script>
<template>
<b-navbar toggleable="md" type="light" variant="light">
<b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
<b-navbar-brand>
<img src="../../assets/brand.png" width="30" class="d-inline-block align-top">
Grady
</b-navbar-brand>
<b-collapse is-nav id="nav_collapse">
<b-navbar-nav id="nav-left">
<b-nav-item class="active" href="#">Results</b-nav-item>
<b-nav-item href="#">Statistics</b-nav-item>
</b-navbar-nav>
<!-- Right aligned nav items -->
<b-navbar-nav class="ml-auto">
<b-nav-item>{{ this.$store.state.username }}</b-nav-item>
<router-link to="/">
<b-button class="btn-dark" @click="logout()" >Signout</b-button>
</router-link>
</b-navbar-nav>
</b-collapse>
</b-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>
<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>
<submission-list :submissions="submissions"></submission-list>
</div>
</div>
</div>
</div>
</template>
<script>
import ax from '@/store/api'
import GradyNav from './StudentNav.vue'
import SubmissionList from './SubmissionList.vue'
import ExamInformation from './ExamInformation.vue'
export default {
components: {
ExamInformation,
SubmissionList,
GradyNav},
name: 'student-page',
data () {
return {
studentData: {},
doneLoading: false
}
},
created: function () {
this.doneLoading = false
ax.get('api/student/').then(response => {
this.studentData = response.data
this.doneLoading = true
})
},
computed: {
submissions () {
return this.studentData.submissions
},
exam () {
return this.studentData.exam
}
}
}
</script>
<style scoped>
</style>
<template>
</template>
<script>
export default {
computed: {
}
}
</script>
<template>
<ul>
<li v-for="sub in submissions">
<span>{{sub.type}}</span>
<span>{{sub.text}}</span>
<span>{{sub.feedback}}</span>
</li>
</ul>
<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>
</div>
</template>
<script>
export default {
data: function () {
name: 'submission-list',
data () {
return {
submissions: []
fields: [
{ key: 'type', sortable: true },
{ key: 'score', label: 'Score', sortable: true },
{ key: 'full_score', sortable: true }
]
}
},
created: function () {
this.getSubmissions()
},
methods: {
getSubmissions () {
this.$http.get('http://localhost:8000/api/student/1').then(student => {
return Promise.all(student.body.submissions.map(id => {
return this.$http.get(`http://localhost:8000/api/submission/${id}`)
}))
}).then(response => {
console.log(response)
this.submissions = response.map(item => { return item.body })
}).catch(console.log.bind(console))
props: ['submissions'],
computed: {
sumScore () {
return this.submissions.map(a => a.score).reduce((a, b) => a + b)
},
sumFullScore () {
return this.submissions.map(a => a.full_score).reduce((a, b) => a + b)
},
pointRatio () {
return ((this.sumScore / this.sumFullScore) * 100).toFixed(2)
}
}
}
......
......@@ -2,16 +2,20 @@
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import BootstrapVue from 'bootstrap-vue'
import router from './router'
import VueResource from 'vue-resource'
import store from './store/store'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import 'bootstrap/dist/css/bootstrap.css'
Vue.use(VueResource)
Vue.use(BootstrapVue)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
template: '<App/>',
components: { App }
......
import Vue from 'vue'
import Router from 'vue-router'
import SubmissionList from '@/components/student/SubmissionList'
import Login from '@/components/Login'
import StudentPage from '@/components/student/StudentPage'
Vue.use(Router)
......@@ -8,8 +9,13 @@ export default new Router({
routes: [
{
path: '/',
name: 'Submissions',
component: SubmissionList
name: 'grady-login',
component: Login
},
{
path: '/student/',
name: 'student-page',
component: StudentPage
}
]
})
import Vue from 'vue'
import VueResource from 'vue-resource'
import axios from 'axios'
Vue.use(VueResource)
var ax = axios.create({
baseURL: 'http://localhost:8000/'
})
export default {
get (url, request) {
return Vue.http.get(url, request)
.then((response) => Promise.resolve(response))
.catch((error) => Promise.reject(error))
},
post (url, request) {
return Vue.http.post(url, request)
.then((response) => Promise.resolve(response))
.catch((error) => Promise.reject(error))
},
patch (url, request) {
return Vue.http.patch(url, request)
.then((response) => Promise.resolve(response))
.catch((error) => Promise.reject(error))
},
delete (url, request) {
return Vue.http.delete(url, request)
.then((response) => Promise.resolve(response))
.catch((error) => Promise.reject(error))
}
}
export default ax
import Vuex from 'vuex'
import Vue from 'vue'
import ax from './api'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
token: '',
loggedIn: false,
username: ''
},
mutations: {
'LOGIN': function (state, creds) {
state.token = creds.token
state.loggedIn = true
state.username = creds.username
},
'LOGOUT': function (state) {
state.token = ''
state.loggedIn = false
}
},
actions: {
async getToken (store, credentials) {
const response = await ax.post('api-token-auth/', credentials)
store.commit('LOGIN', {
token: response.data.token,
username: credentials.username
})
ax.defaults.headers.common['Authorization'] = 'JWT ' + response.data.token
},
logout (store) {
store.commit('LOGOUT')
}
}
})
export default store
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