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 .topic import *
|
||||||
from .chapter import *
|
from .chapter import *
|
||||||
from .contentblock import *
|
from .contentblock import *
|
||||||
|
from .snapshot import *
|
||||||
|
|
|
||||||
|
|
@ -110,3 +110,13 @@ class ContentBlock(StrictHierarchyPage):
|
||||||
survey.module = module
|
survey.module = module
|
||||||
survey.save()
|
survey.save()
|
||||||
super().save(*args, **kwargs)
|
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.chapter import UpdateChapterVisibility
|
||||||
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, UpdateLastModule, SyncModuleVisibility
|
from books.schema.mutations.module import UpdateSolutionVisibility, UpdateLastModule, SyncModuleVisibility
|
||||||
|
from books.schema.mutations.snapshot import CreateSnapshot
|
||||||
from books.schema.mutations.topic import UpdateLastTopic
|
from books.schema.mutations.topic import UpdateLastTopic
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,3 +14,4 @@ class BookMutations(object):
|
||||||
update_last_topic = UpdateLastTopic.Field()
|
update_last_topic = UpdateLastTopic.Field()
|
||||||
update_chapter_visibility = UpdateChapterVisibility.Field()
|
update_chapter_visibility = UpdateChapterVisibility.Field()
|
||||||
sync_module_visibility = SyncModuleVisibility.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 django.test import TestCase, RequestFactory
|
||||||
from graphene.test import Client
|
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 api.schema import schema
|
||||||
from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory
|
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):
|
class CreateSnapshotTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
create_users()
|
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')
|
second_class = SchoolClass.objects.get(name='second_class')
|
||||||
# teacher will create snapshot
|
# teacher will create snapshot
|
||||||
self.teacher = User.objects.get(username='teacher')
|
self.teacher = User.objects.get(username='teacher')
|
||||||
|
|
@ -56,38 +90,68 @@ class CreateSnapshotTestCase(TestCase):
|
||||||
# module M has a chapter
|
# module M has a chapter
|
||||||
self.chapter = ChapterFactory(parent=self.module, slug='some-chapter')
|
self.chapter = ChapterFactory(parent=self.module, slug='some-chapter')
|
||||||
# chapter has some content blocks a, b, c
|
# chapter has some content blocks a, b, c
|
||||||
self.a = ContentBlockFactory(parent=self.chapter, module=self.module, slug='cb-a')
|
self.title_visible = 'visible'
|
||||||
self.b = ContentBlockFactory(parent=self.chapter, module=self.module, slug='cb-b')
|
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
|
# 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
|
# content block a and c are visible to school class X
|
||||||
self.b.hidden_for.add(skillbox_class)
|
self.hidden_content_block.hidden_for.add(self.skillbox_class)
|
||||||
self.c.visible_for.add(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 = RequestFactory().get('/')
|
||||||
request.user = self.teacher
|
request.user = self.teacher
|
||||||
self.client = Client(schema=schema, context_value=request)
|
self.client = Client(schema=schema, context_value=request)
|
||||||
|
|
||||||
|
|
||||||
# we make a snapshot S of the module M
|
# we make a snapshot S of the module M
|
||||||
# snapshot S looks like module M for school class X
|
# 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={
|
result = self.client.execute(MODULE_QUERY, variables={
|
||||||
'slug': self.module.slug
|
'slug': self.module.slug
|
||||||
})
|
})
|
||||||
self.assertIsNone(result.get('errors'))
|
self.assertIsNone(result.get('errors'))
|
||||||
module = result.get('data').get('module')
|
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)
|
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]
|
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.visible_content_block.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.hidden_content_block.id) in content_block_ids)
|
||||||
self.assertTrue(to_global_id('ContentBlockNode', self.c.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.b.id)][0]
|
b = [node for node in content_blocks if
|
||||||
c = [node for node in content_blocks if node['id'] == to_global_id('ContentBlockNode', self.c.id)][0]
|
node['id'] == to_global_id('ContentBlockNode', self.hidden_content_block.id)][0]
|
||||||
self.assertTrue('skillbox' in [edge['node']['name'] for edge in b.get('hiddenFor').get('edges')])
|
c = [node for node in content_blocks if
|
||||||
self.assertTrue('skillbox' in [edge['node']['name'] for edge in c.get('visibleFor').get('edges')])
|
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