Add mutations and tests
This commit is contained in:
parent
add2c21815
commit
f84efc7f1c
|
|
@ -3,7 +3,7 @@ import random
|
||||||
import factory
|
import factory
|
||||||
|
|
||||||
from books.factories import ModuleFactory
|
from books.factories import ModuleFactory
|
||||||
from .models import Assignment, StudentSubmission
|
from .models import Assignment, StudentSubmission, SubmissionFeedback
|
||||||
|
|
||||||
from core.factories import fake
|
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)))
|
text = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(4, 8)))
|
||||||
assignment = factory.SubFactory(AssignmentFactory)
|
assignment = factory.SubFactory(AssignmentFactory)
|
||||||
final = False
|
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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -42,4 +42,6 @@ class StudentSubmission(TimeStampedModel):
|
||||||
class SubmissionFeedback(TimeStampedModel):
|
class SubmissionFeedback(TimeStampedModel):
|
||||||
text = models.TextField(blank=True)
|
text = models.TextField(blank=True)
|
||||||
teacher = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='feedbacks')
|
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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,10 @@ class AssignmentInput(InputObjectType):
|
||||||
answer = graphene.String(required=True)
|
answer = graphene.String(required=True)
|
||||||
document = graphene.String()
|
document = graphene.String()
|
||||||
final = graphene.Boolean()
|
final = graphene.Boolean()
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionFeedbackInput(InputObjectType):
|
||||||
|
id = graphene.ID()
|
||||||
|
student_submission = graphene.ID(required=True)
|
||||||
|
text = graphene.String(required=True)
|
||||||
|
final = graphene.Boolean()
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import graphene
|
import graphene
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
|
from graphql_relay import from_global_id
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
from assignments.models import Assignment
|
from assignments.models import Assignment, SubmissionFeedback
|
||||||
from assignments.schema.types import AssignmentNode, StudentSubmissionNode
|
from assignments.schema.types import AssignmentNode, StudentSubmissionNode, SubmissionFeedbackNode
|
||||||
from .inputs import AssignmentInput
|
from .inputs import AssignmentInput, SubmissionFeedbackInput
|
||||||
|
|
||||||
|
|
||||||
class UpdateAssignment(relay.ClientIDMutation):
|
class UpdateAssignment(relay.ClientIDMutation):
|
||||||
|
|
@ -30,5 +32,33 @@ class UpdateAssignment(relay.ClientIDMutation):
|
||||||
return cls(successful=True, updated_assignment=assignment, submission=submission, errors=None)
|
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):
|
class AssignmentMutations(object):
|
||||||
update_assignment = UpdateAssignment.Field()
|
update_assignment = UpdateAssignment.Field()
|
||||||
|
update_submission_feedback = UpdateSubmissionFeedback.Field()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
|
import graphene
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
from assignments.models import StudentSubmission
|
from api.utils import get_by_id_or_slug
|
||||||
from assignments.schema.types import AssignmentNode, StudentSubmissionNode
|
from assignments.models import StudentSubmission, SubmissionFeedback
|
||||||
|
from assignments.schema.types import AssignmentNode, StudentSubmissionNode, SubmissionFeedbackNode
|
||||||
|
|
||||||
|
|
||||||
class AssignmentsQuery(object):
|
class AssignmentsQuery(object):
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,48 @@
|
||||||
import graphene
|
import graphene
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
from graphene_django import DjangoObjectType
|
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
|
from books.utils import are_solutions_enabled_for
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionFeedbackNode(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = SubmissionFeedback
|
||||||
|
filter_fields = []
|
||||||
|
interfaces = (relay.Node,)
|
||||||
|
|
||||||
|
|
||||||
class StudentSubmissionNode(DjangoObjectType):
|
class StudentSubmissionNode(DjangoObjectType):
|
||||||
|
submissionfeedback = graphene.Field(SubmissionFeedbackNode)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = StudentSubmission
|
model = StudentSubmission
|
||||||
filter_fields = []
|
filter_fields = []
|
||||||
interfaces = (relay.Node,)
|
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):
|
class AssignmentNode(DjangoObjectType):
|
||||||
submission = graphene.Field(StudentSubmissionNode)
|
submission = graphene.Field(StudentSubmissionNode)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ class AssignmentPermissionsTestCase(DefaultUserTestCase):
|
||||||
self.assignment_id = to_global_id('AssignmentNode', self.assignment.pk)
|
self.assignment_id = to_global_id('AssignmentNode', self.assignment.pk)
|
||||||
self.module_id = to_global_id('ModuleNode', self.assignment.module.pk)
|
self.module_id = to_global_id('ModuleNode', self.assignment.module.pk)
|
||||||
|
|
||||||
|
|
||||||
def _submit_submission(self, user=None):
|
def _submit_submission(self, user=None):
|
||||||
mutation = '''
|
mutation = '''
|
||||||
mutation UpdateAssignment($input: UpdateAssignmentInput!) {
|
mutation UpdateAssignment($input: UpdateAssignmentInput!) {
|
||||||
|
|
|
||||||
|
|
@ -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 <christian.cueni@iterativ.ch>
|
||||||
|
|
||||||
|
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'))
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import graphene
|
import graphene
|
||||||
from graphene import relay
|
from graphene import relay
|
||||||
|
from django.db.models import Q
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class User(AbstractUser):
|
||||||
def get_teacher(self):
|
def get_teacher(self):
|
||||||
if self.user_roles.filter(role__key='teacher').exists():
|
if self.user_roles.filter(role__key='teacher').exists():
|
||||||
return self
|
return self
|
||||||
elif self.school_classes.count()>0:
|
elif self.school_classes.count() > 0:
|
||||||
return self.school_classes.first().get_teacher()
|
return self.school_classes.first().get_teacher()
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue