diff --git a/server/config/urls.py b/server/config/urls.py index 209ba4bf..aee079ea 100644 --- a/server/config/urls.py +++ b/server/config/urls.py @@ -18,9 +18,9 @@ from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt from vbv_lernwelt.core.views import ( rate_limit_exceeded_view, permission_denied_view, - check_rate_limit, vue_home, cypress_reset_view, + check_rate_limit, cypress_reset_view, ) -from .wagtail_api import api_router +from .wagtail_api import wagtail_api_router def raise_example_error(request): @@ -42,6 +42,7 @@ urlpatterns = [ path('cms/', include(wagtailadmin_urls)), path('documents/', include(wagtaildocs_urls)), path('pages/', include(wagtail_urls)), + path('learnpath/', include("vbv_lernwelt.learnpath.urls")), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: # Static file serving when using Gunicorn + Uvicorn for local web socket development @@ -56,7 +57,7 @@ if settings.ALLOW_LOCAL_LOGIN: urlpatterns += [ # API base url path("api/", include("config.api_router")), - path('wagtailapi/v2/', api_router.urls), + path('wagtailapi/v2/', wagtail_api_router.urls), # DRF auth token path("auth-token/", obtain_auth_token), @@ -111,5 +112,5 @@ if settings.DEBUG: # serve everything else via the vue app -urlpatterns += [re_path(r'^.*$', vue_home, name='home')] +# urlpatterns += [re_path(r'^.*$', vue_home, name='home')] diff --git a/server/config/wagtail_api.py b/server/config/wagtail_api.py index 1647ee43..d12d1b50 100644 --- a/server/config/wagtail_api.py +++ b/server/config/wagtail_api.py @@ -1,16 +1,16 @@ -from wagtail.api.v2.views import PagesAPIViewSet from wagtail.api.v2.router import WagtailAPIRouter -from wagtail.images.api.v2.views import ImagesAPIViewSet +from wagtail.api.v2.views import PagesAPIViewSet from wagtail.documents.api.v2.views import DocumentsAPIViewSet +from wagtail.images.api.v2.views import ImagesAPIViewSet # Create the router. "wagtailapi" is the URL namespace -api_router = WagtailAPIRouter('wagtailapi') +wagtail_api_router = WagtailAPIRouter('wagtailapi') # Add the three endpoints using the "register_endpoint" method. # The first parameter is the name of the endpoint (eg. pages, images). This # is used in the URL of the endpoint # The second parameter is the endpoint class that handles the requests -api_router.register_endpoint('pages', PagesAPIViewSet) -api_router.register_endpoint('images', ImagesAPIViewSet) -api_router.register_endpoint('documents', DocumentsAPIViewSet) +wagtail_api_router.register_endpoint('pages', PagesAPIViewSet) +wagtail_api_router.register_endpoint('images', ImagesAPIViewSet) +wagtail_api_router.register_endpoint('documents', DocumentsAPIViewSet) diff --git a/server/vbv_lernwelt/learnpath/migrations/0001_initial.py b/server/vbv_lernwelt/learnpath/migrations/0001_initial.py index 3be5dbd6..feb259fa 100644 --- a/server/vbv_lernwelt/learnpath/migrations/0001_initial.py +++ b/server/vbv_lernwelt/learnpath/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.12 on 2022-06-01 13:32 +# Generated by Django 3.2.12 on 2022-06-01 15:39 from django.db import migrations, models import django.db.models.deletion @@ -16,19 +16,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='Circle', - 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')), - ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), - ('description', models.TextField(blank=True, default='')), - ('goals', models.TextField(blank=True, default='')), - ], - options={ - 'verbose_name': 'Circle', - }, - bases=('wagtailcore.page', models.Model), - ), migrations.CreateModel( name='Competence', fields=[ @@ -64,14 +51,23 @@ class Migration(migrations.Migration): migrations.CreateModel( name='LearningSequence', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), - ('title', models.CharField(default='', max_length=256)), - ('circle', modelcluster.fields.ParentalKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='learning_sequences', to='learnpath.circle')), + ('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={ 'verbose_name': 'Learning Sequence', }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='LearningUnit', + 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')), + ('contents', wagtail.core.fields.StreamField([('web_based_training', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('video', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())]))], blank=True, null=True)), + ], + options={ + 'verbose_name': 'Learning Unit', + }, + bases=('wagtailcore.page',), ), migrations.CreateModel( name='Topic', @@ -86,19 +82,6 @@ class Migration(migrations.Migration): 'verbose_name': 'Topic', }, ), - migrations.CreateModel( - name='LearningUnit', - 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')), - ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), - ('contents', wagtail.core.fields.StreamField([('web_based_training', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())])), ('video', wagtail.core.blocks.StructBlock([('url', wagtail.core.blocks.URLBlock())]))], blank=True, null=True)), - ('learning_sequence', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='learning_units', to='learnpath.learningsequence')), - ], - options={ - 'verbose_name': 'Learning Unit', - }, - bases=('wagtailcore.page', models.Model), - ), migrations.CreateModel( name='FullfillmentCriteria', fields=[ @@ -116,9 +99,17 @@ class Migration(migrations.Migration): name='competence_page', field=modelcluster.fields.ParentalKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='competences', to='learnpath.competencepage'), ), - migrations.AddField( - model_name='circle', - name='topic', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='circles', to='learnpath.topic'), + migrations.CreateModel( + name='Circle', + 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')), + ('description', models.TextField(blank=True, default='')), + ('goals', models.TextField(blank=True, default='')), + ('topic', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='circles', to='learnpath.topic')), + ], + options={ + 'verbose_name': 'Circle', + }, + bases=('wagtailcore.page',), ), ] diff --git a/server/vbv_lernwelt/learnpath/migrations/0002_remove_learningunit_sort_order.py b/server/vbv_lernwelt/learnpath/migrations/0002_remove_learningunit_sort_order.py deleted file mode 100644 index 86489dd0..00000000 --- a/server/vbv_lernwelt/learnpath/migrations/0002_remove_learningunit_sort_order.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.12 on 2022-06-01 14:18 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('learnpath', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='learningunit', - name='sort_order', - ), - ] diff --git a/server/vbv_lernwelt/learnpath/migrations/0003_remove_circle_sort_order.py b/server/vbv_lernwelt/learnpath/migrations/0003_remove_circle_sort_order.py deleted file mode 100644 index 5ceb1c13..00000000 --- a/server/vbv_lernwelt/learnpath/migrations/0003_remove_circle_sort_order.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.12 on 2022-06-01 14:40 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('learnpath', '0002_remove_learningunit_sort_order'), - ] - - operations = [ - migrations.RemoveField( - model_name='circle', - name='sort_order', - ), - ] diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index 31e49b7f..be8bfbfe 100644 --- a/server/vbv_lernwelt/learnpath/models.py +++ b/server/vbv_lernwelt/learnpath/models.py @@ -6,10 +6,10 @@ from wagtail.api import APIField from wagtail.core.blocks import StreamBlock from wagtail.core.fields import StreamField from wagtail.core.models import Page, Orderable -from wagtail.snippets.models import register_snippet from vbv_lernwelt.learnpath.models_competences import * from vbv_lernwelt.learnpath.models_learning_unit_content import WebBasedTrainingBlock, VideoBlock +from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class @register_query_field("learning_path") @@ -22,7 +22,6 @@ class LearningPath(Page): subpage_types = ['learnpath.Circle'] - class Meta: verbose_name = "Learning Path" @@ -83,23 +82,20 @@ class Circle(Page): ) parent_page_types = ['learnpath.LearningPath'] - subpage_types = ['learnpath.LearningUnit'] + subpage_types = ['learnpath.LearningSequence', 'learnpath.LearningUnit'] content_panels = Page.content_panels + [ FieldPanel('description'), FieldPanel('goals'), - InlinePanel('learning_sequences', label="Learning Sequences"), ] api_fields = [ APIField('title'), APIField('description'), - APIField('topic'), - APIField('learning_sequences'), ] def full_clean(self, *args, **kwargs): - self.slug = find_available_slug(Circle, slugify(self.title, allow_unicode=True)) + # self.slug = find_available_slug(Circle, slugify(self.title, allow_unicode=True)) super(Circle, self).full_clean(*args, **kwargs) class Meta: @@ -109,33 +105,21 @@ class Circle(Page): return f"{self.title}" -@register_snippet -class LearningSequence(Orderable): - # TODO: How to do a icon choice field? - title = models.CharField(max_length=256, default='') - - circle = ParentalKey( - 'learnpath.Circle', - null=True, - blank=True, - on_delete=models.CASCADE, - related_name='learning_sequences', - ) +class LearningSequence(Page): + parent_page_types = ['learnpath.Circle'] panels = [FieldPanel('title'), FieldPanel('circle')] - api_fields = [ - APIField('title'), - APIField('category'), - APIField('learning_packages'), - ] - class Meta: verbose_name = "Learning Sequence" def __str__(self): return f"{self.title}" + @classmethod + def get_serializer_class(cls): + return get_it_serializer_class(cls, field_names=['id', 'title', 'slug', 'type']) + def full_clean(self, *args, **kwargs): super(LearningSequence, self).full_clean(*args, **kwargs) @@ -146,14 +130,7 @@ class LearningUnit(Page): """ # TODO: Review model architecture, is the stream field the right thing here? - parent_page_types = ['learnpath.Circle'] - learning_sequence = models.ForeignKey( - 'learnpath.LearningSequence', - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name='learning_units', - ) + parent_page_types = ['learnpath.LearningSequence'] content_blocks = [ ('web_based_training', WebBasedTrainingBlock()), @@ -166,14 +143,13 @@ class LearningUnit(Page): content_panels = [ FieldPanel('title', classname="full title"), - FieldPanel('learning_sequence'), + # FieldPanel('learning_sequence'), StreamFieldPanel('contents'), ] api_fields = [ APIField('title'), APIField('contents'), - APIField('content_blocks'), ] subpage_types = [] @@ -185,6 +161,10 @@ class LearningUnit(Page): self.slug = find_available_slug(LearningUnit, slugify(self.title, allow_unicode=True)) super(LearningUnit, self).full_clean(*args, **kwargs) + @classmethod + def get_serializer_class(cls): + return get_it_serializer_class(cls, field_names=['id', 'title', 'contents', 'slug', 'type']) + def __str__(self): return f"{self.title}" diff --git a/server/vbv_lernwelt/learnpath/models_learning_unit_content.py b/server/vbv_lernwelt/learnpath/models_learning_unit_content.py index 59e2ffc1..b818db6d 100644 --- a/server/vbv_lernwelt/learnpath/models_learning_unit_content.py +++ b/server/vbv_lernwelt/learnpath/models_learning_unit_content.py @@ -11,9 +11,6 @@ class VideoBlock(blocks.StructBlock): class Meta: icon = 'media' - def get_api_representation(self, value, context=None): - return {'sdfsdf': 1, - 'sldkfm': 3} RISE = 'rise' @@ -32,8 +29,3 @@ class WebBasedTrainingBlock(blocks.StructBlock): class Meta: icon = 'media' - - def get_api_representation(self, value, context=None): - - return {'sdfsdf': 1, - 'sldkfm': 3} diff --git a/server/vbv_lernwelt/learnpath/serializer_helpers.py b/server/vbv_lernwelt/learnpath/serializer_helpers.py new file mode 100644 index 00000000..81bd41f9 --- /dev/null +++ b/server/vbv_lernwelt/learnpath/serializer_helpers.py @@ -0,0 +1,15 @@ +import wagtail.api.v2.serializers as wagtail_serializers + + +def get_it_serializer_class(model, field_names): + return wagtail_serializers.get_serializer_class(model, field_names=field_names, meta_fields=[], base=ItBaseSerializer) + + +class ItTypeField(wagtail_serializers.TypeField): + def to_representation(self, obj): + name = type(obj)._meta.app_label + '.' + type(obj).__name__ + return name + + +class ItBaseSerializer(wagtail_serializers.BaseSerializer): + type = ItTypeField(read_only=True) diff --git a/server/vbv_lernwelt/learnpath/serializers.py b/server/vbv_lernwelt/learnpath/serializers.py new file mode 100644 index 00000000..0e2201af --- /dev/null +++ b/server/vbv_lernwelt/learnpath/serializers.py @@ -0,0 +1,20 @@ +from rest_framework import serializers + +from vbv_lernwelt.learnpath.models import Circle +from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class + + +class CircleSerializer(get_it_serializer_class(Circle, [])): + children = serializers.SerializerMethodField() + + meta_fields = [] + + def get_children(self, obj): + return [c.specific.get_serializer_class()(c.specific).data for c in obj.get_children()] + + def get_meta_label(self, obj): + return obj._meta.label + + class Meta: + model = Circle + fields = ['id', 'title', 'slug', 'children', 'type'] diff --git a/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py b/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py index cb258aae..275ec4a0 100644 --- a/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py +++ b/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py @@ -62,10 +62,10 @@ von Neukunden zu benützen — Lösungsvorschläge zu skizzieren und zu visualisieren""") - sequence_1 = LearningSequenceFactory(title="Starten", circle=circle_4) + sequence_1 = LearningSequenceFactory(title="Starten", parent=circle_4) - learning_unit = LearningUnitFactory(title='Einleitung Circle "Anlayse"', parent=circle_4, learning_sequence=sequence_1) - learning_unit = LearningUnitFactory.create(title='** Einstieg Video"', parent=circle_4, learning_sequence=sequence_1) + learning_unit = LearningUnitFactory(title='Einleitung Circle "Anlayse"', parent=circle_4) + learning_unit = LearningUnitFactory.create(title='** Einstieg Video"', parent=circle_4) video_url = "https://www.vbv.ch/fileadmin/vbv/Videos/Statements_Externe/Janos_M/Testimonial_Janos_Mischler_PositiveEffekte.mp4" video_title = "Ausbildung ist pflicht" video_description = "Erfahren Sie, was für Janos Mischler die positiven Aspekte von ständiger Weiterbildung sind – aus fachlicher und aus persönlicher Sicht." @@ -73,38 +73,38 @@ von Neukunden zu benützen learning_unit.contents.append(('video', video_block)) learning_unit.save() - learning_unit = LearningUnitFactory.create(title='** Web Based Training"', parent=circle_4, learning_sequence=sequence_1) + learning_unit = LearningUnitFactory.create(title='** Web Based Training"', parent=circle_4) wbt_url = "web_based_trainings/rise_cmi5_test_export/scormcontent/index.html" wbt_block = WebBasedTrainingBlockFactory(content_type="web_based_training", url=wbt_url) learning_unit.contents.append(('web_based_training', wbt_block)) learning_unit.save() - learning_unit = LearningUnitFactory.create(title="Selbsteinschätzung", parent=circle_4, learning_sequence=sequence_1) + learning_unit = LearningUnitFactory.create(title="Selbsteinschätzung", parent=circle_4) - sequence_2 = LearningSequenceFactory.create(title="Beobachten", circle=circle_4) - learning_unit = LearningUnitFactory.create(title="Mein Motorfahrzeug kaufen", parent=circle_4, learning_sequence=sequence_2) - learning_unit = LearningUnitFactory.create(title="Sich selbständig machen", parent=circle_4, learning_sequence=sequence_2) + sequence_2 = LearningSequenceFactory.create(title="Beobachten", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Mein Motorfahrzeug kaufen", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Sich selbständig machen", parent=circle_4) - sequence_3 = LearningSequenceFactory.create(title="Anwenden", circle=circle_4) - learning_unit = LearningUnitFactory.create(title="Nora kauft sich ein neues Auto", parent=circle_4, learning_sequence=sequence_3) - learning_unit = LearningUnitFactory.create(title="Manuel träumt von einem neuen Tesla", parent=circle_4, learning_sequence=sequence_3) - learning_unit = LearningUnitFactory.create(title="Deine Erkenntnisse und Learnings", parent=circle_4, learning_sequence=sequence_3) + sequence_3 = LearningSequenceFactory.create(title="Anwenden", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Nora kauft sich ein neues Auto", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Manuel träumt von einem neuen Tesla", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Deine Erkenntnisse und Learnings", parent=circle_4) - sequence_4 = LearningSequenceFactory.create(title="Üben", circle=circle_4) - learning_unit = LearningUnitFactory.create(title="Ermittlung des Kundenbedarfs", parent=circle_4, learning_sequence=sequence_4) - learning_unit = LearningUnitFactory.create(title="Aktives Zuhören", parent=circle_4, learning_sequence=sequence_4) - learning_unit = LearningUnitFactory.create(title="In Bildern Sprechen", parent=circle_4, learning_sequence=sequence_4) - learning_unit = LearningUnitFactory.create(title="Priorisieren des Bedarfs", parent=circle_4, learning_sequence=sequence_4) - learning_unit = LearningUnitFactory.create(title="Zusammenfassung des Bedarfs", parent=circle_4, learning_sequence=sequence_4) + sequence_4 = LearningSequenceFactory.create(title="Üben", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Ermittlung des Kundenbedarfs", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Aktives Zuhören", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="In Bildern Sprechen", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Priorisieren des Bedarfs", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Zusammenfassung des Bedarfs", parent=circle_4) - sequence_5 = LearningSequenceFactory.create(title="Testen", circle=circle_4) - learning_unit = LearningUnitFactory.create(title="Bedarfsfragen", parent=circle_4, learning_sequence=sequence_5) - learning_unit = LearningUnitFactory.create(title="Andwendung der Fragetechniken", parent=circle_4, learning_sequence=sequence_5) + sequence_5 = LearningSequenceFactory.create(title="Testen", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Bedarfsfragen", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Andwendung der Fragetechniken", parent=circle_4) - sequence_5 = LearningSequenceFactory.create(title="Vernetzen", circle=circle_4) - learning_unit = LearningUnitFactory.create(title="Online Training", parent=circle_4, learning_sequence=sequence_5) + sequence_5 = LearningSequenceFactory.create(title="Vernetzen", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Online Training", parent=circle_4) - sequence_6 = LearningSequenceFactory.create(title="Beenden", circle=circle_4) - learning_unit = LearningUnitFactory.create(title="Selbsteinschätzung", parent=circle_4, learning_sequence=sequence_6) + sequence_6 = LearningSequenceFactory.create(title="Beenden", parent=circle_4) + learning_unit = LearningUnitFactory.create(title="Selbsteinschätzung", parent=circle_4) circle_5 = CircleFactory.create(title="Lösung", parent=lp, diff --git a/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py b/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py index efed959d..8cb9476b 100644 --- a/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py +++ b/server/vbv_lernwelt/learnpath/tests/learning_path_factories.py @@ -27,7 +27,7 @@ class CircleFactory(wagtail_factories.PageFactory): model = Circle -class LearningSequenceFactory(factory.django.DjangoModelFactory): +class LearningSequenceFactory(wagtail_factories.PageFactory): title = "Grundlagen" class Meta: diff --git a/server/vbv_lernwelt/learnpath/urls.py b/server/vbv_lernwelt/learnpath/urls.py new file mode 100644 index 00000000..2bae3734 --- /dev/null +++ b/server/vbv_lernwelt/learnpath/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url, include +from django.urls import path +from rest_framework.routers import DefaultRouter + +from . import views +from .views import circle_view + +urlpatterns = [ + path(r"api/circle//", circle_view, name="circle_view"), +] diff --git a/server/vbv_lernwelt/learnpath/views.py b/server/vbv_lernwelt/learnpath/views.py index 91ea44a2..4607a9d3 100644 --- a/server/vbv_lernwelt/learnpath/views.py +++ b/server/vbv_lernwelt/learnpath/views.py @@ -1,3 +1,13 @@ -from django.shortcuts import render - # Create your views here. +from rest_framework.decorators import api_view +from rest_framework.response import Response + +from vbv_lernwelt.learnpath.models import Circle +from vbv_lernwelt.learnpath.serializers import CircleSerializer + + +@api_view(['GET']) +def circle_view(request, slug): + circle = Circle.objects.get(slug=slug) + serializer = CircleSerializer(circle) + return Response(serializer.data)