Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 169-add-date-to-examtype
  • 233-make-exam-a-many-to-many-field-on-studentinfo-model
  • 236-improve-importer-experience
  • 243-replace-toggle-buttons-with-switches
  • 250-update-vuetify
  • 258-add-markdown-viewer
  • 265-fix-selection-changing-on-window-switching
  • 272-reviewers-should-be-able-to-assign-exercise-groups-to-tutors
  • 276-create-new-yarn-lockfile
  • 279-tutor-overview-no-scrolling
  • 282-copy-button-does-not-work-when-reviewing-corrections
  • 286-fix-misalignment-of-hide-show-sidebar-buttons
  • 287-build-test-image-constantly-failing
  • 288-add-dropdown-to-participantspage-to-set-students-groups
  • 289-fix-change-log-card
  • 291-revise-to-old-export-scheme
  • 292-update-gitlab-ci-config-for-new-runner
  • 292-update-gitlab-ci-config-for-new-runner-2
  • add-exercise-util-script
  • document-frontend-components
  • grady-exam
  • jakob.dieterle-master-patch-13835
  • master
  • parallel-test
  • test-233-branch-remove-examtype-foreign-key-on-group
  • update-export-dialogs
  • 0.0.1
  • 0.1
  • 0.2
  • 0.3
  • 0.4
  • 0.4.1
  • 0.4.2
  • 0.5.0
  • 0.5.1
  • 1.0.0
  • 1.1.0
  • 2.0.0
  • 2.0.1
  • 2.1.0
  • 2.1.1
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 4.0.0
  • 4.1.0
  • 4.2.0
  • 4.3.0
  • 4.4.0
  • 4.4.1
  • 5.0.0
  • 5.0.1
  • 5.1.0
  • 5.1.1
  • 5.1.2
  • 5.1.3
  • 5.1.4
  • 5.1.5
  • 5.1.6
  • 5.1.7
  • 5.2.0
  • 5.3.0
  • 5.3.1
  • 5.3.2
  • 5.4.0
  • 5.4.1
  • 5.4.2
  • 6.0.0
  • 6.1.0
  • legacy
70 results

Target

Select target project
  • j.michal/grady
1 result
Select Git revision
  • 169-add-date-to-examtype
  • 233-make-exam-a-many-to-many-field-on-studentinfo-model
  • 236-improve-importer-experience
  • 243-replace-toggle-buttons-with-switches
  • 250-update-vuetify
  • 258-add-markdown-viewer
  • 265-fix-selection-changing-on-window-switching
  • 272-reviewers-should-be-able-to-assign-exercise-groups-to-tutors
  • 276-create-new-yarn-lockfile
  • 279-tutor-overview-no-scrolling
  • 282-copy-button-does-not-work-when-reviewing-corrections
  • 286-fix-misalignment-of-hide-show-sidebar-buttons
  • 287-build-test-image-constantly-failing
  • 288-add-dropdown-to-participantspage-to-set-students-groups
  • 289-fix-change-log-card
  • 291-revise-to-old-export-scheme
  • 292-update-gitlab-ci-config-for-new-runner
  • 292-update-gitlab-ci-config-for-new-runner-2
  • add-exercise-util-script
  • document-frontend-components
  • grady-exam
  • jakob.dieterle-master-patch-13835
  • master
  • parallel-test
  • test-233-branch-remove-examtype-foreign-key-on-group
  • update-export-dialogs
  • 0.0.1
  • 0.1
  • 0.2
  • 0.3
  • 0.4
  • 0.4.1
  • 0.4.2
  • 0.5.0
  • 0.5.1
  • 1.0.0
  • 1.1.0
  • 2.0.0
  • 2.0.1
  • 2.1.0
  • 2.1.1
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 4.0.0
  • 4.1.0
  • 4.2.0
  • 4.3.0
  • 4.4.0
  • 4.4.1
  • 5.0.0
  • 5.0.1
  • 5.1.0
  • 5.1.1
  • 5.1.2
  • 5.1.3
  • 5.1.4
  • 5.1.5
  • 5.1.6
  • 5.1.7
  • 5.2.0
  • 5.3.0
  • 5.3.1
  • 5.3.2
  • 5.4.0
  • 5.4.1
  • 5.4.2
  • 6.0.0
  • 6.1.0
  • legacy
70 results
Show changes
Commits on Source (3)
  • robinwilliam.hundt's avatar
    Student page is fixed. Subscription & Feedback creation partially working · 32dd9a3f
    robinwilliam.hundt authored
    Fixed reverse query bug in Subscription model
    Fixed bug in subscription view resulting in uncaught exception
    Creating a subscription with a query/key/stage combination for which no assignments were available would result in an uncaught SubscriptionEnded exception and a 500 response to the client. Instead an error message with the status code 410_GONE is now sent.
    
    Fixed reverse query bug in Subscription model
    
    chnaged
    type_query_mapper = {
    	...
            SUBMISSION_TYPE_QUERY: 'type__title',
        }
    
    to
    type_query_mapper = {
            SUBMISSION_TYPE_QUERY: 'type__name',
        }
    
    Refactored serializer id fields and camelCase names
    
    To provide a uniform api and to save us from further work i've refactored the existing fields that used
    camelCase names to use the names specified in the models (which are kebab-case).
    Also everywhere where id's (whether normal or uuid ones) have been included in the serializers, the field names have been changed to 'pk' or '<model>_pk'. Pk will always link to the primary key of the model and will save us great pain should we decide to convert the pk's of more models to uuid's. Also we won't have to remebre a bunch of different ways of referring to the id for the frontend, it's always pk.
    I also included the pk field in all modelserializers since this will be necessary for the frontend state management.
    
    Frontend now expects pk fields and snake_case
    
    Solution is highlighted / Desc. HTML is rendered
    
    Frontend test is only manually run
    
    Added vue-notification library
    
    Inactivity detection preperly implementd
    
    Client inactivity is now properly detected. A vuex plugin is used to store the time of the last commited mutation. This roughly equals the last user interaction.
    If the users session is expired he will be redirected to the login page. Before that a dialog is displayed notifieng the user that they are about to be logged out.
    
    Added created / of_tutor info to feedback comment
    32dd9a3f
  • robinwilliam.hundt's avatar
    Constant for vuex Mutations / Exam Type subscriptions · a22a5c50
    robinwilliam.hundt authored
    See #90 for Problem with ExamType subscriptions
    a22a5c50
  • robinwilliam.hundt's avatar
    a2a13770
Showing
with 516 additions and 246 deletions
......@@ -33,6 +33,7 @@ public/
*.sublime-*
.idea/
.vscode/
anon-export/
# node
node_modules
......@@ -64,6 +64,7 @@ test_flake8:
test_frontend:
<<: *test_definition_frontend
when: manual
stage: test
script:
- yarn install
......
......@@ -434,8 +434,8 @@ class GeneralTaskSubscription(models.Model):
type_query_mapper = {
RANDOM: '__any',
STUDENT_QUERY: 'student__student_id',
EXAM_TYPE_QUERY: 'student__examtype__module_reference',
SUBMISSION_TYPE_QUERY: 'type__title',
EXAM_TYPE_QUERY: 'student__exam__module_reference',
SUBMISSION_TYPE_QUERY: 'type__name',
}
QUERY_CHOICE = (
......
......@@ -3,7 +3,6 @@ import logging
from django.core.exceptions import ObjectDoesNotExist
from drf_dynamic_fields import DynamicFieldsMixin
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from core import models
from core.models import (ExamType, Feedback, GeneralTaskSubscription,
......@@ -45,7 +44,7 @@ class ExamSerializer(DynamicFieldsModelSerializer):
class Meta:
model = ExamType
fields = ('module_reference', 'total_score',
fields = ('pk', 'module_reference', 'total_score',
'pass_score', 'pass_only',)
......@@ -62,16 +61,10 @@ class FeedbackForSubmissionLineSerializer(serializers.BaseSerializer):
class FeedbackSerializer(DynamicFieldsModelSerializer):
assignment_id = serializers.UUIDField(write_only=True)
assignment_pk = serializers.UUIDField(write_only=True)
feedback_lines = FeedbackForSubmissionLineSerializer(
required=False
)
isFinal = serializers.BooleanField(source="is_final", required=False)
ofSubmission = serializers.PrimaryKeyRelatedField(
source='of_submission',
required=False,
queryset=Submission.objects.all(),
validators=[UniqueValidator(queryset=Feedback.objects.all()), ])
def create(self, validated_data) -> Feedback:
feedback = Feedback.objects.create(
......@@ -96,13 +89,13 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
def validate(self, data):
log.debug(data)
assignment_id = data.pop('assignment_id')
assignment_pk = data.pop('assignment_pk')
score = data.get('score')
is_final = data.get('is_final', False)
try:
assignment = TutorSubmissionAssignment.objects.get(
assignment_id=assignment_id)
pk=assignment_pk)
except ObjectDoesNotExist as err:
raise serializers.ValidationError('No assignment for given id.')
......@@ -133,21 +126,20 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
class Meta:
model = Feedback
fields = ('assignment_id', 'isFinal', 'score',
'ofSubmission', 'feedback_lines')
fields = ('pk', 'assignment_pk', 'is_final', 'score',
'of_submission', 'feedback_lines')
read_only_fields = ('of_submission', )
class FeedbackCommentSerializer(serializers.ModelSerializer):
ofTutor = serializers.StringRelatedField(source='of_tutor.username')
isFinalComment = serializers.BooleanField(source='is_final')
of_tutor = serializers.StringRelatedField(source='of_tutor.username')
def to_internal_value(self, data):
return data
class Meta:
model = models.FeedbackComment
fields = ('text', 'ofTutor', 'created', 'isFinalComment')
fields = ('text', 'of_tutor', 'created', 'is_final')
read_only_fields = ('created',)
......@@ -155,22 +147,21 @@ class TestSerializer(DynamicFieldsModelSerializer):
class Meta:
model = Test
fields = ('name', 'label', 'annotation')
fields = ('pk', 'name', 'label', 'annotation')
class SubmissionTypeListSerializer(DynamicFieldsModelSerializer):
fullScore = serializers.IntegerField(source='full_score')
class Meta:
model = SubmissionType
fields = ('id', 'name', 'fullScore')
fields = ('pk', 'name', 'full_score')
class SubmissionTypeSerializer(SubmissionTypeListSerializer):
class Meta:
model = SubmissionType
fields = ('id', 'name', 'fullScore', 'description', 'solution')
fields = ('pk', 'name', 'full_score', 'description', 'solution')
class SubmissionSerializer(DynamicFieldsModelSerializer):
......@@ -180,17 +171,17 @@ class SubmissionSerializer(DynamicFieldsModelSerializer):
class Meta:
model = Submission
fields = ('type', 'text', 'feedback', 'tests')
fields = ('pk', 'type', 'text', 'feedback', 'tests')
class SubmissionListSerializer(DynamicFieldsModelSerializer):
type = SubmissionTypeListSerializer(fields=('id', 'name', 'fullScore'))
type = SubmissionTypeListSerializer(fields=('pk', 'name', 'full_score'))
# TODO change this according to new feedback model
feedback = FeedbackSerializer(fields=('score',))
class Meta:
model = Submission
fields = ('type', 'feedback')
fields = ('pk', 'type', 'feedback')
class StudentInfoSerializer(DynamicFieldsModelSerializer):
......@@ -201,17 +192,17 @@ class StudentInfoSerializer(DynamicFieldsModelSerializer):
class Meta:
model = StudentInfo
fields = ('name', 'user', 'matrikel_no', 'exam', 'submissions')
fields = ('pk', 'name', 'user', 'matrikel_no', 'exam', 'submissions')
class SubmissionNoTextFieldsSerializer(DynamicFieldsModelSerializer):
score = serializers.ReadOnlyField(source='feedback.score')
type = serializers.ReadOnlyField(source='type.name')
fullScore = serializers.ReadOnlyField(source='type.full_score')
full_score = serializers.ReadOnlyField(source='type.full_score')
class Meta:
model = Submission
fields = ('type', 'score', 'fullScore')
fields = ('pk', 'type', 'score', 'full_score')
class StudentInfoSerializerForListView(DynamicFieldsModelSerializer):
......@@ -222,7 +213,7 @@ class StudentInfoSerializerForListView(DynamicFieldsModelSerializer):
class Meta:
model = StudentInfo
fields = ('name', 'user', 'exam', 'submissions')
fields = ('pk', 'name', 'user', 'exam', 'submissions')
class TutorSerializer(DynamicFieldsModelSerializer):
......@@ -236,26 +227,26 @@ class TutorSerializer(DynamicFieldsModelSerializer):
class Meta:
model = UserAccount
fields = ('username', 'done_assignments_count')
fields = ('pk', 'username', 'done_assignments_count')
class AssignmentSerializer(DynamicFieldsModelSerializer):
submission_id = serializers.ReadOnlyField(
source='submission.submission_id')
submission_pk = serializers.ReadOnlyField(
source='submission.pk')
class Meta:
model = TutorSubmissionAssignment
fields = ('assignment_id', 'submission_id', 'is_done',)
fields = ('pk', 'submission_pk', 'is_done',)
class SubmissionAssignmentSerializer(DynamicFieldsModelSerializer):
text = serializers.ReadOnlyField()
typeId = serializers.ReadOnlyField(source='type.id')
fullScore = serializers.ReadOnlyField(source='type.full_score')
type_pk = serializers.ReadOnlyField(source='type.pk')
full_score = serializers.ReadOnlyField(source='type.full_score')
class Meta:
model = Submission
fields = ('submission_id', 'typeId', 'text', 'fullScore')
fields = ('pk', 'type_pk', 'text', 'full_score')
class AssignmentDetailSerializer(DynamicFieldsModelSerializer):
......@@ -264,7 +255,7 @@ class AssignmentDetailSerializer(DynamicFieldsModelSerializer):
class Meta:
model = TutorSubmissionAssignment
fields = ('assignment_id', 'feedback', 'submission', 'is_done',)
fields = ('pk', 'feedback', 'submission', 'is_done',)
class SubscriptionSerializer(DynamicFieldsModelSerializer):
......@@ -300,7 +291,7 @@ class SubscriptionSerializer(DynamicFieldsModelSerializer):
class Meta:
model = GeneralTaskSubscription
fields = (
'subscription_id',
'pk',
'owner',
'query_type',
'query_key',
......
......@@ -138,10 +138,11 @@ class AccessRightsOfExamTypeAPIViewTest(APITestCase):
response = self.view(self.request)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_tutor_has_no_access(self):
force_authenticate(self.request, user=self.tutor)
response = self.view(self.request)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
# TODO see issue #90 for details
# def test_tutor_has_no_access(self):
# force_authenticate(self.request, user=self.tutor)
# response = self.view(self.request)
# self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_reviewer_has_access(self):
force_authenticate(self.request, user=self.reviewer)
......
......@@ -69,8 +69,8 @@ class FeedbackRetrieveTestCase(APITestCase):
self.assertIn(2, self.data['feedback_lines'])
def test_if_feedback_contains_final(self):
self.assertIn('isFinal', self.data)
self.assertIsNotNone(self.data['isFinal'])
self.assertIn('is_final', self.data)
self.assertIsNotNone(self.data['is_final'])
def test_if_comment_contains_text(self):
self.assertIn('text', self.data['feedback_lines'][1][0])
......@@ -82,15 +82,15 @@ class FeedbackRetrieveTestCase(APITestCase):
self.assertIsNotNone(self.data['feedback_lines'][1][0]['created'])
def test_if_comment_has_tutor(self):
self.assertIn('ofTutor', self.data['feedback_lines'][1][0])
self.assertIn('of_tutor', self.data['feedback_lines'][1][0])
self.assertEqual(
self.tutor.username,
self.data['feedback_lines'][1][0]['ofTutor'])
self.data['feedback_lines'][1][0]['of_tutor'])
def test_if_comment_has_final(self):
self.assertIn('isFinalComment', self.data['feedback_lines'][1][0])
self.assertIn('is_final', self.data['feedback_lines'][1][0])
self.assertIsNotNone(
self.data['feedback_lines'][1][0]['isFinalComment'])
self.data['feedback_lines'][1][0]['is_final'])
class FeedbackCreateTestCase(APITestCase):
......@@ -123,8 +123,8 @@ class FeedbackCreateTestCase(APITestCase):
# to the max Score for this submission
data = {
'score': 10,
'isFinal': False,
'assignment_id': self.assignment.assignment_id
'is_final': False,
'assignment_pk': self.assignment.pk
}
self.assertEqual(Feedback.objects.count(), 0)
......@@ -135,8 +135,8 @@ class FeedbackCreateTestCase(APITestCase):
def test_cannot_create_feedback_with_score_higher_than_max(self):
data = {
'score': 101,
'isFinal': False,
'assignment_id': self.assignment.assignment_id
'is_final': False,
'assignment_pk': self.assignment.pk
}
self.assertEqual(Feedback.objects.count(), 0)
response = self.client.post(self.url, data, format='json')
......@@ -146,8 +146,8 @@ class FeedbackCreateTestCase(APITestCase):
def test_cannot_create_feedback_with_score_less_than_zero(self):
data = {
'score': -1,
'isFinal': False,
'assignment_id': self.assignment.assignment_id
'is_final': False,
'assignment_pk': self.assignment.pk
}
self.assertEqual(Feedback.objects.count(), 0)
response = self.client.post(self.url, data, format='json')
......@@ -157,8 +157,8 @@ class FeedbackCreateTestCase(APITestCase):
def test_check_score_is_set_accordingly(self):
data = {
'score': 5,
'isFinal': False,
'assignment_id': self.assignment.assignment_id
'is_final': False,
'assignment_pk': self.assignment.pk
}
self.client.post(self.url, data, format='json')
object_score = self.sub.feedback.score
......@@ -167,8 +167,8 @@ class FeedbackCreateTestCase(APITestCase):
def test_can_create_feedback_with_comment(self):
data = {
'score': 0,
'isFinal': False,
'assignment_id': self.assignment.assignment_id,
'is_final': False,
'assignment_pk': self.assignment.pk,
'feedback_lines': {
'5': {
'text': 'Nice meth!'
......@@ -183,8 +183,8 @@ class FeedbackCreateTestCase(APITestCase):
def test_feedback_comment_is_created_correctly(self):
data = {
'score': 0,
'isFinal': False,
'assignment_id': self.assignment.assignment_id,
'is_final': False,
'assignment_pk': self.assignment.pk,
'feedback_lines': {
'5': {
'text': 'Nice meth!'
......@@ -202,8 +202,8 @@ class FeedbackCreateTestCase(APITestCase):
def test_can_create_multiple_feedback_comments(self):
data = {
'score': 0,
'isFinal': False,
'assignment_id': self.assignment.assignment_id,
'is_final': False,
'assignment_pk': self.assignment.pk,
'feedback_lines': {
'5': {
'text': 'Nice meth!'
......
......@@ -104,12 +104,12 @@ class StudentPageTests(APITestCase):
def test_a_student_submissions_contains_type_id(self):
self.assertEqual(
self.submission_list_first_entry['type']['id'],
self.submission_list_first_entry['type']['pk'],
self.student_info.submissions.first().type.id)
def test_submission_data_contains_full_score(self):
self.assertEqual(
self.submission_list_first_entry['type']['fullScore'],
self.submission_list_first_entry['type']['full_score'],
self.student_info.submissions.first().type.full_score)
def test_submission_data_contains_feedback_score(self):
......@@ -182,12 +182,12 @@ class StudentSelfSubmissionsTests(APITestCase):
def test_a_student_submissions_contains_type_id(self):
self.assertEqual(
self.submission_list_first_entry['type']['id'],
self.student_info.submissions.first().type.id)
self.submission_list_first_entry['type']['pk'],
self.student_info.submissions.first().type.pk)
def test_submission_data_contains_full_score(self):
self.assertEqual(
self.submission_list_first_entry['type']['fullScore'],
self.submission_list_first_entry['type']['full_score'],
self.student_info.submissions.first().type.full_score)
def test_submission_data_contains_description(self):
......@@ -202,7 +202,7 @@ class StudentSelfSubmissionsTests(APITestCase):
def test_submission_data_contains_final_status(self):
self.assertEqual(
self.submission_list_first_entry['feedback']['isFinal'],
self.submission_list_first_entry['feedback']['is_final'],
self.student_info.submissions.first().feedback.is_final)
def test_submission_data_contains_feedback_score(self):
......
......@@ -76,4 +76,4 @@ class StudentPageTests(APITestCase):
def test_submissions_full_score_is_included(self):
print(self.response.data[0]['submissions'][0])
self.assertEqual(self.student.submissions.first().type.full_score,
self.response.data[0]['submissions'][0]['fullScore'])
self.response.data[0]['submissions'][0]['full_score'])
......@@ -37,7 +37,7 @@ class SubmissionTypeViewTestList(APITestCase):
self.assertEqual('Hard question', self.response.data[0]['name'])
def test_get_full_score(self):
self.assertEqual(20, self.response.data[0]['fullScore'])
self.assertEqual(20, self.response.data[0]['full_score'])
class SubmissionTypeViewTestRetrieve(APITestCase):
......@@ -62,13 +62,13 @@ class SubmissionTypeViewTestRetrieve(APITestCase):
self.assertEqual(self.response.status_code, status.HTTP_200_OK)
def test_get_id(self):
self.assertEqual(self.pk, self.response.data['id'])
self.assertEqual(self.pk, self.response.data['pk'])
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'])
self.assertEqual(20, self.response.data['full_score'])
def test_get_descritpion(self):
self.assertEqual('Whatever', self.response.data['description'])
......
......@@ -160,15 +160,15 @@ class TestApiEndpoints(APITestCase):
response_subs = client.post(
'/api/subscription/', {'query_type': 'random'})
subscription_id = response_subs.data['subscription_id']
assignment_id = response_subs.data['assignments'][0]['assignment_id']
subscription_id = response_subs.data['pk']
assignment_pk = response_subs.data['assignments'][0]['pk']
response = client.get(
f'/api/subscription/{subscription_id}/assignments/current/')
self.assertEqual(1, len(response_subs.data['assignments']))
self.assertEqual(assignment_id,
response.data['assignment_id'])
self.assertEqual(assignment_pk,
response.data['pk'])
response_next = client.get(
f'/api/subscription/{subscription_id}/assignments/next/')
......@@ -176,7 +176,7 @@ class TestApiEndpoints(APITestCase):
client.get(f'/api/subscription/{subscription_id}/')
self.assertEqual(2, len(response_detail_subs.data['assignments']))
self.assertNotEqual(assignment_id, response_next.data['assignment_id'])
self.assertNotEqual(assignment_pk, response_next.data['pk'])
def test_subscription_can_assign_to_student(self):
client = APIClient()
......@@ -207,11 +207,11 @@ class TestApiEndpoints(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
assignment_id = response.data['assignments'][0]['assignment_id']
assignment_pk = response.data['assignments'][0]['pk']
response = client.post(
f'/api/feedback/', {
"score": 23,
"assignment_id": assignment_id,
"assignment_pk": assignment_pk,
"feedback_lines": {
2: {"text": "< some string >"},
3: {"text": "< some string >"}
......@@ -232,7 +232,7 @@ class TestApiEndpoints(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
subscription_id = response.data['subscription_id']
subscription_id = response.data['pk']
response = client.get(
f'/api/subscription/{subscription_id}/assignments/current/')
......@@ -240,7 +240,7 @@ class TestApiEndpoints(APITestCase):
submission_id_in_database = models.Feedback.objects.filter(
is_final=False).first().of_submission.submission_id
submission_id_in_response = \
response.data['submission']['submission_id']
response.data['submission']['pk']
self.assertEqual(
str(submission_id_in_database),
......
......@@ -50,7 +50,7 @@ class StudentSelfSubmissionsApiView(generics.ListAPIView):
class ExamApiViewSet(viewsets.ReadOnlyModelViewSet):
""" Gets a list of an individual exam by Id if provided """
permission_classes = (IsReviewer,)
permission_classes = (IsTutorOrReviewer,)
queryset = ExamType.objects.all()
serializer_class = ExamSerializer
......@@ -163,10 +163,17 @@ class SubscriptionApiViewSet(
serializer.is_valid(raise_exception=True)
subscription = serializer.save()
if subscription.query_type == GeneralTaskSubscription.STUDENT_QUERY:
subscription.reserve_all_assignments_for_a_student()
else:
subscription.get_oldest_unfinished_assignment()
try:
if subscription.query_type == \
GeneralTaskSubscription.STUDENT_QUERY:
subscription.reserve_all_assignments_for_a_student()
else:
subscription.get_oldest_unfinished_assignment()
except models.SubscriptionEnded as err:
return Response(
{'Error': 'This subscription has no available submissions'},
status.HTTP_410_GONE
)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data,
......
......@@ -18,9 +18,11 @@
"material-design-icons": "^3.0.1",
"v-clipboard": "^1.0.4",
"vue": "^2.5.2",
"vue-notification": "^1.3.6",
"vue-router": "^3.0.1",
"vuetify": "^0.17.3",
"vuex": "^3.0.1"
"vuex": "^3.0.1",
"vuex-persistedstate": "^2.4.2"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
......
<template>
<div id="app">
<v-app>
<notifications/>
<router-view/>
<v-dialog
persistent
width="fit-content"
v-model="logoutDialog"
>
<v-card>
<v-card-title class="headline">
You'll be logged out!
</v-card-title>
<v-card-text>
Due to inactivity you'll be logged out in a couple of moments.<br/>
Any unsaved work will be lost.
Click Continue to stay logged in.
</v-card-text>
<v-card-actions>
<v-btn flat color="grey lighten-0"
@click="logout"
>Logout now</v-btn>
<v-spacer/>
<v-btn flat color="blue darken-2"
@click="continueWork"
>Continue</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-app>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'app',
components: {
data () {
return {
timer: 0,
logoutDialog: false
}
},
computed: {
...mapState([
'lastAppInteraction'
]),
...mapState({
tokenCreationTime: state => state.authentication.tokenCreationTime,
refreshingToken: state => state.authentication.refreshingToken,
jwtTimeDelta: state => state.authentication.jwtTimeDelta
})
},
methods: {
logout () {
this.logoutDialog = false
this.$store.dispatch('logout')
},
continueWork () {
this.$store.dispatch('refreshJWT')
this.logoutDialog = false
}
},
watch: {
lastAppInteraction: function (val) {
const timeSinceLastRefresh = Date.now() - this.tokenCreationTime
const timeDelta = this.jwtTimeDelta
// refresh jwt if it's older than 20% of his maximum age
if (timeDelta > 0 && timeSinceLastRefresh > timeDelta * 0.2 &&
!this.refreshingToken) {
this.$store.dispatch('refreshJWT')
}
}
},
mounted () {
const oneAndHalfMinute = 90 * 1e3
this.timer = setInterval(() => {
if (this.$route.path !== '/') {
if (Date.now() > this.tokenCreationTime + this.jwtTimeDelta) {
this.logoutDialog = false
this.$store.dispatch('logout', "You've been logged out due to inactivity.")
} else if (Date.now() + oneAndHalfMinute > this.tokenCreationTime + this.jwtTimeDelta) {
this.logoutDialog = true
}
}
}, 5 * 1e3)
},
beforeDestroy () {
clearInterval(this.timer)
}
}
</script>
......
import axios from 'axios'
// import store from '@/store/store'
function addFieldsToUrl ({url, fields = []}) {
return fields.length > 0 ? url + '?fields=pk,' + fields : url
}
let ax = axios.create({
baseURL: 'http://localhost:8000/',
headers: {'Authorization': 'JWT ' + sessionStorage.getItem('token')}
})
export async function fetchJWT (credentials) {
const token = (await ax.post('/api-token-auth/', credentials)).data.token
ax.defaults.headers['Authorization'] = `JWT ${token}`
return token
}
export async function refreshJWT (token) {
const newToken = (await ax.post('/api-token-refresh/', {token})).data.token
ax.defaults.headers['Authorization'] = `JWT ${newToken}`
return token
}
export async function fetchJWTTimeDelta () {
return (await ax.get('/api/jwt-time-delta/')).data.timeDelta
}
export async function fetchUserRole () {
return (await ax.get('/api/user-role/')).data.role
}
export async function fetchStudentSelfData () {
return (await ax.get('/api/student-page/')).data
}
export async function fetchStudentSubmissions () {
return (await ax.get('/api/student-submissions/')).data
}
export async function fetchSubscriptions () {
return (await ax.get('/api/subscription/')).data
}
export async function fetchSubscription (subscriptionPk) {
return (await ax.get(`/api/subscription/${subscriptionPk}`)).data
}
export async function fetchExamType ({examPk, fields = []}) {
let url = addFieldsToUrl({
url: `/api/examtype/${examPk !== undefined ? examPk + '/' : ''}`,
fields})
return (await ax.get(url)).data
}
export async function subscribeTo (type, key, stage) {
let data = {
query_type: type
}
if (key) {
data.query_key = key
}
if (stage) {
data.feedback_stage = stage
}
return (await ax.post('/api/subscription/', data)).data
}
export async function fetchCurrentAssignment (subscriptionPk) {
return (await ax.get(`/api/subscription/${subscriptionPk}/assignments/current/`)).data
}
export async function fetchNextAssignment (subscriptionPk) {
return (await ax.get(`/api/subscription/${subscriptionPk}/assignments/next/`)).data
}
export async function submitFeedbackForAssignment (feedback, assignmentPk) {
const data = {
...feedback,
assignment_pk: assignmentPk
}
return (await ax.post('/api/feedback/', data)).data
}
export async function fetchSubmissionTypes (fields = []) {
let url = '/api/submissiontype/'
if (fields.length > 0) {
url += '?fields=pk,' + fields
}
return (await ax.get(url)).data
}
export default ax
......@@ -7,7 +7,7 @@
permanent
:mini-variant="mini"
>
<v-toolbar flat>
<v-toolbar>
<v-list>
<v-list-tile>
<v-list-tile-action v-if="mini">
......@@ -15,10 +15,12 @@
<v-icon>chevron_right</v-icon>
</v-btn>
</v-list-tile-action>
<v-list-tile-content class="title">
<v-list-tile-content
class="title"
>
<slot name="header"></slot>
</v-list-tile-content>
<v-list-tile-action>
<v-list-tile-action v-if="!mini">
<v-btn icon @click.native.stop="mini = !mini">
<v-icon>chevron_left</v-icon>
</v-btn>
......@@ -54,7 +56,9 @@
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex'
import { mapGetters, mapState } from 'vuex'
// import {act} from '@/store/actions'
export default {
name: 'base-layout',
data () {
......@@ -66,15 +70,15 @@
...mapGetters([
'gradySpeak'
]),
...mapState([
'username',
'userRole'
])
...mapState({
username: state => state.authentication.username,
userRole: state => state.authentication.userRole
})
},
methods: {
...mapActions([
'logout'
])
logout () {
this.$store.dispatch('logout')
}
},
watch: {
mini: function () {
......@@ -92,4 +96,8 @@
.grady-toolbar {
font-weight: bold;
}
.title {
color: gray;
}
</style>
<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-layout column>
<span class="title mb-2">{{ name }} - Full score: {{ full_score }}</span>
<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
v-if="item.title === 'Description'"
color="grey lighten-4">
<v-card-text class="ml-2">
<span
v-html="item.text"
></span>
</v-card-text>
</v-card>
<v-flex v-else-if="item.title === 'Solution'">
<pre
class="prettyprint elevation-2 solution-code"
:class="language"
>{{item.text}}</pre>
</v-flex>
</v-expansion-panel-content>
</v-expansion-panel>
</v-layout>
</v-container>
</template>
......@@ -34,10 +46,14 @@
type: String,
required: true
},
fullScore: {
full_score: {
type: Number,
required: true
},
language: {
type: String,
default: 'lang-c'
},
reverse: {
type: Boolean,
default: false
......@@ -70,7 +86,19 @@
return items
}
}
},
mounted () {
this.$nextTick(() => {
window.PR.prettyPrint()
})
}
}
</script>
<style scoped>
.solution-code {
border-width: 0px;
white-space: pre-wrap;
}
</style>
......@@ -9,8 +9,8 @@
<template slot="items" slot-scope="props">
<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>
<td class="text-xs-right">{{ props.item.type.full_score }}</td>
<td class="text-xs-right"><v-btn :to="`/student/submission/${props.item.type.pk}`" color="orange lighten-2"><v-icon>chevron_right</v-icon></v-btn></td>
</template>
</v-data-table>
<v-alert color="info" value="true">
......@@ -38,7 +38,7 @@
},
{
text: 'Maximum Score',
value: 'type.fullScore'
value: 'type.full_score'
}
]
}
......@@ -54,10 +54,15 @@
return this.submissions.map(a => a.feedback.score).reduce((a, b) => a + b)
},
sumFullScore () {
return this.submissions.map(a => a.type.fullScore).reduce((a, b) => a + b)
return this.submissions.map(a => a.type.full_score).reduce((a, b) => a + b)
},
pointRatio () {
return ((this.sumScore / this.sumFullScore) * 100).toFixed(2)
},
students () {
this.items.forEach(item => {
})
}
}
}
......
<template>
<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 } 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
}
},
data: function () {
return {
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])
}
},
mounted () {
window.PR.prettyPrint()
}
}
</script>
<style scoped>
table {
table-layout: auto;
border-collapse: collapse;
width: 100%;
}
.line-number-cell {
vertical-align: top;
}
pre.prettyprint {
padding: 0;
border: 0;
}
code {
width: 100%;
box-shadow: None;
}
.line-number-btn {
height: fit-content;
min-width: fit-content;
margin: 0;
}
</style>
<template>
<v-container>
<base-annotated-submission>
<annotated-submission-top-toolbar
class="mb-1 elevation-1"
slot="header"
/>
<template slot="table-content">
<tr v-for="(code, lineNo) in submission" :key="lineNo">
<submission-line
:code="code"
:line-no="lineNo"
@toggleEditor="toggleEditorOnLine(lineNo)"
>
<template>
<feedback-comment
v-if="origFeedback[lineNo]"
v-for="(comment, index) in origFeedback[lineNo]"
v-bind="comment"
:key="index"
@click.native="toggleEditorOnLine(lineNo, comment)"
/>
</template>
<feedback-comment
v-if="updatedFeedback[lineNo]"
borderColor="orange"
v-bind="updatedFeedback[lineNo]"
:deletable="true"
@click.native="toggleEditorOnLine(lineNo, updatedFeedback[lineNo])"
@delete="deleteFeedback(lineNo)"
/>
<comment-form
v-if="showEditorOnLine[lineNo]"
:feedback="selectedComment[lineNo].text"
:lineNo="lineNo"
@collapseFeedbackForm="toggleEditorOnLine(lineNo)"
@submitFeedback=""
>
</comment-form>
</submission-line>
</tr>
</template>
<annotated-submission-bottom-toolbar
class="mt-1 elevation-1"
slot="footer"
:loading="loading"
:fullScore="submissionObj['full_score']"
@submitFeedback="submitFeedback"
/>
</base-annotated-submission>
</v-container>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import CommentForm from '@/components/submission_notes/base/CommentForm.vue'
import FeedbackComment from '@/components/submission_notes/base/FeedbackComment.vue'
import AnnotatedSubmissionTopToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar'
import AnnotatedSubmissionBottomToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar'
import BaseAnnotatedSubmission from '@/components/submission_notes/base/BaseAnnotatedSubmission'
import SubmissionLine from '@/components/submission_notes/base/SubmissionLine'
import {subNotesMut, subNotesNamespace} from '@/store/modules/submission-notes'
export default {
components: {
SubmissionLine,
BaseAnnotatedSubmission,
AnnotatedSubmissionBottomToolbar,
AnnotatedSubmissionTopToolbar,
FeedbackComment,
CommentForm},
name: 'submission-correction',
data () {
return {
loading: false
}
},
props: {
assignment: {
type: Object
},
submissionWithoutAssignment: {
type: Object
},
feedback: {
type: Object
}
},
computed: {
...mapState({
showEditorOnLine: state => state.submissionNotes.ui.showEditorOnLine,
selectedComment: state => state.submissionNotes.ui.selectedCommentOnLine,
origFeedback: state => state.submissionNotes.orig.feedbackLines,
updatedFeedback: state => state.submissionNotes.updated.feedbackLines
}),
...mapGetters([
'isStudent',
'isTutor',
'isReviewer',
'getSubmission',
'getFeedback',
'getSubmissionType'
]),
submission () {
return this.$store.getters['submissionNotes/submission']
},
submissionObj () {
return this.assignment ? this.assignment.submission : this.submissionWithoutAssignment
},
feedbackObj () {
return this.assignment ? this.assignment.feedback : this.feedback
}
},
methods: {
deleteFeedback (lineNo) {
this.$store.commit(subNotesNamespace(subNotesMut.DELETE_FEEDBACK_LINE), lineNo)
},
toggleEditorOnLine (lineNo, comment = '') {
this.$store.commit(subNotesNamespace(subNotesMut.TOGGLE_EDITOR_ON_LINE), {lineNo, comment})
},
submitFeedback () {
this.loading = true
this.$store.dispatch(subNotesNamespace('submitFeedback'), this.assignment).then(() => {
this.$store.commit(subNotesNamespace(subNotesMut.RESET_STATE))
this.$emit('feedbackCreated')
}).catch(err => {
this.$notify({
title: 'Feedback creation Error!',
text: err.message,
type: 'error'
})
}).finally(() => {
this.loading = false
})
},
init () {
this.$store.commit(subNotesNamespace(subNotesMut.RESET_STATE))
this.$store.commit(subNotesNamespace(subNotesMut.SET_RAW_SUBMISSION), this.submissionObj.text)
this.$store.commit(subNotesNamespace(subNotesMut.SET_ORIG_FEEDBACK), this.feedbackObj)
this.$nextTick(() => {
window.PR.prettyPrint()
})
}
},
mounted () {
this.init()
}
}
</script>
<style scoped>
</style>
<template>
<div>
<slot name="header"/>
<table class="submission-table elevation-1">
<slot name="table-content"/>
</table>
<slot name="footer"/>
</div>
</template>
<script>
export default {
name: 'base-annotated-submission'
}
</script>
<style scoped>
.submission-table {
table-layout: auto;
border-collapse: collapse;
width: 100%;
}
</style>