Add unit test, model and mutation for snapshots
This commit is contained in:
parent
85706d73d1
commit
15aff9054c
|
|
@ -0,0 +1,49 @@
|
|||
# Generated by Django 2.2.19 on 2021-04-14 21:16
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('books', '0024_auto_20210218_1336'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ChapterSnapshot',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title_hidden', models.BooleanField(default=False)),
|
||||
('description_hidden', models.BooleanField(default=False)),
|
||||
('chapter', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='chapter_snapshots', to='books.Chapter')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Snapshot',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('chapters', models.ManyToManyField(through='books.ChapterSnapshot', to='books.Chapter')),
|
||||
('hidden_content_blocks', models.ManyToManyField(related_name='hidden_for_snapshots', to='books.ContentBlock')),
|
||||
('module', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='books.Module')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ContentBlockSnapshot',
|
||||
fields=[
|
||||
('contentblock_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='books.ContentBlock')),
|
||||
('hidden', models.BooleanField(default=False)),
|
||||
('snapshot', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='custom_content_blocks', to='books.Snapshot')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('books.contentblock',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chaptersnapshot',
|
||||
name='snapshot',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chapter_snapshots', to='books.Snapshot'),
|
||||
),
|
||||
]
|
||||
|
|
@ -3,3 +3,4 @@ from .module import *
|
|||
from .topic import *
|
||||
from .chapter import *
|
||||
from .contentblock import *
|
||||
from .snapshot import *
|
||||
|
|
|
|||
|
|
@ -110,3 +110,13 @@ class ContentBlock(StrictHierarchyPage):
|
|||
survey.module = module
|
||||
survey.save()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ContentBlockSnapshot(ContentBlock):
|
||||
hidden = models.BooleanField(default=False)
|
||||
snapshot = models.ForeignKey(
|
||||
'books.snapshot',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name='custom_content_blocks'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
from books.models import Chapter, ContentBlock, ContentBlockSnapshot
|
||||
|
||||
|
||||
class ChapterSnapshot(models.Model):
|
||||
"""
|
||||
Captures the state of a chapter at the time when the snapshot was taken, for the school class that was selected
|
||||
for the user creating the snapshot
|
||||
"""
|
||||
chapter = models.ForeignKey(
|
||||
'books.Chapter',
|
||||
related_name='chapter_snapshots',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
snapshot = models.ForeignKey(
|
||||
'books.Snapshot',
|
||||
related_name='chapter_snapshots',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
title_hidden = models.BooleanField(default=False)
|
||||
description_hidden = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class SnapshotManager(models.Manager):
|
||||
def create_snapshot(self, module, school_class, user, *args, **kwargs):
|
||||
snapshot = self.create(module=module, *args, **kwargs)
|
||||
chapters = Chapter.get_by_parent(module).filter(
|
||||
Q(description_hidden_for=school_class)
|
||||
| Q(title_hidden_for=school_class)
|
||||
)
|
||||
for chapter in chapters:
|
||||
ChapterSnapshot.objects.create(
|
||||
chapter=chapter,
|
||||
snapshot=snapshot,
|
||||
title_hidden=chapter.title_hidden_for.filter(id=school_class.id).exists(),
|
||||
description_hidden=chapter.description_hidden_for.filter(id=school_class.id).exists()
|
||||
)
|
||||
base_qs = ContentBlock.get_by_parent(chapter).filter(snapshotcontentblock__isnull=True)
|
||||
for content_block in base_qs.filter(user_created=False):
|
||||
if content_block.hidden_for.filter(id=school_class.id).exists():
|
||||
snapshot.hidden_content_blocks.add(content_block)
|
||||
for content_block in base_qs.filter(user_created=True).filter(owner=user):
|
||||
new_content_block = SnapshotContentBlock(
|
||||
hidden=False,
|
||||
snapshot=snapshot,
|
||||
contents=content_block.contents,
|
||||
type=content_block.type,
|
||||
title=content_block.title
|
||||
)
|
||||
content_block.add_sibling(instance=new_content_block, pos='right')
|
||||
revision = new_content_block.save_revision()
|
||||
revision.publish()
|
||||
new_content_block.save()
|
||||
|
||||
return snapshot
|
||||
|
||||
|
||||
class Snapshot(models.Model):
|
||||
module = models.ForeignKey(
|
||||
'books.Module',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
chapters = models.ManyToManyField(
|
||||
'books.Chapter',
|
||||
through=ChapterSnapshot
|
||||
)
|
||||
hidden_content_blocks = models.ManyToManyField(
|
||||
'books.ContentBlock',
|
||||
related_name='hidden_for_snapshots'
|
||||
)
|
||||
objects = SnapshotManager()
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from .chapter import *
|
||||
from .module import *
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import graphene
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
|
||||
class ChapterInterface(graphene.Interface):
|
||||
content_blocks = DjangoFilterConnectionField('books.schema.nodes.ContentBlockNode')
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import graphene
|
||||
|
||||
|
||||
class ModuleInterface(graphene.Interface):
|
||||
pk = graphene.Int()
|
||||
|
||||
def resolve_pk(self, info, **kwargs):
|
||||
return self.id
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from books.schema.mutations.chapter import UpdateChapterVisibility
|
||||
from books.schema.mutations.contentblock import MutateContentBlock, AddContentBlock, DeleteContentBlock
|
||||
from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, SyncModuleVisibility
|
||||
from books.schema.mutations.snapshot import CreateSnapshot
|
||||
from books.schema.mutations.topic import UpdateLastTopic
|
||||
|
||||
|
||||
|
|
@ -13,3 +14,4 @@ class BookMutations(object):
|
|||
update_last_topic = UpdateLastTopic.Field()
|
||||
update_chapter_visibility = UpdateChapterVisibility.Field()
|
||||
sync_module_visibility = SyncModuleVisibility.Field()
|
||||
create_snapshot = CreateSnapshot.Field()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import graphene
|
||||
from graphene import relay
|
||||
|
||||
from api.utils import get_object
|
||||
from books.models import Module
|
||||
from books.models.snapshot import Snapshot
|
||||
from books.schema.nodes import SnapshotNode
|
||||
from users.models import SchoolClass
|
||||
|
||||
|
||||
class CreateSnapshot(relay.ClientIDMutation):
|
||||
class Input:
|
||||
module = graphene.ID(required=True)
|
||||
selected_class = graphene.ID(required=True)
|
||||
|
||||
snapshot = graphene.Field(SnapshotNode)
|
||||
success = graphene.Boolean()
|
||||
|
||||
@classmethod
|
||||
def mutate_and_get_payload(cls, root, info, **args):
|
||||
module_id = args.get('module')
|
||||
module = get_object(Module, module_id)
|
||||
user = info.context.user
|
||||
selected_class_id = args.get('selected_class')
|
||||
selected_class = get_object(SchoolClass, selected_class_id)
|
||||
snapshot = Snapshot.objects.create_snapshot(module, selected_class, user)
|
||||
return cls(snapshot=snapshot, success=True)
|
||||
|
|
@ -1,260 +0,0 @@
|
|||
import graphene
|
||||
from django.db.models import Q
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
|
||||
from assignments.models import StudentSubmission
|
||||
from assignments.schema.types import StudentSubmissionNode
|
||||
from books.models import ContentBlock, Chapter, Module, RecentModule, Topic, Book
|
||||
from books.utils import are_solutions_enabled_for
|
||||
from core.logger import get_logger
|
||||
from notes.models import ContentBlockBookmark, ChapterBookmark, ModuleBookmark
|
||||
from notes.schema import ContentBlockBookmarkNode, ChapterBookmarkNode, ModuleBookmarkNode
|
||||
from rooms.models import ModuleRoomSlug
|
||||
from surveys.models import Answer
|
||||
from surveys.schema import AnswerNode
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class TextBlockNode(graphene.ObjectType):
|
||||
text = graphene.String()
|
||||
|
||||
def resolve_text(root, info, **kwargs):
|
||||
return root['value']['text']
|
||||
|
||||
|
||||
class ContentNode(graphene.Union):
|
||||
class Meta:
|
||||
types = (TextBlockNode,)
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
logger.info(instance)
|
||||
if instance['type'] == 'text_block':
|
||||
return TextBlockNode
|
||||
|
||||
|
||||
class ContentBlockNode(DjangoObjectType):
|
||||
mine = graphene.Boolean()
|
||||
bookmarks = graphene.List(ContentBlockBookmarkNode)
|
||||
|
||||
# contents = graphene.List(ContentNode)
|
||||
|
||||
class Meta:
|
||||
model = ContentBlock
|
||||
only_fields = [
|
||||
'slug', 'title', 'type', 'contents', 'hidden_for', 'visible_for', 'user_created'
|
||||
]
|
||||
filter_fields = [
|
||||
'slug', 'title',
|
||||
]
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
def resolve_mine(self, info, **kwargs):
|
||||
return self.owner is not None and self.owner.pk == info.context.user.pk
|
||||
|
||||
def resolve_contents(self, info, **kwargs):
|
||||
updated_stream_data = []
|
||||
for content in self.contents.stream_data:
|
||||
# only show solutions to teachers and students for whom their teachers have them enabled
|
||||
if content['type'] == 'solution' \
|
||||
and not (are_solutions_enabled_for(info.context.user, self.module) or info.context.user.is_teacher()):
|
||||
logger.debug('Solution is hidden for this user')
|
||||
continue
|
||||
|
||||
if content['type'] == 'content_list_item':
|
||||
for index, list_block in enumerate(content['value']):
|
||||
content['value'][index] = process_module_room_slug_block(list_block)
|
||||
|
||||
content = process_module_room_slug_block(content)
|
||||
updated_stream_data.append(content)
|
||||
|
||||
self.contents.stream_data = updated_stream_data
|
||||
return self.contents
|
||||
|
||||
def resolve_bookmarks(self, info, **kwargs):
|
||||
return ContentBlockBookmark.objects.filter(
|
||||
user=info.context.user,
|
||||
content_block=self
|
||||
)
|
||||
|
||||
|
||||
class ChapterNode(DjangoObjectType):
|
||||
content_blocks = DjangoFilterConnectionField(ContentBlockNode)
|
||||
bookmark = graphene.Field(ChapterBookmarkNode)
|
||||
|
||||
class Meta:
|
||||
model = Chapter
|
||||
only_fields = [
|
||||
'slug', 'title', 'description', 'title_hidden_for', 'description_hidden_for'
|
||||
]
|
||||
filter_fields = [
|
||||
'slug', 'title',
|
||||
]
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
def resolve_content_blocks(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
school_classes = user.school_classes.values_list('pk')
|
||||
|
||||
by_parent = ContentBlock.get_by_parent(self) \
|
||||
.prefetch_related('visible_for') \
|
||||
.prefetch_related('hidden_for')
|
||||
|
||||
# don't filter the hidden blocks on the server any more, we do this on the client now, as they are not secret
|
||||
default_blocks = Q(user_created=False)
|
||||
owned_by_user = Q(user_created=True, owner=user)
|
||||
teacher_created_and_visible = Q(Q(user_created=True) & Q(visible_for__in=school_classes))
|
||||
|
||||
if user.has_perm('users.can_manage_school_class_content'): # teacher
|
||||
return by_parent.filter(default_blocks | owned_by_user | teacher_created_and_visible).distinct()
|
||||
else: # student
|
||||
return by_parent.filter(default_blocks | teacher_created_and_visible).distinct()
|
||||
|
||||
def resolve_bookmark(self, info, **kwargs):
|
||||
return ChapterBookmark.objects.filter(
|
||||
user=info.context.user,
|
||||
chapter=self
|
||||
).first()
|
||||
|
||||
|
||||
class ModuleNode(DjangoObjectType):
|
||||
pk = graphene.Int()
|
||||
chapters = DjangoFilterConnectionField(ChapterNode)
|
||||
topic = graphene.Field('books.schema.queries.TopicNode')
|
||||
hero_image = graphene.String()
|
||||
solutions_enabled = graphene.Boolean()
|
||||
bookmark = graphene.Field(ModuleBookmarkNode)
|
||||
my_submissions = DjangoFilterConnectionField(StudentSubmissionNode)
|
||||
my_answers = DjangoFilterConnectionField(AnswerNode)
|
||||
my_content_bookmarks = DjangoFilterConnectionField(ContentBlockBookmarkNode)
|
||||
my_chapter_bookmarks = DjangoFilterConnectionField(ChapterBookmarkNode)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
only_fields = [
|
||||
'slug', 'title', 'meta_title', 'teaser', 'intro', 'objective_groups', 'assignments', 'hero_image', 'topic'
|
||||
]
|
||||
filter_fields = {
|
||||
'slug': ['exact', 'icontains', 'in'],
|
||||
'title': ['exact', 'icontains', 'in'],
|
||||
}
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
def resolve_pk(self, info, **kwargs):
|
||||
return self.id
|
||||
|
||||
def resolve_hero_image(self, info, **kwargs):
|
||||
if self.hero_image:
|
||||
return self.hero_image.file.url
|
||||
|
||||
def resolve_chapters(self, info, **kwargs):
|
||||
return Chapter.get_by_parent(self)
|
||||
|
||||
def resolve_topic(self, info, **kwargs):
|
||||
return self.get_parent().specific
|
||||
|
||||
def resolve_solutions_enabled(self, info, **kwargs):
|
||||
school_class = info.context.user.selected_class()
|
||||
return self.solutions_enabled_for.filter(pk=school_class.pk).exists() if school_class is not None else False
|
||||
|
||||
def resolve_bookmark(self, info, **kwags):
|
||||
return ModuleBookmark.objects.filter(
|
||||
user=info.context.user,
|
||||
module=self
|
||||
).first()
|
||||
|
||||
def resolve_my_submissions(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
return StudentSubmission.objects.filter(student=user, assignment__module=self)
|
||||
# we want:
|
||||
# StudentSubmission
|
||||
|
||||
def resolve_my_answers(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
return Answer.objects.filter(owner=user, survey__module=self)
|
||||
# Survey
|
||||
|
||||
def resolve_my_content_bookmarks(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
content_blocks = ContentBlock.objects.live().descendant_of(self)
|
||||
return ContentBlockBookmark.objects.filter(content_block__in=content_blocks, user=user)
|
||||
# Bookmark Text
|
||||
# Bookmark Image etc
|
||||
# Bookmark Other
|
||||
# Note
|
||||
#
|
||||
|
||||
def resolve_my_chapter_bookmarks(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
chapters = Chapter.objects.live().descendant_of(self)
|
||||
return ChapterBookmark.objects.filter(chapter__in=chapters, user=user)
|
||||
|
||||
def resolve_objective_groups(self, root, **kwargs):
|
||||
return self.objective_groups.all() \
|
||||
.prefetch_related('hidden_for')
|
||||
|
||||
|
||||
class RecentModuleNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = RecentModule
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
|
||||
class TopicNode(DjangoObjectType):
|
||||
pk = graphene.Int()
|
||||
modules = DjangoFilterConnectionField(ModuleNode)
|
||||
|
||||
class Meta:
|
||||
model = Topic
|
||||
only_fields = [
|
||||
'slug', 'title', 'teaser', 'description', 'vimeo_id', 'order', 'instructions'
|
||||
]
|
||||
filter_fields = {
|
||||
'slug': ['exact', 'icontains', 'in'],
|
||||
'title': ['exact', 'icontains', 'in'],
|
||||
}
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
def resolve_pk(self, *args, **kwargs):
|
||||
return self.id
|
||||
|
||||
def resolve_modules(self, *args, **kwargs):
|
||||
return Module.get_by_parent(self)
|
||||
|
||||
|
||||
class BookNode(DjangoObjectType):
|
||||
pk = graphene.Int()
|
||||
topics = DjangoFilterConnectionField(TopicNode)
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
only_fields = [
|
||||
'slug', 'title',
|
||||
]
|
||||
filter_fields = {
|
||||
'slug': ['exact', 'icontains', 'in'],
|
||||
'title': ['exact', 'icontains', 'in'],
|
||||
}
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
def resolve_pk(self, *args, **kwargs):
|
||||
return self.id
|
||||
|
||||
def resolve_topics(self, *args, **kwargs):
|
||||
return Topic.get_by_parent(self)
|
||||
|
||||
|
||||
def process_module_room_slug_block(content):
|
||||
if content['type'] == 'module_room_slug':
|
||||
try:
|
||||
module_room_slug = ModuleRoomSlug.objects.get(title=content['value']['title'])
|
||||
content['value'] = {
|
||||
'title': content['value']['title'],
|
||||
'slug': module_room_slug.slug
|
||||
}
|
||||
except ModuleRoomSlug.DoesNotExist:
|
||||
pass
|
||||
return content
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from .chapter import *
|
||||
from .module import *
|
||||
from .content import *
|
||||
from .snapshot import *
|
||||
from .topic import *
|
||||
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import graphene
|
||||
from django.db.models import Q
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphql_relay import to_global_id
|
||||
|
||||
from books.models import Chapter, ContentBlock
|
||||
from books.models.snapshot import ChapterSnapshot
|
||||
from books.schema.interfaces import ChapterInterface
|
||||
from notes.models import ChapterBookmark
|
||||
from notes.schema import ChapterBookmarkNode
|
||||
|
||||
|
||||
class ChapterNode(DjangoObjectType):
|
||||
bookmark = graphene.Field(ChapterBookmarkNode)
|
||||
|
||||
class Meta:
|
||||
model = Chapter
|
||||
only_fields = [
|
||||
'slug', 'title', 'description', 'title_hidden_for', 'description_hidden_for'
|
||||
]
|
||||
filter_fields = [
|
||||
'slug', 'title',
|
||||
]
|
||||
interfaces = (relay.Node, ChapterInterface,)
|
||||
|
||||
def resolve_content_blocks(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
school_classes = user.school_classes.values_list('pk')
|
||||
|
||||
by_parent = ContentBlock.get_by_parent(self) \
|
||||
.prefetch_related('visible_for') \
|
||||
.prefetch_related('hidden_for')
|
||||
|
||||
# don't filter the hidden blocks on the server any more, we do this on the client now, as they are not secret
|
||||
default_blocks = Q(user_created=False)
|
||||
owned_by_user = Q(user_created=True, owner=user)
|
||||
teacher_created_and_visible = Q(Q(user_created=True) & Q(visible_for__in=school_classes))
|
||||
|
||||
if user.can_manage_school_class_content: # teacher
|
||||
return by_parent.filter(default_blocks | owned_by_user | teacher_created_and_visible).distinct()
|
||||
else: # student
|
||||
return by_parent.filter(default_blocks | teacher_created_and_visible).distinct()
|
||||
|
||||
def resolve_bookmark(self, info, **kwargs):
|
||||
return ChapterBookmark.objects.filter(
|
||||
user=info.context.user,
|
||||
chapter=self
|
||||
).first()
|
||||
|
||||
|
||||
class SnapshotChapterNode(DjangoObjectType):
|
||||
title_hidden = graphene.Boolean()
|
||||
description_hidden = graphene.Boolean()
|
||||
title = graphene.String()
|
||||
description = graphene.String()
|
||||
id = graphene.ID(required=True)
|
||||
|
||||
class Meta:
|
||||
model = ChapterSnapshot
|
||||
only_fields = '__all__'
|
||||
filter_fields = [
|
||||
'id',
|
||||
]
|
||||
interfaces = (relay.Node, ChapterInterface,)
|
||||
|
||||
@staticmethod
|
||||
def resolve_title_hidden(parent, info):
|
||||
return parent
|
||||
|
||||
@staticmethod
|
||||
def resolve_title(parent, info):
|
||||
return parent.chapter.title
|
||||
|
||||
@staticmethod
|
||||
def resolve_description(parent, info):
|
||||
return parent.chapter.description
|
||||
|
||||
@staticmethod
|
||||
def resolve_content_blocks(parent, info, **kwargs):
|
||||
snapshot = parent.snapshot
|
||||
|
||||
user_created = Q(user_created=True)
|
||||
hidden_for_snapshot = Q(hidden_for_snapshots=snapshot)
|
||||
custom_hidden = Q(snapshotcontentblock__hidden=True)
|
||||
|
||||
qs = ContentBlock.get_by_parent(parent.chapter) \
|
||||
.exclude(user_created) \
|
||||
.exclude(hidden_for_snapshot) \
|
||||
.exclude(custom_hidden)
|
||||
|
||||
|
||||
# exclude hidden for snapshot
|
||||
# exclude with owner
|
||||
# include visible snapshot content blocks
|
||||
# todo
|
||||
return qs
|
||||
|
||||
def resolve_id(self, *args):
|
||||
return to_global_id('SnapshotChapterNode', self.chapter.pk)
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import graphene
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
|
||||
from books.models import ContentBlock
|
||||
from books.utils import are_solutions_enabled_for
|
||||
from notes.models import ContentBlockBookmark
|
||||
from notes.schema import ContentBlockBookmarkNode
|
||||
from rooms.models import ModuleRoomSlug
|
||||
|
||||
|
||||
from core.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
|
||||
class TextBlockNode(graphene.ObjectType):
|
||||
text = graphene.String()
|
||||
|
||||
def resolve_text(root, info, **kwargs):
|
||||
return root['value']['text']
|
||||
|
||||
|
||||
class ContentNode(graphene.Union):
|
||||
class Meta:
|
||||
types = (TextBlockNode,)
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
logger.info(instance)
|
||||
if instance['type'] == 'text_block':
|
||||
return TextBlockNode
|
||||
|
||||
|
||||
class ContentBlockNode(DjangoObjectType):
|
||||
mine = graphene.Boolean()
|
||||
bookmarks = graphene.List(ContentBlockBookmarkNode)
|
||||
|
||||
# contents = graphene.List(ContentNode)
|
||||
|
||||
class Meta:
|
||||
model = ContentBlock
|
||||
only_fields = [
|
||||
'slug', 'title', 'type', 'contents', 'hidden_for', 'visible_for', 'user_created'
|
||||
]
|
||||
filter_fields = [
|
||||
'slug', 'title',
|
||||
]
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
def resolve_mine(self, info, **kwargs):
|
||||
return self.owner is not None and self.owner.pk == info.context.user.pk
|
||||
|
||||
def resolve_contents(self, info, **kwargs):
|
||||
updated_stream_data = []
|
||||
for content in self.contents.stream_data:
|
||||
# only show solutions to teachers and students for whom their teachers have them enabled
|
||||
if content['type'] == 'solution' \
|
||||
and not (are_solutions_enabled_for(info.context.user, self.module) or info.context.user.is_teacher()):
|
||||
logger.debug('Solution is hidden for this user')
|
||||
continue
|
||||
|
||||
if content['type'] == 'content_list_item':
|
||||
for index, list_block in enumerate(content['value']):
|
||||
content['value'][index] = process_module_room_slug_block(list_block)
|
||||
|
||||
content = process_module_room_slug_block(content)
|
||||
updated_stream_data.append(content)
|
||||
|
||||
self.contents.stream_data = updated_stream_data
|
||||
return self.contents
|
||||
|
||||
def resolve_bookmarks(self, info, **kwargs):
|
||||
return ContentBlockBookmark.objects.filter(
|
||||
user=info.context.user,
|
||||
content_block=self
|
||||
)
|
||||
|
||||
|
||||
def process_module_room_slug_block(content):
|
||||
if content['type'] == 'module_room_slug':
|
||||
try:
|
||||
module_room_slug = ModuleRoomSlug.objects.get(title=content['value']['title'])
|
||||
content['value'] = {
|
||||
'title': content['value']['title'],
|
||||
'slug': module_room_slug.slug
|
||||
}
|
||||
except ModuleRoomSlug.DoesNotExist:
|
||||
pass
|
||||
return content
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import graphene
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
|
||||
from assignments.models import StudentSubmission
|
||||
from assignments.schema.types import StudentSubmissionNode
|
||||
from books.models import Module, Chapter, ContentBlock, RecentModule
|
||||
from books.schema.interfaces.module import ModuleInterface
|
||||
from books.schema.nodes.chapter import ChapterNode
|
||||
from notes.models import ModuleBookmark, ContentBlockBookmark, ChapterBookmark
|
||||
from notes.schema import ModuleBookmarkNode, ContentBlockBookmarkNode, ChapterBookmarkNode
|
||||
from surveys.models import Answer
|
||||
from surveys.schema import AnswerNode
|
||||
|
||||
|
||||
class ModuleNode(DjangoObjectType):
|
||||
chapters = DjangoFilterConnectionField(ChapterNode)
|
||||
topic = graphene.Field('books.schema.queries.TopicNode')
|
||||
hero_image = graphene.String()
|
||||
solutions_enabled = graphene.Boolean()
|
||||
bookmark = graphene.Field(ModuleBookmarkNode)
|
||||
my_submissions = DjangoFilterConnectionField(StudentSubmissionNode)
|
||||
my_answers = DjangoFilterConnectionField(AnswerNode)
|
||||
my_content_bookmarks = DjangoFilterConnectionField(ContentBlockBookmarkNode)
|
||||
my_chapter_bookmarks = DjangoFilterConnectionField(ChapterBookmarkNode)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
only_fields = [
|
||||
'slug', 'title', 'meta_title', 'teaser', 'intro', 'objective_groups', 'assignments', 'hero_image', 'topic'
|
||||
]
|
||||
filter_fields = {
|
||||
'slug': ['exact', 'icontains', 'in'],
|
||||
'title': ['exact', 'icontains', 'in'],
|
||||
}
|
||||
interfaces = (relay.Node, ModuleInterface, )
|
||||
|
||||
def resolve_hero_image(self, info, **kwargs):
|
||||
if self.hero_image:
|
||||
return self.hero_image.file.url
|
||||
|
||||
def resolve_chapters(self, info, **kwargs):
|
||||
return Chapter.get_by_parent(self)
|
||||
|
||||
def resolve_topic(self, info, **kwargs):
|
||||
return self.get_parent().specific
|
||||
|
||||
def resolve_solutions_enabled(self, info, **kwargs):
|
||||
school_class = info.context.user.selected_class()
|
||||
return self.solutions_enabled_for.filter(pk=school_class.pk).exists() if school_class is not None else False
|
||||
|
||||
def resolve_bookmark(self, info, **kwags):
|
||||
return ModuleBookmark.objects.filter(
|
||||
user=info.context.user,
|
||||
module=self
|
||||
).first()
|
||||
|
||||
def resolve_my_submissions(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
return StudentSubmission.objects.filter(student=user, assignment__module=self)
|
||||
# we want:
|
||||
# StudentSubmission
|
||||
|
||||
def resolve_my_answers(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
return Answer.objects.filter(owner=user, survey__module=self)
|
||||
# Survey
|
||||
|
||||
def resolve_my_content_bookmarks(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
content_blocks = ContentBlock.objects.live().descendant_of(self)
|
||||
return ContentBlockBookmark.objects.filter(content_block__in=content_blocks, user=user)
|
||||
# Bookmark Text
|
||||
# Bookmark Image etc
|
||||
# Bookmark Other
|
||||
# Note
|
||||
#
|
||||
|
||||
def resolve_my_chapter_bookmarks(self, info, **kwargs):
|
||||
user = info.context.user
|
||||
chapters = Chapter.objects.live().descendant_of(self)
|
||||
return ChapterBookmark.objects.filter(chapter__in=chapters, user=user)
|
||||
|
||||
def resolve_objective_groups(self, root, **kwargs):
|
||||
return self.objective_groups.all() \
|
||||
.prefetch_related('hidden_for')
|
||||
|
||||
|
||||
class RecentModuleNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = RecentModule
|
||||
interfaces = (relay.Node,)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
from .chapter import SnapshotChapterNode
|
||||
|
||||
from books.models.snapshot import Snapshot
|
||||
|
||||
|
||||
class SnapshotNode(DjangoObjectType):
|
||||
class Meta:
|
||||
model = Snapshot
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
# chapters = relay.ConnectionField('books.schema.connections.ChapterSnapshotConnection')
|
||||
chapters = DjangoFilterConnectionField(SnapshotChapterNode)
|
||||
|
||||
def resolve_chapters(self, info, **kwargs):
|
||||
# return Chapter.objects.filter(chapter_snapshots__snapshot=self)
|
||||
return self.chapters.through.objects.all()
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import graphene
|
||||
from graphene import relay
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
|
||||
from books.models import Topic, Module
|
||||
from books.schema.nodes import ModuleNode
|
||||
|
||||
|
||||
class TopicNode(DjangoObjectType):
|
||||
pk = graphene.Int()
|
||||
modules = DjangoFilterConnectionField(ModuleNode)
|
||||
|
||||
class Meta:
|
||||
model = Topic
|
||||
only_fields = [
|
||||
'slug', 'title', 'teaser', 'description', 'vimeo_id', 'order', 'instructions'
|
||||
]
|
||||
filter_fields = {
|
||||
'slug': ['exact', 'icontains', 'in'],
|
||||
'title': ['exact', 'icontains', 'in'],
|
||||
}
|
||||
interfaces = (relay.Node,)
|
||||
|
||||
def resolve_pk(self, *args, **kwargs):
|
||||
return self.id
|
||||
|
||||
def resolve_modules(self, *args, **kwargs):
|
||||
return Module.get_by_parent(self)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from django.test import TestCase, RequestFactory
|
||||
from graphene.test import Client
|
||||
from graphql_relay import to_global_id
|
||||
from graphql_relay import to_global_id, from_global_id
|
||||
|
||||
from api.schema import schema
|
||||
from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory
|
||||
|
|
@ -44,11 +44,45 @@ query ModulesQuery($slug: String!) {
|
|||
}
|
||||
"""
|
||||
|
||||
CREATE_SNAPSHOT_MUTATION = """
|
||||
mutation CreateSnapshot($input: CreateSnapshotInput!) {
|
||||
createSnapshot(input: $input) {
|
||||
snapshot {
|
||||
id
|
||||
chapters {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
descriptionHidden
|
||||
titleHidden
|
||||
title
|
||||
description
|
||||
contentBlocks {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
success
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def edges_to_array(entity):
|
||||
return [edge['node'] for edge in entity.get('edges')]
|
||||
|
||||
|
||||
class CreateSnapshotTestCase(TestCase):
|
||||
def setUp(self):
|
||||
create_users()
|
||||
skillbox_class = SchoolClass.objects.get(name='skillbox')
|
||||
self.skillbox_class = SchoolClass.objects.get(name='skillbox')
|
||||
second_class = SchoolClass.objects.get(name='second_class')
|
||||
# teacher will create snapshot
|
||||
self.teacher = User.objects.get(username='teacher')
|
||||
|
|
@ -56,38 +90,68 @@ class CreateSnapshotTestCase(TestCase):
|
|||
# module M has a chapter
|
||||
self.chapter = ChapterFactory(parent=self.module, slug='some-chapter')
|
||||
# chapter has some content blocks a, b, c
|
||||
self.a = ContentBlockFactory(parent=self.chapter, module=self.module, slug='cb-a')
|
||||
self.b = ContentBlockFactory(parent=self.chapter, module=self.module, slug='cb-b')
|
||||
self.title_visible = 'visible'
|
||||
self.title_hidden = 'hidden'
|
||||
self.title_custom = 'custom'
|
||||
self.visible_content_block = ContentBlockFactory(parent=self.chapter, module=self.module,
|
||||
title=self.title_visible, slug='cb-a')
|
||||
self.hidden_content_block = ContentBlockFactory(parent=self.chapter, module=self.module, title=self.title_hidden, slug='cb-b')
|
||||
# content block c is user created
|
||||
self.c = ContentBlockFactory(parent=self.chapter, owner=self.teacher, user_created=True, module=self.module, slug='cb-c')
|
||||
self.custom_content_block = ContentBlockFactory(parent=self.chapter, owner=self.teacher, user_created=True,
|
||||
module=self.module, title=self.title_custom,
|
||||
slug='cb-c')
|
||||
# content block a and c are visible to school class X
|
||||
self.b.hidden_for.add(skillbox_class)
|
||||
self.c.visible_for.add(skillbox_class)
|
||||
self.hidden_content_block.hidden_for.add(self.skillbox_class)
|
||||
self.custom_content_block.visible_for.add(self.skillbox_class)
|
||||
|
||||
# chapter description is hidden for school class X
|
||||
self.chapter.title_hidden_for.add(self.skillbox_class)
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request.user = self.teacher
|
||||
self.client = Client(schema=schema, context_value=request)
|
||||
|
||||
|
||||
# we make a snapshot S of the module M
|
||||
# snapshot S looks like module M for school class X
|
||||
|
||||
def test_visible_and_hidden(self):
|
||||
def test_setup(self):
|
||||
# make sure everything is setup correctly
|
||||
result = self.client.execute(MODULE_QUERY, variables={
|
||||
'slug': self.module.slug
|
||||
})
|
||||
self.assertIsNone(result.get('errors'))
|
||||
module = result.get('data').get('module')
|
||||
chapter = module.get('chapters').get('edges')[0]['node']
|
||||
chapter = edges_to_array(module.get('chapters'))[0]
|
||||
self.assertIsNotNone(chapter)
|
||||
content_blocks = [edge['node'] for edge in chapter.get('contentBlocks').get('edges')]
|
||||
content_blocks = edges_to_array(chapter.get('contentBlocks'))
|
||||
content_block_ids = [node['id'] for node in content_blocks]
|
||||
self.assertTrue(to_global_id('ContentBlockNode', self.a.id) in content_block_ids)
|
||||
self.assertTrue(to_global_id('ContentBlockNode', self.b.id) in content_block_ids)
|
||||
self.assertTrue(to_global_id('ContentBlockNode', self.c.id) in content_block_ids)
|
||||
b = [node for node in content_blocks if node['id'] == to_global_id('ContentBlockNode', self.b.id)][0]
|
||||
c = [node for node in content_blocks if node['id'] == to_global_id('ContentBlockNode', self.c.id)][0]
|
||||
self.assertTrue('skillbox' in [edge['node']['name'] for edge in b.get('hiddenFor').get('edges')])
|
||||
self.assertTrue('skillbox' in [edge['node']['name'] for edge in c.get('visibleFor').get('edges')])
|
||||
self.assertTrue(to_global_id('ContentBlockNode', self.visible_content_block.id) in content_block_ids)
|
||||
self.assertTrue(to_global_id('ContentBlockNode', self.hidden_content_block.id) in content_block_ids)
|
||||
self.assertTrue(to_global_id('ContentBlockNode', self.custom_content_block.id) in content_block_ids)
|
||||
b = [node for node in content_blocks if
|
||||
node['id'] == to_global_id('ContentBlockNode', self.hidden_content_block.id)][0]
|
||||
c = [node for node in content_blocks if
|
||||
node['id'] == to_global_id('ContentBlockNode', self.custom_content_block.id)][0]
|
||||
self.assertTrue('skillbox' in [school_class['name'] for school_class in edges_to_array(b.get('hiddenFor'))])
|
||||
self.assertTrue('skillbox' in [school_class['name'] for school_class in edges_to_array(c.get('visibleFor'))])
|
||||
|
||||
def test_create_snapshot(self):
|
||||
result = self.client.execute(CREATE_SNAPSHOT_MUTATION, variables={
|
||||
'input': {
|
||||
'module': to_global_id('ContentBlockNode', self.module.pk),
|
||||
'selectedClass': to_global_id('SchoolClassNode', self.skillbox_class.pk),
|
||||
}
|
||||
})
|
||||
self.assertIsNone(result.get('errors'))
|
||||
snapshot = result.get('data').get('createSnapshot').get('snapshot')
|
||||
chapter = snapshot.get('chapters').get('edges')[0]['node']
|
||||
self.assertTrue(chapter['titleHidden'])
|
||||
self.assertFalse(chapter['descriptionHidden'])
|
||||
_, chapter_id = from_global_id(chapter['id'])
|
||||
self.assertEqual(int(chapter_id), self.chapter.id)
|
||||
content_blocks = [edge['node'] for edge in chapter['contentBlocks']['edges']]
|
||||
self.assertEqual(len(content_blocks), 2)
|
||||
self.assertEqual(content_blocks[0]['title'], self.title_visible)
|
||||
self.assertEqual(content_blocks[1]['title'], self.title_custom)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue