diff --git a/.gitignore b/.gitignore
index 6bda23f3..e0b494ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -249,7 +249,8 @@ bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
-
+.direnv
+.idea
### Vim template
# Swap
diff --git a/README.md b/README.md
index 81d7559d..544ed13b 100644
--- a/README.md
+++ b/README.md
@@ -56,3 +56,7 @@ npm run dev
* In the .idea/vbv_lernwelt.iml file change the module type to "PYTHON_MODULE".
* Add django facet in "Project Structure".
* Run configuration with "Python -> server.py" to have async debugging support.
+### Optional
+* Install the EnvFile Plugin
+* Install the tailwind css Plugin from Jetbrains
+
diff --git a/env_secrets/local_lorenz.env b/env_secrets/local_lorenz.env
new file mode 100644
index 00000000..01cac659
Binary files /dev/null and b/env_secrets/local_lorenz.env differ
diff --git a/git-crypt-encrypted-files.txt b/git-crypt-encrypted-files.txt
index 279b8b0e..dd4c2af7 100644
--- a/git-crypt-encrypted-files.txt
+++ b/git-crypt-encrypted-files.txt
@@ -1,2 +1,3 @@
encrypted: env_secrets/caprover.env
encrypted: env_secrets/production.env
+ encrypted: env_secrets/local_lorenz.env
diff --git a/local-setup-for-tests.sh b/local-setup-for-tests.sh
new file mode 100755
index 00000000..f861cfe5
--- /dev/null
+++ b/local-setup-for-tests.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+#export DATABASE_HOST=postgres
+#export DATABASE_PORT=5432
+#export DATABASE_URL=postgres://$DATABASE_USER:$PG_PASSWORD@$DATABASE_HOST:$DATABASE_PORT/$DATABASE_NAME
+#
+#echo $DATABASE_URL
+#DJANGO_SETTINGS_MODULE=config.settings.base
+#DATABASE_NAME=vbv_lernwelt
+SKIP_SETUP=false
+##
+
+echo "Setting up VBV Project for Local usage"
+if [ "$SKIP_SETUP" = false ]; then
+ if [ -z "$PG_PORT" ]; then # if the port is set in the env, use iterg
+ DB_PORT="";
+ else
+ DB_PORT="-p $PG_PORT";
+ fi
+ if [ -z "$PG_USER" ]; then # if the user is set in the env, use it
+ DB_USER="";
+ else
+ DB_USER="-U $PG_USER";
+ fi
+ echo "psql -h localhost --port=$DB_PORT --username=$DB_USER -c 'drop database if exists' $DATABASE_NAME;"
+
+ echo "Drop all connections to the database"
+ psql -h localhost --port=$DB_PORT --username=$DB_USER -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '$DATABASE_NAME' AND pid <> pg_backend_pid();"
+
+ echo "Drop database: $DATABASE_NAME"
+ psql -h localhost --port=$DB_PORT --username=$DB_USER -c "drop database if exists $DATABASE_NAME;"
+
+ echo "Create database: $DATABASE_NAME"
+ psql -h localhost --port=$DB_PORT --username=$DB_USER -c "create database $DATABASE_NAME;"
+
+ # reset data
+ 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_learningpath --settings="$DJANGO_SETTINGS_MODULE"
+#
+# # make django translations
+ (cd server && python3 manage.py compilemessages --settings="$DJANGO_SETTINGS_MODULE")
+fi
+
diff --git a/package.json b/package.json
index 3f5ec7aa..c358f6b8 100644
--- a/package.json
+++ b/package.json
@@ -10,5 +10,8 @@
},
"devDependencies": {
"cypress": "^9.4.1"
+ },
+ "dependencies": {
+ "tailwindcss": "^3.0.24"
}
}
diff --git a/prepare_server_cypress.sh b/prepare_server_cypress.sh
index 49612f88..8ef42ba9 100755
--- a/prepare_server_cypress.sh
+++ b/prepare_server_cypress.sh
@@ -43,7 +43,7 @@ DJANGO_SETTINGS_MODULE=config.settings.test_cypress
CYPRESS_DB=vbv_lernwelt_cypress
if [ "$SKIP_SETUP" = false ]; then
- if [ -z "$PG_PORT" ]; then # if the port is set in the env, use it
+ if [ -z "$PG_PORT" ]; then # if the port is set in the env, use iterg
DB_PORT="";
else
DB_PORT="-p $PG_PORT";
@@ -66,12 +66,7 @@ if [ "$SKIP_SETUP" = false ]; then
# make django translations
(cd server && python3 manage.py compilemessages --settings="$DJANGO_SETTINGS_MODULE")
-# python3 src/manage.py constance --settings="$DJANGO_SETTINGS_MODULE" set API_WFM_BACKEND_ENABLED true
-# python3 src/manage.py constance --settings="$DJANGO_SETTINGS_MODULE" set TIBCO_SOAP_CUSTOMER_INTERACTION_CLIENT_ENABLED true
-# python3 src/manage.py constance --settings="$DJANGO_SETTINGS_MODULE" set API_EMAIL_MESSAGING_ENABLED true
-# python3 src/manage.py constance --settings="$DJANGO_SETTINGS_MODULE" set C4_NOTIFICATIONS_ENABLED true
-# python3 src/manage.py constance --settings="$DJANGO_SETTINGS_MODULE" set SFTP_POSTFINANCE_ENABLED true
-# python3 src/manage.py constance --settings="$DJANGO_SETTINGS_MODULE" set EASY_INSURANCE_AGENT_CAN_CREATE true
+
else
echo "else"
# python3 src/manage.py recreate_customer_data_for_integration_tests --settings="$DJANGO_SETTINGS_MODULE"
diff --git a/server/config/settings/base.py b/server/config/settings/base.py
index bb1ae595..bc29a5aa 100644
--- a/server/config/settings/base.py
+++ b/server/config/settings/base.py
@@ -31,7 +31,7 @@ DEBUG = env.bool("VBV_DJANGO_DEBUG", False)
# In Windows, this must be set to your system time zone.
TIME_ZONE = "Europe/Zurich"
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
-LANGUAGE_CODE = "en-us"
+LANGUAGE_CODE = "de-CH"
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1
# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
@@ -83,13 +83,32 @@ THIRD_PARTY_APPS = [
"corsheaders",
"drf_spectacular",
"django_htmx",
+
+ "grapple",
+ "graphene_django",
+
+ 'wagtail.contrib.forms',
+ 'wagtail.contrib.redirects',
+ 'wagtail.embeds',
+ 'wagtail.sites',
+ 'wagtail.users',
+ 'wagtail.snippets',
+ 'wagtail.documents',
+ 'wagtail.images',
+ 'wagtail.search',
+ 'wagtail.admin',
+ 'wagtail.core',
+ 'wagtail.locales',
+
+ 'modelcluster',
+ 'taggit',
]
LOCAL_APPS = [
"vbv_lernwelt.core",
"vbv_lernwelt.simpletodo",
"vbv_lernwelt.sso",
- # Your stuff: custom apps go here
+ "vbv_lernwelt.learnpath",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
@@ -153,6 +172,7 @@ MIDDLEWARE = [
"django_htmx.middleware.HtmxMiddleware",
"vbv_lernwelt.core.middleware.auth.AuthenticationRequiredMiddleware",
"vbv_lernwelt.core.middleware.security.SecurityRequestResponseLoggingMiddleware",
+ "wagtail.contrib.redirects.middleware.RedirectMiddleware",
"vbv_lernwelt.core.middleware.auth.UserLoggedInCookieMiddleWare",
]
@@ -177,6 +197,41 @@ MEDIA_ROOT = str(APPS_DIR / "media")
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = "/media/"
+
+# WAGTAIL
+# ------------------------------------------------------------------------------
+WAGTAIL_SITE_NAME = 'VBV Lernwelt'
+WAGTAIL_I18N_ENABLED = True
+
+LANGUAGES = [
+ ('en-US', "English (American)"),
+ ('fr-CH', "Swiss French"),
+ ('de-CH', "Swiss German"),
+ ('it-CH', "Swiss Italian")
+]
+
+
+WAGTAIL_CONTENT_LANGUAGES = [
+ ('fr-CH', "Swiss French"),
+ ('de-CH', "Swiss German"),
+ ('it-CH', "Swiss Italian")
+]
+
+WAGTAILSEARCH_BACKENDS = {
+ 'default': {
+ 'BACKEND': 'wagtail.search.backends.database',
+ }
+}
+
+# Wagtails Grapple Config:
+GRAPHENE = {"SCHEMA": "grapple.schema.schema"}
+GRAPPLE = {
+ "APPS": ["learnpath"],
+"EXPOSE_GRAPHIQL" : True
+
+}
+
+
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
@@ -476,14 +531,14 @@ if DJANGO_DEV_MODE == "development":
# django-debug-toolbar
# ------------------------------------------------------------------------------
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
- INSTALLED_APPS += ["debug_toolbar"] # noqa F405
+ #INSTALLED_APPS += ["debug_toolbar"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
- MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405
+ # MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
- DEBUG_TOOLBAR_CONFIG = {
- "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
- "SHOW_TEMPLATE_CONTEXT": True,
- }
+ # DEBUG_TOOLBAR_CONFIG = {
+ # "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
+ # "SHOW_TEMPLATE_CONTEXT": True,
+ # }
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"]
if env.bool("VBV_DJANGO_LOCAL_DOCKER", False):
diff --git a/server/config/settings/local.py b/server/config/settings/local.py
new file mode 100644
index 00000000..e0af5343
--- /dev/null
+++ b/server/config/settings/local.py
@@ -0,0 +1,42 @@
+# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
+import getpass
+import os
+
+from .base import * # noqa
+from .base import env
+
+# GENERAL
+# ------------------------------------------------------------------------------
+# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
+SECRET_KEY = env(
+ "VBV_DJANGO_SECRET_KEY",
+ default="1NpUCSvAKLpDZL9e3tqDaUesdfsadfasdfasdfMD3UjB72ZS",
+)
+# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
+TEST_RUNNER = "django.test.runner.DiscoverRunner"
+
+# PASSWORDS
+# ------------------------------------------------------------------------------
+# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
+PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
+
+# EMAIL
+# ------------------------------------------------------------------------------
+# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
+EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
+
+
+class DisableMigrations(dict):
+ def __contains__(self, item):
+ return True
+
+ def __getitem__(self, item):
+ return None
+
+#MIGRATION_MODULES = DisableMigrations()
+
+
+
+
+# Your stuff...
+# ------------------------------------------------------------------------------
diff --git a/server/config/settings/test.py b/server/config/settings/test.py
index ed59a58d..90c4b66c 100644
--- a/server/config/settings/test.py
+++ b/server/config/settings/test.py
@@ -39,7 +39,7 @@ MIGRATION_MODULES = DisableMigrations()
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
- "NAME": "vbv_lernwelt",
+ "NAME": "vbv_lernwelt_test",
"USER": os.environ.get("PG_USER", getpass.getuser()),
"PASSWORD": os.environ.get("PG_PASSWORD"),
"HOST": "localhost",
@@ -47,5 +47,6 @@ DATABASES = {
}
}
+
# Your stuff...
# ------------------------------------------------------------------------------
diff --git a/server/config/urls.py b/server/config/urls.py
index 85f51a68..d9f489b5 100644
--- a/server/config/urls.py
+++ b/server/config/urls.py
@@ -1,3 +1,4 @@
+
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
@@ -10,13 +11,16 @@ from django.views.generic import TemplateView
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from ratelimit.exceptions import Ratelimited
from rest_framework.authtoken.views import obtain_auth_token
-
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,
)
+from wagtail.admin import urls as wagtailadmin_urls
+from wagtail.core import urls as wagtail_urls
+from wagtail.documents import urls as wagtaildocs_urls
+from grapple import urls as grapple_urls
def raise_example_error(request):
@@ -36,6 +40,9 @@ urlpatterns = [
path("checkratelimit/", check_rate_limit),
path("todo/", include("vbv_lernwelt.simpletodo.urls")),
path("sso/", include("vbv_lernwelt.sso.urls")),
+ path('cms/', include(wagtailadmin_urls)),
+ path('documents/', include(wagtaildocs_urls)),
+ path('pages/', include(wagtail_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
@@ -54,6 +61,7 @@ urlpatterns += [
path("auth-token/", obtain_auth_token),
path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"),
path("api/docs/", SpectacularSwaggerView.as_view(url_name="api-schema"), name="api-docs",),
+ path("", include(grapple_urls)),
]
# fmt: on
diff --git a/server/local.env b/server/local.env
new file mode 100644
index 00000000..e69de29b
diff --git a/server/requirements/requirements.in b/server/requirements/requirements.in
index 292a80b4..0f2aa542 100644
--- a/server/requirements/requirements.in
+++ b/server/requirements/requirements.in
@@ -32,3 +32,9 @@ sentry-sdk
structlog
python-json-logger
concurrent-log-handler
+
+wagtail<3
+wagtail-factories
+wagtail-grapple==0.14.1
+
+
diff --git a/server/setup_test_data.sh b/server/setup_test_data.sh
new file mode 100644
index 00000000..7a1dd173
--- /dev/null
+++ b/server/setup_test_data.sh
@@ -0,0 +1,5 @@
+python manage.py migrate
+python manage.py createcachetable
+python manage.py create_default_users
+#python manage.py create_default_learingpath
+
diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py
new file mode 100644
index 00000000..82960734
--- /dev/null
+++ b/server/vbv_lernwelt/core/create_default_users.py
@@ -0,0 +1,34 @@
+from django.contrib.auth.hashers import make_password
+from django.contrib.auth.models import Group
+
+from vbv_lernwelt.core.models import User
+
+
+def create_default_users(user_model=User, group_model=Group):
+ admin_group, created = group_model.objects.get_or_create(name='admin_group')
+ content_creator_grop, created = group_model.objects.get_or_create(name='content_creator_grop')
+ student_group, created = group_model.objects.get_or_create(name='student_group')
+
+ admin_user, created = _get_or_create_user(user_model=user_model,
+ username='admin',
+ password='admin')
+ admin_user.is_superuser=True
+ admin_user.groups.add(admin_group)
+
+ admin_user.save()
+ student_user, created = _get_or_create_user(user_model=user_model, username='student', password='student')
+ student_user.groups.add(student_group)
+ student_user.save()
+
+
+def _get_or_create_user(user_model, *args, **kwargs):
+ username = kwargs.get('username', None)
+ password = kwargs.get('password', None)
+ created = False
+
+ user = user_model.objects.filter(username=username).first()
+
+ if not user:
+ user = user_model.objects.create(username=username, password=make_password(password))
+ created = True
+ return user, created
diff --git a/server/vbv_lernwelt/core/management/commands/create_default_users.py b/server/vbv_lernwelt/core/management/commands/create_default_users.py
new file mode 100644
index 00000000..2d7c365d
--- /dev/null
+++ b/server/vbv_lernwelt/core/management/commands/create_default_users.py
@@ -0,0 +1,10 @@
+from vbv_lernwelt.core.create_default_users import create_default_users
+
+
+import djclick as click
+
+
+@click.command()
+def command():
+ print("Creating default users.")
+ create_default_users()
diff --git a/server/vbv_lernwelt/core/management/commands/migrate_locales.py b/server/vbv_lernwelt/core/management/commands/migrate_locales.py
new file mode 100644
index 00000000..4bc583e4
--- /dev/null
+++ b/server/vbv_lernwelt/core/management/commands/migrate_locales.py
@@ -0,0 +1,9 @@
+import djclick as click
+from django.conf import settings
+from wagtail.core.models import Locale
+
+
+@click.command()
+def command():
+ for language in settings.WAGTAIL_CONTENT_LANGUAGES:
+ Locale.objects.create(language_code=language[0])
diff --git a/server/vbv_lernwelt/core/tests/test_user.py b/server/vbv_lernwelt/core/tests/test_user.py
new file mode 100644
index 00000000..442d7b34
--- /dev/null
+++ b/server/vbv_lernwelt/core/tests/test_user.py
@@ -0,0 +1,16 @@
+from django.test import TestCase
+
+from vbv_lernwelt.core.models import User
+from vbv_lernwelt.core.tests.factories import UserFactory
+from vbv_lernwelt.simpletodo.models import SimpleList
+
+
+class TestUserCreation(TestCase):
+ def test_create_user(self):
+ User(last_name='Sepp').save()
+
+ def test_simple(self):
+ # create_default_learning_path()
+ self.user = UserFactory()
+ SimpleList.objects.get_or_create(title='Default', user=self.user)
+ self.assertTrue(True)
diff --git a/server/vbv_lernwelt/core/utils.py b/server/vbv_lernwelt/core/utils.py
index e8137755..af7f5938 100644
--- a/server/vbv_lernwelt/core/utils.py
+++ b/server/vbv_lernwelt/core/utils.py
@@ -4,7 +4,7 @@ import structlog
from django.conf import settings
from rest_framework.throttling import UserRateThrottle
from structlog.types import EventDict
-
+#from .models import User
def structlog_add_app_info(
logger: logging.Logger, method_name: str, event_dict: EventDict
diff --git a/server/vbv_lernwelt/learnpath/__init__.py b/server/vbv_lernwelt/learnpath/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/learnpath/admin.py b/server/vbv_lernwelt/learnpath/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/server/vbv_lernwelt/learnpath/apps.py b/server/vbv_lernwelt/learnpath/apps.py
new file mode 100644
index 00000000..384ba36a
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/apps.py
@@ -0,0 +1,13 @@
+from django.apps import AppConfig
+
+
+class LearnpathConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'vbv_lernwelt.learnpath'
+
+ def ready(self):
+ try:
+ # pylint: disable=unused-import,import-outside-toplevel
+ import vbv_lernwelt.learnpath.signals # noqa F401
+ except ImportError:
+ pass
diff --git a/server/vbv_lernwelt/learnpath/management/__init__.py b/server/vbv_lernwelt/learnpath/management/__init__.py
new file mode 100644
index 00000000..536f0e29
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/management/__init__.py
@@ -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
diff --git a/server/vbv_lernwelt/learnpath/management/commands/__init__.py b/server/vbv_lernwelt/learnpath/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/learnpath/management/commands/create_default_learningpath.py b/server/vbv_lernwelt/learnpath/management/commands/create_default_learningpath.py
new file mode 100644
index 00000000..5faf3e59
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/management/commands/create_default_learningpath.py
@@ -0,0 +1,11 @@
+# pylint: disable=import-outside-toplevel
+
+import djclick as click
+from django.contrib.auth import get_user_model
+
+from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path
+
+
+@click.command()
+def command():
+ create_default_learning_path()
diff --git a/server/vbv_lernwelt/learnpath/management/commands/delete_default_learningpath.py b/server/vbv_lernwelt/learnpath/management/commands/delete_default_learningpath.py
new file mode 100644
index 00000000..2da68a66
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/management/commands/delete_default_learningpath.py
@@ -0,0 +1,14 @@
+# pylint: disable=import-outside-toplevel
+
+import djclick as click
+from django.contrib.auth import get_user_model
+
+from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path, \
+ delete_default_learning_path
+
+import djclick as click
+
+
+@click.command()
+def command():
+ delete_default_learning_path()
diff --git a/server/vbv_lernwelt/learnpath/migrations/0001_initial.py b/server/vbv_lernwelt/learnpath/migrations/0001_initial.py
new file mode 100644
index 00000000..6b3a574a
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0001_initial.py
@@ -0,0 +1,124 @@
+# Generated by Django 3.2.12 on 2022-05-04 15:52
+
+from django.db import migrations, models
+import django.db.models.deletion
+import modelcluster.fields
+import wagtail.core.blocks
+import wagtail.core.fields
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('wagtailcore', '0066_collection_management_permissions'),
+ ]
+
+ 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=[
+ ('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='LearningPath',
+ 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='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)),
+ ('category', models.CharField(choices=[('INCIRCLE', 'In Circle'), ('START', 'Start'), ('END', 'End')], default='INCIRCLE', max_length=16)),
+ ('circle', modelcluster.fields.ParentalKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='learning_sequences', to='learnpath.circle')),
+ ],
+ options={
+ 'verbose_name': 'Learning Sequence',
+ },
+ ),
+ migrations.CreateModel(
+ name='Topic',
+ 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.TextField(default='')),
+ ('is_visible', models.BooleanField(default=True)),
+ ('learning_path', modelcluster.fields.ParentalKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topics', to='learnpath.learningpath')),
+ ],
+ options={
+ '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.CASCADE, related_name='learning_units', to='learnpath.learningsequence')),
+ ],
+ options={
+ 'verbose_name': 'Learning Unit',
+ },
+ bases=('wagtailcore.page', models.Model),
+ ),
+ migrations.CreateModel(
+ name='FullfillmentCriteria',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('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'),
+ ),
+ 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'),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/migrations/0002_fullfillmentcriteria_sort_order.py b/server/vbv_lernwelt/learnpath/migrations/0002_fullfillmentcriteria_sort_order.py
new file mode 100644
index 00000000..5fe74a62
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0002_fullfillmentcriteria_sort_order.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.12 on 2022-05-04 16:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('learnpath', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='fullfillmentcriteria',
+ name='sort_order',
+ field=models.IntegerField(blank=True, editable=False, null=True),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/migrations/0003_auto_20220512_1456.py b/server/vbv_lernwelt/learnpath/migrations/0003_auto_20220512_1456.py
new file mode 100644
index 00000000..6e3dd602
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0003_auto_20220512_1456.py
@@ -0,0 +1,36 @@
+# Generated by Django 3.2.12 on 2022-05-12 12:56
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('learnpath', '0002_fullfillmentcriteria_sort_order'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='learningunit',
+ name='learning_sequence',
+ ),
+ migrations.CreateModel(
+ name='LearningPackage',
+ 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)),
+ ('learning_sequence', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='learning_packages', to='learnpath.learningsequence')),
+ ],
+ options={
+ 'ordering': ['sort_order'],
+ 'abstract': False,
+ },
+ ),
+ migrations.AddField(
+ model_name='learningunit',
+ name='learning_package',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='learning_units', to='learnpath.learningpackage'),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/migrations/0004_alter_learningpackage_learning_sequence.py b/server/vbv_lernwelt/learnpath/migrations/0004_alter_learningpackage_learning_sequence.py
new file mode 100644
index 00000000..fd4b51c7
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0004_alter_learningpackage_learning_sequence.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.12 on 2022-05-12 12:56
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('learnpath', '0003_auto_20220512_1456'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='learningpackage',
+ name='learning_sequence',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='learning_packages', to='learnpath.learningsequence'),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/migrations/0005_alter_learningunit_learning_package.py b/server/vbv_lernwelt/learnpath/migrations/0005_alter_learningunit_learning_package.py
new file mode 100644
index 00000000..6a1128a7
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/migrations/0005_alter_learningunit_learning_package.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.12 on 2022-05-12 12:56
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('learnpath', '0004_alter_learningpackage_learning_sequence'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='learningunit',
+ name='learning_package',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='learning_units', to='learnpath.learningpackage'),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/learnpath/migrations/__init__.py b/server/vbv_lernwelt/learnpath/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py
new file mode 100644
index 00000000..179f72c2
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/models.py
@@ -0,0 +1,264 @@
+# Create your models here.
+
+from django.utils.text import slugify
+from wagtail.core.blocks import StreamBlock
+from wagtail.core.fields import StreamField
+from wagtail.core.models import Page, Orderable
+
+from vbv_lernwelt.learnpath.models_competences import *
+from vbv_lernwelt.learnpath.models_learning_unit_content import WebBasedTrainingBlock, VideoBlock
+from grapple.helpers import register_query_field
+import graphene
+
+from grapple.models import (
+ GraphQLString, GraphQLPage,
+ GraphQLStreamfield, GraphQLBoolean, GraphQLInt, GraphQLForeignKey, GraphQLField
+)
+
+
+@register_query_field("learning_path")
+class LearningPath(Page):
+ # PageChooserPanel('related_page', 'demo.PublisherPage'),
+
+ content_panels = Page.content_panels + [
+ InlinePanel('topics', label="Topics"),
+ ]
+
+ subpage_types = ['learnpath.Circle']
+
+ graphql_fields = [
+ GraphQLString("title", required=True),
+ ]
+
+ class Meta:
+ verbose_name = "Learning Path"
+
+ def __str__(self):
+ return f"{self.title}"
+
+
+class Topic(Orderable):
+ title = models.TextField(default='')
+ is_visible = models.BooleanField(default=True)
+
+ learning_path = ParentalKey('learnpath.LearningPath',
+ null=True,
+ blank=True,
+ on_delete=models.CASCADE,
+ related_name='topics',
+ )
+
+ panels = [FieldPanel('title'),
+ FieldPanel('is_visible'),
+ ]
+
+ graphql_fields = [
+ GraphQLString("title"),
+ GraphQLBoolean("is_visible"),
+ ]
+
+ # content_panels = Page.content_panels + [
+ # FieldPanel('is_visible', classname="full"),
+ # PageChooserPanel('learning_path', 'learnpath.LearningPath'),
+ # ]
+
+ # parent_page_types = ['learnpath.LearningPath']
+ # subpage_types = ['learnpath.Circle']
+ def full_clean(self, *args, **kwargs):
+ self.slug = find_available_slug(Topic, slugify(self.title, allow_unicode=True))
+ super(Topic, self).full_clean(*args, **kwargs)
+
+ class Meta:
+ verbose_name = "Topic"
+
+ def __str__(self):
+ return f"{self.title}"
+
+
+class Circle(Page, Orderable):
+ description = models.TextField(default="", blank=True)
+ goals = models.TextField(default="", blank=True)
+ topic = models.ForeignKey(
+ 'learnpath.Topic',
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name='circles'
+ )
+
+ parent_page_types = ['learnpath.Learningpath']
+ subpage_types = ['learnpath.LearningUnit']
+
+ content_panels = Page.content_panels + [
+ FieldPanel('description'),
+ FieldPanel('topic'),
+ FieldPanel('goals'),
+ InlinePanel('learning_sequences', label="Learning Sequences"),
+ ]
+ #
+ graphql_fields = [
+ GraphQLString("title", required=True),
+ GraphQLString("description"),
+ GraphQLString("goals"),
+ ]
+
+
+ def full_clean(self, *args, **kwargs):
+ self.slug = find_available_slug(Circle, slugify(self.title, allow_unicode=True))
+ super(Circle, self).full_clean(*args, **kwargs)
+
+ class Meta:
+ verbose_name = "Circle"
+
+ def __str__(self):
+ return f"{self.title}"
+
+
+IN_CIRCLE = 'INCIRCLE'
+START = 'START'
+END = 'END'
+
+LEARNING_SEQUENCE_CATEGORIES = [
+ (IN_CIRCLE, 'In Circle'),
+ (START, 'Start'),
+ (END, 'End')
+]
+
+
+class LearningSequence(Orderable):
+ # TODO: How to do a icon choice field?
+ title = models.CharField(max_length=256, default='')
+ category = models.CharField(max_length=16, choices=LEARNING_SEQUENCE_CATEGORIES, default=IN_CIRCLE)
+
+ circle = ParentalKey(
+ 'learnpath.Circle',
+ null=True,
+ blank=True,
+ on_delete=models.CASCADE,
+ related_name='learning_sequences',
+ )
+
+ panels = [FieldPanel('title'), FieldPanel('category'), FieldPanel('circle')]
+
+
+
+ graphql_fields = [
+ GraphQLString("title", required=True),
+ GraphQLBoolean("category"),
+ ]
+
+
+ class Meta:
+ verbose_name = "Learning Sequence"
+
+ def __str__(self):
+ return f"{self.title}"
+
+ def full_clean(self, *args, **kwargs):
+ self.slug = find_available_slug(LearningSequence, slugify(self.title, allow_unicode=True))
+ super(LearningSequence, self).full_clean(*args, **kwargs)
+
+
+
+class LearningPackage(Orderable):
+ title = models.CharField(max_length=256, default='')
+
+ learning_sequence = models.ForeignKey(
+ 'learnpath.LearningSequence',
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name='learning_packages',
+ )
+ panels = [FieldPanel('title')]
+
+ graphql_fields = [
+ GraphQLString("title", required=False),
+ ]
+
+
+ def full_clean(self, *args, **kwargs):
+ self.slug = find_available_slug(LearningPackage, slugify(self.title, allow_unicode=True))
+ super(LearningPackage, self).full_clean(*args, **kwargs)
+
+
+class LearningUnit(Page, Orderable):
+ """
+ This is a group of contents, with the übung and test it is one unit. ... more of a structural charactacter.
+
+ """
+ # TODO: Review model architecture, is the stream field the right thing here?
+ parent_page_types = ['learnpath.Circle']
+ learning_package = models.ForeignKey(
+ 'learnpath.LearningPackage',
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name='learning_units',
+ )
+
+ content_blocks = [
+ ('web_based_training', WebBasedTrainingBlock()),
+ ('video', VideoBlock()),
+ ]
+
+ contents = StreamField(StreamBlock(content_blocks),
+ null=True, blank=True, min_num=1, max_num=1)
+
+ content_panels = [
+ FieldPanel('title', classname="full title"),
+ FieldPanel('learning_package'),
+ StreamFieldPanel('contents'),
+ ]
+
+ graphql_fields = [
+ GraphQLString("title", required=True),
+ GraphQLStreamfield('contents')
+ ]
+
+ subpage_types = []
+
+ class Meta:
+ verbose_name = "Learning Unit"
+
+ def full_clean(self, *args, **kwargs):
+ self.slug = find_available_slug(LearningUnit, slugify(self.title, allow_unicode=True))
+ super(LearningUnit, self).full_clean(*args, **kwargs)
+
+ def __str__(self):
+ return f"{self.title}"
+
+
+
+def find_available_slug(model, 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.
+ """
+
+ # TODO: In comparison ot wagtails own function, I look for the same model instead of the parent
+ pages = model.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
diff --git a/server/vbv_lernwelt/learnpath/models_competences.py b/server/vbv_lernwelt/learnpath/models_competences.py
new file mode 100644
index 00000000..f7d70107
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/models_competences.py
@@ -0,0 +1,66 @@
+from django.db import models
+from wagtail.core.models import Page, Orderable
+from modelcluster.fields import ParentalKey
+from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel, InlinePanel
+
+
+class CompetencePage(Page):
+ """This is the page where the competences and Fullfillment criterias are manged
+ For one Learning Path"""
+
+
+ content_panels = Page.content_panels + [
+ InlinePanel('competences', label="Competences"),
+ ]
+
+ subpage_types = ['learnpath.Circle']
+
+ parent_page_types = ['learnpath.LearningPath']
+
+
+ class Meta:
+ verbose_name = "Learning Path"
+
+ def __str__(self):
+ return f"{self.title}"
+
+
+class Competence(Orderable):
+ """ In VBV Terms this is a "Handlungskompetenz"""
+ category_short = models.CharField(max_length=3, default='')
+ name = models.CharField(max_length=2048)
+
+
+ competence_page = ParentalKey('learnpath.CompetencePage',
+ null=True,
+ blank=True,
+ on_delete=models.CASCADE,
+ related_name='competences',
+ )
+
+
+
+ def get_short_info(self):
+ return f"{self.category_short}{self.sort_order}"
+
+ def __str__(self):
+ return f"{self.get_short_info()}: {self.name}"
+
+
+ class Meta:
+ verbose_name = "Competence"
+
+
+class FullfillmentCriteria(Orderable):
+ """ VBV Term Leistungskriterium"""
+ name = models.CharField(max_length=2048)
+ competence = models.ForeignKey(Competence, on_delete=models.CASCADE, null=True)
+
+ def get_short_info(self):
+ return f"{self.competence.get_short_info()}.{self.sort_order}"
+
+ def __str__(self):
+ return f"{self.get_short_info()}: {self.name}"
+
+ class Meta:
+ verbose_name = "Fullfillment Criteria"
diff --git a/server/vbv_lernwelt/learnpath/models_learning_unit_content.py b/server/vbv_lernwelt/learnpath/models_learning_unit_content.py
new file mode 100644
index 00000000..9a1277e7
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/models_learning_unit_content.py
@@ -0,0 +1,41 @@
+from django.db import models
+from wagtail.core import blocks
+
+
+# 'video_block'
+class VideoBlock(blocks.StructBlock):
+ # TODO: Possible video Types for the user, upload file, add URL
+ title = models.CharField(max_length=128, default="")
+ description = models.TextField(default="")
+ url = blocks.URLBlock()
+
+ class Meta:
+ icon = 'media'
+
+
+
+# 'Web based training Block'
+class WebBasedTrainingBlock(blocks.StructBlock):
+ RISE = 'rise'
+
+ WBT_TYPE_CHOICES = (
+ (RISE, 'Rise'),
+ )
+
+ url = blocks.URLBlock()
+ type = models.CharField(
+ max_length=100,
+ choices=WBT_TYPE_CHOICES,
+ default=RISE
+ )
+
+ class Meta:
+ icon = 'media'
+
+# 'Transver Task'
+class TranverTaskBlock(blocks.StructBlock):
+ title = models.CharField(max_length=128, default="")
+ description = models.TextField(default="")
+
+ class Meta:
+ icon = 'media'
diff --git a/server/vbv_lernwelt/learnpath/tests/__init__.py b/server/vbv_lernwelt/learnpath/tests/__init__.py
new file mode 100644
index 00000000..33212a11
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/__init__.py
@@ -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-29
+# @author: lorenz.padberg@iterativ.ch
diff --git a/server/vbv_lernwelt/learnpath/tests/competences.json b/server/vbv_lernwelt/learnpath/tests/competences.json
new file mode 100644
index 00000000..b50e04e5
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/competences.json
@@ -0,0 +1,78 @@
+{
+ "competences": [
+ {
+ "name": "Weiterempfehlung für Neukunden generieren",
+ "category_short": "A",
+ "fullfillment_criteria": [
+ {
+ "name": "bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden"
+ },
+ {
+ "name": "geeignete Personen wie z.B. Garagisten, Architekten, Treuhänder auf die Vermittlung/Zusammenarbeit anzusprechen"
+ },
+ {
+ "name": "verschiedene Datenquellen wie Internet, Telefonbuch, Handelszeitung, Baugesuche etc. gezielt für die Gewinnung von Neukunden zu benützen"
+ },
+ {
+ "name": "ein beliebiges Gespräch resp. einen bestehenden Kontakt in die Richtung «Versicherung» zu lenken"
+ },
+ {
+ "name": "das Thema Risiko und Sicherheit in einem Gespräch gezielt und auf die Situation des jeweiligen Gesprächspartners bezogen einfliessen zu lassen"
+ },
+ {
+ "name": "im täglichen Kontakt potentielle Kundinnen und Kunden zu erkennen"
+ }
+ ]
+ },
+ {
+ "name": "Kundengespräche vereinbaren",
+ "category_short": "A",
+ "fullfillment_criteria": [
+ {
+ "name": "je nach (Neu-) Kunde Form und Ort für das Gespräch festzulegen"
+ },
+ {
+ "name": "sich intern und extern die nötigen Informationen über den (Neu-) Kunde zu beschaffen"
+ },
+ {
+ "name": "die Terminierung auf ein bestimmtes Thema wie z.B. Rechtsschutz, Vorsorge, Krankenversicherung etc. auszurichten"
+ },
+ {
+ "name": "für das zu führende Gespräch eine Agenda zu erstellen"
+ },
+ {
+ "name": "für das zu führende Gespräch geeignete Hilfsmittel und Unterlagen zusammenzustellen"
+ }, {
+ "name": "eine Kaltakquise durchzuführen und auf mögliche Einwände reagieren zu können"
+ }
+]
+
+ },
+ {
+ "name": "Auftritt in den sozialen Medien zeitgemäss halten",
+ "category_short": "A",
+ "fullfillment_criteria": [ {
+ "name": "in Zusammenarbeit mit den IT-Spezialisten und der Marketingabteilung die Inhalte für den zu realisierenden Medienauftritt zielgruppengerecht festzulegen"
+ },
+ {
+ "name": "für die verschiedenen Kundensegmente die passenden sozialen Medien zu definieren"
+ },
+ {
+ "name": "die Inhalte compliant zu halten"
+ }
+ ]
+ },
+ {
+ "name": "Kundendaten erfassen",
+ "category_short": "A",
+ "fullfillment_criteria": []
+ },
+ {
+ "name": "Wünsche, Ziele und Bedürfnisse der Kunden im Gespräch ermitteln",
+ "category_short": "B",
+ "fullfillment_criteria": []
+ }
+ ]
+}
+
+
diff --git a/server/vbv_lernwelt/learnpath/tests/competences_factories.py b/server/vbv_lernwelt/learnpath/tests/competences_factories.py
new file mode 100644
index 00000000..6a043add
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/competences_factories.py
@@ -0,0 +1,30 @@
+import factory
+import wagtail_factories
+
+from vbv_lernwelt.learnpath.models_competences import Competence, FullfillmentCriteria, CompetencePage
+from vbv_lernwelt.learnpath.tests.learningpath_factories import LearningPathFactory
+
+
+class CompetencePageFactory(wagtail_factories.PageFactory):
+ # learning_path = factory.SubFactory(LearningPathFactory)
+
+ class Meta:
+ model = CompetencePage
+
+
+class CompetenceFactory(factory.django.DjangoModelFactory):
+ category_short = 'A'
+ name = "Weiterempfehung für neukunden generieren"
+ competence_page = factory.SubFactory(CompetencePageFactory)
+
+ class Meta:
+ model = Competence
+
+
+class FullfilmentCriteriaFactory(factory.django.DjangoModelFactory):
+ name = 'Bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden'
+ competence = factory.SubFactory(CompetenceFactory)
+
+ class Meta:
+ model = FullfillmentCriteria
+
diff --git a/server/vbv_lernwelt/learnpath/tests/create_default_competences.py b/server/vbv_lernwelt/learnpath/tests/create_default_competences.py
new file mode 100644
index 00000000..9a8716ae
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/create_default_competences.py
@@ -0,0 +1,23 @@
+import json
+
+
+import os.path
+
+from vbv_lernwelt.learnpath.tests.competences_factories import CompetenceFactory, FullfilmentCriteriaFactory
+
+competences_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'competences.json')
+
+
+def create_default_competences(competences_json=competences_file):
+ with open(competences_json) as f:
+ competences_json = json.load(f)
+
+ for index, compentence in enumerate(competences_json['competences']):
+
+ competence_model = CompetenceFactory(name=compentence['name'], category_short=compentence['category_short'], sort_order=index)
+ print(competence_model)
+
+ for criteria_index, criteria in enumerate(compentence['fullfillment_criteria']):
+ criteria_model = FullfilmentCriteriaFactory(name=criteria['name'], competence=competence_model, sort_order=criteria_index)
+ print(criteria_model)
+
diff --git a/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py b/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py
new file mode 100644
index 00000000..f9e84e86
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/create_default_learning_path.py
@@ -0,0 +1,167 @@
+import wagtail_factories
+from wagtail.core.models import Site
+
+from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningUnit
+from vbv_lernwelt.learnpath.tests.create_default_competences import create_default_competences
+from vbv_lernwelt.learnpath.tests.learningpath_factories import LearningPathFactory, TopicFactory, CircleFactory, \
+ LearningSequenceFactory, LearningUnitFactory, VideoBlockFactory, WebBasedTrainingBlockFactory, LearningPackageFactory
+
+
+def create_default_learning_path():
+ site = Site.objects.filter(is_default_site=True).first()
+
+ if not site:
+ site = wagtail_factories.SiteFactory(is_default_site=True)
+
+ create_default_competences()
+
+ lp = LearningPathFactory(title="Versicherungsvermittler/in", parent=site.root_page)
+
+ tp = TopicFactory(title="Basis", is_visible=False, learning_path=lp)
+
+ circle_1 = CircleFactory(title="Basis", parent=lp, topic=tp, description="""In diesem Circle erklären wir dir, wie der Lehrgang
+ Versicherungsvermittler / in " aufgebaut ist. Zudem vermitteln wir dir die wichtigsten Grundlagen,
+ damit erfolgreich mit deinem Lernpfad starten kannst.""")
+
+ ls_1 = LearningSequenceFactory(title='Einleitung', circle=circle_1)
+
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=ls_1)
+
+ lu_1 = LearningUnitFactory(title="Herzlich Willkommmen", parent=circle_1, learning_package=lpck_1)
+ lu_1 = LearningUnitFactory(title="Herzlich Willkommmen 1", parent=circle_1, learning_package=lpck_1)
+ lu_1 = LearningUnitFactory(title="Herzlich Willkommmen 2", parent=circle_1, learning_package=lpck_1)
+
+ ls_2 = LearningSequenceFactory(title='Grundlagen', circle=circle_1)
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=ls_2)
+
+ lu_1 = LearningUnitFactory(title="Aber jetzt, Butter bei die Fische", parent=circle_1, learning_package=lpck_1)
+
+ tp = TopicFactory(title="Gewinnen von Kunden", learning_path=lp)
+
+ circle_2 = CircleFactory(title="Gewinnen", parent=lp, description="""Versicherungsvermittlerinnen und -vermittler verfügen über
+ein starkes Netzwerk, das sie gezielt pflegen und ausbauen. Sie beraten und betreuen ihre bestehenden Kundinnen und Kunden professionell und gewinnen so ihr Vertrauen. Dadurch schaffen sie die Basis für das Gewinnen
+von neuen Kundinnen und Kunden. Versicherungsvermittlerinnen und -vermittler sprechen ihre bestehenden Kundinnen
+und Kunden auf Weiterempfehlung an. So nutzen sie ihre
+bestehenden Kontakte geschickt für das Anwerben von
+Neukundinnen und -kunden.
+ """, goals="""— Bestehende Kunden so zu beraten, dass
+sie von diesen weiterempfohlen werden
+— Geeignete Personen wie z.B. Garagisten, Architekten, Treuhänder auf die
+Vermittlung/Zusammenarbeit anzusprechen
+— Verschiedene Datenquellen wie Internet, Telefonbuch, Handelszeitung, Baugesuche etc. Gezielt für die Gewinnung
+von Neukunden zu benützen
+— Ein beliebiges Gespräch resp. Einen bestehenden Kontakt in die Richtung
+«Versicherung» zu lenken
+— Das Thema Risiko und Sicherheit in einem Gespräch gezielt und auf die Situation des jeweiligen Gesprächspartners bezogen einfliessen zu lassen
+— Im täglichen Kontakt potenzielle Kundinnen und Kunden zu erkennen""")
+
+ tp = TopicFactory(title="Beraten der Kunden", learning_path=lp)
+ circle_3 = CircleFactory(title="Einstieg", parent=lp, topic=tp)
+ circle_4 = CircleFactory(title="Analyse", parent=lp, topic=tp,
+ description="""Nach dem Gespräch werten sie die Analyse aus und erstellen mit den
+ zur Verfügung stehenden Systemen formal korrekte Lösungsvorschläge bzw.
+ Ausschreibungen. Je nach Komplexität der Situation ziehen sie die nötigen
+ Fachspezialisten bei.""",
+ goals="""
+ — Aus dem IST-Zustand (aus der durchgeführten Analyse) individuelle, risikogewichtete und finanzierbare Lösungsvorschläge zu erarbeiten
+ — Eine Unterversicherung, eine Doppeloder Überversicherung oder einen fehlenden Versicherungsschutz festzustellen
+ — Mögliches Optimierungspotential unter Berücksichtigung der finanziellen
+ Situation des Kunden zu erkennen
+ — Lösungsvorschläge zu skizzieren und
+ zu visualisieren""")
+
+ sequence_1 = LearningSequenceFactory(title="Starten", circle=circle_4)
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=sequence_1)
+
+ learning_unit = LearningUnitFactory(title='Einleitung Circle "Anlayse"', parent=circle_4, learning_package=lpck_1)
+ learning_unit = LearningUnitFactory.create(title='** Einstieg Video"', parent=circle_4, learning_package=lpck_1)
+ 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."
+ video_block = VideoBlockFactory(type="video", url=video_url, title=video_title, description=video_description)
+ learning_unit.contents.append(('video', video_block))
+ learning_unit.save()
+
+ learning_unit = LearningUnitFactory.create(title='** Web Based Training"', parent=circle_4, learning_package=lpck_1)
+ wbt_url = "web_based_trainings/rise_cmi5_test_export/scormcontent/index.html"
+ wbt_block = WebBasedTrainingBlockFactory(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_package=lpck_1)
+
+ sequence_2 = LearningSequenceFactory.create(title="Beobachten", circle=circle_4)
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=sequence_2)
+
+ learning_unit = LearningUnitFactory.create(title="Mein Motorfahrzeug kaufen", parent=circle_4, learning_package=lpck_1)
+ learning_unit = LearningUnitFactory.create(title="Sich selbständig machen", parent=circle_4, learning_package=lpck_1)
+
+ sequence_3 = LearningSequenceFactory.create(title="Anwenden", circle=circle_4)
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=sequence_3)
+
+ learning_unit = LearningUnitFactory.create(title="Nora kauft sich ein neues Auto", parent=circle_4, learning_package=lpck_1)
+ learning_unit = LearningUnitFactory.create(title="Manuel träumt von einem neuen Tesla", parent=circle_4, learning_package=lpck_1)
+
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=sequence_3)
+
+ learning_unit = LearningUnitFactory.create(title="Deine Erkenntnisse und Learnings", parent=circle_4, learning_package=lpck_1)
+
+ sequence_4 = LearningSequenceFactory.create(title="Üben", circle=circle_4)
+
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=sequence_4)
+ learning_unit = LearningUnitFactory.create(title="Ermittlung des Kundenbedarfs", parent=circle_4, learning_package=lpck_1)
+ learning_unit = LearningUnitFactory.create(title="Aktives Zuhören", parent=circle_4, learning_package=lpck_1)
+ learning_unit = LearningUnitFactory.create(title="In Bildern Sprechen", parent=circle_4, learning_package=lpck_1)
+
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=sequence_4)
+ learning_unit = LearningUnitFactory.create(title="Priorisieren des Bedarfs", parent=circle_4, learning_package=lpck_1)
+ learning_unit = LearningUnitFactory.create(title="Zusammenfassung des Bedarfs", parent=circle_4, learning_package=lpck_1)
+
+ sequence_5 = LearningSequenceFactory.create(title="Testen", circle=circle_4)
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=sequence_5)
+
+ learning_unit = LearningUnitFactory.create(title="Bedarfsfragen", parent=circle_4, learning_package=lpck_1)
+ learning_unit = LearningUnitFactory.create(title="Andwendung der Fragetechniken", parent=circle_4, learning_package=lpck_1)
+
+ sequence_5 = LearningSequenceFactory.create(title="Vernetzen", circle=circle_4)
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=sequence_5)
+
+ learning_unit = LearningUnitFactory.create(title="Online Training", parent=circle_4, learning_package=lpck_1)
+
+ sequence_6 = LearningSequenceFactory.create(title="Beenden", circle=circle_4)
+ lpck_1 = LearningPackageFactory(title="Wunderbar !", learning_sequence=sequence_6)
+
+ learning_unit = LearningUnitFactory.create(title="Selbsteinschätzung", parent=circle_4, learning_package=lpck_1)
+
+ circle_5 = CircleFactory.create(title="Lösung",
+ parent=lp,
+ topic=tp,
+ goals="""— Die Daten des Kunden korrekt in die notwendigen Systeme einzutragen
+ — Fachspezialisten beizuziehen, falls dies angezeigt ist
+ — Mit den zur Verfügung stehenden Systemen korrekte Lösungsvorschläge
+ (z.B. Offerten oder Ausschreibungen) zu verfassen
+ — Falls nötig die Lösungsvorschläge dem Underwriting weiterzuleiten und
+ Unklarheiten zu bereinigen """)
+
+ circle_6 = CircleFactory.create(title="Abschluss",
+ parent=lp,
+ topic=tp,
+ goals="""— Je nach Komplexität der Lösungsvorschläge (z.B. Offerten oder Offertvergleich) einen Fachspezialisten aufzubieten
+ — Sich kundenorientiert auf das Gespräch vorzubereiten und sich passend zu präsentieren""")
+
+ tp = TopicFactory.create(title="Betreuen und Ausbauen des Kundenstamms", learning_path=lp)
+ circle_7 = CircleFactory.create(title="Betreuen", parent=lp, topic=tp)
+
+ tp = TopicFactory.create(title="Prüfung", is_visible=False, learning_path=lp)
+ circle_7 = CircleFactory.create(title="Prüfungsvorbereitung", parent=lp, topic=tp)
+
+
+
+
+
+def delete_default_learning_path():
+ LearningUnit.objects.all().delete()
+ LearningSequence.objects.all().delete()
+ Circle.objects.all().delete()
+ Topic.objects.all().delete()
+ LearningPath.objects.all().delete()
diff --git a/server/vbv_lernwelt/learnpath/tests/grapple_test.http b/server/vbv_lernwelt/learnpath/tests/grapple_test.http
new file mode 100644
index 00000000..883b9d03
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/grapple_test.http
@@ -0,0 +1,20 @@
+GET http://localhost:8000/graphql/
+Accept: application/json
+
+###
+{
+ page(id: 8) {
+
+ children {
+ __typename
+ id
+ title
+ children {
+ __typename
+
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/server/vbv_lernwelt/learnpath/tests/grapple_test.txt b/server/vbv_lernwelt/learnpath/tests/grapple_test.txt
new file mode 100644
index 00000000..51917e32
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/grapple_test.txt
@@ -0,0 +1,17 @@
+{
+ page(id: 8) {
+
+ children {
+ __typename
+ id
+ title
+ children {
+ __typename
+
+ id
+ title
+ }
+ }
+ }
+}
+
diff --git a/server/vbv_lernwelt/learnpath/tests/learningpath_factories.py b/server/vbv_lernwelt/learnpath/tests/learningpath_factories.py
new file mode 100644
index 00000000..3e9ff9c8
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/learningpath_factories.py
@@ -0,0 +1,62 @@
+import wagtail_factories
+import factory
+from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningUnit, LearningPackage
+from vbv_lernwelt.learnpath.models_learning_unit_content import VideoBlock, WebBasedTrainingBlock
+
+
+class LearningPathFactory(wagtail_factories.PageFactory):
+ title = "Versicherungsvermittler/in"
+
+ class Meta:
+ model = LearningPath
+
+
+class TopicFactory(factory.django.DjangoModelFactory):
+ title = "Gewinnen von Kunden"
+ is_visible = True
+
+ class Meta:
+ model = Topic
+
+
+class CircleFactory(wagtail_factories.PageFactory):
+ title = "Gewinnen"
+
+ class Meta:
+ model = Circle
+
+
+class LearningSequenceFactory(factory.django.DjangoModelFactory):
+ title = "Grundlagen"
+
+ class Meta:
+ model = LearningSequence
+
+class LearningPackageFactory(factory.django.DjangoModelFactory):
+ title = "Whatever"
+
+ class Meta:
+ model = LearningPackage
+
+
+class LearningUnitFactory(wagtail_factories.PageFactory):
+ title = "Herzlich Willkommen"
+
+ class Meta:
+ model = LearningUnit
+
+
+class VideoBlockFactory(wagtail_factories.StructBlockFactory):
+ title = "Ausbildung ist Pflicht"
+ url = "https://www.vbv.ch/fileadmin/vbv/Videos/Statements_Externe/Janos_M/Testimonial_Janos_Mischler_PositiveEffekte.mp4"
+
+ class Meta:
+ model = VideoBlock
+
+class WebBasedTrainingBlockFactory(wagtail_factories.StructBlockFactory):
+ title = "Beispiel Rise Modul"
+ url = "https://docs.wagtail.org/en/stable/topics/streamfield.html"
+
+ class Meta:
+ model = WebBasedTrainingBlock
+
diff --git a/server/vbv_lernwelt/learnpath/tests/test_competences_factories.py b/server/vbv_lernwelt/learnpath/tests/test_competences_factories.py
new file mode 100644
index 00000000..baf72c70
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/test_competences_factories.py
@@ -0,0 +1,18 @@
+from django.test import TestCase
+
+from vbv_lernwelt.learnpath.models_competences import Competence, FullfillmentCriteria
+from vbv_lernwelt.learnpath.tests.competences_factories import CompetencePageFactory, CompetenceFactory, \
+ FullfilmentCriteriaFactory
+
+
+class TestCompetencesFactories(TestCase):
+ def test_create_competences_page(self):
+ CompetencePageFactory()
+
+ def test_create_competence(self):
+ CompetenceFactory(name='Boogie Woogie')
+ self.assertEqual(Competence.objects.filter(name='Boogie Woogie').count(), 1)
+
+ def test_create_fullfillment_criteria(self):
+ FullfilmentCriteriaFactory(name='shuffle like ...')
+ self.assertEqual(FullfillmentCriteria.objects.filter(name='shuffle like ...').count(), 1)
diff --git a/server/vbv_lernwelt/learnpath/tests/test_create_default_competences.py b/server/vbv_lernwelt/learnpath/tests/test_create_default_competences.py
new file mode 100644
index 00000000..714dc179
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/test_create_default_competences.py
@@ -0,0 +1,13 @@
+from django.conf import settings
+from django.test import TestCase
+from wagtail.core.models import Locale
+
+from vbv_lernwelt.learnpath.models import LearningPath
+from vbv_lernwelt.learnpath.tests.create_default_competences import create_default_competences
+from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path
+
+
+class TestCreateDefaultCompetences(TestCase):
+ def test_create_default_competeneces(self):
+ create_default_competences()
+
diff --git a/server/vbv_lernwelt/learnpath/tests/test_create_default_learning_path.py b/server/vbv_lernwelt/learnpath/tests/test_create_default_learning_path.py
new file mode 100644
index 00000000..bbe0be8f
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/test_create_default_learning_path.py
@@ -0,0 +1,21 @@
+from django.conf import settings
+from django.test import TestCase
+from wagtail.core.models import Locale
+
+from vbv_lernwelt.learnpath.models import LearningPath
+from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path
+
+
+class TestCreateDefaultLearningPaths(TestCase):
+ def setUp(self) -> None:
+ create_locales_for_wagtail()
+
+ def test_create_learning_path(self):
+ create_default_learning_path()
+ qs = LearningPath.objects.filter(title="Versicherungsvermittler/in")
+ self.assertTrue(qs.exists())
+
+
+def create_locales_for_wagtail():
+ for language in settings.WAGTAIL_CONTENT_LANGUAGES:
+ Locale.objects.get_or_create(language_code=language[0])
diff --git a/server/vbv_lernwelt/learnpath/tests/test_factories.py b/server/vbv_lernwelt/learnpath/tests/test_factories.py
new file mode 100644
index 00000000..33212a11
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/tests/test_factories.py
@@ -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-29
+# @author: lorenz.padberg@iterativ.ch
diff --git a/server/vbv_lernwelt/learnpath/views.py b/server/vbv_lernwelt/learnpath/views.py
new file mode 100644
index 00000000..91ea44a2
--- /dev/null
+++ b/server/vbv_lernwelt/learnpath/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/server/vbv_lernwelt/media/web_based_trainings/rise_cmi5_test_export/cmi5.xml b/server/vbv_lernwelt/media/web_based_trainings/rise_cmi5_test_export/cmi5.xml
new file mode 100644
index 00000000..3ef0b6cd
--- /dev/null
+++ b/server/vbv_lernwelt/media/web_based_trainings/rise_cmi5_test_export/cmi5.xml
@@ -0,0 +1,108 @@
+
+