diff --git a/core/permissions.py b/core/permissions.py index bb478558c4f08d7f219bd683d7ab42eadea1d00b..211ed2fb337a792156257d30b2fc808ace7a9f7c 100644 --- a/core/permissions.py +++ b/core/permissions.py @@ -22,7 +22,7 @@ class IsUserGenericPermission(permissions.BasePermission): ) user = request.user - is_authorized = user.is_authenticated() and any(isinstance( + is_authorized = user.is_authenticated and any(isinstance( user.get_associated_user(), models) for models in self.models) if not is_authorized: diff --git a/core/serializers.py b/core/serializers.py index 44b91717174fe9af54cddc52ce0e668564f31b28..74db00cfbd19e3cbe4cc08981ee15ad471859d05 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -4,7 +4,7 @@ from drf_dynamic_fields import DynamicFieldsMixin from rest_framework import serializers from core.models import (ExamType, Feedback, Student, Submission, - SubmissionType, Tutor) + SubmissionType, Test, Tutor) from util.factories import GradyUserFactory log = logging.getLogger(__name__) @@ -13,7 +13,19 @@ user_factory = GradyUserFactory() class DynamicFieldsModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer): - pass + def __init__(self, *args, **kwargs): + # Don't pass the 'fields' arg up to the superclass + fields = kwargs.pop('fields', None) + + # Instantiate the superclass normally + super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) + + if fields is not None: + # Drop any fields that are not specified in the `fields` argument. + allowed = set(fields) + existing = set(self.fields.keys()) + for field_name in existing - allowed: + self.fields.pop(field_name) class ExamSerializer(DynamicFieldsModelSerializer): @@ -31,35 +43,50 @@ class FeedbackSerializer(DynamicFieldsModelSerializer): fields = ('text', 'score') +class TestSerializer(DynamicFieldsModelSerializer): + + class Meta: + model = Test + fields = ('name', 'label', 'annotation') + + class SubmissionTypeSerializer(DynamicFieldsModelSerializer): + fullScore = serializers.IntegerField(source='full_score') class Meta: model = SubmissionType - fields = ('name', 'full_score', 'description', 'solution') + fields = ('id', 'name', 'fullScore', 'description', 'solution') class SubmissionSerializer(DynamicFieldsModelSerializer): - feedback = serializers.ReadOnlyField(source='feedback.text') - score = serializers.ReadOnlyField(source='feedback.score') - type_id = serializers.ReadOnlyField(source='type.id') - type_name = serializers.ReadOnlyField(source='type.name') - full_score = serializers.ReadOnlyField(source='type.full_score') + type = SubmissionTypeSerializer() + feedback = FeedbackSerializer() + tests = TestSerializer(many=True) class Meta: model = Submission - fields = ('type_id', 'type_name', 'text', - 'feedback', 'score', 'full_score') + fields = ('type', 'text', 'feedback', 'tests') + + +class SubmissionListSerializer(DynamicFieldsModelSerializer): + type = SubmissionTypeSerializer(fields=('id', 'name', 'fullScore')) + # TODO change this according to new feedback model + feedback = FeedbackSerializer(fields=('score',)) + + class Meta: + model = Submission + fields = ('type', 'feedback') class StudentSerializer(DynamicFieldsModelSerializer): name = serializers.ReadOnlyField(source='user.fullname') - user = serializers.ReadOnlyField(source='user.username') + matrikel_no = serializers.ReadOnlyField(source='user.matrikel_no') exam = ExamSerializer() - submissions = SubmissionSerializer(many=True) + submissions = SubmissionListSerializer(many=True) class Meta: model = Student - fields = ('name', 'user', 'exam', 'submissions') + fields = ('name', 'user', 'matrikel_no', 'exam', 'submissions') class SubmissionNoTextFieldsSerializer(DynamicFieldsModelSerializer): diff --git a/core/tests/test_student_page.py b/core/tests/test_student_page.py index 8bd9f349564483dea02e3c4ffc927f4ec65c8e4c..2af05f2be40a884bb09934e1155265f3f2660a82 100644 --- a/core/tests/test_student_page.py +++ b/core/tests/test_student_page.py @@ -3,7 +3,7 @@ from rest_framework.test import (APIRequestFactory, APITestCase, force_authenticate) from core.models import SubmissionType -from core.views import StudentSelfApiView +from core.views import StudentSelfApiView, StudentSelfSubmissionsApiView from util.factories import make_test_data @@ -57,7 +57,6 @@ class StudentPageTests(APITestCase): 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) @@ -69,10 +68,6 @@ class StudentPageTests(APITestCase): self.assertEqual( self.response.data['name'], self.student.user.fullname) - def test_student_contains_associated_user(self): - self.assertEqual( - self.response.data['user'], self.student.user.username) - def test_all_student_submissions_are_loded(self): self.assertEqual(len(self.submission_list), SubmissionType.objects.count()) @@ -98,34 +93,116 @@ class StudentPageTests(APITestCase): # Tests concerning submission data def test_a_student_submissions_contains_type_name(self): self.assertEqual( - self.submission_list_first_entry['type_name'], + self.submission_list_first_entry['type']['name'], self.student.submissions.first().type.name) def test_a_student_submissions_contains_type_id(self): self.assertEqual( - self.submission_list_first_entry['type_id'], + self.submission_list_first_entry['type']['id'], self.student.submissions.first().type.id) - def test_submission_data_contains_text(self): + def test_submission_data_contains_full_score(self): self.assertEqual( - self.submission_list_first_entry['text'], - self.student.submissions.first().text) + self.submission_list_first_entry['type']['fullScore'], + self.student.submissions.first().type.full_score) - def test_submission_data_contains_feedback(self): + def test_submission_data_contains_feedback_score(self): self.assertEqual( - self.submission_list_first_entry['feedback'], - self.student.submissions.first().feedback.text) + self.submission_list_first_entry['feedback']['score'], + self.student.submissions.first().feedback.score) + + # We don't want a matriculation number here + def test_matriculation_number_is_not_send(self): + self.assertNotIn('matrikel_no', self.submission_list_first_entry) + + +class StudentSelfSubmissionsTests(APITestCase): + + @classmethod + def setUpTestData(cls): + cls.factory = APIRequestFactory() + + def setUp(self): + self.test_data = make_test_data(data_dict={ + 'submission_types': [{ + 'name': 'problem01', + 'full_score': 10, + 'description': 'Very hard', + 'solution': 'Impossible!' + }], + 'students': [{ + 'username': 'user01', + }], + 'tutors': [{ + 'username': 'tutor01' + }], + 'submissions': [{ + 'user': 'user01', + 'type': 'problem01', + 'text': 'Too hard for me ;-(', + 'feedback': { + 'of_tutor': 'tutor01', + 'text': 'Very bad!', + 'score': 3 + } + }] + }) + + self.student = self.test_data['students'][0] + self.tutor = self.test_data['tutors'][0] + self.submission = self.test_data['submissions'][0] + self.feedback = self.submission.feedback + + self.request = self.factory.get(reverse('student-submissions')) + self.view = StudentSelfSubmissionsApiView.as_view() + + force_authenticate(self.request, user=self.student.user) + self.response = self.view(self.request) - def test_submission_data_contains_score(self): + self.submission_list = self.response.data + self.submission_list_first_entry = self.submission_list[0] + + # Tests concerning submission data + def test_a_student_submissions_contains_type_name(self): self.assertEqual( - self.submission_list_first_entry['score'], - self.student.submissions.first().feedback.score) + self.submission_list_first_entry['type']['name'], + self.student.submissions.first().type.name) + + def test_a_student_submissions_contains_type_id(self): + self.assertEqual( + self.submission_list_first_entry['type']['id'], + self.student.submissions.first().type.id) def test_submission_data_contains_full_score(self): self.assertEqual( - self.submission_list_first_entry['full_score'], + self.submission_list_first_entry['type']['fullScore'], self.student.submissions.first().type.full_score) + def test_submission_data_contains_description(self): + self.assertEqual( + self.submission_list_first_entry['type']['description'], + self.student.submissions.first().type.description) + + def test_submission_data_contains_solution(self): + self.assertEqual( + self.submission_list_first_entry['type']['solution'], + self.student.submissions.first().type.solution) + + def test_submission_data_contains_text(self): + self.assertEqual( + self.submission_list_first_entry['text'], + self.student.submissions.first().text) + + def test_submission_data_contains_feedback_score(self): + self.assertEqual( + self.submission_list_first_entry['feedback']['score'], + self.student.submissions.first().feedback.score) + + def test_submission_data_contains_feedback_text(self): + self.assertEqual( + self.submission_list_first_entry['feedback']['text'], + self.student.submissions.first().feedback.text) + # We don't want a matriculation number here def test_matriculation_number_is_not_send(self): self.assertNotIn('matrikel_no', self.submission_list_first_entry) diff --git a/core/tests/test_submissiontypeview.py b/core/tests/test_submissiontypeview.py index c1c7f4b096e2bebee299d89abefb04fbd254816f..0bd370da38fb30272d295784593848663ac09314 100644 --- a/core/tests/test_submissiontypeview.py +++ b/core/tests/test_submissiontypeview.py @@ -10,7 +10,7 @@ from core.views import SubmissionTypeApiView from util.factories import GradyUserFactory -class SubmissionTypeViewTest(APITestCase): +class SubmissionTypeViewTestList(APITestCase): @classmethod def setUpTestData(cls): @@ -37,10 +37,41 @@ class SubmissionTypeViewTest(APITestCase): self.assertEqual('Hard question', self.response.data[0]['name']) def test_get_full_score(self): - self.assertEqual(20, self.response.data[0]['full_score']) + self.assertEqual(20, self.response.data[0]['fullScore']) + + +class SubmissionTypeViewTestRetrieve(APITestCase): + + @classmethod + def setUpTestData(cls): + cls.factory = APIRequestFactory() + cls.user_factory = GradyUserFactory() + + def setUp(self): + self.request = self.factory.get('/api/submissiontype/') + SubmissionType.objects.create(name='Hard question', + full_score=20, + description='Whatever') + self.pk = SubmissionType.objects.first().pk + force_authenticate(self.request, + self.user_factory.make_reviewer().user) + self.view = SubmissionTypeApiView.as_view({'get': 'retrieve'}) + self.response = self.view(self.request, pk=self.pk) + + def test_can_access_when_authenticated(self): + self.assertEqual(self.response.status_code, status.HTTP_200_OK) + + def test_get_id(self): + self.assertEqual(self.pk, self.response.data['id']) + + def test_get_sumbission_type_name(self): + self.assertEqual('Hard question', self.response.data['name']) + + def test_get_full_score(self): + self.assertEqual(20, self.response.data['fullScore']) def test_get_descritpion(self): - self.assertEqual('Whatever', self.response.data[0]['description']) + self.assertEqual('Whatever', self.response.data['description']) def test_there_is_no_solution_to_nothing(self): - self.assertEqual('', self.response.data[0]['solution']) + self.assertEqual('', self.response.data['solution']) diff --git a/core/urls.py b/core/urls.py index cb66e7d2222fa0fd4a9706a00c8bac8339c93646..2f2e47e682c450f8efe0bdd2a892b3424a7e5f01 100644 --- a/core/urls.py +++ b/core/urls.py @@ -17,6 +17,8 @@ router.register(r'tutor', views.TutorApiViewSet) regular_views_urlpatterns = [ url(r'student-page', views.StudentSelfApiView.as_view(), name='student-page'), + url(r'student-submissions', views.StudentSelfSubmissionsApiView.as_view(), + name='student-submissions'), url(r'user-role', views.get_user_role, name='user-role'), url(r'jwt-time-delta', views.get_jwt_expiration_delta, name='jwt-time-delta') diff --git a/core/views.py b/core/views.py index 6d5a9595d19d6c8451c84da2f92c6f2a674c0dcc..2b87315b923a92222eb84cffa8b04734c20245a1 100644 --- a/core/views.py +++ b/core/views.py @@ -10,7 +10,8 @@ from core.models import ExamType, Student, SubmissionType, Tutor from core.permissions import IsReviewer, IsStudent from core.serializers import (ExamSerializer, StudentSerializer, StudentSerializerForListView, - SubmissionTypeSerializer, TutorSerializer) + SubmissionSerializer, SubmissionTypeSerializer, + TutorSerializer) @api_view() @@ -35,6 +36,14 @@ class StudentSelfApiView(generics.RetrieveAPIView): return self.request.user.student +class StudentSelfSubmissionsApiView(generics.ListAPIView): + permission_classes = (IsStudent, ) + serializer_class = SubmissionSerializer + + def get_queryset(self): + return self.request.user.student.submissions + + class ExamApiViewSet(viewsets.ReadOnlyModelViewSet): """ Gets a list of an individual exam by Id if provided """ permission_classes = (IsReviewer,) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 47893229c88166fe70838840516122f9687ae87a..0e0910b3cbff0ccb4a7884cf6d4abd3832648c76 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -15,4 +15,7 @@ </script> <style> + #app { + font-family: Roboto, sans-serif; + } </style> diff --git a/frontend/src/components/base/BaseLayout.vue b/frontend/src/components/BaseLayout.vue similarity index 88% rename from frontend/src/components/base/BaseLayout.vue rename to frontend/src/components/BaseLayout.vue index 5daac11f8551b3b9a797f77b59aacd987f8a9026..3f18a9866469951978cbc9420c4b34edfd9c3184 100644 --- a/frontend/src/components/base/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -5,7 +5,7 @@ clipped app permanent - :mini-variant.sync="mini" + :mini-variant="mini" > <v-toolbar flat> <v-list> @@ -26,7 +26,7 @@ </v-list-tile> </v-list> </v-toolbar> - <slot name="navigation"></slot> + <slot name="sidebar-content"></slot> </v-navigation-drawer> <v-toolbar app @@ -38,7 +38,7 @@ > <v-toolbar-title> <v-avatar> - <img src="../../assets/brand.png"> + <img src="../assets/brand.png"> </v-avatar> </v-toolbar-title> <span class="pl-2 grady-speak">{{ gradySpeak }}</span> @@ -48,7 +48,7 @@ <v-btn color="blue darken-1" to="/" @click.native="logout">Logout</v-btn> </v-toolbar> <v-content> - <slot></slot> + <router-view></router-view> </v-content> </div> </template> @@ -67,7 +67,6 @@ 'gradySpeak' ]), ...mapState([ - 'examInstance', 'username', 'userRole' ]) @@ -76,6 +75,11 @@ ...mapActions([ 'logout' ]) + }, + watch: { + mini: function () { + this.$emit('sidebarMini', this.mini) + } } } </script> diff --git a/frontend/src/components/SubmissionType.vue b/frontend/src/components/SubmissionType.vue new file mode 100644 index 0000000000000000000000000000000000000000..5827cbb6836d6bc58373ac3b32bbc09b21eb6cab --- /dev/null +++ b/frontend/src/components/SubmissionType.vue @@ -0,0 +1,54 @@ +<template> + <v-container> + <h2 class="mb-2">{{ name }} - Full score: {{ fullScore }}</h2> + <v-expansion-panel expand> + <v-expansion-panel-content + v-for="(item, key, i) in {Description: description, Solution:solution}" + :key="i" + :value="expandedByDefault[key]"> + <div slot="header">{{ key }}</div> + <v-card color="grey lighten-4"> + <v-card-text> + {{ item }} + </v-card-text> + </v-card> + </v-expansion-panel-content> + </v-expansion-panel> + </v-container> +</template> + + +<script> + export default { + name: 'submission-type', + props: { + name: { + type: String, + required: true + }, + description: { + type: String, + required: true + }, + solution: { + type: String, + required: true + }, + fullScore: { + type: Number, + required: true + }, + expandedByDefault: { + type: Object, + default: function () { + return { + Description: true, + Solution: true + } + }, + required: false + } + } + } +</script> + diff --git a/frontend/src/components/student/ExamInformation.vue b/frontend/src/components/student/ExamInformation.vue index 817cd58c1e42da874890066be1d87409c4d5e4cd..21795f0503e1eb9d97590fe94ac943eda1243318 100644 --- a/frontend/src/components/student/ExamInformation.vue +++ b/frontend/src/components/student/ExamInformation.vue @@ -5,12 +5,12 @@ <th>Modul</th> <td>{{ exam.module_reference }}</td> </tr> - <tr v-if="!exam.pass_only"> + <tr> <th>Pass score</th> <td>{{ exam.pass_score }}</td> </tr> - <tr v-else> - <th>Pass only!</th> + <tr v-if="exam.passOnly"> + <th>Pass only exam!</th> </tr> <tr> <th>Total score</th> diff --git a/frontend/src/components/student/SubmissionDetail.vue b/frontend/src/components/student/SubmissionDetail.vue deleted file mode 100644 index 1a757b4618bb608c0cd31ae509646ca2840eb670..0000000000000000000000000000000000000000 --- a/frontend/src/components/student/SubmissionDetail.vue +++ /dev/null @@ -1,17 +0,0 @@ -<template> - <v-layout> - - <annotated-submission class="ma-3" :editable="false"></annotated-submission> - </v-layout> -</template> - - -<script> - import AnnotatedSubmission from '../submission_notes/AnnotatedSubmission' - export default { - components: { - AnnotatedSubmission - }, - name: 'submission-detail' - } -</script> diff --git a/frontend/src/components/student/SubmissionList.vue b/frontend/src/components/student/SubmissionList.vue index 3954d89985f97702e2512e127963ecf6334747ea..7ef34f95222b4308396b03585ca8feda408c84db 100644 --- a/frontend/src/components/student/SubmissionList.vue +++ b/frontend/src/components/student/SubmissionList.vue @@ -7,10 +7,10 @@ item-key="type" > <template slot="items" slot-scope="props"> - <td>{{ props.item.type_name }}</td> - <td class="text-xs-right">{{ props.item.score }}</td> - <td class="text-xs-right">{{ props.item.full_score }}</td> - <td class="text-xs-right"><v-btn :to="`submission/${props.item.type_id}`" color="orange lighten-2">View</v-btn></td> + <td>{{ props.item.type.name }}</td> + <td class="text-xs-right">{{ props.item.feedback.score }}</td> + <td class="text-xs-right">{{ props.item.type.fullScore }}</td> + <td class="text-xs-right"><v-btn :to="`submission/${props.item.type.id}`" color="orange lighten-2"><v-icon>chevron_right</v-icon></v-btn></td> </template> </v-data-table> <v-alert color="info" value="true"> @@ -29,22 +29,17 @@ { text: 'Task', align: 'left', - value: 'type' + value: 'type', + sortable: false }, { text: 'Score', - value: 'score' + value: 'feedback.score' }, { text: 'Maximum Score', - value: 'full_score' + value: 'type.fullScore' } - ], - - fields: [ - { key: 'type', sortable: true }, - { key: 'score', label: 'Score', sortable: true }, - { key: 'full_score', sortable: true } ] } }, @@ -56,11 +51,10 @@ }, computed: { sumScore () { - console.log(this.submissions) - return this.submissions.map(a => a.score).reduce((a, b) => a + b) + return this.submissions.map(a => a.feedback.score).reduce((a, b) => a + b) }, sumFullScore () { - return this.submissions.map(a => a.full_score).reduce((a, b) => a + b) + return this.submissions.map(a => a.type.fullScore).reduce((a, b) => a + b) }, pointRatio () { return ((this.sumScore / this.sumFullScore) * 100).toFixed(2) diff --git a/frontend/src/components/submission_notes/AnnotatedSubmission.vue b/frontend/src/components/submission_notes/AnnotatedSubmission.vue index 774d55a825b5a8756522067fd27f6faec25cb2b1..d5c24b1bc2ed03e7ed30ac554e0bf9ee9cc7a564 100644 --- a/frontend/src/components/submission_notes/AnnotatedSubmission.vue +++ b/frontend/src/components/submission_notes/AnnotatedSubmission.vue @@ -1,11 +1,8 @@ <template> - <table> + <table class="elevation-1"> <tr v-for="(code, index) in submission" :key="index"> <td class="line-number-cell"> - <!--<v-tooltip left close-delay="20" color="transparent" content-class="comment-icon">--> - <v-btn block class="line-number-btn" slot="activator" @click="toggleEditorOnLine(index)">{{ index }}</v-btn> - <!--<v-icon small color="indigo accent-3" class="comment-icon">comment</v-icon>--> - <!--</v-tooltip>--> + <v-btn block class="line-number-btn" @click="toggleEditorOnLine(index)">{{ index }}</v-btn> </td> <td> <pre class="prettyprint"><code class="lang-c"> {{ code }}</code></pre> @@ -26,7 +23,6 @@ <script> - import {mapGetters, mapState} from 'vuex' import CommentForm from '@/components/submission_notes/FeedbackForm.vue' import FeedbackComment from '@/components/submission_notes/FeedbackComment.vue' @@ -36,24 +32,34 @@ CommentForm}, name: 'annotated-submission', props: { + rawSubmission: { + type: String, + required: true + }, + score: { + type: Number, + required: true + }, + feedback: { + type: Object, + required: true + }, editable: { type: Boolean, default: false } }, - beforeCreate () { - this.$store.dispatch('getFeedback', 0) - this.$store.dispatch('getSubmission', 0) - }, - computed: { - ...mapState({ - feedback: state => state.submissionNotes.feedback - }), - ...mapGetters(['submission']) - }, data: function () { return { - showEditorOnLine: { } + showEditorOnLine: {} + } + }, + computed: { + submission () { + return this.rawSubmission.split('\n').reduce((acc, cur, index) => { + acc[index + 1] = cur + return acc + }, {}) } }, methods: { @@ -75,13 +81,8 @@ border-collapse: collapse; } - td { - /*white-space: nowrap;*/ - /*border: 1px solid green;*/ - } .line-number-cell { - /*padding-left: 50px;*/ vertical-align: top; } @@ -101,9 +102,4 @@ min-width: fit-content; margin: 0; } - - .comment-icon { - border: 0; - } - </style> diff --git a/frontend/src/components/submission_notes/FeedbackComment.vue b/frontend/src/components/submission_notes/FeedbackComment.vue index a63b4de6a32267608763b9f999f5088adf462e71..637fce73cc1aba84136cc7b87ded37f22d423db9 100644 --- a/frontend/src/components/submission_notes/FeedbackComment.vue +++ b/frontend/src/components/submission_notes/FeedbackComment.vue @@ -40,12 +40,11 @@ margin: 20px 10px 10px 10px; padding: 5px; background-color: #F3F3F3; - border-radius: 5px; - border: 5px solid #3D8FC1; + border-radius: 0px; + border: 2px solid #3D8FC1; } .body .message { - font-family: Roboto, sans-serif; min-height: 30px; border-radius: 3px; font-size: 14px; diff --git a/frontend/src/components/Login.vue b/frontend/src/pages/Login.vue similarity index 100% rename from frontend/src/components/Login.vue rename to frontend/src/pages/Login.vue diff --git a/frontend/src/components/reviewer/ReviewerPage.vue b/frontend/src/pages/reviewer/ReviewerPage.vue similarity index 100% rename from frontend/src/components/reviewer/ReviewerPage.vue rename to frontend/src/pages/reviewer/ReviewerPage.vue diff --git a/frontend/src/components/reviewer/ReviewerToolbar.vue b/frontend/src/pages/reviewer/ReviewerToolbar.vue similarity index 100% rename from frontend/src/components/reviewer/ReviewerToolbar.vue rename to frontend/src/pages/reviewer/ReviewerToolbar.vue diff --git a/frontend/src/components/reviewer/StudentListOverview.vue b/frontend/src/pages/reviewer/StudentListOverview.vue similarity index 100% rename from frontend/src/components/reviewer/StudentListOverview.vue rename to frontend/src/pages/reviewer/StudentListOverview.vue diff --git a/frontend/src/components/student/StudentLayout.vue b/frontend/src/pages/student/StudentLayout.vue similarity index 50% rename from frontend/src/components/student/StudentLayout.vue rename to frontend/src/pages/student/StudentLayout.vue index e7f12d7e18c370097fce830654470f1f224d499f..28bcabe7d01421d6df21290284122123787418a0 100644 --- a/frontend/src/components/student/StudentLayout.vue +++ b/frontend/src/pages/student/StudentLayout.vue @@ -1,9 +1,12 @@ <template> - <base-layout> + <base-layout @sidebarMini="mini = $event"> + <template slot="header"> {{ module_reference }} </template> - <v-list dense slot="navigation"> + + <v-list dense slot="sidebar-content"> + <v-list-tile exact v-for="(item, i) in generalNavItems" :key="i" :to="item.route"> <v-list-tile-action> <v-icon>{{ item.icon }}</v-icon> @@ -14,30 +17,38 @@ </v-list-tile-title> </v-list-tile-content> </v-list-tile> + <v-divider></v-divider> - <v-list-tile exact v-for="(item, i) in submissionNavItems" :key="i" :to="item.route"> - <v-list-tile-action> - <v-icon>assignment</v-icon> - </v-list-tile-action> - <v-list-tile-content> - <v-list-tile-title> - {{ item.name }} - </v-list-tile-title> - </v-list-tile-content> - </v-list-tile> + <v-card color="grey lighten-2" v-if="!mini"> + <v-card-title primary-title> + <exam-information :exam="exam"></exam-information> + </v-card-title> + </v-card> + <v-list-tile exact v-for="(item, i) in submissionNavItems" :key="i" :to="item.route"> + <v-list-tile-action> + <v-icon v-if="!visited[item.id]">assignment</v-icon> + <v-icon v-else>check</v-icon> + </v-list-tile-action> + <v-list-tile-content> + <v-list-tile-title> + {{ item.name }} + </v-list-tile-title> + </v-list-tile-content> + </v-list-tile> </v-list> - <router-view></router-view> </base-layout> </template> <script> import { mapState } from 'vuex' - import BaseLayout from '../base/BaseLayout' + import BaseLayout from '@/components/BaseLayout' + import ExamInformation from '@/components/student/ExamInformation' export default { - components: {BaseLayout}, + components: {BaseLayout, ExamInformation}, name: 'student-layout', data () { return { + mini: false, generalNavItems: [ { name: 'Overview', @@ -55,13 +66,16 @@ computed: { ...mapState({ module_reference: state => state.studentPage.exam.module_reference, - submissions: state => state.studentPage.submissions + submissions: state => state.studentPage.submissionsForList, + exam: state => state.studentPage.exam, + visited: state => state.studentPage.visited }), submissionNavItems: function () { return this.submissions.map((sub, index) => { return { - name: sub.type_name, - route: `/student/submission/${sub.type_id}` + name: sub.type.name, + id: sub.type.id, + route: `/student/submission/${sub.type.id}` } }) } diff --git a/frontend/src/components/student/StudentPage.vue b/frontend/src/pages/student/StudentPage.vue similarity index 59% rename from frontend/src/components/student/StudentPage.vue rename to frontend/src/pages/student/StudentPage.vue index 1bcb1fc7dd9111067b9f0110a879b862f3751925..f42887fafa50ebe7338e25107e5f28aba38be1cd 100644 --- a/frontend/src/components/student/StudentPage.vue +++ b/frontend/src/pages/student/StudentPage.vue @@ -1,12 +1,8 @@ <template> <v-container fluid> <v-layout justify center> - <v-flex md3> - <h2>Exam Overview</h2> - <exam-information v-if="!loading" :exam="exam"></exam-information> - </v-flex> - <template v-if="!loading"> - <v-flex md7 offset-md1> + <template v-if="loaded"> + <v-flex md10 mt-5 offset-xs1> <h2>Submissions of {{ studentName }}</h2> <submission-list :submissions="submissions"></submission-list> </v-flex> @@ -19,8 +15,8 @@ <script> import {mapState} from 'vuex' import StudentLayout from './StudentLayout.vue' - import SubmissionList from './SubmissionList.vue' - import ExamInformation from './ExamInformation.vue' + import SubmissionList from '@/components/student/SubmissionList.vue' + import ExamInformation from '@/components/student/ExamInformation.vue' export default { components: { @@ -29,14 +25,18 @@ StudentLayout}, name: 'student-page', created: function () { - this.$store.dispatch('getStudentData') + if (!this.loaded) { + this.$store.dispatch('getStudentData').then(() => { + this.$store.dispatch('getStudentSubmissions') + }) + } }, computed: { ...mapState({ studentName: state => state.studentPage.studentName, exam: state => state.studentPage.exam, - submissions: state => state.studentPage.submissions, - loading: state => state.studentPage.loading + submissions: state => state.studentPage.submissionsForList, + loaded: state => state.studentPage.loaded }) } } diff --git a/frontend/src/pages/student/StudentSubmissionPage.vue b/frontend/src/pages/student/StudentSubmissionPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..1d13fc4ccad88fb4198b5e72e052678827878f75 --- /dev/null +++ b/frontend/src/pages/student/StudentSubmissionPage.vue @@ -0,0 +1,48 @@ +<template> + <v-container flex> + <v-layout> + <v-flex xs-12 sm-6 md-6 ma-5> + <annotated-submission + :rawSubmission="rawSubmission" + :score="score" + :feedback="{}"> + </annotated-submission> + </v-flex> + <v-flex xs-12 sm-6 md-6> + <submission-type + v-bind="submissionType"> + </submission-type> + </v-flex> + </v-layout> + </v-container> +</template> + + +<script> + import { mapState } from 'vuex' + import AnnotatedSubmission from '@/components/submission_notes/AnnotatedSubmission' + import SubmissionType from '@/components/SubmissionType' + export default { + name: 'student-submission-page', + components: {AnnotatedSubmission, SubmissionType}, + computed: { + id: function () { + return this.$route.params.id + }, + ...mapState({ + rawSubmission: function (state) { return state.studentPage.submissionData[this.id].text }, + score: function (state) { return state.studentPage.submissionData[this.id].feedback.score }, + submissionType: function (state) { return state.studentPage.submissionData[this.id].type } + // feedback: function (state) { return state.studentPage.submissionData[this.$route.params.id].feedback.text } + }) + }, + mounted: function () { + this.$store.commit('SET_VISITED', { index: this.id, visited: true }) + }, + updated: function () { + if (this.id) { + this.$store.commit('SET_VISITED', { index: this.id, visited: true }) + } + } + } +</script> diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 849fbf66c4d47a38992f6ea97e5dcf25776d7966..9ff1b8396d577be2140bdcd7f47140c012d2ae05 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -1,14 +1,12 @@ import Vue from 'vue' import Router from 'vue-router' import store from '../store/store' -import Login from '@/components/Login' -import StudentPage from '@/components/student/StudentPage' -import StudentLayout from '@/components/student/StudentLayout' -import SubmissionDetail from '@/components/student/SubmissionDetail' -import ReviewerPage from '@/components/reviewer/ReviewerPage' -import StudentListOverview from '@/components/reviewer/StudentListOverview' -import BaseLayout from '@/components/base/BaseLayout' -import AnnotatedSubmission from '@/components/submission_notes/AnnotatedSubmission' +import Login from '@/pages/Login' +import StudentPage from '@/pages/student/StudentPage' +import StudentSubmissionPage from '@/pages/student/StudentSubmissionPage' +import StudentLayout from '@/pages/student/StudentLayout' +import ReviewerPage from '@/pages/reviewer/ReviewerPage' +import StudentListOverview from '@/pages/reviewer/StudentListOverview' Vue.use(Router) @@ -29,7 +27,7 @@ const router = new Router({ }, { path: 'submission/:id', - component: SubmissionDetail + component: StudentSubmissionPage } ] @@ -43,28 +41,16 @@ const router = new Router({ path: 'reviewer/student-overview/', name: 'student-overview', component: StudentListOverview - }, - { - path: '/base/', - name: 'base-layout', - component: BaseLayout - }, - { - path: '/notes/', - name: 'annotated-submission', - component: AnnotatedSubmission } ] }) router.beforeEach((to, from, next) => { - if (to.path === '/') { + if (to.path === '/' || from.path === '/') { next() } else { - const now = new Date() + const now = Date.now() if (now - store.state.logInTime > store.state.jwtTimeDelta * 1000) { - console.log(now) - console.log(store.state.logInTime) store.dispatch('logout').then(() => { store.commit('API_FAIL', 'You\'ve been logged out due to inactivity') next('/') diff --git a/frontend/src/store/modules/student-page.js b/frontend/src/store/modules/student-page.js index ce6cc28b92de50247aea85562782e5b4924b8968..45a5b34f3d678182a596e039f83d48aaf83d98bd 100644 --- a/frontend/src/store/modules/student-page.js +++ b/frontend/src/store/modules/student-page.js @@ -4,9 +4,10 @@ const studentPage = { state: { studentName: '', exam: {}, - submissionTypes: [], - submissions: [], - loading: true + submissionsForList: [], + submissionData: {}, + visited: {}, + loaded: false }, mutations: { 'SET_STUDENT_NAME': function (state, name) { @@ -18,26 +19,81 @@ const studentPage = { 'SET_SUBMISSION_TYPES': function (state, submissionTypes) { state.submissionTypes = submissionTypes }, - 'SET_SUBMISSIONS': function (state, submissions) { - state.submissions = submissions + 'SET_SUBMISSIONS_FOR_LIST': function (state, submissions) { + state.submissionsForList = submissions }, - 'SET_LOADING': function (state, loading) { - state.loading = loading + /** + * Reduces the array submissionData returned by the /api/student-submissions + * into an object where the keys are the SubmissionType id's and the values + * the former array elements. This is done to have direct access to the data + * via the SubmissionType id. + */ + 'SET_FULL_SUBMISSION_DATA': function (state, submissionData) { + state.submissionData = submissionData.reduce((acc, cur, index) => { + acc[cur.type.id] = cur + return acc + }, {}) + }, + 'SET_VISITED': function (state, visited) { + state.visited = { ...state.visited, [visited.index]: visited.visited } + }, + 'SET_LOADED': function (state, loaded) { + state.loaded = loaded } }, actions: { getStudentData (context) { - context.commit('SET_LOADING', true) + context.commit('SET_LOADED', false) ax.get('api/student-page/').then(response => { const data = response.data context.commit('SET_STUDENT_NAME', data.name) context.commit('SET_EXAM', data.exam) - context.commit('SET_SUBMISSIONS', data.submissions) - context.commit('SET_LOADING', false) + context.commit('SET_SUBMISSIONS_FOR_LIST', data.submissions) + context.commit('SET_LOADED', true) }) + }, + + async getStudentSubmissions (context) { + const response = await ax.get('/api/student-submissions') + context.commit('SET_FULL_SUBMISSION_DATA', response.data) } } } +// const mockSubmission = '//Procedural Programming technique shows creation of Pascal\'s Triangl\n' + +// '#include <iostream>\n' + +// '#include <iomanip>\n' + +// '\n' + +// 'using namespace std;\n' + +// '\n' + +// '\n' + +// 'int** comb(int** a , int row , int col)\n' + +// '{\n' + +// ' int mid = col/2;\n' + +// ' //clear matrix\n' + +// ' for( int i = 0 ; i < row ; i++)\n' + +// ' for( int j = 0 ; j < col ; j++)\n' + +// ' a[i][j] = 0;\n' + +// ' a[0][mid] = 1; //put 1 in the middle of first row\n' + +// ' //build up Pascal\'s Triangle matrix\n' + +// ' for( int i = 1 ; i < row ; i++)\n' + +// ' {\n' + +// ' for( int j = 1 ; j < col - 1 ; j++)\n' + +// ' a[i][j] = a[i-1][j-1] + a[i-1][j+1];\n' + +// ' }\n' + +// ' return a;\n' + +// '}\n' + +// 'void disp(int** ptr, int row, int col)\n' + +// '{\n' + +// ' cout << endl << endl;\n' + +// ' for ( int i = 0 ; i < row ; i++)\n' + +// ' {\n' + +// ' for ( int j = 0 ; j < col ; j++)\n' + +// const mockFeedback = { +// '1': 'Youre STUPID', +// '4': 'Very much so' +// } + export default studentPage diff --git a/frontend/src/store/modules/submission-notes.js b/frontend/src/store/modules/submission-notes.js index cde09fa0c39e2f4a38869dc8746aee0c3be86628..10a47e24d2471d5d27e39900a0d8002c228f8007 100644 --- a/frontend/src/store/modules/submission-notes.js +++ b/frontend/src/store/modules/submission-notes.js @@ -1,76 +1,44 @@ -import Vue from 'vue' +// import Vue from 'vue' -const mockSubmission = '//Procedural Programming technique shows creation of Pascal\'s Triangl\n' + - '#include <iostream>\n' + - '#include <iomanip>\n' + - 'using namespace std;\n' + - 'int** comb(int** a , int row , int col)\n' + - '{\n' + - ' int mid = col/2;\n' + - ' //clear matrix\n' + - ' for( int i = 0 ; i < row ; i++)\n' + - ' for( int j = 0 ; j < col ; j++)\n' + - ' a[i][j] = 0;\n' + - ' a[0][mid] = 1; //put 1 in the middle of first row\n' + - ' //build up Pascal\'s Triangle matrix\n' + - ' for( int i = 1 ; i < row ; i++)\n' + - ' {\n' + - ' for( int j = 1 ; j < col - 1 ; j++)\n' + - ' a[i][j] = a[i-1][j-1] + a[i-1][j+1];\n' + - ' }\n' + - ' return a;\n' + - '}\n' + - 'void disp(int** ptr, int row, int col)\n' + - '{\n' + - ' cout << endl << endl;\n' + - ' for ( int i = 0 ; i < row ; i++)\n' + - ' {\n' + - ' for ( int j = 0 ; j < col ; j++)\n' +// const submissionNotes = { +// state: { +// rawSubmission: '', +// feedback: {} +// }, +// getters: { +// // reduce the string rawSubmission into an object where the keys are the +// // line indexes starting at one and the values the corresponding submission line +// // this makes iterating over the submission much more pleasant +// submission: state => { +// return state.rawSubmission.split('\n').reduce((acc, cur, index) => { +// acc[index + 1] = cur +// return acc +// }, {}) +// } +// }, +// mutations: { +// 'SET_RAW_SUBMISSION': function (state, submission) { +// state.rawSubmission = mockSubmission +// }, +// 'SET_FEEDBACK': function (state, feedback) { +// state.feedback = feedback +// }, +// 'UPDATE_FEEDBACK': function (state, feedback) { +// Vue.set(state.feedback, feedback.lineIndex, feedback.content) +// } +// }, +// actions: { +// // TODO remove mock data +// getSubmission (context, submissionId) { +// context.commit('SET_RAW_SUBMISSION', mockSubmission) +// }, +// getFeedback (context, feedbackId) { +// context.commit('SET_FEEDBACK', mockFeedback) +// }, +// updateFeedback (context, lineIndex, feedbackContent) { +// context.commit('UPDATE_FEEDBACK', lineIndex, feedbackContent) +// } +// } +// } -const mockFeedback = { - '1': 'Youre STUPID', - '4': 'Very much so' -} - -const submissionNotes = { - state: { - rawSubmission: '', - feedback: {} - }, - getters: { - // reduce the string rawSubmission into an object where the keys are the - // line indexes starting at one and the values the corresponding submission line - // this makes iterating over the submission much more pleasant - submission: state => { - return state.rawSubmission.split('\n').reduce((acc, cur, index) => { - acc[index + 1] = cur - return acc - }, {}) - } - }, - mutations: { - 'SET_RAW_SUBMISSION': function (state, submission) { - state.rawSubmission = mockSubmission - }, - 'SET_FEEDBACK': function (state, feedback) { - state.feedback = feedback - }, - 'UPDATE_FEEDBACK': function (state, feedback) { - Vue.set(state.feedback, feedback.lineIndex, feedback.content) - } - }, - actions: { - // TODO remove mock data - getSubmission (context, submissionId) { - context.commit('SET_RAW_SUBMISSION', mockSubmission) - }, - getFeedback (context, feedbackId) { - context.commit('SET_FEEDBACK', mockFeedback) - }, - updateFeedback (context, lineIndex, feedbackContent) { - context.commit('UPDATE_FEEDBACK', lineIndex, feedbackContent) - } - } -} - -export default submissionNotes +// export default submissionNotes diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index e9c4797677a92abc9ea439689f159bad28cbf567..0eb0f5fbfdc57213ffbe3d1967864ceb73f3a184 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -3,22 +3,20 @@ import Vue from 'vue' import ax from './api' import gradySays from './grady_speak' -import submissionNotes from './modules/submission-notes' import studentPage from './modules/student-page' Vue.use(Vuex) const store = new Vuex.Store({ modules: { - submissionNotes, studentPage }, state: { token: sessionStorage.getItem('jwtToken'), loggedIn: !!sessionStorage.getItem('jwtToken'), - logInTime: sessionStorage.getItem('logInTime'), + logInTime: Number(sessionStorage.getItem('logInTime')), username: sessionStorage.getItem('username'), - jwtTimeDelta: sessionStorage.getItem('jwtTimeDelta'), + jwtTimeDelta: Number(sessionStorage.getItem('jwtTimeDelta')), userRole: sessionStorage.getItem('userRole'), error: '' }, @@ -36,7 +34,7 @@ const store = new Vuex.Store({ state.logInTime = Date.now() ax.defaults.headers['Authorization'] = 'JWT ' + token sessionStorage.setItem('jwtToken', token) - sessionStorage.setItem('logInTime', state.logInTime) + sessionStorage.setItem('logInTime', String(state.logInTime)) }, 'SET_JWT_TIME_DELTA': function (state, timeDelta) { state.jwtTimeDelta = timeDelta diff --git a/frontend/test/unit/specs/SubmissionList.spec.js b/frontend/test/unit/specs/SubmissionList.spec.js index 8e855486029934dc781d0058d6bc7e11c7d2ac03..59542a0f8b2fa47dc6c9755a78071f42dc551b9a 100644 --- a/frontend/test/unit/specs/SubmissionList.spec.js +++ b/frontend/test/unit/specs/SubmissionList.spec.js @@ -4,18 +4,24 @@ import SubmissionList from '@/components/student/SubmissionList' describe('SubmissionList.vue', () => { it('tests the SubmissionList for students', () => { const data = [{ - 'type': 'Aufgabe 01', - 'text': 'I dont know the answer.', - 'feedback': 'I am very disappointed.', - 'score': 5, - 'full_score': 14 + type: { + name: 'Aufgabe 01', + fullScore: 14 + }, + feedback: { + text: 'I am very disappointed.', + score: 5 + } }, { - 'type': 'Aufgabe 01', - 'text': 'A very good solution, indeed', - 'feedback': 'I am still very disappointed.', - 'score': 7, - 'full_score': 10 + type: { + name: 'Aufgabe 02', + fullScore: 10 + }, + feedback: { + text: 'I am still very disappointed.', + score: 7 + } }] const Constructor = Vue.extend(SubmissionList) diff --git a/util/factories.py b/util/factories.py index 63da6ecf45e9311f49225f1e29f29344b293431e..f90e649c35bca6d74429aa48707ff27f0fdc922c 100644 --- a/util/factories.py +++ b/util/factories.py @@ -134,7 +134,8 @@ def make_submission_types(submission_types=[], **kwargs): def make_students(students=[], **kwargs): return [GradyUserFactory().make_student( username=student['username'], - exam=ExamType.objects.get(module_reference=student['exam']), + exam=ExamType.objects.get(module_reference=student['exam']) if + 'exam' in student else None, password=student.get('password', None) ) for student in students]