Add first skeleton of a duplicate method
Add other base ideas for the copy mechanism for content block attached entities like assignments and surveys Relates to MS-651 Add test for duplicating entries, also update implementation
This commit is contained in:
parent
d6221e8cd5
commit
71dbfeb1f4
|
|
@ -9,12 +9,38 @@ from wagtail.models import Page, Site
|
||||||
from wagtail.rich_text import RichText
|
from wagtail.rich_text import RichText
|
||||||
|
|
||||||
from assignments.models import Assignment
|
from assignments.models import Assignment
|
||||||
from basicknowledge.models import BasicKnowledge, INTERDISCIPLINARY, INTERDISCIPLINARY_LABEL, InstrumentCategory, \
|
from basicknowledge.models import (
|
||||||
InstrumentType, \
|
BasicKnowledge,
|
||||||
LANGUAGE_COMMUNICATION, LANGUAGE_COMMUNICATION_LABEL, SOCIETY, SOCIETY_LABEL
|
INTERDISCIPLINARY,
|
||||||
from books.blocks import AssignmentBlock, BasicKnowledgeBlock, ImageUrlBlock, LinkBlock, VideoBlock
|
INTERDISCIPLINARY_LABEL,
|
||||||
|
InstrumentCategory,
|
||||||
|
InstrumentType,
|
||||||
|
LANGUAGE_COMMUNICATION,
|
||||||
|
LANGUAGE_COMMUNICATION_LABEL,
|
||||||
|
SOCIETY,
|
||||||
|
SOCIETY_LABEL,
|
||||||
|
)
|
||||||
|
from books.blocks import (
|
||||||
|
AssignmentBlock,
|
||||||
|
BasicKnowledgeBlock,
|
||||||
|
ImageUrlBlock,
|
||||||
|
LinkBlock,
|
||||||
|
SurveyBlock,
|
||||||
|
VideoBlock,
|
||||||
|
)
|
||||||
from books.models import Book, Chapter, ContentBlock, Module, TextBlock, Topic
|
from books.models import Book, Chapter, ContentBlock, Module, TextBlock, Topic
|
||||||
from core.factories import BasePageFactory, DummyImageFactory, fake, fake_paragraph, fake_title
|
from core.factories import (
|
||||||
|
BasePageFactory,
|
||||||
|
DummyImageFactory,
|
||||||
|
fake,
|
||||||
|
fake_paragraph,
|
||||||
|
fake_title,
|
||||||
|
)
|
||||||
|
from core.logger import get_logger
|
||||||
|
from surveys.factories import SurveyFactory
|
||||||
|
from surveys.models import Survey
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BookFactory(BasePageFactory):
|
class BookFactory(BasePageFactory):
|
||||||
|
|
@ -24,18 +50,25 @@ class BookFactory(BasePageFactory):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_default_structure():
|
def create_default_structure():
|
||||||
site = wagtail_factories.SiteFactory.create(is_default_site=True)
|
site = wagtail_factories.SiteFactory.create(is_default_site=True)
|
||||||
Page.objects.get(title='Root').delete()
|
Page.objects.get(title="Root").delete()
|
||||||
|
|
||||||
book = BookFactory.create(parent=site.root_page, title='A book')
|
book = BookFactory.create(parent=site.root_page, title="A book")
|
||||||
topic = TopicFactory.create(parent=book, order=1, title='A topic')
|
topic = TopicFactory.create(parent=book, order=1, title="A topic")
|
||||||
module = ModuleFactory.create(parent=topic,
|
module = ModuleFactory.create(
|
||||||
title="A module",
|
parent=topic,
|
||||||
meta_title="Modul 1",
|
title="A module",
|
||||||
teaser="Whatever",
|
meta_title="Modul 1",
|
||||||
intro="<p>Hello</p>")
|
teaser="Whatever",
|
||||||
|
intro="<p>Hello</p>",
|
||||||
|
)
|
||||||
chapter = ChapterFactory.create(parent=module, title="A chapter")
|
chapter = ChapterFactory.create(parent=module, title="A chapter")
|
||||||
content_block = ContentBlockFactory.create(parent=chapter, module=module, title="A content block", type="task",
|
content_block = ContentBlockFactory.create(
|
||||||
contents=[])
|
parent=chapter,
|
||||||
|
module=module,
|
||||||
|
title="A content block",
|
||||||
|
type="task",
|
||||||
|
contents=[],
|
||||||
|
)
|
||||||
|
|
||||||
return book, topic, module, chapter, content_block
|
return book, topic, module, chapter, content_block
|
||||||
|
|
||||||
|
|
@ -45,7 +78,9 @@ class TopicFactory(BasePageFactory):
|
||||||
model = Topic
|
model = Topic
|
||||||
|
|
||||||
order = 0
|
order = 0
|
||||||
teaser = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(8, 12)))
|
teaser = factory.LazyAttribute(
|
||||||
|
lambda x: fake.sentence(nb_words=random.randint(8, 12))
|
||||||
|
)
|
||||||
description = factory.LazyAttribute(lambda x: fake.text(max_nb_chars=200))
|
description = factory.LazyAttribute(lambda x: fake.text(max_nb_chars=200))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,7 +89,9 @@ class ModuleFactory(BasePageFactory):
|
||||||
model = Module
|
model = Module
|
||||||
|
|
||||||
meta_title = factory.LazyAttribute(lambda x: fake.text(max_nb_chars=20))
|
meta_title = factory.LazyAttribute(lambda x: fake.text(max_nb_chars=20))
|
||||||
teaser = factory.LazyAttribute(lambda x: fake.sentence(nb_words=random.randint(8, 12)))
|
teaser = factory.LazyAttribute(
|
||||||
|
lambda x: fake.sentence(nb_words=random.randint(8, 12))
|
||||||
|
)
|
||||||
intro = factory.LazyAttribute(lambda x: fake.text(max_nb_chars=200))
|
intro = factory.LazyAttribute(lambda x: fake.text(max_nb_chars=200))
|
||||||
|
|
||||||
hero_image = factory.SubFactory(DummyImageFactory)
|
hero_image = factory.SubFactory(DummyImageFactory)
|
||||||
|
|
@ -75,11 +112,14 @@ class TextBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
class InstrumentCategoryFactory(factory.DjangoModelFactory):
|
class InstrumentCategoryFactory(factory.DjangoModelFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InstrumentCategory
|
model = InstrumentCategory
|
||||||
django_get_or_create = ('name',)
|
django_get_or_create = ("name",)
|
||||||
|
|
||||||
|
name = factory.Iterator(
|
||||||
|
[LANGUAGE_COMMUNICATION_LABEL, SOCIETY_LABEL, INTERDISCIPLINARY_LABEL]
|
||||||
|
)
|
||||||
|
foreground = factory.Iterator(["FF0000", "FFFFFF", "000000"])
|
||||||
|
background = factory.Iterator(["FF0000", "FFFFFF", "000000"])
|
||||||
|
|
||||||
name = factory.Iterator([LANGUAGE_COMMUNICATION_LABEL, SOCIETY_LABEL, INTERDISCIPLINARY_LABEL])
|
|
||||||
foreground = factory.Iterator(['FF0000', 'FFFFFF', '000000'])
|
|
||||||
background = factory.Iterator(['FF0000', 'FFFFFF', '000000'])
|
|
||||||
|
|
||||||
class InstrumentTypeFactory(factory.DjangoModelFactory):
|
class InstrumentTypeFactory(factory.DjangoModelFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -99,7 +139,7 @@ class InstrumentFactory(BasePageFactory):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create(cls, model_class, *args, **kwargs):
|
def _create(cls, model_class, *args, **kwargs):
|
||||||
kwargs['parent'] = Site.objects.get(is_default_site=True).root_page
|
kwargs["parent"] = Site.objects.get(is_default_site=True).root_page
|
||||||
return super()._create(model_class, *args, **kwargs)
|
return super()._create(model_class, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -113,7 +153,7 @@ class BasicKnowledgeBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
|
|
||||||
class ImageUrlBlockFactory(wagtail_factories.StructBlockFactory):
|
class ImageUrlBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
title = fake_title()
|
title = fake_title()
|
||||||
url = factory.LazyAttribute(lambda x: 'https://picsum.photos/600/400/?random')
|
url = factory.LazyAttribute(lambda x: "https://picsum.photos/600/400/?random")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ImageUrlBlock
|
model = ImageUrlBlock
|
||||||
|
|
@ -121,127 +161,202 @@ class ImageUrlBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
|
|
||||||
class LinkBlockFactory(wagtail_factories.StructBlockFactory):
|
class LinkBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
text = fake_title()
|
text = fake_title()
|
||||||
url = factory.LazyAttribute(lambda x: 'https://picsum.photos/600/400/?random')
|
url = factory.LazyAttribute(lambda x: "https://picsum.photos/600/400/?random")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = LinkBlock
|
model = LinkBlock
|
||||||
|
|
||||||
|
|
||||||
class AssignmentBlockFactory(wagtail_factories.StructBlockFactory):
|
class EntityBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
class Meta:
|
|
||||||
model = AssignmentBlock
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build(cls, model_class, *args, **kwargs):
|
def _build(cls, model_class, *args, **kwargs):
|
||||||
block = model_class()
|
block = model_class()
|
||||||
return blocks.StructValue(
|
logger.debug(cls.id_key)
|
||||||
block,
|
logger.debug(cls.entity_key)
|
||||||
# todo: build in a more generic fashion
|
logger.debug(kwargs)
|
||||||
[
|
value = block.to_python({cls.id_key: kwargs.get(cls.entity_key).id})
|
||||||
(name, kwargs['assignment']) for name, child_block in block.child_blocks.items()
|
clean_value = block.clean(value)
|
||||||
],
|
return clean_value
|
||||||
)
|
|
||||||
|
|
||||||
|
class AssignmentBlockFactory(EntityBlockFactory):
|
||||||
|
class Meta:
|
||||||
|
model = AssignmentBlock
|
||||||
|
|
||||||
|
id_key = "assignment_id"
|
||||||
|
entity_key = "assignment"
|
||||||
|
|
||||||
|
|
||||||
|
class SurveyBlockFactory(EntityBlockFactory):
|
||||||
|
class Meta:
|
||||||
|
model = SurveyBlock
|
||||||
|
|
||||||
|
id_key = "survey_id"
|
||||||
|
entity_key = "survey"
|
||||||
|
|
||||||
|
|
||||||
class VideoBlockFactory(wagtail_factories.StructBlockFactory):
|
class VideoBlockFactory(wagtail_factories.StructBlockFactory):
|
||||||
url = factory.LazyAttribute(lambda x: 'https://www.youtube.com/watch?v=lO9d-AJai8Q')
|
url = factory.LazyAttribute(lambda x: "https://www.youtube.com/watch?v=lO9d-AJai8Q")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VideoBlock
|
model = VideoBlock
|
||||||
|
|
||||||
|
|
||||||
block_types = ['text_block', 'basic_knowledge', 'student_entry', 'image_url_block', 'solution']
|
block_types = [
|
||||||
|
"text_block",
|
||||||
|
"basic_knowledge",
|
||||||
|
"student_entry",
|
||||||
|
"image_url_block",
|
||||||
|
"solution",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ContentBlockFactory(BasePageFactory):
|
class ContentBlockFactory(BasePageFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContentBlock
|
model = ContentBlock
|
||||||
|
|
||||||
type = factory.LazyAttribute(lambda x: random.choice(['normal', 'instrument', 'task',]))
|
type = factory.LazyAttribute(
|
||||||
|
lambda x: random.choice(
|
||||||
|
[
|
||||||
|
"normal",
|
||||||
|
"instrument",
|
||||||
|
"task",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
contents = wagtail_factories.StreamFieldFactory({
|
contents = wagtail_factories.StreamFieldFactory(
|
||||||
'text_block': TextBlockFactory,
|
{
|
||||||
'basic_knowledge': BasicKnowledgeBlockFactory,
|
"text_block": TextBlockFactory,
|
||||||
'assignment': AssignmentBlockFactory,
|
"basic_knowledge": BasicKnowledgeBlockFactory,
|
||||||
'image_block': wagtail_factories.ImageChooserBlockFactory,
|
"assignment": AssignmentBlockFactory,
|
||||||
'image_url_block': ImageUrlBlockFactory,
|
"image_block": wagtail_factories.ImageChooserBlockFactory,
|
||||||
'link_block': LinkBlockFactory,
|
"image_url_block": ImageUrlBlockFactory,
|
||||||
'video_block': VideoBlockFactory,
|
"link_block": LinkBlockFactory,
|
||||||
'solution': TextBlockFactory
|
"video_block": VideoBlockFactory,
|
||||||
})
|
"solution": TextBlockFactory,
|
||||||
|
"survey": SurveyBlockFactory,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stream_field_magic(cls, module, kwargs, stream_field_name):
|
def stream_field_magic(cls, module, kwargs, stream_field_name):
|
||||||
if stream_field_name in kwargs:
|
if stream_field_name in kwargs:
|
||||||
"""
|
"""
|
||||||
stream_field_name is most likely 'contents'
|
stream_field_name is most likely 'contents'
|
||||||
this means: if there is a property named contents, use the defined ones in this block.
|
this means: if there is a property named contents, use the defined ones in this block.
|
||||||
otherwise, go into the other block and randomize the contents
|
otherwise, go into the other block and randomize the contents
|
||||||
"""
|
"""
|
||||||
for idx, resource in enumerate(kwargs[stream_field_name]):
|
for idx, resource in enumerate(kwargs[stream_field_name]):
|
||||||
value = resource['value']
|
value = resource["value"]
|
||||||
block_type = resource['type']
|
block_type = resource["type"]
|
||||||
|
|
||||||
if block_type == 'assignment':
|
if block_type == "assignment":
|
||||||
user = get_user_model().objects.first()
|
user = get_user_model().objects.first()
|
||||||
assignment = Assignment.objects.create(
|
assignment = Assignment.objects.create(
|
||||||
title=value['title'],
|
title=value["title"],
|
||||||
assignment=value['assignment'],
|
assignment=value["assignment"],
|
||||||
owner=user,
|
owner=user,
|
||||||
module=module
|
module=module,
|
||||||
)
|
)
|
||||||
kwargs['{}__{}__{}__{}'.format(stream_field_name, idx, block_type, 'assignment')] = assignment
|
kwargs[
|
||||||
|
"{}__{}__{}__{}".format(
|
||||||
|
stream_field_name, idx, block_type, "assignment"
|
||||||
|
)
|
||||||
|
] = assignment
|
||||||
|
elif block_type == "survey":
|
||||||
|
survey = Survey.objects.create(
|
||||||
|
title=value["title"], data=value["data"], module=module
|
||||||
|
)
|
||||||
|
kwargs[
|
||||||
|
"{}__{}__{}__{}".format(
|
||||||
|
stream_field_name, idx, block_type, "survey"
|
||||||
|
)
|
||||||
|
] = survey
|
||||||
else:
|
else:
|
||||||
for jdx, field in enumerate(value):
|
for jdx, field in enumerate(value):
|
||||||
|
if block_type == "text_block":
|
||||||
if block_type == 'text_block':
|
kwargs[
|
||||||
kwargs['{}__{}__{}__{}'.format(stream_field_name, idx, block_type, field)] = RichText(
|
"{}__{}__{}__{}".format(
|
||||||
value[field])
|
stream_field_name, idx, block_type, field
|
||||||
elif block_type == 'solution':
|
)
|
||||||
kwargs['{}__{}__{}__{}'.format(stream_field_name, idx, block_type, field)] = RichText(
|
] = RichText(value[field])
|
||||||
value[field])
|
elif block_type == "solution":
|
||||||
elif block_type == 'basic_knowledge':
|
kwargs[
|
||||||
if field == 'description':
|
"{}__{}__{}__{}".format(
|
||||||
|
stream_field_name, idx, block_type, field
|
||||||
|
)
|
||||||
|
] = RichText(value[field])
|
||||||
|
elif block_type == "basic_knowledge":
|
||||||
|
if field == "description":
|
||||||
kwargs[
|
kwargs[
|
||||||
'{}__{}__{}__{}'.format(stream_field_name, idx, block_type, field)] = RichText(
|
"{}__{}__{}__{}".format(
|
||||||
value[field])
|
stream_field_name, idx, block_type, field
|
||||||
|
)
|
||||||
|
] = RichText(value[field])
|
||||||
else:
|
else:
|
||||||
kwargs[
|
kwargs[
|
||||||
'{}__{}__{}__{}'.format(stream_field_name, idx, block_type,
|
"{}__{}__{}__{}".format(
|
||||||
field)] = 'https://google.ch'
|
stream_field_name, idx, block_type, field
|
||||||
elif block_type == 'image_url_block':
|
)
|
||||||
|
] = "https://google.ch"
|
||||||
|
elif block_type == "image_url_block":
|
||||||
kwargs[
|
kwargs[
|
||||||
'{}__{}__{}__{}'.format(stream_field_name, idx, block_type, field)] = value[field]
|
"{}__{}__{}__{}".format(
|
||||||
|
stream_field_name, idx, block_type, field
|
||||||
|
)
|
||||||
|
] = value[field]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
kwargs[
|
kwargs[
|
||||||
'{}__{}__{}__{}'.format(stream_field_name, idx, block_type, field)] = value[field]
|
"{}__{}__{}__{}".format(
|
||||||
|
stream_field_name, idx, block_type, field
|
||||||
|
)
|
||||||
|
] = value[field]
|
||||||
|
|
||||||
del kwargs[stream_field_name]
|
del kwargs[stream_field_name]
|
||||||
else: # random contents from generator
|
else: # random contents from generator
|
||||||
for i in range(0, random.randint(3, 7)):
|
for i in range(0, random.randint(3, 7)):
|
||||||
block_type = random.choice(block_types)
|
block_type = random.choice(block_types)
|
||||||
if block_type == 'text_block':
|
if block_type == "text_block":
|
||||||
kwargs['{}__{}__{}__{}'.format(stream_field_name, i, 'text_block', 'text')] = RichText(
|
|
||||||
fake_paragraph())
|
|
||||||
elif block_type == 'basic_knowledge':
|
|
||||||
kwargs['{}__{}__{}__{}'.format(stream_field_name, i, 'basic_knowledge', 'description')] = RichText(
|
|
||||||
fake_paragraph())
|
|
||||||
elif block_type == 'assignment':
|
|
||||||
kwargs['{}__{}__{}__{}'.format(stream_field_name, i, 'assignment', 'task_text')] = RichText(
|
|
||||||
fake_paragraph())
|
|
||||||
elif block_type == 'image_url_block':
|
|
||||||
kwargs[
|
kwargs[
|
||||||
'{}__{}__{}__{}'.format(stream_field_name, i, 'image_url_block', 'title')] = fake_paragraph()
|
"{}__{}__{}__{}".format(
|
||||||
|
stream_field_name, i, "text_block", "text"
|
||||||
|
)
|
||||||
|
] = RichText(fake_paragraph())
|
||||||
|
elif block_type == "basic_knowledge":
|
||||||
kwargs[
|
kwargs[
|
||||||
'{}__{}__{}__{}'.format(stream_field_name, i, 'image_url_block',
|
"{}__{}__{}__{}".format(
|
||||||
'url')] = 'https://picsum.photos/400/?random={}'.format(
|
stream_field_name, i, "basic_knowledge", "description"
|
||||||
''.join(random.choice('abcdefghiklmn') for _ in range(6)))
|
)
|
||||||
elif block_type == 'solution':
|
] = RichText(fake_paragraph())
|
||||||
kwargs['{}__{}__{}__{}'.format(stream_field_name, i, 'solution', 'text')] = RichText(
|
elif block_type == "assignment":
|
||||||
fake_paragraph())
|
kwargs[
|
||||||
|
"{}__{}__{}__{}".format(
|
||||||
|
stream_field_name, i, "assignment", "task_text"
|
||||||
|
)
|
||||||
|
] = RichText(fake_paragraph())
|
||||||
|
elif block_type == "image_url_block":
|
||||||
|
kwargs[
|
||||||
|
"{}__{}__{}__{}".format(
|
||||||
|
stream_field_name, i, "image_url_block", "title"
|
||||||
|
)
|
||||||
|
] = fake_paragraph()
|
||||||
|
kwargs[
|
||||||
|
"{}__{}__{}__{}".format(
|
||||||
|
stream_field_name, i, "image_url_block", "url"
|
||||||
|
)
|
||||||
|
] = "https://picsum.photos/400/?random={}".format(
|
||||||
|
"".join(random.choice("abcdefghiklmn") for _ in range(6))
|
||||||
|
)
|
||||||
|
elif block_type == "solution":
|
||||||
|
kwargs[
|
||||||
|
"{}__{}__{}__{}".format(
|
||||||
|
stream_field_name, i, "solution", "text"
|
||||||
|
)
|
||||||
|
] = RichText(fake_paragraph())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, module, **kwargs):
|
def create(cls, module, **kwargs):
|
||||||
cls.stream_field_magic(module, kwargs, 'contents')
|
cls.stream_field_magic(module, kwargs, "contents")
|
||||||
return cls._generate(CREATE_STRATEGY, kwargs)
|
return cls._generate(CREATE_STRATEGY, kwargs)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList
|
from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList
|
||||||
|
from books.models.contentblock import ContentBlock
|
||||||
|
|
||||||
from core.wagtail_utils import StrictHierarchyPage, get_default_settings
|
from core.wagtail_utils import StrictHierarchyPage, get_default_settings
|
||||||
from users.models import SchoolClass
|
from users.models import SchoolClass
|
||||||
|
|
@ -38,6 +39,9 @@ class Chapter(StrictHierarchyPage, GraphqlNodeMixin):
|
||||||
SchoolClass, related_name="hidden_chapter_descriptions"
|
SchoolClass, related_name="hidden_chapter_descriptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_content_blocks(self):
|
||||||
|
return ContentBlock.objects.all().descendants_of(self)
|
||||||
|
|
||||||
def sync_title_visibility(self, school_class_template, school_class_to_sync):
|
def sync_title_visibility(self, school_class_template, school_class_to_sync):
|
||||||
if (
|
if (
|
||||||
self.title_hidden_for.filter(id=school_class_template.id).exists()
|
self.title_hidden_for.filter(id=school_class_template.id).exists()
|
||||||
|
|
@ -53,8 +57,7 @@ class Chapter(StrictHierarchyPage, GraphqlNodeMixin):
|
||||||
|
|
||||||
def sync_description_visibility(self, school_class_template, school_class_to_sync):
|
def sync_description_visibility(self, school_class_template, school_class_to_sync):
|
||||||
if (
|
if (
|
||||||
self.description_hidden_for.filter(
|
self.description_hidden_for.filter(id=school_class_template.id).exists()
|
||||||
id=school_class_template.id).exists()
|
|
||||||
and not self.description_hidden_for.filter(
|
and not self.description_hidden_for.filter(
|
||||||
id=school_class_to_sync.id
|
id=school_class_to_sync.id
|
||||||
).exists()
|
).exists()
|
||||||
|
|
@ -62,8 +65,7 @@ class Chapter(StrictHierarchyPage, GraphqlNodeMixin):
|
||||||
self.description_hidden_for.add(school_class_to_sync)
|
self.description_hidden_for.add(school_class_to_sync)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.description_hidden_for.filter(
|
self.description_hidden_for.filter(id=school_class_to_sync.id).exists()
|
||||||
id=school_class_to_sync.id).exists()
|
|
||||||
and not self.description_hidden_for.filter(
|
and not self.description_hidden_for.filter(
|
||||||
id=school_class_template.id
|
id=school_class_template.id
|
||||||
).exists()
|
).exists()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from assignments.models import Assignment
|
||||||
from wagtail.admin.panels import (
|
from wagtail.admin.panels import (
|
||||||
FieldPanel,
|
FieldPanel,
|
||||||
TabbedInterface,
|
TabbedInterface,
|
||||||
|
|
@ -11,6 +10,7 @@ from wagtail.fields import StreamField
|
||||||
from wagtail.images.blocks import ImageChooserBlock
|
from wagtail.images.blocks import ImageChooserBlock
|
||||||
|
|
||||||
from books.managers import ContentBlockManager
|
from books.managers import ContentBlockManager
|
||||||
|
from core.logger import get_logger
|
||||||
from core.wagtail_utils import get_default_settings
|
from core.wagtail_utils import get_default_settings
|
||||||
from books.blocks import (
|
from books.blocks import (
|
||||||
CMSDocumentBlock,
|
CMSDocumentBlock,
|
||||||
|
|
@ -30,14 +30,53 @@ from books.blocks import (
|
||||||
ThinglinkBlock,
|
ThinglinkBlock,
|
||||||
InstructionBlock,
|
InstructionBlock,
|
||||||
)
|
)
|
||||||
from books.utils import get_type_and_value
|
|
||||||
from core.wagtail_utils import StrictHierarchyPage
|
from core.wagtail_utils import StrictHierarchyPage
|
||||||
from notes.models import ContentBlockBookmark
|
from notes.models import ContentBlockBookmark
|
||||||
from surveys.models import Survey
|
from surveys.models import Survey
|
||||||
from users.models import SchoolClass, User
|
from users.models import SchoolClass, User
|
||||||
from core.mixins import GraphqlNodeMixin
|
from core.mixins import GraphqlNodeMixin
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def duplicate_entities_generator(new_module):
|
||||||
|
def duplicate_entities(block):
|
||||||
|
content_type = block.block_type
|
||||||
|
value = block.value
|
||||||
|
logger.debug(block)
|
||||||
|
if content_type == "assignment":
|
||||||
|
assignment = value.get("assignment_id")
|
||||||
|
assignment.pk = None
|
||||||
|
assignment.title = f"{assignment.title} (Kopie)"
|
||||||
|
assignment.module = new_module
|
||||||
|
logger.debug(f"setting new module {new_module}, {assignment.module}")
|
||||||
|
assignment.save()
|
||||||
|
block = AssignmentBlock()
|
||||||
|
data = {"assignment_id": assignment.pk}
|
||||||
|
value = block.to_python(data)
|
||||||
|
cleaned_value = block.clean(value)
|
||||||
|
new_block = ("assignment", cleaned_value)
|
||||||
|
logger.debug(new_block)
|
||||||
|
return new_block
|
||||||
|
if content_type == "survey":
|
||||||
|
logger.debug(value)
|
||||||
|
survey = value.get("survey_id")
|
||||||
|
survey.pk = None
|
||||||
|
survey.title = f"{survey.title} (Kopie)"
|
||||||
|
survey.module = new_module
|
||||||
|
logger.debug(f"setting new module {new_module}, {survey.module}")
|
||||||
|
survey.save()
|
||||||
|
block = SurveyBlock()
|
||||||
|
data = {"survey_id": survey.pk}
|
||||||
|
value = block.to_python(data)
|
||||||
|
cleaned_value = block.clean(value)
|
||||||
|
new_block = ("survey", cleaned_value)
|
||||||
|
# logger.debug(new_block)
|
||||||
|
logger.debug(new_block)
|
||||||
|
return new_block
|
||||||
|
return block
|
||||||
|
|
||||||
|
return duplicate_entities
|
||||||
|
|
||||||
|
|
||||||
class ContentBlock(StrictHierarchyPage, GraphqlNodeMixin):
|
class ContentBlock(StrictHierarchyPage, GraphqlNodeMixin):
|
||||||
|
|
@ -102,8 +141,7 @@ class ContentBlock(StrictHierarchyPage, GraphqlNodeMixin):
|
||||||
use_json_field=True,
|
use_json_field=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
type = models.CharField(
|
type = models.CharField(max_length=100, choices=TYPE_CHOICES, default=NORMAL)
|
||||||
max_length=100, choices=TYPE_CHOICES, default=NORMAL)
|
|
||||||
|
|
||||||
content_panels = [
|
content_panels = [
|
||||||
FieldPanel("title", classname="full title"),
|
FieldPanel("title", classname="full title"),
|
||||||
|
|
@ -127,30 +165,56 @@ class ContentBlock(StrictHierarchyPage, GraphqlNodeMixin):
|
||||||
def module(self):
|
def module(self):
|
||||||
return self.get_parent().get_parent().specific
|
return self.get_parent().get_parent().specific
|
||||||
|
|
||||||
|
# duplicate all attached Surveys and Assignments
|
||||||
|
def duplicate_attached_entities(self):
|
||||||
|
logger.debug("starting to duplicate inside the content block")
|
||||||
|
duplicate_entities = duplicate_entities_generator(self.module)
|
||||||
|
logger.debug(f"new module: {self.module}")
|
||||||
|
iterator = map(duplicate_entities, self.contents)
|
||||||
|
logger.debug("here is the iterator")
|
||||||
|
new_contents = list(iterator)
|
||||||
|
logger.debug(new_contents)
|
||||||
|
# we can't just insert a list here, we need a StreamValue data type
|
||||||
|
# so we need to clear the list, then add each element in turn
|
||||||
|
self.contents.clear()
|
||||||
|
# like this, the internal methods of the SteamValue data type can work on the single elements
|
||||||
|
for content in new_contents:
|
||||||
|
logger.debug(content)
|
||||||
|
self.contents.append(content)
|
||||||
|
|
||||||
|
# as an illustration
|
||||||
|
# block = SolutionBlock()
|
||||||
|
# data = {'text': 'This is me'}
|
||||||
|
# value = block.to_python(data)
|
||||||
|
# clean_value = block.clean(value)
|
||||||
|
# self.contents.append(('solution', clean_value))
|
||||||
|
logger.debug("self.contents")
|
||||||
|
logger.debug(self.contents)
|
||||||
|
self.save()
|
||||||
|
|
||||||
def is_hidden_for_class(self, school_class):
|
def is_hidden_for_class(self, school_class):
|
||||||
return (
|
return (
|
||||||
not self.user_created
|
not self.user_creted and self.hidden_for.filter(id=school_class.id).exists()
|
||||||
and self.hidden_for.filter(id=school_class.id).exists()
|
|
||||||
) or (
|
) or (
|
||||||
self.user_created
|
self.user_created
|
||||||
and not self.visible_for.filter(id=school_class.id).exists()
|
and not self.visible_for.filter(id=school_class.id).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
# def save(self, *args, **kwargs):
|
||||||
# todo: move this to the after_create_page and after_edit_page hooks, and remove from here.
|
# todo: move this to the after_create_page and after_edit_page hooks, and remove from here.
|
||||||
for data in self.contents.raw_data:
|
# for data in self.contents.raw_data:
|
||||||
block_type, value = get_type_and_value(data)
|
# block_type, value = get_type_and_value(data)
|
||||||
|
|
||||||
# todo: also do the same for assignments
|
# todo: also do the same for assignments
|
||||||
if block_type == "survey":
|
# if block_type == "survey":
|
||||||
module = self.module
|
# module = self.module
|
||||||
survey = value["survey_id"]
|
# survey = value["survey_id"]
|
||||||
if isinstance(survey, int):
|
# if isinstance(survey, int):
|
||||||
survey = Survey.objects.get(pk=survey)
|
# survey = Survey.objects.get(pk=survey)
|
||||||
if survey.module != module:
|
# if survey.module != module:
|
||||||
survey.module = module
|
# survey.module = module
|
||||||
survey.save()
|
# survey.save()
|
||||||
super().save(*args, **kwargs)
|
# super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ContentBlockSnapshot(ContentBlock):
|
class ContentBlockSnapshot(ContentBlock):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from assignments.factories import AssignmentFactory
|
||||||
|
from assignments.models import Assignment
|
||||||
|
|
||||||
|
from books.factories import BookFactory, ContentBlockFactory
|
||||||
|
from books.models.contentblock import ContentBlock
|
||||||
|
from core.logger import get_logger
|
||||||
|
from surveys.factories import SurveyFactory
|
||||||
|
from surveys.models import Survey
|
||||||
|
from users.services import create_users
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateContentsTestCase(TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
create_users()
|
||||||
|
_, _, self.module, chapter, _ = BookFactory.create_default_structure()
|
||||||
|
text = {"type": "text_block", "value": {"text": "Hallo"}}
|
||||||
|
assignment = {
|
||||||
|
"type": "assignment",
|
||||||
|
"value": {"title": "Hello", "assignment": "Assignment"},
|
||||||
|
}
|
||||||
|
survey = {"type": "survey", "value": {"title": "Survey Title", "data": "null"}}
|
||||||
|
self.content_block = ContentBlockFactory.create(
|
||||||
|
parent=chapter,
|
||||||
|
module=self.module,
|
||||||
|
title="Another content block",
|
||||||
|
type="task",
|
||||||
|
contents=[text, assignment, survey],
|
||||||
|
)
|
||||||
|
self.assignment = Assignment.objects.first()
|
||||||
|
|
||||||
|
def test_duplicate_entities(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.content_block.contents[1].value["assignment_id"], self.assignment
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.content_block.contents[1].value["assignment_id"].title, "Hello"
|
||||||
|
)
|
||||||
|
self.assertEqual(Assignment.objects.count(), 1)
|
||||||
|
self.assertEqual(Survey.objects.count(), 1)
|
||||||
|
self.content_block.duplicate_attached_entities()
|
||||||
|
self.assertEqual(Assignment.objects.count(), 2)
|
||||||
|
self.assertEqual(Survey.objects.count(), 2)
|
||||||
|
new_assignment = Assignment.objects.get(id=2)
|
||||||
|
new_survey = Survey.objects.get(id=2)
|
||||||
|
content_block = ContentBlock.objects.get(id=self.content_block.id)
|
||||||
|
self.assertEqual(len(content_block.contents), 3)
|
||||||
|
self.assertEqual(
|
||||||
|
content_block.contents[1].value["assignment_id"], new_assignment
|
||||||
|
)
|
||||||
|
self.assertEqual(content_block.contents[2].value["survey_id"], new_survey)
|
||||||
|
|
@ -1,107 +1,125 @@
|
||||||
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
|
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
|
||||||
from wagtail.admin.rich_text.converters.html_to_contentstate import InlineStyleElementHandler
|
from wagtail.admin.rich_text.converters.html_to_contentstate import (
|
||||||
|
InlineStyleElementHandler,
|
||||||
|
)
|
||||||
from wagtail import hooks
|
from wagtail import hooks
|
||||||
|
|
||||||
from basicknowledge.models import BasicKnowledge
|
from basicknowledge.models import BasicKnowledge
|
||||||
from books.models import ContentBlockSnapshot
|
from books.models import ContentBlockSnapshot
|
||||||
|
from books.models.contentblock import ContentBlock
|
||||||
from core.logger import get_logger
|
from core.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# 1. Use the register_rich_text_features hook.
|
# 1. Use the register_rich_text_features hook.
|
||||||
@hooks.register('register_rich_text_features')
|
@hooks.register("register_rich_text_features")
|
||||||
def register_brand_feature(features):
|
def register_brand_feature(features):
|
||||||
"""
|
"""
|
||||||
Registering the feature, which uses the `BRAND` Draft.js inline style type,
|
Registering the feature, which uses the `BRAND` Draft.js inline style type,
|
||||||
and is stored as HTML with a `<span class="brand">` tag.
|
and is stored as HTML with a `<span class="brand">` tag.
|
||||||
"""
|
"""
|
||||||
feature_name = 'brand'
|
feature_name = "brand"
|
||||||
type_ = 'BRAND'
|
type_ = "BRAND"
|
||||||
|
|
||||||
# 2. Configure how Draftail handles the feature in its toolbar.
|
# 2. Configure how Draftail handles the feature in its toolbar.
|
||||||
control = {
|
control = {
|
||||||
'type': type_,
|
"type": type_,
|
||||||
'label': 'Grün',
|
"label": "Grün",
|
||||||
'description': 'Grün',
|
"description": "Grün",
|
||||||
'style': {
|
"style": {"color": "#17A887", "font-weight": "600"},
|
||||||
'color': '#17A887',
|
|
||||||
'font-weight': '600'
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 3. Call register_editor_plugin to register the configuration for Draftail.
|
# 3. Call register_editor_plugin to register the configuration for Draftail.
|
||||||
features.register_editor_plugin(
|
features.register_editor_plugin(
|
||||||
'draftail', feature_name, draftail_features.InlineStyleFeature(control)
|
"draftail", feature_name, draftail_features.InlineStyleFeature(control)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4.configure the content transform from the DB to the editor and back.
|
# 4.configure the content transform from the DB to the editor and back.
|
||||||
db_conversion = {
|
db_conversion = {
|
||||||
'from_database_format': {'span[class="brand"]': InlineStyleElementHandler(type_)},
|
"from_database_format": {
|
||||||
'to_database_format': {'style_map': {type_: 'span class="brand""'}},
|
'span[class="brand"]': InlineStyleElementHandler(type_)
|
||||||
|
},
|
||||||
|
"to_database_format": {"style_map": {type_: 'span class="brand""'}},
|
||||||
}
|
}
|
||||||
|
|
||||||
# 5. Call register_converter_rule to register the content transformation conversion.
|
# 5. Call register_converter_rule to register the content transformation conversion.
|
||||||
features.register_converter_rule('contentstate', feature_name, db_conversion)
|
features.register_converter_rule("contentstate", feature_name, db_conversion)
|
||||||
|
|
||||||
# 6. (optional) Add the feature to the default features list to make it available
|
# 6. (optional) Add the feature to the default features list to make it available
|
||||||
# on rich text fields that do not specify an explicit 'features' list
|
# on rich text fields that do not specify an explicit 'features' list
|
||||||
features.default_features.append(feature_name)
|
features.default_features.append(feature_name)
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('register_rich_text_features')
|
@hooks.register("register_rich_text_features")
|
||||||
def register_secondary_feature(features):
|
def register_secondary_feature(features):
|
||||||
"""
|
"""
|
||||||
Registering the feature, which uses the `SECONDARY` Draft.js inline style type,
|
Registering the feature, which uses the `SECONDARY` Draft.js inline style type,
|
||||||
and is stored as HTML with a `<span class="secondary">` tag.
|
and is stored as HTML with a `<span class="secondary">` tag.
|
||||||
"""
|
"""
|
||||||
feature_name = 'secondary'
|
feature_name = "secondary"
|
||||||
type_ = 'SECONDARY'
|
type_ = "SECONDARY"
|
||||||
|
|
||||||
# 2. Configure how Draftail handles the feature in its toolbar.
|
# 2. Configure how Draftail handles the feature in its toolbar.
|
||||||
control = {
|
control = {
|
||||||
'type': type_,
|
"type": type_,
|
||||||
'label': 'Blau',
|
"label": "Blau",
|
||||||
'description': 'Blau',
|
"description": "Blau",
|
||||||
'style': {
|
"style": {"color": "#078CC6", "font-weight": "600"},
|
||||||
'color': '#078CC6',
|
|
||||||
'font-weight': '600'
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 3. Call register_editor_plugin to register the configuration for Draftail.
|
# 3. Call register_editor_plugin to register the configuration for Draftail.
|
||||||
features.register_editor_plugin(
|
features.register_editor_plugin(
|
||||||
'draftail', feature_name, draftail_features.InlineStyleFeature(control)
|
"draftail", feature_name, draftail_features.InlineStyleFeature(control)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4.configure the content transform from the DB to the editor and back.
|
# 4.configure the content transform from the DB to the editor and back.
|
||||||
db_conversion = {
|
db_conversion = {
|
||||||
'from_database_format': {'span[class="secondary"]': InlineStyleElementHandler(type_)},
|
"from_database_format": {
|
||||||
'to_database_format': {'style_map': {type_: 'span class="secondary"'}},
|
'span[class="secondary"]': InlineStyleElementHandler(type_)
|
||||||
|
},
|
||||||
|
"to_database_format": {"style_map": {type_: 'span class="secondary"'}},
|
||||||
}
|
}
|
||||||
|
|
||||||
# 5. Call register_converter_rule to register the content transformation conversion.
|
# 5. Call register_converter_rule to register the content transformation conversion.
|
||||||
features.register_converter_rule('contentstate', feature_name, db_conversion)
|
features.register_converter_rule("contentstate", feature_name, db_conversion)
|
||||||
|
|
||||||
# 6. (optional) Add the feature to the default features list to make it available
|
# 6. (optional) Add the feature to the default features list to make it available
|
||||||
# on rich text fields that do not specify an explicit 'features' list
|
# on rich text fields that do not specify an explicit 'features' list
|
||||||
features.default_features.append(feature_name)
|
features.default_features.append(feature_name)
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('construct_explorer_page_queryset')
|
@hooks.register("construct_explorer_page_queryset")
|
||||||
def remove_page_types_from_menu(parent_page, pages, request):
|
def remove_page_types_from_menu(parent_page, pages, request):
|
||||||
return pages.not_type(ContentBlockSnapshot).not_type(BasicKnowledge).exclude(contentblock__user_created=True)
|
return (
|
||||||
|
pages.not_type(ContentBlockSnapshot)
|
||||||
|
.not_type(BasicKnowledge)
|
||||||
|
.exclude(contentblock__user_created=True)
|
||||||
|
)
|
||||||
|
|
||||||
@hooks.register('after_copy_page')
|
|
||||||
|
@hooks.register("after_copy_page")
|
||||||
def after_copy_hook(request, page, new_page):
|
def after_copy_hook(request, page, new_page):
|
||||||
# todo: find every ContentBlock further down in the tree, see if there are any Surveys or Assignments and copy them and reassign them
|
# todo: find every ContentBlock further down in the tree, see if there are any Surveys or Assignments and copy them and reassign them
|
||||||
logger.debug(f'After copy page {page.title}, {new_page.title}')
|
if type(page.specific) == ContentBlock:
|
||||||
|
logger.debug("It's a content block")
|
||||||
|
content_block: ContentBlock = new_page.specific
|
||||||
|
logger.debug(f"duplicatin {content_block.title, content_block.pk}")
|
||||||
|
content_block.duplicate_attached_entities()
|
||||||
|
|
||||||
@hooks.register('after_edit_page')
|
else:
|
||||||
|
logger.debug(f"It's something else {type(page.specific)}, {ContentBlock}")
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"After copy page old: {page.title} {page.pk}, {new_page.title} {new_page.pk}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register("after_edit_page")
|
||||||
def after_edit_hook(request, page):
|
def after_edit_hook(request, page):
|
||||||
logger.debug(f'After edit page {page.title}, {type(page).__name__}')
|
logger.debug(f"After edit page {page.title}, {type(page).__name__}")
|
||||||
|
|
||||||
@hooks.register('after_create_page')
|
|
||||||
|
@hooks.register("after_create_page")
|
||||||
def after_create_hook(request, page):
|
def after_create_hook(request, page):
|
||||||
logger.debug(f'After create page {page.title}')
|
logger.debug(f"After create page {page.title}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,25 @@ class StrictHierarchyPage(Page):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_parent(cls, parent):
|
def get_by_parent(cls, parent):
|
||||||
return cls.objects.filter(id__in=parent.get_child_ids()).live()
|
return cls.objects.filter(id__in=parent.get_child_ids()).live()
|
||||||
|
|
||||||
|
def get_content_blocks(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def wagtail_parent_filter(parent_cls, child_cls):
|
def wagtail_parent_filter(parent_cls, child_cls):
|
||||||
class ParentValueFilter(admin.SimpleListFilter):
|
class ParentValueFilter(admin.SimpleListFilter):
|
||||||
title = 'parent'
|
title = "parent"
|
||||||
parameter_name = 'parent'
|
parameter_name = "parent"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
return list((parent.slug, parent.title) for parent in parent_cls.objects.all())
|
return list(
|
||||||
|
(parent.slug, parent.title) for parent in parent_cls.objects.all()
|
||||||
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
filter_value = self.value()
|
filter_value = self.value()
|
||||||
|
|
@ -34,7 +39,4 @@ def wagtail_parent_filter(parent_cls, child_cls):
|
||||||
|
|
||||||
|
|
||||||
def get_default_settings():
|
def get_default_settings():
|
||||||
return ObjectList([
|
return ObjectList([FieldPanel("slug"), CommentPanel()], heading="Settings")
|
||||||
FieldPanel('slug'),
|
|
||||||
CommentPanel()
|
|
||||||
], heading='Settings')
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue