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:
Ramon Wenger 2023-03-07 23:53:53 +01:00
parent d6221e8cd5
commit 71dbfeb1f4
6 changed files with 416 additions and 162 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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):

View File

@ -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)

View File

@ -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}")

View File

@ -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')