diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index 6e6a4d13..dff1e8cf 100644 --- a/server/vbv_lernwelt/learnpath/models.py +++ b/server/vbv_lernwelt/learnpath/models.py @@ -16,7 +16,7 @@ class LearningPath(Page): # PageChooserPanel('related_page', 'demo.PublisherPage'), content_panels = Page.content_panels - subpage_types = ['learnpath.Circle', 'learnpath.Topic'] + subpage_types = ['learnpath.Circle', 'learnpath.Topic', 'media_library.MediaLibrary'] class Meta: verbose_name = "Learning Path" diff --git a/server/vbv_lernwelt/media_library/content_blocks.py b/server/vbv_lernwelt/media_library/content_blocks.py new file mode 100644 index 00000000..9bb07a18 --- /dev/null +++ b/server/vbv_lernwelt/media_library/content_blocks.py @@ -0,0 +1,72 @@ +from django.db import models +from wagtail import blocks +from wagtail.admin.panels import FieldPanel +from wagtail.snippets.models import register_snippet + +from wagtail.documents.blocks import DocumentChooserBlock +from django.utils.translation import gettext_lazy as _ + + +class VisualisationType(models.TextChoices): + LEARNING_MEDIA = 'LearningMedia', _('Lernmedien') + LINK = 'Link', _('Links') + ANKER = 'Anker', _('Verankerung') + CROSSREFERENCE = 'CrossReference', _('Querverweise') + + +@register_snippet +class MediaLibraryContent(models.Model): + title = models.TextField() + description = models.TextField() + link_display_text = models.CharField(max_length=255) + # TODO: Revisions only work with wagtail 4.0, can not migrate since wagtail localize is not ready yet. + # _revisions = GenericRelation("wagtailcore.Revision", related_query_name="media_library_content") + + panels = [ + FieldPanel('title'), + FieldPanel('description'), + FieldPanel('link_display_text'), + ] + + @property + def revisions(self): + return self._revisions + + +class AnkerBlock(blocks.PageChooserBlock): + """ + Verankerung im Lernpfad. Link to a Learning Content. + """ + page_type = 'learnpath.LearningUnit' + + +class LinkBlock(blocks.StructBlock): + title = blocks.TextBlock(blank=False, null=False) + description = blocks.TextBlock(default='') + link_display_text = blocks.CharBlock(max_length=255, default='Link öffnen') + url = blocks.URLBlock() + + +class CrossReferenceBlock(blocks.StructBlock): + title = models.TextField(blank=False, null=False) + description = blocks.TextBlock(default='') + link_display_text = blocks.CharBlock(max_length=255, default='Link öffnen') + category = blocks.PageChooserBlock(page_type='media_library.Category') + + +class ContentCollection(blocks.StructBlock): + """ + Lernmedien, Links, Querverweise, Verankerung + """ + title = blocks.TextBlock() + collection_type = blocks.MultipleChoiceBlock(choices=VisualisationType.choices, + max_length=20, + default=VisualisationType.LEARNING_MEDIA) + contents = blocks.StreamBlock([('Links', LinkBlock()), + ('Documents', DocumentChooserBlock()), + ('Ankers', AnkerBlock()), + ('CrossReference', CrossReferenceBlock()) + ]) + + class Meta: + icon = 'link' diff --git a/server/vbv_lernwelt/media_library/create_default_media_library.py b/server/vbv_lernwelt/media_library/create_default_media_library.py new file mode 100644 index 00000000..f2c837ec --- /dev/null +++ b/server/vbv_lernwelt/media_library/create_default_media_library.py @@ -0,0 +1,28 @@ +import json + +from vbv_lernwelt.learnpath.models import LearningPath +from vbv_lernwelt.media_library.tests.media_library_factories import MediaLibraryFactory, TopCategoryFactory, \ + CategoryFactory, ContentCollectionFactory, collection_body_dict +from vbv_lernwelt.media_library.models import Category + + +def create_default_media_library(): + lp = LearningPath.objects.all().first() + + m = MediaLibraryFactory(title='Mediathek', parent=lp) + + top_cat = TopCategoryFactory(title='Handlungsfelder', parent=m) + + handlungsfelder = ['Fahrzeug', 'Reisen', 'Einkommenssicherung', 'Gesundheit', 'Haushalt', 'Sparen', 'Pensionierung', + 'KMU', 'Wohneigentum', 'Rechtsstreitigkeiten', 'Erben / Vererben', 'Selbständigkeit'] + + for title in handlungsfelder: + introduction_text = 'Das Auto ist für viele der grösste Stolz! Es birgt aber auch ein grosses Gefahrenpotenzial. Dabei geht es bei den heutigen Fahrzeugpreisen und Reparaturkosten rasch um namhafte Summen, die der Fahrzeugbesitzer und die Fahrzeugbesitzerin in einem grösseren Schadenfall oft nur schwer selbst aufbringen kann. ' + description = ' Supi' + category = CategoryFactory(title=title, + parent=top_cat, + introduction_text=introduction_text, + description=description, + body=json.dumps(collection_body_dict())) + + top_cat = TopCategoryFactory(title='Lernmedien', parent=m) diff --git a/server/vbv_lernwelt/media_library/example_content_from_db.py b/server/vbv_lernwelt/media_library/example_content_from_db.py new file mode 100644 index 00000000..ea769f8e --- /dev/null +++ b/server/vbv_lernwelt/media_library/example_content_from_db.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Iterativ GmbH +# http://www.iterativ.ch/ +# +# Copyright (c) 2015 Iterativ GmbH. All rights reserved. +# +# Created on 2022-09-14 +# @author: lorenz.padberg@iterativ.ch +[{"id": "a2a7ef07-64d9-444e-8069-4894a1e857ee", + "type": "content_collection", + "value": {"title": "Lernmedien", + "contents": [{ + "id": "5b24aa0b-17d7-4b04-8698-f86d2116d6df", + "type": "Documents", + "value": 2}, { + "id": "e2d43794-037f-4a19-8a71-c7e9d96d8dac", + "type": "Documents", + "value": 1}], + "collection_type": [ + "LearningMedia"]} + }, + {"id": "23d13543-2f37-44b8-b3e2-0ef36f7c5a13", + "type": "content_collection", + "value": {"title": "Links", "contents": [ + {"id": "57e35e12-ceb3-4873-9629-a6c5a3c2f6da", + "type": "Links", + "value": {"url": "http://www.admin.ch", + "title": "Wichtige Webseite", + "description": "interessantes zu versicherungen", + "link_display_text": "Link öffnen"}}], + "collection_type": [ + "LearningMedia"]}}] diff --git a/server/vbv_lernwelt/media_library/management/commands/create_default_media_library.py b/server/vbv_lernwelt/media_library/management/commands/create_default_media_library.py index 93a44ca0..8280a35c 100644 --- a/server/vbv_lernwelt/media_library/management/commands/create_default_media_library.py +++ b/server/vbv_lernwelt/media_library/management/commands/create_default_media_library.py @@ -1,10 +1,11 @@ import djclick as click from vbv_lernwelt.media_library.create_default_documents import create_default_collections, create_default_documents +from vbv_lernwelt.media_library.create_default_media_library import create_default_media_library @click.command() def command(): + create_default_media_library() create_default_collections() create_default_documents() - diff --git a/server/vbv_lernwelt/media_library/migrations/0002_auto_20220818_1414.py b/server/vbv_lernwelt/media_library/migrations/0002_auto_20220818_1414.py deleted file mode 100644 index 70da2f30..00000000 --- a/server/vbv_lernwelt/media_library/migrations/0002_auto_20220818_1414.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 3.2.13 on 2022-08-18 12:14 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('wagtailcore', '0069_log_entry_jsonfield'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('taggit', '0004_alter_taggeditem_content_type_alter_taggeditem_tag'), - ('media_library', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Category', - fields=[ - ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), - ], - options={ - 'abstract': False, - }, - bases=('wagtailcore.page',), - ), - migrations.CreateModel( - name='TopCategory', - fields=[ - ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), - ], - options={ - 'abstract': False, - }, - bases=('wagtailcore.page',), - ), - migrations.RenameModel( - old_name='CustomDocument', - new_name='LibraryDocument', - ), - ] diff --git a/server/vbv_lernwelt/media_library/migrations/0002_auto_20220914_1502.py b/server/vbv_lernwelt/media_library/migrations/0002_auto_20220914_1502.py new file mode 100644 index 00000000..909f1400 --- /dev/null +++ b/server/vbv_lernwelt/media_library/migrations/0002_auto_20220914_1502.py @@ -0,0 +1,68 @@ +# Generated by Django 3.2.13 on 2022-09-14 13:02 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import vbv_lernwelt.media_library.content_blocks +import wagtail.blocks +import wagtail.documents.blocks +import wagtail.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('taggit', '0004_alter_taggeditem_content_type_alter_taggeditem_tag'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('wagtailcore', '0069_log_entry_jsonfield'), + ('media_library', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), + ('introduction_text', models.TextField(default='')), + ('description', wagtail.fields.RichTextField(default='')), + ('body', wagtail.fields.StreamField([('content_collection', wagtail.blocks.StructBlock([('title', wagtail.blocks.TextBlock()), ('collection_type', wagtail.blocks.MultipleChoiceBlock(choices=[('LearningMedia', 'Lernmedien'), ('Link', 'Links'), ('Anker', 'Verankerung'), ('CrossReference', 'Querverweise')], max_length=20)), ('contents', wagtail.blocks.StreamBlock([('Links', wagtail.blocks.StructBlock([('title', wagtail.blocks.TextBlock(blank=False, null=False)), ('description', wagtail.blocks.TextBlock(default='')), ('link_display_text', wagtail.blocks.CharBlock(default='Link öffnen', max_length=255)), ('url', wagtail.blocks.URLBlock())])), ('Documents', wagtail.documents.blocks.DocumentChooserBlock()), ('Ankers', vbv_lernwelt.media_library.content_blocks.AnkerBlock()), ('CrossReference', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock(default='')), ('link_display_text', wagtail.blocks.CharBlock(default='Link öffnen', max_length=255)), ('category', wagtail.blocks.PageChooserBlock(page_type=['media_library.Category']))]))]))]))], null=True, use_json_field=True)), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='MediaLibrary', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='MediaLibraryContent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.TextField()), + ('description', models.TextField()), + ('link_display_text', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='TopCategory', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + migrations.RenameModel( + old_name='CustomDocument', + new_name='LibraryDocument', + ), + ] diff --git a/server/vbv_lernwelt/media_library/models.py b/server/vbv_lernwelt/media_library/models.py index 74d3e2ee..1224a92f 100644 --- a/server/vbv_lernwelt/media_library/models.py +++ b/server/vbv_lernwelt/media_library/models.py @@ -1,8 +1,55 @@ +from wagtail import blocks, fields +from wagtail.admin.panels import FieldPanel, StreamFieldPanel + from django.db import models # Create your models here. from wagtail.models import Page from wagtail.documents.models import AbstractDocument, Document +from wagtail.snippets.blocks import SnippetChooserBlock + +from vbv_lernwelt.media_library.content_blocks import ContentCollection + + +class MediaLibrary(Page): + parent_page_types = ['learnpath.LearningPath'] + subpage_types = ['media_library.TopCategory'] + + content_panels = [ + FieldPanel('title', classname="full title"), + ] + + +class TopCategory(Page): + """ + Handlungsfelder + """ + parent_page_types = ['media_library.MediaLibrary'] + subpage_types = ['media_library.Category'] + + content_panels = [ + FieldPanel('title', classname="full title"), + ] + + +# Todo: use wagtail collections for this... Only applicable for documents, since links etc. dont have collections +class Category(Page): + """ + Handlungsfeld. zB. Fahrzeug + """ + parent_page_types = ['media_library.TopCategory'] + introduction_text = models.TextField(default='') + description = fields.RichTextField(default='') + + body = fields.StreamField([('content_collection', ContentCollection()) + ], use_json_field=True, null=True) + + content_panels = [ + FieldPanel('title', classname="full title"), + FieldPanel('introduction_text', classname="introduction text"), + FieldPanel('description', classname="introduction text"), + StreamFieldPanel('body') + ] class LibraryDocument(AbstractDocument): @@ -14,58 +61,6 @@ class LibraryDocument(AbstractDocument): link_display_text = models.CharField(max_length=1024, default='') thumbnail = models.URLField() - admin_form_fields = Document.admin_form_fields + ( 'display_text', 'description', 'link_display_text', 'thumbnail' ) - - -class TopCategory(Page): - """ - Handlungsfelder - """ - parent_page_types = ['learnpath.LearningPath'] - subpage_types = ['media_library.Category'] - - - -# Todo: use wagtail collections for this... - - -class Category(Page): - """ - Handlungsfeld - """ - - parent_page_types = ['media_library.TopCategory'] - -# -# description -# thumbnail_image -# description_image -# additional_content # Rich text field -# documents = [] -# -# -# class LibraryDocument(CustomDocument): -# """ -# Extension from the standart Wagtail document. -# """ -# pass -# -# -# class LibraryLink(): -# """ -# Custom Link Block -# -# """ -# pass -# -# -# class LearningPathReference(): -# icon -# pass -# -# -# class CrossReference(): -# pass diff --git a/server/vbv_lernwelt/media_library/tests/media_library_factories.py b/server/vbv_lernwelt/media_library/tests/media_library_factories.py index 267a193e..37388c52 100644 --- a/server/vbv_lernwelt/media_library/tests/media_library_factories.py +++ b/server/vbv_lernwelt/media_library/tests/media_library_factories.py @@ -1,9 +1,11 @@ +import json import wagtail_factories - +import factory from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningContent, LearningUnit, \ LearningUnitQuestion -from vbv_lernwelt.media_library.models import LibraryDocument +from vbv_lernwelt.media_library.models import LibraryDocument, MediaLibrary, TopCategory, Category +from vbv_lernwelt.media_library.content_blocks import ContentCollection, AnkerBlock, LinkBlock, CrossReferenceBlock class LibraryDocumentFactory(wagtail_factories.DocumentFactory): @@ -13,3 +15,110 @@ class LibraryDocumentFactory(wagtail_factories.DocumentFactory): class Meta: model = LibraryDocument + +class MediaLibraryFactory(wagtail_factories.PageFactory): + title = 'Mediathek' + + class Meta: + model = MediaLibrary + + +class TopCategoryFactory(wagtail_factories.PageFactory): + title = 'Handlungsfelder' + + class Meta: + model = TopCategory + + +class AnkerBlockFactory(wagtail_factories.StructBlockFactory): + class Meta: + model = AnkerBlock + + +class LinkBlockFactory(wagtail_factories.StructBlockFactory): + title = 'Interesting link' + description = 'This link is really interesting...' + url = 'www.example.com' + + class Meta: + model = LinkBlock + + +class CrossReferenceBlockFactory(wagtail_factories.StructBlockFactory): + class Meta: + model = CrossReferenceBlock + + +class ContentCollectionFactory(wagtail_factories.StructBlockFactory): + title = 'Links' + contents = wagtail_factories.StreamFieldFactory({ + 'Links': LinkBlockFactory, + 'Documents': LibraryDocumentFactory + }) + + class Meta: + model = ContentCollection + + +class CategoryFactory(wagtail_factories.PageFactory): + title = 'Fahrzeug' + introduction_text = 'Das Auto ist für viele der grösste Stolz! Es birgt aber ...' + description = 'Das erwartet dich in diesem Handlungsfeld' + body = wagtail_factories.StreamFieldFactory({'contents': ContentCollectionFactory}) + + class Meta: + model = Category + + +def generate_default_category(): + category = CategoryFactory() + category.body = json.dumps(collection_body_dict()) + category.save() + return category + + +def generate_default_content_string( + block_type='Links', + content_idx=0, + content_property='url', + stream_field_name='contents'): + return f'{stream_field_name}__{content_idx}__{block_type}__{content_property}' + + +def generate_default_content2(**contents_dict): + # TODO: hierarchical test, has to be refactored. + stream_field_name = 'contents' + content_idx = 0 + block_type = 'Links' + content_property = 'url' + value = 'iterativ.ch' + contents_dict[ + f'body__content_collection__0__{stream_field_name}__{content_idx}__{block_type}__{content_property}'] = value + return contents_dict + + +def link_dict(link_block=LinkBlockFactory()): + d = { + "type": "Links", + "collection_type": ["LearningMedia"], + "value": block_to_dict(link_block)} + return d + + +def document_dict(document=LibraryDocumentFactory()): + d = { + "type": "Documents", + "id": document.id + } + return d + + +def block_to_dict(block): + return dict(block.items()) + + +def collection_body_dict(): + d = [{"type": "content_collection", + "value": {"title": "Links", "contents": [link_dict()], }}, + {"type": "content_collection", + "value": {"title": "Lernmedien", "contents": [document_dict()], }}] diff --git a/server/vbv_lernwelt/media_library/tests/test_create_default_media_library.py b/server/vbv_lernwelt/media_library/tests/test_create_default_media_library.py new file mode 100644 index 00000000..bdcfeb80 --- /dev/null +++ b/server/vbv_lernwelt/media_library/tests/test_create_default_media_library.py @@ -0,0 +1,32 @@ +from django.test import TestCase +from wagtail.core.models import Collection + +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail +from vbv_lernwelt.media_library.create_default_documents import create_default_collections, create_default_documents +from vbv_lernwelt.media_library.create_default_media_library import create_default_media_library +from vbv_lernwelt.media_library.models import LibraryDocument +from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFactory +from vbv_lernwelt.media_library.models import MediaLibrary, TopCategory, Category + +class TestCreateDefaultDocuments(TestCase): + def setUp(self) -> None: + create_default_users() + create_locales_for_wagtail() + LearningPathFactory() + create_default_media_library() + + + def test_create_default_media_library(self): + + self.assertEqual(MediaLibrary.objects.all().count(), 1) + self.assertEqual(TopCategory.objects.all().count(), 2) + self.assertEqual(Category.objects.all().count(), 12) + + def test_create_category_fahrzeug_contains_content(self): + fahrzeug = Category.objects.get(title='Fahrzeug') + + + + + diff --git a/server/vbv_lernwelt/media_library/tests/test_media_library_factories.py b/server/vbv_lernwelt/media_library/tests/test_media_library_factories.py new file mode 100644 index 00000000..b0bcf7db --- /dev/null +++ b/server/vbv_lernwelt/media_library/tests/test_media_library_factories.py @@ -0,0 +1,49 @@ +import json + +from django.test import TestCase +from wagtail.core.models import Collection + +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail +from vbv_lernwelt.media_library.create_default_documents import create_default_collections, create_default_documents +from vbv_lernwelt.media_library.models import LibraryDocument, Category +from vbv_lernwelt.media_library.tests.media_library_factories import ContentCollectionFactory, CategoryFactory, \ + LinkBlockFactory, generate_default_category, generate_default_content2, collection_body_dict + + +class TestMediaLibraryFactories(TestCase): + def setUp(self) -> None: + create_default_users() + create_locales_for_wagtail() + + def test_content_collection_factory(self): + content_collection = ContentCollectionFactory() + self.assertEqual(content_collection.get('title'), 'Links') + self.assertEqual(content_collection.get('collection_type'), 'LearningMedia') + + def test_link_block_factory(self): + link = LinkBlockFactory(title='MyLink') + self.assertEqual(link.get('description'), 'This link is really interesting...') + self.assertEqual(link.get('url'), 'www.example.com') + self.assertEqual(link.get('link_display_text'), 'Link öffnen') + self.assertEqual(link.get('title'), 'MyLink') + + def test_category_contains_content_collection(self): + default_content = generate_default_content2() + default_content['body__content_collection__0__title'] = 'Spidf' + + category = CategoryFactory(**default_content) + print(category.body.raw_data) + self.assertNotEqual(category.body.raw_data, []) + + def collection_via_dict_generation(self): + category = CategoryFactory() + category.body = json.dumps(collection_body_dict()) + category.save() + category_id = category.id + new_category = Category.objects.get(id=category_id) + self.assertNotEqual(new_category.body, []) + self.assertNotEqual(new_category.body, []) + + +