Merge branch 'feature/mediathek-server' into feature/mediathek-frontend
This commit is contained in:
commit
8941f4ad24
|
|
@ -4,6 +4,7 @@ import type { LearningContent, LearningSequence } from '@/types'
|
|||
import { useCircleStore } from '@/stores/circle'
|
||||
import { computed } from 'vue'
|
||||
import _ from 'lodash'
|
||||
import { humanizeDuration } from '@/utils/humanizeDuration'
|
||||
|
||||
const props = defineProps<{
|
||||
learningSequence: LearningSequence
|
||||
|
|
@ -73,14 +74,14 @@ const learningSequenceBorderClass = computed(() => {
|
|||
<h3 class="text-xl font-semibold">
|
||||
{{ learningSequence.title }}
|
||||
</h3>
|
||||
<div>{{ learningSequence.minutes }} Minuten</div>
|
||||
<div>{{ humanizeDuration(learningSequence.minutes) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white px-4 lg:px-6 border border-gray-500" :class="learningSequenceBorderClass">
|
||||
<div v-for="learningUnit in learningSequence.learningUnits" :key="learningUnit.id" class="pt-3 lg:pt-6">
|
||||
<div class="pb-3 lg:pg-6 flex gap-4 text-blue-900" v-if="learningUnit.title">
|
||||
<div class="font-semibold">{{ learningUnit.title }}</div>
|
||||
<div>{{ learningUnit.minutes }} Minuten</div>
|
||||
<div>{{ humanizeDuration(learningUnit.minutes) }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ def main():
|
|||
)
|
||||
|
||||
response = client.get(
|
||||
'http://localhost:8000/api/learnpath/page/unit-test-lernpfad/',
|
||||
'http://localhost:8000/api/course/page/unit-test-lernpfad/',
|
||||
)
|
||||
print(response.status_code)
|
||||
print(response.json())
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const useLearningPathStore = defineStore({
|
|||
if (this.learningPath && !reload) {
|
||||
return this.learningPath;
|
||||
}
|
||||
const learningPathData = await itGet(`/api/learnpath/page/${slug}/`);
|
||||
const learningPathData = await itGet(`/api/course/page/${slug}/`);
|
||||
const completionData = await itGet(`/api/completion/learning_path/${learningPathData.translation_key}/`);
|
||||
|
||||
if (!learningPathData) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import { expect, test } from 'vitest'
|
||||
import { humanizeDuration } from '../humanizeDuration'
|
||||
|
||||
test('format duration for humans', () => {
|
||||
expect(humanizeDuration(1)).toBe('1 Minute')
|
||||
expect(humanizeDuration(15)).toBe('15 Minuten')
|
||||
expect(humanizeDuration(42)).toBe('45 Minuten')
|
||||
expect(humanizeDuration(60)).toBe('1 Stunde')
|
||||
expect(humanizeDuration(122)).toBe('2 Stunden')
|
||||
expect(humanizeDuration(120)).toBe('2 Stunden')
|
||||
expect(humanizeDuration(132)).toBe('2 Stunden 15 Minuten')
|
||||
expect(humanizeDuration(632)).toBe('10 Stunden')
|
||||
})
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
function pluralize(text: string, count: number) {
|
||||
if (count === 1) {
|
||||
return text;
|
||||
}
|
||||
return text + 'n';
|
||||
}
|
||||
|
||||
|
||||
export function humanizeDuration(minutes: number) {
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const remainingMinutes = minutes % 60
|
||||
|
||||
if (hours === 0 && minutes < 16) {
|
||||
return pluralize(`${remainingMinutes} Minute`, remainingMinutes)
|
||||
}
|
||||
|
||||
// Remaining minutes are rounded to 15 mins
|
||||
const roundToMinutes = 15
|
||||
const roundedMinutes = Math.round((minutes % 60) / roundToMinutes) * roundToMinutes
|
||||
|
||||
const hoursString = hours > 0 ? pluralize(`${hours} Stunde`, hours) : ''
|
||||
|
||||
const showMinutesUpToHours = 10
|
||||
const minutesString = roundedMinutes > 0 && hours < showMinutesUpToHours
|
||||
? pluralize(`${roundedMinutes} Minute`, roundedMinutes) : ''
|
||||
|
||||
const delimiter = hoursString && minutesString ? ' ' : ''
|
||||
return `${hoursString}${delimiter}${minutesString}`
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import { useCircleStore } from '@/stores/circle'
|
|||
import { useAppStore } from '@/stores/app'
|
||||
import { useRoute } from 'vue-router'
|
||||
import _ from 'lodash'
|
||||
import { humanizeDuration } from '@/utils/humanizeDuration'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ const circleStore = useCircleStore()
|
|||
const duration = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
const minutes = _.sumBy(circleStore.circle.learningSequences, 'minutes')
|
||||
return `${minutes} Minuten`
|
||||
return humanizeDuration(minutes)
|
||||
}
|
||||
|
||||
return ''
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const userStore = useUserStore()
|
|||
<div class="mt-8 p-8 break-words bg-white max-w-xl">
|
||||
<h3>Versicherungsvermittler/in</h3>
|
||||
<div class="mt-4">
|
||||
<router-link class="btn-blue" to="/learn/versicherungsvermittlerin"> Weiter geht's </router-link>
|
||||
<router-link class="btn-blue" to="/learn/versicherungsvermittlerin-lp"> Weiter geht's </router-link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ if [ "$SKIP_SETUP" = false ]; then
|
|||
python3 server/manage.py createcachetable --settings="$DJANGO_SETTINGS_MODULE"
|
||||
python3 server/manage.py migrate --settings="$DJANGO_SETTINGS_MODULE"
|
||||
python3 server/manage.py create_default_users --settings="$DJANGO_SETTINGS_MODULE"
|
||||
python3 server/manage.py create_default_courses --settings="$DJANGO_SETTINGS_MODULE"
|
||||
python3 server/manage.py create_default_learning_path --settings="$DJANGO_SETTINGS_MODULE"
|
||||
python3 server/manage.py create_default_media_library --settings="$DJANGO_SETTINGS_MODULE"
|
||||
|
||||
|
|
|
|||
|
|
@ -103,9 +103,10 @@ THIRD_PARTY_APPS = [
|
|||
LOCAL_APPS = [
|
||||
"vbv_lernwelt.core",
|
||||
"vbv_lernwelt.sso",
|
||||
"vbv_lernwelt.course",
|
||||
"vbv_lernwelt.learnpath",
|
||||
"vbv_lernwelt.completion",
|
||||
"vbv_lernwelt.media_library",
|
||||
"vbv_lernwelt.completion",
|
||||
]
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||
|
|
@ -514,7 +515,7 @@ if "django_redis.cache.RedisCache" in env("IT_DJANGO_CACHE_BACKEND", default="")
|
|||
},
|
||||
}
|
||||
|
||||
CACHES["learning_path_cache"] = {
|
||||
CACHES["api_page_cache"] = {
|
||||
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
|
||||
"LOCATION": "django_cache_learning_path",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from vbv_lernwelt.core.views import (
|
|||
rate_limit_exceeded_view,
|
||||
permission_denied_view,
|
||||
check_rate_limit, cypress_reset_view, vue_home, vue_login, me_user_view, vue_logout, generate_web_component_icons, )
|
||||
from vbv_lernwelt.learnpath.views import page_api_view
|
||||
from vbv_lernwelt.course.views import page_api_view
|
||||
|
||||
|
||||
def raise_example_error(request):
|
||||
|
|
@ -47,8 +47,8 @@ urlpatterns = [
|
|||
# core
|
||||
re_path(r"server/core/icons/$", generate_web_component_icons, name="generate_web_component_icons"),
|
||||
|
||||
# learnpath
|
||||
path(r"api/learnpath/page/<slug:slug>/", page_api_view, name="page_api_view"),
|
||||
# course
|
||||
path(r"api/course/page/<slug:slug>/", page_api_view, name="page_api_view"),
|
||||
|
||||
# completion
|
||||
path(r"api/completion/circle/<uuid:circle_key>/", request_circle_completion, name="request_circle_completion"),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 3.2.13 on 2022-07-04 09:58
|
||||
# Generated by Django 3.2.13 on 2022-09-19 14:37
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
|
@ -22,8 +22,8 @@ class Migration(migrations.Migration):
|
|||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('page_key', models.UUIDField()),
|
||||
('page_type', models.CharField(blank=True, default='', max_length=255)),
|
||||
('circle_key', models.UUIDField()),
|
||||
('learning_path_key', models.UUIDField()),
|
||||
('circle_key', models.UUIDField(blank=True, default='')),
|
||||
('learning_path_key', models.UUIDField(blank=True, default='')),
|
||||
('completed', models.BooleanField(default=False)),
|
||||
('json_data', models.JSONField(blank=True, default=dict)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
from wagtail.models import Page
|
||||
|
||||
|
||||
def find_available_slug(requested_slug, ignore_page_id=None):
|
||||
"""
|
||||
Finds an available slug within the specified parent.
|
||||
|
||||
If the requested slug is not available, this adds a number on the end, for example:
|
||||
|
||||
- 'requested-slug'
|
||||
- 'requested-slug-1'
|
||||
- 'requested-slug-2'
|
||||
|
||||
And so on, until an available slug is found.
|
||||
|
||||
The `ignore_page_id` keyword argument is useful for when you are updating a page,
|
||||
you can pass the page being updated here so the page's current slug is not
|
||||
treated as in use by another page.
|
||||
"""
|
||||
|
||||
pages = Page.objects.filter(slug__startswith=requested_slug)
|
||||
|
||||
if ignore_page_id:
|
||||
pages = pages.exclude(id=ignore_page_id)
|
||||
|
||||
existing_slugs = set(pages.values_list("slug", flat=True))
|
||||
slug = requested_slug
|
||||
number = 1
|
||||
|
||||
while slug in existing_slugs:
|
||||
slug = requested_slug + "-" + str(number)
|
||||
number += 1
|
||||
|
||||
return slug
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CourseConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'vbv_lernwelt.course'
|
||||
|
||||
def ready(self):
|
||||
try:
|
||||
# pylint: disable=unused-import,import-outside-toplevel
|
||||
import vbv_lernwelt.course.signals # noqa F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
|
@ -0,0 +1 @@
|
|||
COURSE_VERSICHERUNGSVERMITTLERIN = -1
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import wagtail_factories
|
||||
from django.conf import settings
|
||||
from wagtail.models import Site
|
||||
|
||||
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN
|
||||
from vbv_lernwelt.course.factories import CoursePageFactory
|
||||
|
||||
|
||||
def create_versicherungsvermittlerin_with_categories(apps=None, schema_editor=None):
|
||||
if apps is not None:
|
||||
Course = apps.get_model('course', 'Course')
|
||||
CourseCategory = apps.get_model('course', 'CourseCategory')
|
||||
else:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from vbv_lernwelt.course.models import Course, CourseCategory
|
||||
|
||||
course, _ = Course.objects.get_or_create(
|
||||
id=COURSE_VERSICHERUNGSVERMITTLERIN,
|
||||
name='Versicherungsvermittler/in',
|
||||
category_name='Handlungsfeld'
|
||||
)
|
||||
|
||||
CourseCategory.objects.get_or_create(course=course, name='Allgemein', general=True)
|
||||
|
||||
for cat in [
|
||||
'Fahrzeug', 'Reisen', 'Einkommensicherung', 'Gesundheit', 'Haushalt', 'Sparen',
|
||||
'Pensionierung', 'KMU', 'Wohneigentum', 'Rechtsstreitigkeiten', 'Erben / Vererben',
|
||||
'Selbständigkeit',
|
||||
]:
|
||||
CourseCategory.objects.get_or_create(course=course, name=cat)
|
||||
|
||||
# create default course page
|
||||
site = Site.objects.filter(is_default_site=True).first()
|
||||
if not site:
|
||||
site = wagtail_factories.SiteFactory(is_default_site=True)
|
||||
|
||||
if settings.APP_ENVIRONMENT == 'development':
|
||||
site.port = 8000
|
||||
site.save()
|
||||
|
||||
course_page = CoursePageFactory(
|
||||
title="Versicherungsvermittler/in",
|
||||
parent=site.root_page,
|
||||
course=course,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import wagtail_factories
|
||||
from factory.django import DjangoModelFactory
|
||||
|
||||
from vbv_lernwelt.course.models import CoursePage, Course
|
||||
|
||||
|
||||
class CourseFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Course
|
||||
|
||||
name = 'Versicherungsvermittler/in'
|
||||
category_name = 'Handlungsfeld'
|
||||
|
||||
|
||||
class CoursePageFactory(wagtail_factories.PageFactory):
|
||||
title = "Versicherungsvermittler/in"
|
||||
|
||||
class Meta:
|
||||
model = CoursePage
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Iterativ GmbH
|
||||
# http://www.iterativ.ch/
|
||||
#
|
||||
# Copyright (c) 2015 Iterativ GmbH. All rights reserved.
|
||||
#
|
||||
# Created on 2022-03-31
|
||||
# @author: lorenz.padberg@iterativ.ch
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import djclick as click
|
||||
|
||||
from vbv_lernwelt.course.creators import create_versicherungsvermittlerin_with_categories
|
||||
|
||||
|
||||
@click.command()
|
||||
def command():
|
||||
create_versicherungsvermittlerin_with_categories()
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Generated by Django 3.2.13 on 2022-09-19 14:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0069_log_entry_jsonfield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Course',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Titel')),
|
||||
('category_name', models.CharField(default='Kategorie', max_length=255, verbose_name='Kategorie-Name')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lerngang',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CoursePage',
|
||||
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')),
|
||||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.course')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lerngang-Seite',
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CourseCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=255, verbose_name='Titel')),
|
||||
('general', models.BooleanField(default=False, verbose_name='Allgemein')),
|
||||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.course')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail.models import Page
|
||||
|
||||
from vbv_lernwelt.core.model_utils import find_available_slug
|
||||
|
||||
|
||||
class Course(models.Model):
|
||||
name = models.CharField(_('Titel'), max_length=255)
|
||||
category_name = models.CharField(_('Kategorie-Name'), max_length=255, default='Kategorie')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Lerngang")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class CourseCategory(models.Model):
|
||||
# Die Handlungsfelder im "Versicherungsvermittler/in"
|
||||
name = models.CharField(_('Titel'), max_length=255, blank=True)
|
||||
course = models.ForeignKey('course.Course', on_delete=models.CASCADE)
|
||||
general = models.BooleanField(_('Allgemein'), default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.course} / {self.name}"
|
||||
|
||||
|
||||
class CoursePage(Page):
|
||||
content_panels = Page.content_panels
|
||||
subpage_types = ['learnpath.LearningPath', 'media_library.MediaLibraryPage']
|
||||
course = models.ForeignKey('course.Course', on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Lerngang-Seite")
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(slugify(self.title, allow_unicode=True))
|
||||
super(CoursePage, self).full_clean(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}"
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from vbv_lernwelt.course.models import CourseCategory, Course
|
||||
|
||||
|
||||
class CourseSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Course
|
||||
fields = ['id', 'name', 'category_name']
|
||||
|
||||
|
||||
class CourseCategorySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CourseCategory
|
||||
fields = ['id', 'name', 'general',]
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import structlog
|
||||
from django.core.cache import caches
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from wagtail.models import Page
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def invalidate_api_page_cache(sender, **kwargs):
|
||||
logger.debug('invalidate api_page_cache', label='api_page_cache')
|
||||
caches['api_page_cache'].clear()
|
||||
|
||||
|
||||
for subclass in Page.__subclasses__():
|
||||
post_save.connect(invalidate_api_page_cache, subclass)
|
||||
post_delete.connect(invalidate_api_page_cache, subclass)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import structlog
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from wagtail.models import Page
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
# @cache_page(60 * 60 * 8, cache="api_page_cache")
|
||||
def page_api_view(request, slug):
|
||||
try:
|
||||
page = Page.objects.get(slug=slug, locale__language_code='de-CH')
|
||||
serializer = page.specific.get_serializer_class()(page.specific)
|
||||
return Response(serializer.data)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return Response({"error": str(e)}, status=404)
|
||||
|
|
@ -5,6 +5,8 @@ from wagtail.models import Site, Page, Locale
|
|||
from wagtail_localize.models import LocaleSynchronization
|
||||
|
||||
from vbv_lernwelt.core.admin import User
|
||||
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN
|
||||
from vbv_lernwelt.course.models import CoursePage
|
||||
from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningContent, LearningUnit, \
|
||||
LearningUnitQuestion
|
||||
from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFactory, TopicFactory, CircleFactory, \
|
||||
|
|
@ -340,7 +342,11 @@ def create_default_learning_path(user=None, skip_locales=True):
|
|||
|
||||
# create_default_competences()
|
||||
|
||||
lp = LearningPathFactory(title="Versicherungsvermittler/in", parent=site.root_page)
|
||||
course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN)
|
||||
lp = LearningPathFactory(
|
||||
title="Lernpfad",
|
||||
parent=course_page,
|
||||
)
|
||||
|
||||
TopicFactory(title="Basis", is_visible=False, parent=lp)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import djclick as click
|
||||
|
||||
from vbv_lernwelt.learnpath.create_default_learning_path import create_default_learning_path
|
||||
from vbv_lernwelt.learnpath.tests.create_simple_test_learning_path import create_simple_test_learning_path
|
||||
|
||||
|
||||
@click.command()
|
||||
def command():
|
||||
create_default_learning_path(skip_locales=True)
|
||||
create_simple_test_learning_path(skip_locales=True)
|
||||
# FIXME: readd
|
||||
# create_simple_test_learning_path(skip_locales=True)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
# Generated by Django 3.2.13 on 2022-06-22 15:48
|
||||
# Generated by Django 3.2.13 on 2022-09-19 15:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
import wagtail.blocks
|
||||
import wagtail.fields
|
||||
import wagtail.images.blocks
|
||||
|
|
@ -31,34 +30,12 @@ class Migration(migrations.Migration):
|
|||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Competence',
|
||||
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)),
|
||||
('category_short', models.CharField(default='', max_length=3)),
|
||||
('name', models.CharField(max_length=2048)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Competence',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CompetencePage',
|
||||
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={
|
||||
'verbose_name': 'Learning Path',
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LearningContent',
|
||||
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')),
|
||||
('minutes', models.PositiveIntegerField(default=15)),
|
||||
('contents', wagtail.fields.StreamField([('video', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('web_based_training', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('podcast', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('competence', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('exercise', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('document', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('knowledge', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())]))], use_json_field=None)),
|
||||
('contents', wagtail.fields.StreamField([('video', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('web_based_training', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('podcast', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('competence', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('exercise', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('document', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('knowledge', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())]))], use_json_field=None)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Learning Content',
|
||||
|
|
@ -117,21 +94,4 @@ class Migration(migrations.Migration):
|
|||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FullfillmentCriteria',
|
||||
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)),
|
||||
('name', models.CharField(max_length=2048)),
|
||||
('competence', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='learnpath.competence')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Fullfillment Criteria',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='competence',
|
||||
name='competence_page',
|
||||
field=modelcluster.fields.ParentalKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='competences', to='learnpath.competencepage'),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
# Generated by Django 3.2.13 on 2022-08-24 14:47
|
||||
|
||||
from django.db import migrations
|
||||
import wagtail.blocks
|
||||
import wagtail.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('learnpath', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='learningcontent',
|
||||
name='contents',
|
||||
field=wagtail.fields.StreamField([('video', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('web_based_training', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('podcast', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('competence', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('exercise', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock()), ('url', wagtail.blocks.URLBlock())])), ('document', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())])), ('knowledge', wagtail.blocks.StructBlock([('description', wagtail.blocks.TextBlock())]))], use_json_field=None),
|
||||
),
|
||||
]
|
||||
|
|
@ -7,6 +7,7 @@ from wagtail.fields import StreamField
|
|||
from wagtail.images.blocks import ImageChooserBlock
|
||||
from wagtail.models import Page, Orderable
|
||||
|
||||
from vbv_lernwelt.core.model_utils import find_available_slug
|
||||
from vbv_lernwelt.learnpath.models_learning_unit_content import WebBasedTrainingBlock, VideoBlock, PodcastBlock, \
|
||||
CompetenceBlock, ExerciseBlock, DocumentBlock, KnowledgeBlock
|
||||
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
|
||||
|
|
@ -17,12 +18,13 @@ class LearningPath(Page):
|
|||
|
||||
content_panels = Page.content_panels
|
||||
subpage_types = ['learnpath.Circle', 'learnpath.Topic']
|
||||
parent_page_types = ['course.CoursePage']
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Learning Path"
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(slugify(self.title, allow_unicode=True))
|
||||
self.slug = find_available_slug(slugify(f"{self.get_parent().slug}-lp", allow_unicode=True))
|
||||
super(LearningPath, self).full_clean(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -268,36 +270,3 @@ def find_slug_with_parent_prefix(page, type_prefix):
|
|||
slug_prefix = type_prefix
|
||||
|
||||
return find_available_slug(slugify(f'{slug_prefix}-{page.title}', allow_unicode=True))
|
||||
|
||||
|
||||
def find_available_slug(requested_slug, ignore_page_id=None):
|
||||
"""
|
||||
Finds an available slug within the specified parent.
|
||||
|
||||
If the requested slug is not available, this adds a number on the end, for example:
|
||||
|
||||
- 'requested-slug'
|
||||
- 'requested-slug-1'
|
||||
- 'requested-slug-2'
|
||||
|
||||
And so on, until an available slug is found.
|
||||
|
||||
The `ignore_page_id` keyword argument is useful for when you are updating a page,
|
||||
you can pass the page being updated here so the page's current slug is not
|
||||
treated as in use by another page.
|
||||
"""
|
||||
|
||||
pages = Page.objects.filter(slug__startswith=requested_slug)
|
||||
|
||||
if ignore_page_id:
|
||||
pages = pages.exclude(id=ignore_page_id)
|
||||
|
||||
existing_slugs = set(pages.values_list("slug", flat=True))
|
||||
slug = requested_slug
|
||||
number = 1
|
||||
|
||||
while slug in existing_slugs:
|
||||
slug = requested_slug + "-" + str(number)
|
||||
number += 1
|
||||
|
||||
return slug
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import wagtail.api.v2.serializers as wagtail_serializers
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
|
||||
from vbv_lernwelt.course.models import CoursePage
|
||||
from vbv_lernwelt.course.serializers import CourseCategorySerializer, CourseSerializer
|
||||
from vbv_lernwelt.learnpath.utils import get_wagtail_type
|
||||
|
||||
|
||||
|
|
@ -17,6 +19,8 @@ class ItTypeField(wagtail_serializers.TypeField):
|
|||
class ItBaseSerializer(wagtail_serializers.BaseSerializer):
|
||||
type = ItTypeField(read_only=True)
|
||||
children = SerializerMethodField()
|
||||
course = SerializerMethodField()
|
||||
course_category = CourseCategorySerializer(read_only=True)
|
||||
|
||||
meta_fields = []
|
||||
|
||||
|
|
@ -30,6 +34,15 @@ class ItBaseSerializer(wagtail_serializers.BaseSerializer):
|
|||
children = _get_children(self.descendants, obj)
|
||||
return [c.specific.get_serializer_class()(c.specific, descendants=self.descendants).data for c in children]
|
||||
|
||||
def get_course(self, obj):
|
||||
if hasattr(obj, 'course'):
|
||||
return CourseSerializer(obj.course).data
|
||||
else:
|
||||
course_parent_page = obj.get_ancestors().exact_type(CoursePage).last()
|
||||
if course_parent_page:
|
||||
return CourseSerializer(course_parent_page.specific.course).data
|
||||
return ''
|
||||
|
||||
|
||||
def _get_descendants(pages, obj):
|
||||
return [c for c in pages if c.path.startswith(obj.path) and c.depth >= obj.depth]
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import structlog
|
||||
from django.core.cache import caches
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from wagtail.models import Page
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def invalidate_learning_path_cache(sender, **kwargs):
|
||||
logger.debug('invalidate learning_path_cache', label='learning_path_cache')
|
||||
caches['learning_path_cache'].clear()
|
||||
|
||||
|
||||
for subclass in Page.__subclasses__():
|
||||
post_save.connect(invalidate_learning_path_cache, subclass)
|
||||
post_delete.connect(invalidate_learning_path_cache, subclass)
|
||||
|
|
@ -18,7 +18,7 @@ class TestRetrieveLearingPathContents(APITestCase):
|
|||
|
||||
def test_get_learnpathPage(self):
|
||||
learning_path = LearningPath.objects.get(slug='unit-test-lernpfad')
|
||||
response = self.client.get('/api/learnpath/page/unit-test-lernpfad/')
|
||||
response = self.client.get('/api/course/page/unit-test-lernpfad/')
|
||||
print(response)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
# Create your views here.
|
||||
|
||||
import structlog
|
||||
from django.views.decorators.cache import cache_page
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from wagtail.models import Page
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@cache_page(60 * 60 * 8, cache="learning_path_cache")
|
||||
def page_api_view(request, slug):
|
||||
try:
|
||||
page = Page.objects.get(slug=slug, locale__language_code='de-CH')
|
||||
serializer = page.specific.get_serializer_class()(page.specific)
|
||||
return Response(serializer.data)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return Response({"error": str(e)}, status=404)
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
from django.db import models
|
||||
from wagtail import blocks
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.documents.blocks import DocumentChooserBlock
|
||||
from wagtail.snippets.models import register_snippet
|
||||
|
||||
|
||||
@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 AnchorBlock(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.MediaCategoryPage')
|
||||
|
||||
|
||||
class MediaContentCollection(blocks.StructBlock):
|
||||
"""
|
||||
Lernmedien, Links, Querverweise, Verankerung
|
||||
"""
|
||||
title = blocks.TextBlock()
|
||||
contents = blocks.StreamBlock([
|
||||
('Links', LinkBlock()),
|
||||
('Documents', DocumentChooserBlock()),
|
||||
('Ankers', AnchorBlock()),
|
||||
('CrossReference', CrossReferenceBlock())
|
||||
])
|
||||
|
||||
class Meta:
|
||||
icon = 'link'
|
||||
|
|
@ -3,6 +3,7 @@ import os
|
|||
import factory
|
||||
from wagtail.core.models import Collection
|
||||
|
||||
from vbv_lernwelt.course.models import Course
|
||||
from vbv_lernwelt.media_library.models import LibraryDocument
|
||||
from vbv_lernwelt.media_library.tests.media_library_factories import LibraryDocumentFactory
|
||||
|
||||
|
|
@ -11,15 +12,11 @@ def create_default_collections():
|
|||
c = Collection.objects.all().delete()
|
||||
|
||||
root, created = Collection.objects.get_or_create(name='Root', depth=0)
|
||||
versicherungsvermittler = root.add_child(name='Versicherungsvermittler/in')
|
||||
handlungsfelder = versicherungsvermittler.add_child(name='Handlungsfelder')
|
||||
|
||||
handlungsfelder_names = ['Fahrzeug', 'Reisen', 'Einkommensicherung', 'Gesundheit', 'Haushalt', 'Sparen',
|
||||
'Pensionierung', 'KMU', 'Wohneigentum', 'Rechtsstreitigkeiten', 'Erben / Vererben',
|
||||
'Selbständigkeit']
|
||||
|
||||
for handlungsfeld in handlungsfelder_names:
|
||||
versicherungsvermittler = handlungsfelder.add_child(name=handlungsfeld)
|
||||
for course in Course.objects.all():
|
||||
course_collection = root.add_child(name=course.name)
|
||||
for cat in course.coursecategory_set.all():
|
||||
cat_collection = course_collection.add_child(name=cat.name)
|
||||
|
||||
|
||||
def create_default_documents():
|
||||
|
|
@ -33,7 +30,7 @@ def create_default_documents():
|
|||
document = LibraryDocumentFactory(
|
||||
title='V1 C25 ZGB CH',
|
||||
display_text='Schweizerisches Zivilgesetzbuch',
|
||||
description='Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wolbefinden für Handyvekäufer.',
|
||||
description='Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wohlbefinden für Handyvekäufer.',
|
||||
link_display_text='Dokument laden',
|
||||
file=factory.django.FileField(from_path=os.path.join(path, filename), filename=filename),
|
||||
collection=collection
|
||||
|
|
@ -43,10 +40,8 @@ def create_default_documents():
|
|||
document = LibraryDocumentFactory(
|
||||
title='V1 C25 ',
|
||||
display_text='Pdf showcase ',
|
||||
description='Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wolbefinden für Handyvekäufer.',
|
||||
description='Ein wundervolles Dokument, Bachblüten für Leseratten und metaphysisches Wohlbefinden für Handyvekäufer.',
|
||||
link_display_text='Dokument laden',
|
||||
file=factory.django.FileField(from_path=os.path.join(path, filename), filename=filename),
|
||||
collection=collection
|
||||
)
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import json
|
||||
|
||||
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN
|
||||
from vbv_lernwelt.course.models import CoursePage, Course
|
||||
from vbv_lernwelt.media_library.tests.media_library_factories import MediaLibraryPageFactory, MediaCategoryPageFactory, \
|
||||
create_media_content_link, LinkBlockFactory, create_link_collection, create_document_collection
|
||||
|
||||
|
||||
def create_default_media_library():
|
||||
course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN)
|
||||
course_page = CoursePage.objects.get(course_id=COURSE_VERSICHERUNGSVERMITTLERIN)
|
||||
|
||||
media_lib_page = MediaLibraryPageFactory(
|
||||
title='Mediathek',
|
||||
parent=course_page,
|
||||
)
|
||||
|
||||
for cat in course.coursecategory_set.all():
|
||||
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.
|
||||
'''.strip()
|
||||
description = 'Supi'
|
||||
body_data = json.dumps([
|
||||
create_document_collection(),
|
||||
create_link_collection(
|
||||
links_dict=[
|
||||
create_media_content_link(LinkBlockFactory(title='Nationales Versicherungsbüro', url='https://www.vbv.ch/')),
|
||||
create_media_content_link(LinkBlockFactory(title='Adressen der Strassenverkehrsämter', url='https://www.vbv.ch/')),
|
||||
]
|
||||
)
|
||||
])
|
||||
media_category = MediaCategoryPageFactory(
|
||||
title=cat.name,
|
||||
course_category=cat,
|
||||
parent=media_lib_page,
|
||||
introduction_text=introduction_text,
|
||||
description=description,
|
||||
body=body_data,
|
||||
)
|
||||
|
|
@ -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_collections()
|
||||
create_default_documents()
|
||||
|
||||
create_default_media_library()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
# Generated by Django 3.2.13 on 2022-08-16 08:35
|
||||
# Generated by Django 3.2.13 on 2022-09-23 12:42
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import vbv_lernwelt.media_library.content_blocks
|
||||
import wagtail.blocks
|
||||
import wagtail.documents.blocks
|
||||
import wagtail.fields
|
||||
import wagtail.models.collections
|
||||
import wagtail.search.index
|
||||
|
||||
|
|
@ -13,14 +17,48 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0069_log_entry_jsonfield'),
|
||||
('course', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('taggit', '0004_alter_taggeditem_content_type_alter_taggeditem_tag'),
|
||||
('wagtailcore', '0069_log_entry_jsonfield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomDocument',
|
||||
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='MediaLibraryPage',
|
||||
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='MediaCategoryPage',
|
||||
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()), ('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.AnchorBlock()), ('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.MediaCategoryPage']))]))]))]))], null=True, use_json_field=True)),
|
||||
('course_category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.coursecategory')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LibraryDocument',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
@ -1,8 +1,74 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
from wagtail.models import Page
|
||||
from django.utils.text import slugify
|
||||
from wagtail import fields
|
||||
from wagtail.admin.panels import FieldPanel, StreamFieldPanel
|
||||
from wagtail.documents.models import AbstractDocument, Document
|
||||
from wagtail.models import Page
|
||||
|
||||
from vbv_lernwelt.core.model_utils import find_available_slug
|
||||
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
|
||||
from vbv_lernwelt.media_library.content_blocks import MediaContentCollection
|
||||
|
||||
|
||||
class MediaLibraryPage(Page):
|
||||
parent_page_types = ['course.CoursePage']
|
||||
subpage_types = ['media_library.MediaCategoryPage']
|
||||
|
||||
content_panels = [
|
||||
FieldPanel('title', classname="full title"),
|
||||
]
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(slugify(f"{self.get_parent().slug}-media", allow_unicode=True))
|
||||
super(MediaLibraryPage, self).full_clean(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_serializer_class(cls):
|
||||
return get_it_serializer_class(
|
||||
cls, [
|
||||
'id', 'title', 'slug', 'type', 'translation_key',
|
||||
'course',
|
||||
'children',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class MediaCategoryPage(Page):
|
||||
"""
|
||||
Handlungsfeld. zB. Fahrzeug
|
||||
"""
|
||||
course_category = models.ForeignKey('course.CourseCategory', on_delete=models.CASCADE)
|
||||
parent_page_types = ['media_library.MediaLibraryPage']
|
||||
introduction_text = models.TextField(default='')
|
||||
description = fields.RichTextField(default='')
|
||||
|
||||
body = fields.StreamField(
|
||||
[('content_collection', MediaContentCollection())],
|
||||
use_json_field=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
content_panels = [
|
||||
FieldPanel('title', classname="full title"),
|
||||
FieldPanel('course_category'),
|
||||
FieldPanel('introduction_text', classname="introduction text"),
|
||||
FieldPanel('description', classname="introduction text"),
|
||||
StreamFieldPanel('body')
|
||||
]
|
||||
|
||||
def full_clean(self, *args, **kwargs):
|
||||
self.slug = find_available_slug(slugify(f"{self.get_parent()}-cat-{self.title}", allow_unicode=True))
|
||||
super(MediaCategoryPage, self).full_clean(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_serializer_class(cls):
|
||||
return get_it_serializer_class(
|
||||
cls, field_names=[
|
||||
'id', 'title', 'slug', 'type', 'translation_key',
|
||||
'course_category',
|
||||
'introduction_text', 'description', 'body',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class LibraryDocument(AbstractDocument):
|
||||
|
|
@ -14,58 +80,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
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import uuid
|
||||
|
||||
import wagtail_factories
|
||||
|
||||
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.content_blocks import MediaContentCollection, AnchorBlock, LinkBlock, \
|
||||
CrossReferenceBlock
|
||||
from vbv_lernwelt.media_library.models import LibraryDocument, MediaLibraryPage, MediaCategoryPage
|
||||
|
||||
|
||||
class LibraryDocumentFactory(wagtail_factories.DocumentFactory):
|
||||
|
|
@ -13,3 +14,87 @@ class LibraryDocumentFactory(wagtail_factories.DocumentFactory):
|
|||
class Meta:
|
||||
model = LibraryDocument
|
||||
|
||||
|
||||
class MediaLibraryPageFactory(wagtail_factories.PageFactory):
|
||||
title = 'Mediathek'
|
||||
|
||||
class Meta:
|
||||
model = MediaLibraryPage
|
||||
|
||||
|
||||
class AnchorBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
class Meta:
|
||||
model = AnchorBlock
|
||||
|
||||
|
||||
class LinkBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
title = 'Interesting link'
|
||||
description = 'This link is really interesting...'
|
||||
url = 'https://www.vbv.ch/'
|
||||
|
||||
class Meta:
|
||||
model = LinkBlock
|
||||
|
||||
|
||||
class CrossReferenceBlockFactory(wagtail_factories.StructBlockFactory):
|
||||
class Meta:
|
||||
model = CrossReferenceBlock
|
||||
|
||||
|
||||
class MediaContentCollectionFactory(wagtail_factories.StructBlockFactory):
|
||||
title = 'Links'
|
||||
contents = wagtail_factories.StreamFieldFactory({
|
||||
'Links': LinkBlockFactory,
|
||||
'Documents': LibraryDocumentFactory
|
||||
})
|
||||
|
||||
class Meta:
|
||||
model = MediaContentCollection
|
||||
|
||||
|
||||
class MediaCategoryPageFactory(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'
|
||||
|
||||
class Meta:
|
||||
model = MediaCategoryPage
|
||||
|
||||
|
||||
def create_media_content_link(link_block=None):
|
||||
if link_block is None:
|
||||
link_block = LinkBlockFactory()
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"type": "Links",
|
||||
"value": dict(link_block.items())
|
||||
}
|
||||
|
||||
|
||||
def create_link_collection(links_dict=None):
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"type": "content_collection",
|
||||
"value": {
|
||||
"title": "Links",
|
||||
"contents": [link_dict for link_dict in links_dict]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_document_collection(document_ids=None):
|
||||
if document_ids is None:
|
||||
document_ids = [d.id for d in LibraryDocument.objects.all()]
|
||||
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"type": "content_collection",
|
||||
"value": {
|
||||
"title": "Lernmedien",
|
||||
"contents": [{
|
||||
"id": str(uuid.uuid4()),
|
||||
"type": "Documents",
|
||||
"value": doc_id
|
||||
} for doc_id in document_ids]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
from django.test import TestCase
|
||||
|
||||
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.learnpath.tests.learning_path_factories import LearningPathFactory
|
||||
from vbv_lernwelt.media_library.create_default_media_library import create_default_media_library
|
||||
from vbv_lernwelt.media_library.models import MediaLibraryPage, MediaCategoryPage
|
||||
|
||||
|
||||
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(MediaLibraryPage.objects.all().count(), 1)
|
||||
self.assertEqual(MediaCategoryPage.objects.all().count(), 12)
|
||||
|
||||
def test_create_category_fahrzeug_contains_content(self):
|
||||
fahrzeug = MediaCategoryPage.objects.get(title='Fahrzeug')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import json
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
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.models import MediaCategoryPage
|
||||
from vbv_lernwelt.media_library.tests.media_library_factories import MediaContentCollectionFactory, MediaCategoryPageFactory, \
|
||||
LinkBlockFactory, 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 = MediaContentCollectionFactory()
|
||||
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 = MediaCategoryPageFactory(**default_content)
|
||||
print(category.body.raw_data)
|
||||
self.assertNotEqual(category.body.raw_data, [])
|
||||
|
||||
def collection_via_dict_generation(self):
|
||||
category = MediaCategoryPageFactory()
|
||||
category.body = json.dumps(collection_body_dict())
|
||||
category.save()
|
||||
category_id = category.id
|
||||
new_category = MediaCategoryPage.objects.get(id=category_id)
|
||||
self.assertNotEqual(new_category.body, [])
|
||||
self.assertNotEqual(new_category.body, [])
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Loading…
Reference in New Issue