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..b64a4cf14e4b913aa8bee8595a464121ffc07df1 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,57 @@ class FeedbackSerializer(DynamicFieldsModelSerializer): fields = ('text', 'score') -class SubmissionTypeSerializer(DynamicFieldsModelSerializer): +class TestSerializer(DynamicFieldsModelSerializer): + + class Meta: + model = Test + fields = ('name', 'label', 'annotation') + + +class SubmissionTypeListSerializer(DynamicFieldsModelSerializer): + fullScore = serializers.IntegerField(source='full_score') + + class Meta: + model = SubmissionType + fields = ('id', 'name', 'fullScore') + + +class SubmissionTypeSerializer(SubmissionTypeListSerializer): 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', 'text', 'feedback', 'tests') + + +class SubmissionListSerializer(DynamicFieldsModelSerializer): + type = SubmissionTypeListSerializer() + # TODO change this according to new feedback model + feedback = FeedbackSerializer(fields=('score',)) class Meta: model = Submission - fields = ('type_id', 'type_name', 'text', - 'feedback', 'score', 'full_score') + 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/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..0fc5f7125925733e501a808e0c7545e1577c3178 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, + SubmissionTypeListSerializer, 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,) @@ -69,4 +78,12 @@ class StudentReviewerApiViewSet(viewsets.ReadOnlyModelViewSet): class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet): """ Gets a list or a detail view of a single SubmissionType """ queryset = SubmissionType.objects.all() - serializer_class = SubmissionTypeSerializer + + def get_serializer_class(self): + if self.action == 'retrieve': + return SubmissionTypeSerializer + elif self.action == 'list': + return SubmissionTypeListSerializer + else: + raise NotImplementedError('SubmissionTypeViewSet only offers' + + 'list and retrieve action.') diff --git a/frontend/package.json b/frontend/package.json index 9f4245d5f207a1c6fca82b536c426d6f8460c366..03ef1b1cd9bf1edda3018db02a2008091e8256ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "axios": "^0.17.0", "google-code-prettify": "^1.0.5", "material-design-icons": "^3.0.1", + "v-clipboard": "^1.0.4", "vue": "^2.5.2", "vue-router": "^3.0.1", "vuetify": "^0.17.3", 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..6cf35fce198a4f61e0369575a7669f5c7990767b --- /dev/null +++ b/frontend/src/components/SubmissionType.vue @@ -0,0 +1,76 @@ +<template> + <v-container> + <h2 class="mb-2">{{ name }} - Full score: {{ fullScore }}</h2> + <v-expansion-panel expand> + <v-expansion-panel-content + v-for="(item, i) in typeItems" + :key="i" + :value="expandedByDefault[item.title]"> + <div slot="header">{{ item.title }}</div> + <v-card color="grey lighten-4"> + <v-card-text> + {{ item.text }} + </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 + }, + reverse: { + type: Boolean, + default: false + }, + expandedByDefault: { + type: Object, + default: function () { + return { + Description: true, + Solution: true + } + } + } + }, + computed: { + typeItems () { + let items = [ + { + title: 'Description', + text: this.description + }, + { + title: 'Solution', + text: this.solution + } + ] + if (this.reverse) { + return items.reverse() + } else { + return items + } + } + } + } +</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..451404f910874998df123c66d144528d2e11c4fb 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="`/student/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..a3fdb504bf11319d0313c9858c57ec129c7231db 100644 --- a/frontend/src/components/submission_notes/AnnotatedSubmission.vue +++ b/frontend/src/components/submission_notes/AnnotatedSubmission.vue @@ -1,61 +1,88 @@ <template> - <table> - <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>--> - </td> - <td> - <pre class="prettyprint"><code class="lang-c"> {{ code }}</code></pre> - <feedback-comment - v-if="feedback[index] && !showEditorOnLine[index]" - @click="toggleEditorOnLine(index)">{{ feedback[index] }} - </feedback-comment> - <comment-form - v-if="showEditorOnLine[index] && editable" - @collapseFeedbackForm="showEditorOnLine[index] = false" - :feedback="feedback[index]" - :index="index"> - </comment-form> - </td> - </tr> - </table> + <v-container> + <annotated-submission-top-toolbar + v-if="isTutor || isReviewer" + class="mb-1 elevation-1" + :submission="rawSubmission" + /> + <table class="elevation-1"> + <tr v-for="(code, index) in submission" :key="index"> + <td class="line-number-cell"> + <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> + <feedback-comment + v-if="feedback[index] && !showEditorOnLine[index]" + @click.native="toggleEditorOnLine(index)">{{ feedback[index] }} + </feedback-comment> + <comment-form + v-if="showEditorOnLine[index] && editable" + @collapseFeedbackForm="showEditorOnLine[index] = false" + :feedback="feedback[index]" + :index="index"> + </comment-form> + </td> + </tr> + </table> + <annotated-submission-bottom-toolbar + v-if="isTutor || isReviewer" + class="mt-1 elevation-1" + /> + </v-container> </template> <script> - import {mapGetters, mapState} from 'vuex' + import { mapGetters } from 'vuex' import CommentForm from '@/components/submission_notes/FeedbackForm.vue' import FeedbackComment from '@/components/submission_notes/FeedbackComment.vue' + import AnnotatedSubmissionTopToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar' + import AnnotatedSubmissionBottomToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar' export default { components: { + AnnotatedSubmissionBottomToolbar, + AnnotatedSubmissionTopToolbar, FeedbackComment, 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 + }, {}) + }, + ...mapGetters([ + 'isStudent', + 'isTutor', + 'isReviewer' + ]) + }, methods: { toggleEditorOnLine (lineIndex) { this.$set(this.showEditorOnLine, lineIndex, !this.showEditorOnLine[lineIndex]) @@ -73,15 +100,11 @@ table { table-layout: auto; border-collapse: collapse; + width: 100%; } - td { - /*white-space: nowrap;*/ - /*border: 1px solid green;*/ - } .line-number-cell { - /*padding-left: 50px;*/ vertical-align: top; } @@ -101,9 +124,4 @@ min-width: fit-content; margin: 0; } - - .comment-icon { - border: 0; - } - </style> diff --git a/frontend/src/components/submission_notes/CorrectionHelpCard.vue b/frontend/src/components/submission_notes/CorrectionHelpCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..ae204d18c03fcf8d00929ffcef78267a51310f36 --- /dev/null +++ b/frontend/src/components/submission_notes/CorrectionHelpCard.vue @@ -0,0 +1,38 @@ +<template> + <v-card class="help-card"> + <v-card-title> + <v-icon>help_outline</v-icon> + <h3>Tips on using the correction interface</h3> + </v-card-title> + <v-card-text> + Never trade an ale. + The sea-dog leads with yellow fever, crush the captain's quarters until it waves.<br> + Ho-ho-ho! malaria of life.<br> + Halitosis, adventure, and yellow fever.<br> + The girl drinks with halitosis, pull the galley before it laughs.<br> + The moon fires with life, vandalize the bikini atoll before it travels.<br> + The tuna blows with fight, haul the freighter before it whines.<br> + The cannibal robs with hunger, fire the lighthouse until it whines.<br> + The captain loves with death, vandalize the lighthouse before it whines.<br> + The anchor loots with treasure, raid the freighter before it grows.<br> + The reef commands with endurance, view the quarter-deck until it whines.<br> + The scallywag loots with passion, crush the bikini atoll before it falls.<br> + The sea leads with treasure, ransack the brig until it dies.<br> + The parrot robs with desolation, view the seychelles before it screams.<br> + The warm anchor quirky blows the landlubber.<br> + + </v-card-text> + </v-card> +</template> + +<script> + export default { + name: 'correction-help-card' + } +</script> + +<style scoped> + .help-card { + width: fit-content; + } +</style> diff --git a/frontend/src/components/submission_notes/FeedbackComment.vue b/frontend/src/components/submission_notes/FeedbackComment.vue index a63b4de6a32267608763b9f999f5088adf462e71..8af24d6e8b8d011967f105b11f2e69d8f15511ed 100644 --- a/frontend/src/components/submission_notes/FeedbackComment.vue +++ b/frontend/src/components/submission_notes/FeedbackComment.vue @@ -27,7 +27,7 @@ } .tip-up { - top: -25px; /* Same as body margin top + border */ + top: -22px; /* Same as body margin top + border */ left: 10px; border-right-color: transparent; border-left-color: transparent; @@ -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/submission_notes/FeedbackForm.vue b/frontend/src/components/submission_notes/FeedbackForm.vue index a993766260896f1bb56e248d22be58524a99b5b8..403d001547cdbb3476ab9402bb90f92b6602f4d7 100644 --- a/frontend/src/components/submission_notes/FeedbackForm.vue +++ b/frontend/src/components/submission_notes/FeedbackForm.vue @@ -3,15 +3,16 @@ <v-text-field name="feedback-input" label="Please provide your feedback here" - v-model="current_feedback" + v-model="currentFeedback" @keyup.enter.ctrl.exact="submitFeedback" @keyup.esc="collapseTextField" + @focus="selectInput($event)" rows="2" textarea autofocus auto-grow hide-details - ></v-text-field> + /> <v-btn color="success" @click="submitFeedback">Submit</v-btn> <v-btn @click="discardFeedback">Discard changes</v-btn> </div> @@ -23,27 +24,31 @@ name: 'comment-form', props: { feedback: String, - index: Number + index: String }, data () { return { - current_feedback: this.feedback + currentFeedback: this.feedback } }, methods: { - + selectInput (event) { + if (event) { + event.target.select() + } + }, collapseTextField () { this.$emit('collapseFeedbackForm') }, submitFeedback () { this.$store.dispatch('updateFeedback', { lineIndex: this.index, - content: this.current_feedback + content: this.currentFeedback }) this.collapseTextField() }, discardFeedback () { - this.current_feedback = this.feedback + this.currentFeedback = this.feedback } } } diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue new file mode 100644 index 0000000000000000000000000000000000000000..887661e20f30dba96dd787bdd441f2a21225cec7 --- /dev/null +++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar.vue @@ -0,0 +1,64 @@ +<template> + <v-toolbar dense class="bottom-toolbar"> + <v-spacer/> + <v-alert + class="score-alert ma-3" + color="error" + icon="warning" + :value="scoreError" + >{{ scoreError }}</v-alert> + <span class="mr-2">Score:</span> + <input + class="score-text-field" + type="number" + v-model="score" + @input="validateScore" + @change="validateScore" + /> + <v-tooltip top> + <v-btn color="success" slot="activator">Submit<v-icon>chevron_right</v-icon></v-btn> + <span>Submit and continue</span> + </v-tooltip> + </v-toolbar> +</template> + +<script> + export default { + name: 'annotated-submission-bottom-toolbar', + data () { + return { + score: 42, + mockMax: 50, + scoreError: '' + + } + }, + methods: { + validateScore () { + if (this.score < 0) { + this.score = 0 + this.scoreError = 'Score must be 0 or greater.' + } else if (this.score > this.mockMax) { + this.score = this.mockMax + this.scoreError = `Score must be less or equal to ${this.mockMax}` + } + } + } + } +</script> + +<style scoped> + .bottom-toolbar { + font-size: large; + } + .score-text-field { + max-width: 50px; + box-sizing: border-box; + border: 1px solid grey; + border-radius: 2px; + padding: 3px; + } + .score-alert { + max-height: 40px; + } +</style> diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue new file mode 100644 index 0000000000000000000000000000000000000000..845c66087bfc655afda1515b039fe2d2b99b05c3 --- /dev/null +++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar.vue @@ -0,0 +1,44 @@ +<template> + <v-toolbar + dense> + <v-toolbar-side-icon @click.stop="helpDialog=true"> + <v-icon>help_outline</v-icon> + </v-toolbar-side-icon> + <v-dialog + scrollable + max-width="fit-content" + v-model="helpDialog" + > + <correction-help-card></correction-help-card> + </v-dialog> + <v-spacer></v-spacer> + <v-tooltip top> + <v-btn icon slot="activator" v-clipboard="submission"><v-icon>content_copy</v-icon></v-btn> + <span>Copy to clipboard</span> + </v-tooltip> + </v-toolbar> +</template> + +<script> + import CorrectionHelpCard from '@/components/submission_notes/CorrectionHelpCard' + + export default { + components: {CorrectionHelpCard}, + name: 'annotated-submission-top-toolbar', + props: { + submission: { + type: String, + required: true + } + }, + data () { + return { + helpDialog: false + } + } + } +</script> + +<style scoped> + +</style> diff --git a/frontend/src/main.js b/frontend/src/main.js index d7cf28b7ac93b39520f3c04233df92b88fc04f8e..a92f0660684df1256f3e73c16d89dddb59e9d021 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -5,6 +5,7 @@ import App from './App' import router from './router' import store from './store/store' import Vuetify from 'vuetify' +import Cliboard from 'v-clipboard' import 'vuetify/dist/vuetify.min.css' import 'material-design-icons/iconfont/material-icons.css' @@ -12,6 +13,7 @@ import 'google-code-prettify/bin/prettify.min' import 'google-code-prettify/bin/prettify.min.css' Vue.use(Vuetify) +Vue.use(Cliboard) Vue.config.productionTip = false diff --git a/frontend/src/components/Login.vue b/frontend/src/pages/Login.vue similarity index 83% rename from frontend/src/components/Login.vue rename to frontend/src/pages/Login.vue index 8822993bb5c4a9000d3d366dad08a20d23c66536..a5b5d1b40e1d1d9fa53a1814a9396b15dc060e47 100644 --- a/frontend/src/components/Login.vue +++ b/frontend/src/pages/Login.vue @@ -19,13 +19,13 @@ v-model="credentials.username" required autofocus - ></v-text-field> + /> <v-text-field label="Password" v-model="credentials.password" type="password" required - ></v-text-field> + /> <v-btn :loading="loading" type="submit" color="primary">Access</v-btn> </v-form> </v-flex> @@ -49,7 +49,8 @@ }, computed: { ...mapState([ - 'error' + 'error', + 'userRole' ]) }, methods: { @@ -62,10 +63,16 @@ submit () { this.loading = true this.getJWTToken(this.credentials).then(() => { - this.getUserRole() + this.getUserRole().then(() => { + switch (this.userRole) { + case 'Student': this.$router.push('/student') + break + case 'Tutor': this.$router.push('/tutor') + break + } + }) this.getJWTTimeDelta() this.loading = false - this.$router.push('/student/') }).catch(() => { this.loading = false }) } } diff --git a/frontend/src/pages/SubmissionCorrectionPage.vue b/frontend/src/pages/SubmissionCorrectionPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..386e82100baf48e383030bc1995cc8f491974fc7 --- /dev/null +++ b/frontend/src/pages/SubmissionCorrectionPage.vue @@ -0,0 +1,81 @@ +<template> + <v-layout row wrap> + <v-flex xs12 md6> + <annotated-submission + :rawSubmission="mockSubmission" + :feedback="mockFeedback" + :score="mockScore" + :editable="true" + class="ma-4 autofocus" + /> + </v-flex> + + <v-flex md6> + <submission-type + v-bind="mockSubType" + :reverse="true" + :expandedByDefault="{ Description: false, Solution: true }" + /> + </v-flex> + </v-layout> +</template> + +<script> + import AnnotatedSubmission from '@/components/submission_notes/AnnotatedSubmission' + import SubmissionType from '@/components/SubmissionType' + + export default { + components: { + SubmissionType, + AnnotatedSubmission}, + name: 'submission-correction-page', + data () { + return { + 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', + mockFeedback: { + 1: 'Youre STUPID', + 4: 'Very much so' + }, + mockScore: 42, + mockSubType: { + description: 'Space suits meet with devastation! The vogon dies disconnection like an intelligent dosi.', + solution: 'The volume is a remarkable sinner.', + name: 'Seas stutter from graces like wet clouds.', + fullScore: 42 + } + } + } + } +</script> + +<style scoped> + +</style> 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..c20f0943815c5c3298e3a5e86d6fb7a2655b0902 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,39 @@ </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 +67,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/pages/tutor/TutorLayout.vue b/frontend/src/pages/tutor/TutorLayout.vue new file mode 100644 index 0000000000000000000000000000000000000000..b5641050361515af4df61a078b8510628a3521d7 --- /dev/null +++ b/frontend/src/pages/tutor/TutorLayout.vue @@ -0,0 +1,57 @@ +<template> + <base-layout @sidebarMini="mini = $event"> + + <template slot="header"> + Collapse + </template> + + <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> + </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-divider></v-divider> + + + </v-list> + </base-layout> +</template> + + +<script> + import BaseLayout from '@/components/BaseLayout' + + export default { + components: {BaseLayout}, + name: 'tutor-layout', + data () { + return { + generalNavItems: [ + { + name: 'Overview', + icon: 'home', + route: '/tutor' + }, + { + name: 'Progress', + icon: 'trending_up', + route: '/tutor' + }, + { + name: 'Assignments', + icon: 'assignment_turned_in', + route: '/tutor' + } + ] + } + } + } +</script> + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 849fbf66c4d47a38992f6ea97e5dcf25776d7966..babc257c19f2cb17155ebb0f15c1f313335216dd 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -1,14 +1,14 @@ 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 TutorLayout from '@/pages/tutor/TutorLayout' +import StudentPage from '@/pages/student/StudentPage' +import StudentLayout from '@/pages/student/StudentLayout' +import StudentSubmissionPage from '@/pages/student/StudentSubmissionPage' +import SubmissionCorrectionPage from '@/pages/SubmissionCorrectionPage' +import ReviewerPage from '@/pages/reviewer/ReviewerPage' +import StudentListOverview from '@/pages/reviewer/StudentListOverview' Vue.use(Router) @@ -29,10 +29,19 @@ const router = new Router({ }, { path: 'submission/:id', - component: SubmissionDetail + component: StudentSubmissionPage + } + ] + }, + { + path: '/tutor/', + component: TutorLayout, + children: [ + { + path: 'assignment/', + component: SubmissionCorrectionPage } ] - }, { path: '/reviewer/', @@ -43,28 +52,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..5b60e7706279674a107aa04d7e88472cb855661f 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -3,28 +3,35 @@ 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: '' }, getters: { gradySpeak: () => { return gradySays[Math.floor(Math.random() * gradySays.length)] + }, + isStudent: state => { + return state.userRole === 'Student' + }, + isTutor: state => { + return state.userRole === 'Tutor' + }, + isReviewer: state => { + return state.userRole === 'Reviewer' } }, mutations: { @@ -36,7 +43,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 @@ -83,8 +90,9 @@ const store = new Vuex.Store({ context.commit('SET_JWT_TIME_DELTA', response.data.timeDelta) }) }, - getUserRole (context) { - ax.get('api/user-role/').then(response => context.commit('SET_USER_ROLE', response.data.role)) + async getUserRole (context) { + const response = await ax.get('api/user-role/') + context.commit('SET_USER_ROLE', response.data.role) }, logout (store) { store.commit('LOGOUT') diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1e43569c96644d2375e6b23a0c94ead389528e24..1098822b3ab11afca7cf48c304034ec91d5014fb 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5782,6 +5782,10 @@ uuid@^3.0.0, uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" +v-clipboard@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/v-clipboard/-/v-clipboard-1.0.4.tgz#ffd423484c61b81685d7ea23f2abd2c0e25a7de0" + validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"