From add2c2181522331a60798e0e39a4ebc182d0ce08 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 12 Nov 2019 15:15:09 +0100 Subject: [PATCH 01/32] Add feedback model, add migrations --- .../migrations/0006_submissionfeedback.py | 33 +++++++++++++++++++ server/assignments/models.py | 6 ++++ .../migrations/0004_auto_20191112_1413.py | 21 ++++++++++++ .../migrations/0016_auto_20191112_1413.py | 24 ++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 server/assignments/migrations/0006_submissionfeedback.py create mode 100644 server/basicknowledge/migrations/0004_auto_20191112_1413.py create mode 100644 server/books/migrations/0016_auto_20191112_1413.py diff --git a/server/assignments/migrations/0006_submissionfeedback.py b/server/assignments/migrations/0006_submissionfeedback.py new file mode 100644 index 00000000..8dfff620 --- /dev/null +++ b/server/assignments/migrations/0006_submissionfeedback.py @@ -0,0 +1,33 @@ +# Generated by Django 2.0.6 on 2019-11-12 14:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assignments', '0005_assignment_solution'), + ] + + operations = [ + migrations.CreateModel( + name='SubmissionFeedback', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('text', models.TextField(blank=True)), + ('student_submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='assignments.StudentSubmission')), + ('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-modified', '-created'), + 'get_latest_by': 'modified', + 'abstract': False, + }, + ), + ] diff --git a/server/assignments/models.py b/server/assignments/models.py index 52927639..01a6fb3e 100644 --- a/server/assignments/models.py +++ b/server/assignments/models.py @@ -37,3 +37,9 @@ class StudentSubmission(TimeStampedModel): def __str__(self): return '{} - {}'.format(self.student.full_name, self.text) + + +class SubmissionFeedback(TimeStampedModel): + text = models.TextField(blank=True) + teacher = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='feedbacks') + student_submission = models.OneToOneField(StudentSubmission, on_delete=models.CASCADE, related_name='feedback') diff --git a/server/basicknowledge/migrations/0004_auto_20191112_1413.py b/server/basicknowledge/migrations/0004_auto_20191112_1413.py new file mode 100644 index 00000000..14d7396a --- /dev/null +++ b/server/basicknowledge/migrations/0004_auto_20191112_1413.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0.6 on 2019-11-12 14:13 + +from django.db import migrations +import wagtail.core.blocks +import wagtail.core.fields +import wagtail.images.blocks + + +class Migration(migrations.Migration): + + dependencies = [ + ('basicknowledge', '0003_auto_20190912_1228'), + ] + + operations = [ + migrations.AlterField( + model_name='basicknowledge', + name='contents', + field=wagtail.core.fields.StreamField([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['bold', 'ul']))])), ('image_block', wagtail.images.blocks.ImageChooserBlock()), ('link_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('section_title', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())])), ('infogram_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('genially_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('thinglink_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('subtitle', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())]))], blank=True, null=True), + ), + ] diff --git a/server/books/migrations/0016_auto_20191112_1413.py b/server/books/migrations/0016_auto_20191112_1413.py new file mode 100644 index 00000000..ce6ead32 --- /dev/null +++ b/server/books/migrations/0016_auto_20191112_1413.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.6 on 2019-11-12 14:13 + +import assignments.models +from django.db import migrations +import surveys.models +import wagtail.core.blocks +import wagtail.core.fields +import wagtail.images.blocks +import wagtail.snippets.blocks + + +class Migration(migrations.Migration): + + dependencies = [ + ('books', '0015_contentblock_bookmarks'), + ] + + operations = [ + migrations.AlterField( + model_name='contentblock', + name='contents', + field=wagtail.core.fields.StreamField([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))])), ('basic_knowledge', wagtail.core.blocks.StructBlock([('description', wagtail.core.blocks.RichTextBlock(required=False)), ('basic_knowledge', wagtail.core.blocks.PageChooserBlock(required=True, target_model=['basicknowledge.BasicKnowledge']))])), ('assignment', wagtail.core.blocks.StructBlock([('assignment_id', wagtail.snippets.blocks.SnippetChooserBlock(assignments.models.Assignment))])), ('survey', wagtail.core.blocks.StructBlock([('survey_id', wagtail.snippets.blocks.SnippetChooserBlock(surveys.models.Survey))])), ('image_block', wagtail.images.blocks.ImageChooserBlock()), ('image_url_block', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('link_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('solution', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))], icon='tick')), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('infogram_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('genially_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('thinglink_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('subtitle', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())])), ('module_room_slug', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock())])), ('content_list_item', wagtail.core.blocks.StreamBlock([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))])), ('basic_knowledge', wagtail.core.blocks.StructBlock([('description', wagtail.core.blocks.RichTextBlock(required=False)), ('basic_knowledge', wagtail.core.blocks.PageChooserBlock(required=True, target_model=['basicknowledge.BasicKnowledge']))])), ('assignment', wagtail.core.blocks.StructBlock([('assignment_id', wagtail.snippets.blocks.SnippetChooserBlock(assignments.models.Assignment))])), ('survey', wagtail.core.blocks.StructBlock([('survey_id', wagtail.snippets.blocks.SnippetChooserBlock(surveys.models.Survey))])), ('image_block', wagtail.images.blocks.ImageChooserBlock()), ('image_url_block', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('link_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock()), ('url', wagtail.core.blocks.URLBlock())])), ('solution', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock(features=['ul']))], icon='tick')), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('infogram_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock()), ('title', wagtail.core.blocks.TextBlock())])), ('genially_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('thinglink_block', wagtail.core.blocks.StructBlock([('id', wagtail.core.blocks.TextBlock())])), ('subtitle', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.TextBlock())])), ('module_room_slug', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.TextBlock())]))]))], blank=True, null=True), + ), + ] From f84efc7f1cc2d8b621101bc4ac671cca317301ff Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 13 Nov 2019 16:29:53 +0100 Subject: [PATCH 02/32] Add mutations and tests --- server/assignments/factories.py | 11 +- .../0007_submissionfeedback_final.py | 18 ++ .../migrations/0008_auto_20191113_1430.py | 19 ++ .../migrations/0009_auto_20191113_1433.py | 23 ++ server/assignments/models.py | 4 +- server/assignments/schema/inputs.py | 7 + server/assignments/schema/mutations.py | 36 ++- server/assignments/schema/queries.py | 7 +- server/assignments/schema/types.py | 33 ++- .../tests/test_assignment_permissions.py | 1 - server/assignments/tests/test_feedback.py | 205 ++++++++++++++++++ server/books/schema/queries.py | 1 + server/users/models.py | 2 +- 13 files changed, 357 insertions(+), 10 deletions(-) create mode 100644 server/assignments/migrations/0007_submissionfeedback_final.py create mode 100644 server/assignments/migrations/0008_auto_20191113_1430.py create mode 100644 server/assignments/migrations/0009_auto_20191113_1433.py create mode 100644 server/assignments/tests/test_feedback.py diff --git a/server/assignments/factories.py b/server/assignments/factories.py index 7711ff2f..4265bf0c 100644 --- a/server/assignments/factories.py +++ b/server/assignments/factories.py @@ -3,7 +3,7 @@ import random import factory from books.factories import ModuleFactory -from .models import Assignment, StudentSubmission +from .models import Assignment, StudentSubmission, SubmissionFeedback from core.factories import fake @@ -24,3 +24,12 @@ class StudentSubmissionFactory(factory.django.DjangoModelFactory): text = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8))) assignment = factory.SubFactory(AssignmentFactory) final = False + + +class SubmissionFeedbackFactory(factory.django.DjangoModelFactory): + class Meta: + model = SubmissionFeedback + + text = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8))) + student_submission = factory.SubFactory(StudentSubmissionFactory) + final = False diff --git a/server/assignments/migrations/0007_submissionfeedback_final.py b/server/assignments/migrations/0007_submissionfeedback_final.py new file mode 100644 index 00000000..ca38f86d --- /dev/null +++ b/server/assignments/migrations/0007_submissionfeedback_final.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.6 on 2019-11-13 12:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assignments', '0006_submissionfeedback'), + ] + + operations = [ + migrations.AddField( + model_name='submissionfeedback', + name='final', + field=models.BooleanField(default=False), + ), + ] diff --git a/server/assignments/migrations/0008_auto_20191113_1430.py b/server/assignments/migrations/0008_auto_20191113_1430.py new file mode 100644 index 00000000..f59641b7 --- /dev/null +++ b/server/assignments/migrations/0008_auto_20191113_1430.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.6 on 2019-11-13 14:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assignments', '0007_submissionfeedback_final'), + ] + + operations = [ + migrations.AlterField( + model_name='submissionfeedback', + name='student_submission', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='assignments.StudentSubmission'), + ), + ] diff --git a/server/assignments/migrations/0009_auto_20191113_1433.py b/server/assignments/migrations/0009_auto_20191113_1433.py new file mode 100644 index 00000000..267daab4 --- /dev/null +++ b/server/assignments/migrations/0009_auto_20191113_1433.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.6 on 2019-11-13 14:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assignments', '0008_auto_20191113_1430'), + ] + + operations = [ + migrations.RemoveField( + model_name='submissionfeedback', + name='id', + ), + migrations.AlterField( + model_name='submissionfeedback', + name='student_submission', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='assignments.StudentSubmission'), + ), + ] diff --git a/server/assignments/models.py b/server/assignments/models.py index 01a6fb3e..aeafe9f6 100644 --- a/server/assignments/models.py +++ b/server/assignments/models.py @@ -42,4 +42,6 @@ class StudentSubmission(TimeStampedModel): class SubmissionFeedback(TimeStampedModel): text = models.TextField(blank=True) teacher = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='feedbacks') - student_submission = models.OneToOneField(StudentSubmission, on_delete=models.CASCADE, related_name='feedback') + student_submission = models.OneToOneField(StudentSubmission, on_delete=models.CASCADE, primary_key=True) + final = models.BooleanField(default=False) + diff --git a/server/assignments/schema/inputs.py b/server/assignments/schema/inputs.py index 85ae96c3..8847e001 100644 --- a/server/assignments/schema/inputs.py +++ b/server/assignments/schema/inputs.py @@ -7,3 +7,10 @@ class AssignmentInput(InputObjectType): answer = graphene.String(required=True) document = graphene.String() final = graphene.Boolean() + + +class SubmissionFeedbackInput(InputObjectType): + id = graphene.ID() + student_submission = graphene.ID(required=True) + text = graphene.String(required=True) + final = graphene.Boolean() diff --git a/server/assignments/schema/mutations.py b/server/assignments/schema/mutations.py index d0ba62b0..fa752d7f 100644 --- a/server/assignments/schema/mutations.py +++ b/server/assignments/schema/mutations.py @@ -1,10 +1,12 @@ import graphene from graphene import relay +from graphql_relay import from_global_id +from rest_framework.exceptions import PermissionDenied from api.utils import get_object -from assignments.models import Assignment -from assignments.schema.types import AssignmentNode, StudentSubmissionNode -from .inputs import AssignmentInput +from assignments.models import Assignment, SubmissionFeedback +from assignments.schema.types import AssignmentNode, StudentSubmissionNode, SubmissionFeedbackNode +from .inputs import AssignmentInput, SubmissionFeedbackInput class UpdateAssignment(relay.ClientIDMutation): @@ -30,5 +32,33 @@ class UpdateAssignment(relay.ClientIDMutation): return cls(successful=True, updated_assignment=assignment, submission=submission, errors=None) +class UpdateSubmissionFeedback(relay.ClientIDMutation): + class Input: + submission_feedback = graphene.Argument(SubmissionFeedbackInput) + + updated_submission_feedback = graphene.Field(SubmissionFeedbackNode) + successful = graphene.Boolean() + errors = graphene.List(graphene.String) + + @classmethod + def mutate_and_get_payload(cls, root, info, **kwargs): + submission_feedback_data = kwargs.get('submission_feedback') + user = info.context.user + student_submission_id = from_global_id(submission_feedback_data['student_submission'])[1] + + if not user.has_perm('users.can_manage_school_class_content'): + raise PermissionDenied('Missing permissions') + + (submission_feedback, created) = SubmissionFeedback.objects.get_or_create(teacher=user, + student_submission_id=student_submission_id) + + submission_feedback.final = submission_feedback_data.get('final') + submission_feedback.text = submission_feedback_data.get('text') + submission_feedback.save() + + return cls(successful=True, updated_submission_feedback=submission_feedback, errors=None) + + class AssignmentMutations(object): update_assignment = UpdateAssignment.Field() + update_submission_feedback = UpdateSubmissionFeedback.Field() diff --git a/server/assignments/schema/queries.py b/server/assignments/schema/queries.py index 7f9ac9da..80d1e3d9 100644 --- a/server/assignments/schema/queries.py +++ b/server/assignments/schema/queries.py @@ -1,8 +1,11 @@ +import graphene from graphene import relay from graphene_django.filter import DjangoFilterConnectionField +from rest_framework.exceptions import PermissionDenied -from assignments.models import StudentSubmission -from assignments.schema.types import AssignmentNode, StudentSubmissionNode +from api.utils import get_by_id_or_slug +from assignments.models import StudentSubmission, SubmissionFeedback +from assignments.schema.types import AssignmentNode, StudentSubmissionNode, SubmissionFeedbackNode class AssignmentsQuery(object): diff --git a/server/assignments/schema/types.py b/server/assignments/schema/types.py index 92107b8a..25ff3bf9 100644 --- a/server/assignments/schema/types.py +++ b/server/assignments/schema/types.py @@ -1,17 +1,48 @@ import graphene +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from graphene import relay from graphene_django import DjangoObjectType -from assignments.models import Assignment, StudentSubmission +from assignments.models import Assignment, StudentSubmission, SubmissionFeedback from books.utils import are_solutions_enabled_for +class SubmissionFeedbackNode(DjangoObjectType): + class Meta: + model = SubmissionFeedback + filter_fields = [] + interfaces = (relay.Node,) + + class StudentSubmissionNode(DjangoObjectType): + submissionfeedback = graphene.Field(SubmissionFeedbackNode) + class Meta: model = StudentSubmission filter_fields = [] interfaces = (relay.Node,) + def resolve_submissionfeedback(self, info, **kwargs): + + user = info.context.user + + if not hasattr(self, 'submissionfeedback'): + return None + + # teacher path + if user.has_perm('users.can_manage_school_class_content'): + if self.submissionfeedback.teacher == user: + return self.submissionfeedback + else: + raise PermissionDenied('Missing permissions') + + # student path + + if self.submissionfeedback.final: + return self.submissionfeedback + + return None + class AssignmentNode(DjangoObjectType): submission = graphene.Field(StudentSubmissionNode) diff --git a/server/assignments/tests/test_assignment_permissions.py b/server/assignments/tests/test_assignment_permissions.py index 8b9235ce..6980c5b0 100644 --- a/server/assignments/tests/test_assignment_permissions.py +++ b/server/assignments/tests/test_assignment_permissions.py @@ -15,7 +15,6 @@ class AssignmentPermissionsTestCase(DefaultUserTestCase): self.assignment_id = to_global_id('AssignmentNode', self.assignment.pk) self.module_id = to_global_id('ModuleNode', self.assignment.module.pk) - def _submit_submission(self, user=None): mutation = ''' mutation UpdateAssignment($input: UpdateAssignmentInput!) { diff --git a/server/assignments/tests/test_feedback.py b/server/assignments/tests/test_feedback.py new file mode 100644 index 00000000..5a4ab4f5 --- /dev/null +++ b/server/assignments/tests/test_feedback.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# ITerativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2019 ITerativ GmbH. All rights reserved. +# +# Created on 2019-11-13 +# @author: chrigu + +from graphql_relay import to_global_id + +from api.test_utils import create_client, DefaultUserTestCase +from assignments.models import Assignment, StudentSubmission +from users.factories import SchoolClassFactory +from ..factories import AssignmentFactory, StudentSubmissionFactory, SubmissionFeedbackFactory + + +class SubmissionFeedbackTestCase(DefaultUserTestCase): + def setUp(self): + super(SubmissionFeedbackTestCase, self).setUp() + + self.assignment = AssignmentFactory( + owner=self.teacher + ) + self.assignment_id = to_global_id('AssignmentNode', self.assignment.pk) + + self.student_submission = StudentSubmissionFactory(assignment=self.assignment, student=self.student1, final=False) + self.student_submission_id = to_global_id('StudentSubmissionNode', self.student_submission.pk) + + school_class = SchoolClassFactory() + school_class.users.add(self.student1) + school_class.users.add(self.teacher) + school_class.users.add(self.teacher2) + + def _create_submission_feedback(self, user, final, text, student_submission_id): + mutation = ''' + mutation UpdateSubmissionFeedback($input: UpdateSubmissionFeedbackInput!) { + updateSubmissionFeedback(input: $input){ + updatedSubmissionFeedback { + id + text + final + } + } + } + ''' + + client = create_client(user) + + return client.execute(mutation, variables={ + 'input': { + "submissionFeedback": { + "studentSubmission": student_submission_id, + "text": text, + "final": final + } + } + }) + + def _fetch_assignment_student(self, user): + client = create_client(user) + query = ''' + query AssignmentWithSubmissions($id: ID!) { + assignment(id: $id) { + title + submission { + id + text + document + submissionfeedback { + text + } + } + } + } + ''' + return client.execute(query, variables={ + 'id': self.assignment_id + }) + + def _fetch_assignment_teacher(self, user): + client = create_client(user) + query = ''' + query AssignmentWithSubmissions($id: ID!) { + assignment(id: $id) { + title + submissions { + id + text + document + submissionfeedback { + text + } + } + } + } + ''' + return client.execute(query, variables={ + 'id': self.assignment_id + }) + + def _fetch_submission_feedback(self, user): + client = create_client(user) + query = ''' + query AssignmentWithSubmissions($id: ID!) { + assignment(id: $id) { + title + submissions { + id + text + document + submissionfeedback { + text + } + } + } + } + ''' + return client.execute(query, variables={ + 'id': self.assignment_id + }) + + def test_teacher_can_create_feedback(self): + + result = self._create_submission_feedback(self.teacher, False, 'Balalal', self.student_submission_id) + + self.assertIsNone(result.get('errors')) + self.assertIsNotNone(result.get('data').get('updateSubmissionFeedback').get('updatedSubmissionFeedback').get('id')) + + def test_student_cannot_create_feedback(self): + + result = self._create_submission_feedback(self.student1, False, 'Balalal', self.student_submission_id) + self.assertIsNotNone(result.get('errors')) + + def test_teacher_can_update_feedback(self): + + assignment = AssignmentFactory( + owner=self.teacher + ) + + student_submission = StudentSubmissionFactory(assignment=assignment, student=self.student1, final=False) + submission_feedback = SubmissionFeedbackFactory(teacher=self.teacher, final=False, + student_submission=student_submission) + submission_feedback_id = to_global_id('SubmissionFeedback', submission_feedback.pk) + + result = self._create_submission_feedback(self.teacher, True, 'Some', submission_feedback_id) + + self.assertIsNone(result.get('errors')) + + submission_feedback_response = result.get('data').get('updateSubmissionFeedback').get('updatedSubmissionFeedback') + + self.assertTrue(submission_feedback_response.get('final')) + self.assertEqual(submission_feedback_response.get('text'), 'Some') + + def test_rogue_teacher_cannot_update_feedback(self): + + assignment = AssignmentFactory( + owner=self.teacher + ) + + student_submission = StudentSubmissionFactory(assignment=assignment, student=self.student1, final=False) + submission_feedback = SubmissionFeedbackFactory(teacher=self.teacher, final=False, + student_submission=student_submission) + submission_feedback_id = to_global_id('SubmissionFeedback', submission_feedback.pk) + + result = self._create_submission_feedback(self.teacher2, True, 'Some', submission_feedback_id) + + self.assertIsNotNone(result.get('errors')) + + def test_student_does_not_see_non_final_feedback(self): + + SubmissionFeedbackFactory(teacher=self.teacher, final=False, student_submission=self.student_submission) + result = self._fetch_assignment_student(self.student1) + + self.assertIsNone(result.get('data').get('submissionfeedback')) + + def test_student_does_see_final_feedback(self): + + submission_feedback = SubmissionFeedbackFactory(teacher=self.teacher, final=True, + student_submission=self.student_submission) + result = self._fetch_assignment_student(self.student1) + print(result) + self.assertEqual(result.get('data').get('assignment').get('submission').get('submissionfeedback') + .get('text'), submission_feedback.text) + + def test_teacher_can_see_feedback_for_submission(self): + submission_feedback = SubmissionFeedbackFactory(teacher=self.teacher, final=False, + student_submission=self.student_submission) + self.student_submission.final = True + self.student_submission.save() + + result = self._fetch_assignment_teacher(self.teacher) + self.assertEqual(result.get('data').get('assignment').get('submissions')[0].get('submissionfeedback') + .get('text'), submission_feedback.text) + + def test_rogue_teacher_cannot_see_feedback(self): + submission_feedback = SubmissionFeedbackFactory(teacher=self.teacher, final=False, + student_submission=self.student_submission) + self.student_submission.final = True + self.student_submission.save() + + result = self._fetch_assignment_teacher(self.teacher2) + print(result) + self.assertIsNone(result.get('data').get('assignment').get('submissions')[0].get('submissionfeedback')) diff --git a/server/books/schema/queries.py b/server/books/schema/queries.py index 3ac6288b..18d86840 100644 --- a/server/books/schema/queries.py +++ b/server/books/schema/queries.py @@ -1,5 +1,6 @@ import graphene from graphene import relay +from django.db.models import Q from graphene_django import DjangoObjectType from graphene_django.filter import DjangoFilterConnectionField diff --git a/server/users/models.py b/server/users/models.py index 070fa503..321c9f18 100644 --- a/server/users/models.py +++ b/server/users/models.py @@ -44,7 +44,7 @@ class User(AbstractUser): def get_teacher(self): if self.user_roles.filter(role__key='teacher').exists(): return self - elif self.school_classes.count()>0: + elif self.school_classes.count() > 0: return self.school_classes.first().get_teacher() else: return None From ad07ada2f2aede9b65dc244fefc8daa234c02f24 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Wed, 13 Nov 2019 19:25:24 +0100 Subject: [PATCH 03/32] Add feedback to submission view, style view --- .../components/AssignmentWithSubmissions.vue | 32 ++++++++++++++++--- client/src/components/StudentSubmission.vue | 11 +++---- .../gql/assignmentWithSubmissionsQuery.gql | 5 +++ client/src/pages/submissions.vue | 14 ++++---- client/src/styles/_student-submission.scss | 10 ++++++ client/src/styles/main.scss | 1 + 6 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 client/src/styles/_student-submission.scss diff --git a/client/src/components/AssignmentWithSubmissions.vue b/client/src/components/AssignmentWithSubmissions.vue index 9381c655..1e392634 100644 --- a/client/src/components/AssignmentWithSubmissions.vue +++ b/client/src/components/AssignmentWithSubmissions.vue @@ -1,14 +1,24 @@ @@ -80,6 +92,7 @@ } &__text { + font-size: toRem(26px); margin-bottom: 1rem; } @@ -95,10 +108,19 @@ &__link { display: block; - &:first-of-type { - border-top: 1px solid $color-silver-dark; - } + } + + &__submissions { + margin-top: 3rem; } } + + .submission-header { + &__title { + color: $color-silver-dark; + font-family: $sans-serif-font-family; + } + } + diff --git a/client/src/components/StudentSubmission.vue b/client/src/components/StudentSubmission.vue index 386d399c..00584f41 100644 --- a/client/src/components/StudentSubmission.vue +++ b/client/src/components/StudentSubmission.vue @@ -1,5 +1,5 @@ @@ -50,12 +53,6 @@ @import "@/styles/_functions.scss"; .student-submission { - display: grid; - grid-template-columns: 170px 1fr; - grid-column-gap: 80px; - align-items: center; - border-bottom: 1px solid $color-silver-dark; - padding: 15px 0; &__student-name { font-size: toRem(17px); diff --git a/client/src/graphql/gql/assignmentWithSubmissionsQuery.gql b/client/src/graphql/gql/assignmentWithSubmissionsQuery.gql index f98b2636..28dd7206 100644 --- a/client/src/graphql/gql/assignmentWithSubmissionsQuery.gql +++ b/client/src/graphql/gql/assignmentWithSubmissionsQuery.gql @@ -19,6 +19,11 @@ query AssignmentWithSubmissions($id: ID!) { } } } + submissionfeedback { + id + text + final + } } } } diff --git a/client/src/pages/submissions.vue b/client/src/pages/submissions.vue index 4043a5dc..661cadbc 100644 --- a/client/src/pages/submissions.vue +++ b/client/src/pages/submissions.vue @@ -1,9 +1,6 @@