Add mutation to enable solutions by module
This commit is contained in:
parent
07785ae2c1
commit
4b93b410a5
|
|
@ -4,6 +4,7 @@ import factory
|
||||||
import wagtail_factories
|
import wagtail_factories
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from factory import CREATE_STRATEGY
|
from factory import CREATE_STRATEGY
|
||||||
|
from wagtail.core.models import Page
|
||||||
from wagtail.core.rich_text import RichText
|
from wagtail.core.rich_text import RichText
|
||||||
|
|
||||||
from assignments.models import Assignment
|
from assignments.models import Assignment
|
||||||
|
|
@ -16,6 +17,24 @@ class BookFactory(BasePageFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Book
|
model = Book
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_default_structure():
|
||||||
|
site = wagtail_factories.SiteFactory.create(is_default_site=True)
|
||||||
|
Page.objects.get(title='Root').delete()
|
||||||
|
|
||||||
|
book = BookFactory.create(parent=site.root_page, title='A book')
|
||||||
|
topic = TopicFactory.create(parent=book, order=1, title='A topic')
|
||||||
|
module = ModuleFactory.create(parent=topic,
|
||||||
|
title="A module",
|
||||||
|
meta_title="Modul 1",
|
||||||
|
teaser="Whatever",
|
||||||
|
intro="<p>Hello</p>")
|
||||||
|
chapter = ChapterFactory.create(parent=module, title="A chapter")
|
||||||
|
content_block = ContentBlockFactory.create(parent=chapter, module=module, title="A content block", type="task",
|
||||||
|
contents=[])
|
||||||
|
|
||||||
|
return book, topic, module, chapter, content_block
|
||||||
|
|
||||||
|
|
||||||
class TopicFactory(BasePageFactory):
|
class TopicFactory(BasePageFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -170,7 +189,8 @@ class ContentBlockFactory(BasePageFactory):
|
||||||
'url')] = 'https://picsum.photos/400/?random={}'.format(
|
'url')] = 'https://picsum.photos/400/?random={}'.format(
|
||||||
''.join(random.choice('abcdefghiklmn') for _ in range(6)))
|
''.join(random.choice('abcdefghiklmn') for _ in range(6)))
|
||||||
elif block_type == 'solution':
|
elif block_type == 'solution':
|
||||||
kwargs['{}__{}__{}__{}'.format(stream_field_name, i, 'solution', 'text')] = RichText(fake_paragraph())
|
kwargs['{}__{}__{}__{}'.format(stream_field_name, i, 'solution', 'text')] = RichText(
|
||||||
|
fake_paragraph())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, module, **kwargs):
|
def create(cls, module, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 2.0.6 on 2019-02-05 15:29
|
||||||
|
|
||||||
|
import assignments.models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import wagtail.core.blocks
|
||||||
|
import wagtail.core.fields
|
||||||
|
import wagtail.images.blocks
|
||||||
|
import wagtail.snippets.blocks
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('books', '0006_auto_20181204_1629'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='module',
|
||||||
|
name='solutions_enabled_by',
|
||||||
|
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='contentblock',
|
||||||
|
name='contents',
|
||||||
|
field=wagtail.core.fields.StreamField([('text_block', wagtail.core.blocks.StructBlock([('text', wagtail.core.blocks.RichTextBlock())])), ('basic_knowledge', wagtail.core.blocks.StructBlock([('description', wagtail.core.blocks.RichTextBlock()), ('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))])), ('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())], icon='tick')), ('video_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('document_block', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())]))], blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -72,4 +72,6 @@ class ContentBlock(StrictHierarchyPage):
|
||||||
parent_page_types = ['books.Chapter']
|
parent_page_types = ['books.Chapter']
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def module(self):
|
||||||
|
return self.get_parent().get_parent().specific
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from wagtail.images.edit_handlers import ImageChooserPanel
|
||||||
|
|
||||||
from books.blocks import DEFAULT_RICH_TEXT_FEATURES
|
from books.blocks import DEFAULT_RICH_TEXT_FEATURES
|
||||||
from core.wagtail_utils import StrictHierarchyPage
|
from core.wagtail_utils import StrictHierarchyPage
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -31,6 +32,8 @@ class Module(StrictHierarchyPage):
|
||||||
teaser = models.TextField()
|
teaser = models.TextField()
|
||||||
intro = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES)
|
intro = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES)
|
||||||
|
|
||||||
|
solutions_enabled_by = models.ManyToManyField(User)
|
||||||
|
|
||||||
content_panels = [
|
content_panels = [
|
||||||
FieldPanel('title', classname="full title"),
|
FieldPanel('title', classname="full title"),
|
||||||
FieldPanel('meta_title', classname="full title"),
|
FieldPanel('meta_title', classname="full title"),
|
||||||
|
|
@ -55,3 +58,4 @@ class Module(StrictHierarchyPage):
|
||||||
|
|
||||||
def get_child_ids(self):
|
def get_child_ids(self):
|
||||||
return self.get_children().values_list('id', flat=True)
|
return self.get_children().values_list('id', flat=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock
|
from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock
|
||||||
|
from books.schema.mutations.module import UpdateSolutionVisibility
|
||||||
|
|
||||||
|
|
||||||
class BookMutations(object):
|
class BookMutations(object):
|
||||||
mutate_content_block = MutateContentBlock.Field()
|
mutate_content_block = MutateContentBlock.Field()
|
||||||
add_content_block = AddContentBlock.Field()
|
add_content_block = AddContentBlock.Field()
|
||||||
delete_content_block = DeleteContentBlock.Field()
|
delete_content_block = DeleteContentBlock.Field()
|
||||||
|
update_solution_visibility = UpdateSolutionVisibility.Field()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import graphene
|
||||||
|
from graphene import relay
|
||||||
|
|
||||||
|
from api.utils import get_errors, get_object
|
||||||
|
from books.models import Module
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateSolutionVisibility(relay.ClientIDMutation):
|
||||||
|
class Input:
|
||||||
|
id = graphene.ID()
|
||||||
|
enabled = graphene.Boolean()
|
||||||
|
|
||||||
|
success = graphene.Boolean()
|
||||||
|
errors = graphene.List(graphene.String)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mutate_and_get_payload(cls, root, info, **args):
|
||||||
|
try:
|
||||||
|
id = args.get('id')
|
||||||
|
enabled = args.get('enabled')
|
||||||
|
user = info.context.user
|
||||||
|
if 'users.can_manage_school_class_content' not in user.get_role_permissions():
|
||||||
|
raise PermissionError()
|
||||||
|
|
||||||
|
module = get_object(Module, id)
|
||||||
|
if enabled:
|
||||||
|
module.solutions_enabled_by.add(user)
|
||||||
|
else:
|
||||||
|
module.solutions_enabled_by.remove(user)
|
||||||
|
module.save()
|
||||||
|
|
||||||
|
return cls(success=True)
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
errors = ["You don't have the permission to do that."]
|
||||||
|
except Exception as e:
|
||||||
|
errors = ['Error: {}'.format(e)]
|
||||||
|
|
||||||
|
return cls(success=False, errors=errors)
|
||||||
|
|
@ -4,9 +4,15 @@ from graphene_django import DjangoObjectType
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
|
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
|
from users.models import User, Role
|
||||||
from ..models import Book, Topic, Module, Chapter, ContentBlock
|
from ..models import Book, Topic, Module, Chapter, ContentBlock
|
||||||
|
|
||||||
|
|
||||||
|
def are_solutions_enabled_for(user: User, module: Module):
|
||||||
|
teacher = user.users_in_same_school_class().filter(user_roles__role=Role.objects.get_default_teacher_role()).first()
|
||||||
|
return teacher is not None and module.solutions_enabled_by.filter(pk=teacher.pk).exists()
|
||||||
|
|
||||||
|
|
||||||
class ContentBlockNode(DjangoObjectType):
|
class ContentBlockNode(DjangoObjectType):
|
||||||
mine = graphene.Boolean()
|
mine = graphene.Boolean()
|
||||||
|
|
||||||
|
|
@ -23,6 +29,13 @@ class ContentBlockNode(DjangoObjectType):
|
||||||
def resolve_mine(self, info, **kwargs):
|
def resolve_mine(self, info, **kwargs):
|
||||||
return self.owner is not None and self.owner.pk == info.context.user.pk
|
return self.owner is not None and self.owner.pk == info.context.user.pk
|
||||||
|
|
||||||
|
def resolve_contents(self, info, **kwargs):
|
||||||
|
if 'users.can_manage_school_class_content' not in info.context.user.get_role_permissions() \
|
||||||
|
and not are_solutions_enabled_for(info.context.user, self.module):
|
||||||
|
self.contents.stream_data = [content for content in self.contents.stream_data if
|
||||||
|
content['type'] != 'solution']
|
||||||
|
return self.contents
|
||||||
|
|
||||||
|
|
||||||
class ChapterNode(DjangoObjectType):
|
class ChapterNode(DjangoObjectType):
|
||||||
content_blocks = DjangoFilterConnectionField(ContentBlockNode)
|
content_blocks = DjangoFilterConnectionField(ContentBlockNode)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class NewContentBlockMutationTest(TestCase):
|
||||||
request.user = user
|
request.user = user
|
||||||
self.client = Client(schema=schema, context_value=request)
|
self.client = Client(schema=schema, context_value=request)
|
||||||
|
|
||||||
self.sibling_id = to_global_id('ContentBlock', content_block.pk)
|
self.sibling_id = to_global_id('ContentBlockNode', content_block.pk)
|
||||||
|
|
||||||
def test_add_new_content_block(self):
|
def test_add_new_content_block(self):
|
||||||
self.assertEqual(ContentBlock.objects.count(), 1)
|
self.assertEqual(ContentBlock.objects.count(), 1)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
from django.test import TestCase, RequestFactory
|
||||||
|
from graphene.test import Client
|
||||||
|
from graphql_relay import to_global_id
|
||||||
|
|
||||||
|
from api.schema import schema
|
||||||
|
|
||||||
|
from books.factories import BookFactory, TopicFactory, ModuleFactory, ChapterFactory, ContentBlockFactory
|
||||||
|
from books.models import ContentBlock
|
||||||
|
from users.models import User
|
||||||
|
from users.services import create_users
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleSolutionVisibilityTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
create_users()
|
||||||
|
_, _, self.module, chapter, _ = BookFactory.create_default_structure()
|
||||||
|
content = {
|
||||||
|
'type': 'solution',
|
||||||
|
'value': {
|
||||||
|
'text': '<p>Das ist eine Lösung.</p>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content_block = ContentBlockFactory.create(
|
||||||
|
parent=chapter,
|
||||||
|
module=self.module,
|
||||||
|
title="Another content block",
|
||||||
|
type="task",
|
||||||
|
contents=[content]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.teacher = User.objects.get(username="teacher")
|
||||||
|
self.student = User.objects.get(username="student1")
|
||||||
|
student_request = RequestFactory().get('/')
|
||||||
|
student_request.user = self.student
|
||||||
|
self.student_client = Client(schema=schema, context_value=student_request)
|
||||||
|
|
||||||
|
teacher_request = RequestFactory().get('/')
|
||||||
|
teacher_request.user = self.teacher
|
||||||
|
self.teacher_client = Client(schema=schema, context_value=teacher_request)
|
||||||
|
|
||||||
|
self.content_block_id = to_global_id('ContentBlockNode', content_block.pk)
|
||||||
|
|
||||||
|
self.update_mutation = mutation = """
|
||||||
|
mutation UpdateSolutionVisibility($input: UpdateSolutionVisibilityInput!) {
|
||||||
|
updateSolutionVisibility(input: $input) {
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.query = """
|
||||||
|
query ContentBlockQuery($id: ID!) {
|
||||||
|
contentBlock(id: $id) {
|
||||||
|
contents
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_hide_solutions_for_students_and_then_show_them(self):
|
||||||
|
result = self.student_client.execute(self.query, variables={
|
||||||
|
'id': self.content_block_id
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
self.assertEqual(len(result.get('data').get('contentBlock').get('contents')), 0)
|
||||||
|
|
||||||
|
result = self.teacher_client.execute(self.query, variables={
|
||||||
|
'id': self.content_block_id
|
||||||
|
})
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
self.assertEqual(len(result.get('data').get('contentBlock').get('contents')), 1)
|
||||||
|
|
||||||
|
module_id = to_global_id('ModuleNode', self.module.pk)
|
||||||
|
|
||||||
|
result = self.teacher_client.execute(self.update_mutation, variables={
|
||||||
|
'input': {
|
||||||
|
'id': module_id,
|
||||||
|
'enabled': True
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(result.get('data').get('updateSolutionVisibility').get('success'), True)
|
||||||
|
|
||||||
|
self.assertEqual(self.module.solutions_enabled_by.filter(pk=self.teacher.pk).count(), 1)
|
||||||
|
|
||||||
|
result = self.student_client.execute(self.query, variables={
|
||||||
|
'id': self.content_block_id
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
self.assertEqual(len(result.get('data').get('contentBlock').get('contents')), 1)
|
||||||
|
|
||||||
|
def test_try_to_show_solutions_as_student_and_fail(self):
|
||||||
|
result = self.student_client.execute(self.query, variables={
|
||||||
|
'id': self.content_block_id
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(len(result.get('data').get('contentBlock').get('contents')), 0)
|
||||||
|
|
||||||
|
module_id = to_global_id('ModuleNode', self.module.pk)
|
||||||
|
|
||||||
|
result = self.student_client.execute(self.update_mutation, variables={
|
||||||
|
'input': {
|
||||||
|
'id': module_id,
|
||||||
|
'enabled': True
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertIsNone(result.get('errors'))
|
||||||
|
self.assertFalse(result.get('data').get('success'))
|
||||||
|
|
||||||
|
result = self.student_client.execute(self.query, variables={
|
||||||
|
'id': self.content_block_id
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(len(result.get('data').get('contentBlock').get('contents')), 0)
|
||||||
Loading…
Reference in New Issue