Merge branch 'feature/graphql-api' into develop

# Conflicts:
#	server/config/settings/base.py
#	server/config/urls.py
#	server/example.env
#	server/requirements/requirements-dev.txt
#	server/requirements/requirements.txt
This commit is contained in:
Lorenz Padberg 2022-05-16 11:37:29 +02:00
commit 36ffd8ed6f
98 changed files with 5053 additions and 20 deletions

3
.gitignore vendored
View File

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

View File

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

Binary file not shown.

View File

@ -1,2 +1,3 @@
encrypted: env_secrets/caprover.env
encrypted: env_secrets/production.env
encrypted: env_secrets/local_lorenz.env

45
local-setup-for-tests.sh Executable file
View File

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

View File

@ -10,5 +10,8 @@
},
"devDependencies": {
"cypress": "^9.4.1"
},
"dependencies": {
"tailwindcss": "^3.0.24"
}
}

View File

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

View File

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

View File

@ -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...
# ------------------------------------------------------------------------------

View File

@ -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...
# ------------------------------------------------------------------------------

View File

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

0
server/local.env Normal file
View File

View File

@ -32,3 +32,9 @@ sentry-sdk
structlog
python-json-logger
concurrent-log-handler
wagtail<3
wagtail-factories
wagtail-grapple==0.14.1

View File

@ -0,0 +1,5 @@
python manage.py migrate
python manage.py createcachetable
python manage.py create_default_users
#python manage.py create_default_learingpath

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": []
}
]
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,20 @@
GET http://localhost:8000/graphql/
Accept: application/json
###
{
page(id: 8) {
children {
__typename
id
title
children {
__typename
id
title
}
}
}
}

View File

@ -0,0 +1,17 @@
{
page(id: 8) {
children {
__typename
id
title
children {
__typename
id
title
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8" ?>
<courseStructure xmlns="https://w3id.org/xapi/profiles/cmi5/v1/CourseStructure.xsd">
<course id="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/course">
<title>
<langstring lang="en-US">Export - Testmodul</langstring>
</title>
<description>
<langstring lang="en-US">&lt;strong&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;Herzlich willkommen!&amp;nbsp;&lt;/span&gt;&lt;/strong&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;In dieser Lernsequenz zeigt dir ein Fachexperte anhand von Kundensituationen, wie du erfolgreich den &lt;strong&gt;Kundenbedarf&amp;nbsp;&lt;/strong&gt;&lt;strong&gt;ermitteln&lt;/strong&gt;, &lt;strong&gt;analysieren, priorisieren&amp;nbsp;&lt;/strong&gt;und anschliessend&lt;strong&gt;&amp;nbsp;zusammenfassen&amp;nbsp;&lt;/strong&gt;kannst.&amp;nbsp;&lt;/span&gt;&lt;br&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;Zudem erh&amp;auml;ltst du wertvolle &lt;strong&gt;Werkzeuge&amp;nbsp;&lt;/strong&gt;und&lt;strong&gt;&amp;nbsp;Hilfsmittel&lt;/strong&gt;, die du im Gespr&amp;auml;ch einsetzen kannst.&amp;nbsp;&lt;/span&gt;&lt;br&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;In den einzelnen Lektionen findest du ferner Hinweise und Empfehlungen, welches &lt;strong&gt;Versicherungsfachwissen&amp;nbsp;&lt;/strong&gt;du in diesen&lt;strong&gt;&amp;nbsp;Kundensituationen&lt;/strong&gt; ben&amp;ouml;tigst.&lt;/span&gt;&lt;br&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;Die Bearbeitungszeit dieser Lernsequenz dauert&amp;nbsp;&lt;/span&gt;&lt;span style=&#34;color: rgb(228, 79, 59);&#34;&gt;xy&amp;nbsp;&lt;/span&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;min.&amp;nbsp;&lt;/span&gt;&lt;br&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;Die Lektionen k&amp;ouml;nnen auch einzelnen bearbeitet werden.&lt;/span&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;&lt;strong&gt;Viel Erfolg!&lt;/strong&gt;&lt;/span&gt;&lt;span style=&#34;color: rgb(228, 79, 59);&#34;&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/span&gt;&lt;br&gt;</langstring>
</description>
</course>
<objectives>
<objective id="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/zCsrPmg8-DHqvtr02ko_vaZAHxl9PZaN">
<title>
<langstring lang="en-US">
Einstieg
</langstring>
</title>
<description>
<langstring lang="en-US">
</langstring>
</description>
</objective>
<objective id="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/rqh87qZLmckYDhxDILEC2FK_Jd8WnC2D">
<title>
<langstring lang="en-US">
Bei Kunden auf Besuch
</langstring>
</title>
<description>
<langstring lang="en-US">
</langstring>
</description>
</objective>
<objective id="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/kkW2rZVsRdRxeM7Nb5bVLv-_caw7Lcrz">
<title>
<langstring lang="en-US">
Bedarfsbogen einsetzen
</langstring>
</title>
<description>
<langstring lang="en-US">
</langstring>
</description>
</objective>
<objective id="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/CKY2_VgqHGm8_JQxds-QitJbfh-T8Pe6">
<title>
<langstring lang="en-US">
Bedarfsbogen auswerten
</langstring>
</title>
<description>
<langstring lang="en-US">
</langstring>
</description>
</objective>
<objective id="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/StyvsD3zqY2mzlrtnv3NF_PCXJo6_JOg">
<title>
<langstring lang="en-US">
Deine Erkenntnisse und Learnings
</langstring>
</title>
<description>
<langstring lang="en-US">
</langstring>
</description>
</objective>
<objective id="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/_PsT4xluII9_2SZFjWh-MkdVwqWF-Tj-">
<title>
<langstring lang="en-US">
Quizz - zum Testen
</langstring>
</title>
<description>
<langstring lang="en-US">
Hallo ,dies ist nur ein kurzer Test zum ausprobieren.
</langstring>
</description>
</objective>
</objectives>
<au id="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/activity" launchMethod="OwnWindow" moveOn="Passed" >
<title>
<langstring lang="en">
Export - Testmodul
</langstring>
</title>
<description>
<langstring lang="en">
&lt;strong&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;Herzlich willkommen!&amp;nbsp;&lt;/span&gt;&lt;/strong&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;In dieser Lernsequenz zeigt dir ein Fachexperte anhand von Kundensituationen, wie du erfolgreich den &lt;strong&gt;Kundenbedarf&amp;nbsp;&lt;/strong&gt;&lt;strong&gt;ermitteln&lt;/strong&gt;, &lt;strong&gt;analysieren, priorisieren&amp;nbsp;&lt;/strong&gt;und anschliessend&lt;strong&gt;&amp;nbsp;zusammenfassen&amp;nbsp;&lt;/strong&gt;kannst.&amp;nbsp;&lt;/span&gt;&lt;br&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;Zudem erh&amp;auml;ltst du wertvolle &lt;strong&gt;Werkzeuge&amp;nbsp;&lt;/strong&gt;und&lt;strong&gt;&amp;nbsp;Hilfsmittel&lt;/strong&gt;, die du im Gespr&amp;auml;ch einsetzen kannst.&amp;nbsp;&lt;/span&gt;&lt;br&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;In den einzelnen Lektionen findest du ferner Hinweise und Empfehlungen, welches &lt;strong&gt;Versicherungsfachwissen&amp;nbsp;&lt;/strong&gt;du in diesen&lt;strong&gt;&amp;nbsp;Kundensituationen&lt;/strong&gt; ben&amp;ouml;tigst.&lt;/span&gt;&lt;br&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;Die Bearbeitungszeit dieser Lernsequenz dauert&amp;nbsp;&lt;/span&gt;&lt;span style=&#34;color: rgb(228, 79, 59);&#34;&gt;xy&amp;nbsp;&lt;/span&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;min.&amp;nbsp;&lt;/span&gt;&lt;br&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;Die Lektionen k&amp;ouml;nnen auch einzelnen bearbeitet werden.&lt;/span&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&#34;color: rgb(0, 0, 0);&#34;&gt;&lt;strong&gt;Viel Erfolg!&lt;/strong&gt;&lt;/span&gt;&lt;span style=&#34;color: rgb(228, 79, 59);&#34;&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/span&gt;&lt;br&gt;
</langstring>
</description>
<objectives>
<objective idref="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/zCsrPmg8-DHqvtr02ko_vaZAHxl9PZaN" />
<objective idref="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/rqh87qZLmckYDhxDILEC2FK_Jd8WnC2D" />
<objective idref="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/kkW2rZVsRdRxeM7Nb5bVLv-_caw7Lcrz" />
<objective idref="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/CKY2_VgqHGm8_JQxds-QitJbfh-T8Pe6" />
<objective idref="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/StyvsD3zqY2mzlrtnv3NF_PCXJo6_JOg" />
<objective idref="http://W5gboDeZ-uE5O0B683j1lB9eMrngLG2d_rise/objective/_PsT4xluII9_2SZFjWh-MkdVwqWF-Tj-" />
</objectives>
<url>scormdriver/indexAPI.html</url>
<launchParameters>eyJjcHYiOiJndktDa0RlayIsInJsZCI6ZmFsc2V9</launchParameters>
</au>
</courseStructure>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
function SetBookmark() {
var SD = window.parent,
loc = window.location.href
;
SD.SetBookmark(
loc.substring(loc.toLowerCase().lastIndexOf("/scormcontent/") + 14, loc.length),
document.title
);
SD.CommitData();
}
//Automatically set a bookmark for this page.
SetBookmark();

View File

@ -0,0 +1,11 @@
function SetSCOComplete()
{
var SD = window.parent;
//This is the last page so set it complete
SD.SetReachedEnd();
SD.CommitData();
}
//Automatically set the SCO complete.
SetSCOComplete();

View File

@ -0,0 +1,11 @@
function niExit()
{
var SD = window.parent;
var answer = confirm("Are You Sure You Wish To Exit This Course?");
if(answer)
{
SD.ConcedeControl();
}
}

View File

@ -0,0 +1,46 @@
<html>
<!--/* Copyright © 2003-2013 Rustici Software, LLC All Rights Reserved. www.scorm.com */-->
<head>
<script>
window.document.onkeypress = CheckForDebugCommand;
var intQuestionCounter = 0;
var ASCII_QUESTION = 63;
function CheckForDebugCommand(e) {
var intKeyCode = 0;
if (window.event) {
e = window.event;
intKeyCode = e.keyCode;
} else {
intKeyCode = e.which;
}
if (intKeyCode == ASCII_QUESTION) {
intQuestionCounter++;
if (intQuestionCounter == 3) {
intQuestionCounter = 0;
parent.ShowDebugWindow();
}
} else if (intKeyCode != 0) { //in FireFox, the shift key comes through as a keypress with code of 0...we want to ignore this
intQuestionCounter = 0;
}
}
</script>
</head>
<body>
&nbsp;
<!--
If the course does not load and is stuck on this page:
-Click in the middle of the page
-Press the question mark key (?) three times
-A window should pop up containing debug information, send this information to technical support for further assistance
-If no information appears, try again 1 or 2 more times, sometimes that just does the trick
-->
</body>
</html>

View File

@ -0,0 +1,206 @@
//<!--
// Ultimate client-side JavaScript client sniff. Version 3.03
// (C) Netscape Communications 1999-2001. Permission granted to reuse and distribute.
// Revised 17 May 99 to add is_nav5up and is_ie5up (see below).
// Revised 20 Dec 00 to add is_gecko and change is_nav5up to is_nav6up
// also added support for IE5.5 Opera4&5 HotJava3 AOLTV
// Revised 22 Feb 01 to correct Javascript Detection for IE 5.x, Opera 4,
// correct Opera 5 detection
// add support for winME and win2k
// synch with browser-type-oo.js
// Revised 26 Mar 01 to correct Opera detection
// Revised 02 Oct 01 to add IE6 detection
// Revised 16 Oct 03 to add explict NS 6 detection vs NS 7 - Mike Rustici
// Everything you always wanted to know about your JavaScript client
// but were afraid to ask. Creates "is_" variables indicating:
// (1) browser vendor:
// is_nav, is_ie, is_opera, is_hotjava, is_webtv, is_TVNavigator, is_AOLTV
// (2) browser version number:
// is_major (integer indicating major version number: 2, 3, 4 ...)
// is_minor (float indicating full version number: 2.02, 3.01, 4.04 ...)
// (3) browser vendor AND major version number
// is_nav2, is_nav3, is_nav4, is_nav4up, is_nav6, is_nav6up, is_gecko, is_ie3,
// is_ie4, is_ie4up, is_ie5, is_ie5up, is_ie5_5, is_ie5_5up, is_ie6, is_ie6up, is_hotjava3, is_hotjava3up,
// is_opera2, is_opera3, is_opera4, is_opera5, is_opera5up
// (4) JavaScript version number:
// is_js (float indicating full JavaScript version number: 1, 1.1, 1.2 ...)
// (5) OS platform and version:
// is_win, is_win16, is_win32, is_win31, is_win95, is_winnt, is_win98, is_winme, is_win2k
// is_os2
// is_mac, is_mac68k, is_macppc
// is_unix
// is_sun, is_sun4, is_sun5, is_suni86
// is_irix, is_irix5, is_irix6
// is_hpux, is_hpux9, is_hpux10
// is_aix, is_aix1, is_aix2, is_aix3, is_aix4
// is_linux, is_sco, is_unixware, is_mpras, is_reliant
// is_dec, is_sinix, is_freebsd, is_bsd
// is_vms
//
// See http://www.it97.de/JavaScript/JS_tutorial/bstat/navobj.html and
// http://www.it97.de/JavaScript/JS_tutorial/bstat/Browseraol.html
// for detailed lists of userAgent strings.
//
// Note: you don't want your Nav4 or IE4 code to "turn off" or
// stop working when new versions of browsers are released, so
// in conditional code forks, use is_ie5up ("IE 5.0 or greater")
// is_opera5up ("Opera 5.0 or greater") instead of is_ie5 or is_opera5
// to check version in code which you want to work on future
// versions.
// convert all characters to lowercase to simplify testing
var agt=navigator.userAgent.toLowerCase();
// *** BROWSER VERSION ***
// Note: On IE5, these return 4, so use is_ie5up to detect IE5.
var is_major = parseInt(navigator.appVersion);
var is_minor = parseFloat(navigator.appVersion);
// Note: Opera and WebTV spoof Navigator. We do strict client detection.
// If you want to allow spoofing, take out the tests for opera and webtv.
var is_nav = ((agt.indexOf('mozilla')!=-1) && (agt.indexOf('spoofer')==-1)
&& (agt.indexOf('compatible') == -1) && (agt.indexOf('opera')==-1)
&& (agt.indexOf('webtv')==-1) && (agt.indexOf('hotjava')==-1));
var is_nav2 = (is_nav && (is_major == 2));
var is_nav3 = (is_nav && (is_major == 3));
var is_nav4 = (is_nav && (is_major == 4));
var is_nav4up = (is_nav && (is_major >= 4));
var is_navonly = (is_nav && ((agt.indexOf(";nav") != -1) ||
(agt.indexOf("; nav") != -1)) );
var is_nav6 = (is_nav && (is_major == 5) && (agt.indexOf('rv:0') > -1));
var is_nav6up = (is_nav && (is_major >= 5));
var is_gecko = (agt.indexOf('gecko') != -1);
var is_ie = ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1));
var is_ie3 = (is_ie && (is_major < 4));
var is_ie4 = (is_ie && (is_major == 4) && (agt.indexOf("msie 4")!=-1) );
var is_ie4up = (is_ie && (is_major >= 4));
var is_ie5 = (is_ie && (is_major == 4) && (agt.indexOf("msie 5.0")!=-1) );
var is_ie5_5 = (is_ie && (is_major == 4) && (agt.indexOf("msie 5.5") !=-1));
var is_ie5up = (is_ie && !is_ie3 && !is_ie4);
var is_ie5_5up =(is_ie && !is_ie3 && !is_ie4 && !is_ie5);
var is_ie6 = (is_ie && (is_major == 4) && (agt.indexOf("msie 6.")!=-1) );
var is_ie6up = (is_ie && !is_ie3 && !is_ie4 && !is_ie5 && !is_ie5_5);
// KNOWN BUG: On AOL4, returns false if IE3 is embedded browser
// or if this is the first browser window opened. Thus the
// variables is_aol, is_aol3, and is_aol4 aren't 100% reliable.
var is_aol = (agt.indexOf("aol") != -1);
var is_aol3 = (is_aol && is_ie3);
var is_aol4 = (is_aol && is_ie4);
var is_aol5 = (agt.indexOf("aol 5") != -1);
var is_aol6 = (agt.indexOf("aol 6") != -1);
var is_opera = (agt.indexOf("opera") != -1);
var is_opera2 = (agt.indexOf("opera 2") != -1 || agt.indexOf("opera/2") != -1);
var is_opera3 = (agt.indexOf("opera 3") != -1 || agt.indexOf("opera/3") != -1);
var is_opera4 = (agt.indexOf("opera 4") != -1 || agt.indexOf("opera/4") != -1);
var is_opera5 = (agt.indexOf("opera 5") != -1 || agt.indexOf("opera/5") != -1);
var is_opera5up = (is_opera && !is_opera2 && !is_opera3 && !is_opera4);
var is_webtv = (agt.indexOf("webtv") != -1);
var is_TVNavigator = ((agt.indexOf("navio") != -1) || (agt.indexOf("navio_aoltv") != -1));
var is_AOLTV = is_TVNavigator;
var is_hotjava = (agt.indexOf("hotjava") != -1);
var is_hotjava3 = (is_hotjava && (is_major == 3));
var is_hotjava3up = (is_hotjava && (is_major >= 3));
// *** JAVASCRIPT VERSION CHECK ***
var is_js;
if (is_nav2 || is_ie3) is_js = 1.0;
else if (is_nav3) is_js = 1.1;
else if (is_opera5up) is_js = 1.3;
else if (is_opera) is_js = 1.1;
else if ((is_nav4 && (is_minor <= 4.05)) || is_ie4) is_js = 1.2;
else if ((is_nav4 && (is_minor > 4.05)) || is_ie5) is_js = 1.3;
else if (is_hotjava3up) is_js = 1.4;
else if (is_nav6 || is_gecko) is_js = 1.5;
// NOTE: In the future, update this code when newer versions of JS
// are released. For now, we try to provide some upward compatibility
// so that future versions of Nav and IE will show they are at
// *least* JS 1.x capable. Always check for JS version compatibility
// with > or >=.
else if (is_nav6up) is_js = 1.5;
// NOTE: ie5up on mac is 1.4
else if (is_ie5up) is_js = 1.3
// HACK: no idea for other browsers; always check for JS version with > or >=
else is_js = 0.0;
// *** PLATFORM ***
var is_win = ( (agt.indexOf("win")!=-1) || (agt.indexOf("16bit")!=-1) );
// NOTE: On Opera 3.0, the userAgent string includes "Windows 95/NT4" on all
// Win32, so you can't distinguish between Win95 and WinNT.
var is_win95 = ((agt.indexOf("win95")!=-1) || (agt.indexOf("windows 95")!=-1));
// is this a 16 bit compiled version?
var is_win16 = ((agt.indexOf("win16")!=-1) ||
(agt.indexOf("16bit")!=-1) || (agt.indexOf("windows 3.1")!=-1) ||
(agt.indexOf("windows 16-bit")!=-1) );
var is_win31 = ((agt.indexOf("windows 3.1")!=-1) || (agt.indexOf("win16")!=-1) ||
(agt.indexOf("windows 16-bit")!=-1));
var is_winme = ((agt.indexOf("win 9x 4.90")!=-1));
var is_win2k = ((agt.indexOf("windows nt 5.0")!=-1));
// NOTE: Reliable detection of Win98 may not be possible. It appears that:
// - On Nav 4.x and before you'll get plain "Windows" in userAgent.
// - On Mercury client, the 32-bit version will return "Win98", but
// the 16-bit version running on Win98 will still return "Win95".
var is_win98 = ((agt.indexOf("win98")!=-1) || (agt.indexOf("windows 98")!=-1));
var is_winnt = ((agt.indexOf("winnt")!=-1) || (agt.indexOf("windows nt")!=-1));
var is_win32 = (is_win95 || is_winnt || is_win98 ||
((is_major >= 4) && (navigator.platform == "Win32")) ||
(agt.indexOf("win32")!=-1) || (agt.indexOf("32bit")!=-1));
var is_os2 = ((agt.indexOf("os/2")!=-1) ||
(navigator.appVersion.indexOf("OS/2")!=-1) ||
(agt.indexOf("ibm-webexplorer")!=-1));
var is_mac = (agt.indexOf("mac")!=-1);
// hack ie5 js version for mac
if (is_mac && is_ie5up) is_js = 1.4;
var is_mac68k = (is_mac && ((agt.indexOf("68k")!=-1) ||
(agt.indexOf("68000")!=-1)));
var is_macppc = (is_mac && ((agt.indexOf("ppc")!=-1) ||
(agt.indexOf("powerpc")!=-1)));
var is_sun = (agt.indexOf("sunos")!=-1);
var is_sun4 = (agt.indexOf("sunos 4")!=-1);
var is_sun5 = (agt.indexOf("sunos 5")!=-1);
var is_suni86= (is_sun && (agt.indexOf("i86")!=-1));
var is_irix = (agt.indexOf("irix") !=-1); // SGI
var is_irix5 = (agt.indexOf("irix 5") !=-1);
var is_irix6 = ((agt.indexOf("irix 6") !=-1) || (agt.indexOf("irix6") !=-1));
var is_hpux = (agt.indexOf("hp-ux")!=-1);
var is_hpux9 = (is_hpux && (agt.indexOf("09.")!=-1));
var is_hpux10= (is_hpux && (agt.indexOf("10.")!=-1));
var is_aix = (agt.indexOf("aix") !=-1); // IBM
var is_aix1 = (agt.indexOf("aix 1") !=-1);
var is_aix2 = (agt.indexOf("aix 2") !=-1);
var is_aix3 = (agt.indexOf("aix 3") !=-1);
var is_aix4 = (agt.indexOf("aix 4") !=-1);
var is_linux = (agt.indexOf("inux")!=-1);
var is_sco = (agt.indexOf("sco")!=-1) || (agt.indexOf("unix_sv")!=-1);
var is_unixware = (agt.indexOf("unix_system_v")!=-1);
var is_mpras = (agt.indexOf("ncr")!=-1);
var is_reliant = (agt.indexOf("reliantunix")!=-1);
var is_dec = ((agt.indexOf("dec")!=-1) || (agt.indexOf("osf1")!=-1) ||
(agt.indexOf("dec_alpha")!=-1) || (agt.indexOf("alphaserver")!=-1) ||
(agt.indexOf("ultrix")!=-1) || (agt.indexOf("alphastation")!=-1));
var is_sinix = (agt.indexOf("sinix")!=-1);
var is_freebsd = (agt.indexOf("freebsd")!=-1);
var is_bsd = (agt.indexOf("bsd")!=-1);
var is_unix = ((agt.indexOf("x11")!=-1) || is_sun || is_irix || is_hpux ||
is_sco ||is_unixware || is_mpras || is_reliant ||
is_dec || is_sinix || is_aix || is_linux || is_bsd || is_freebsd);
var is_vms = ((agt.indexOf("vax")!=-1) || (agt.indexOf("openvms")!=-1));
//--> end hide JavaScript

View File

@ -0,0 +1,8 @@
function loadDriverOptions (scope) {
scope.APPID = "WQBM2ARBZR";
scope.CLOUDURL = null;
scope.USE_STRICT_SUSPEND_DATA_LIMITS = false;
scope.FORCED_COMMIT_TIME = 60000;
scope.strLMSStandard = "CMI5";
scope.REVIEW_MODE_IS_READ_ONLY = true;
}

View File

@ -0,0 +1,6 @@
<html><body>
<center>
<br><br><br><br><br>
Thank you for exiting the content. You may now navigate away from this content.
</center>
</body></html>

View File

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html>
<!--/* Copyright © 2003-2013 Rustici Software, LLC All Rights Reserved. www.scorm.com */-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title></title>
<style type="text/css">
body,
html {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
#content {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
#content-frame {
width: 1px;
min-width: 100%;
}
</style>
<!-- Resize Hack -->
<script type="text/javascript">
window.resizeTo(screen.width, screen.height);
</script>
<script language="JavaScript1.2" src="driverOptions.js"></script>
<script language="JavaScript1.2" src="scormdriver.js"></script>
<script language="JavaScript1.2" src="preloadIntegrity.js"></script>
<script language="JavaScript1.2">
strContentLocation = "../scormcontent/index.html#/preview"; //Put the link to the start of the content here.
totaliframesloaded = 0;
function updateIframesLoadedCount() {
totaliframesloaded++;
if (totaliframesloaded == window.frames.length) {
window.Start();
}
}
function LoadContent(){
verifySuspendDataVersion(3);
var isAicc = false;
strTemp = GetQueryStringValue("AICC_URL", document.location.search);
if(strTemp!=null && strTemp!=""){
isAicc = true;
}
if(IsLoaded() || !isAicc)
{
//check for bookmark
var bookmark = GetBookmark();
if (bookmark != "") {
//if there is a bookmark, then go to that page
window.scormdriver_content.document.location.href = "../scormcontent/" + bookmark;
}
else {
//if not, go to the start page
window.scormdriver_content.document.location.href = strContentLocation;
}
}
else
{
setTimeout(LoadContent, 500);
}
}
</script>
</head>
<body>
<div id="content">
<iframe id="content-frame" name="scormdriver_content" width="100%" height="100%" scrolling="no" frameborder="0" src="blank.html" onload="updateIframesLoadedCount()" allowfullscreen></iframe>
<iframe name="AICCComm" src="AICCComm.html" frameborder="0" onload="updateIframesLoadedCount()" style="display: none;"></iframe>
<iframe name="rusticisoftware_aicc_results" src="blank.html" frameborder="0" onload="updateIframesLoadedCount()" style="display: none;"></iframe>
<iframe name="NothingFrame" src="blank.html" frameborder="0" onload="updateIframesLoadedCount()" style="display: none;"></iframe>
</div>
<!-- Set iframes to mimic frames -->
<script type="text/javascript">
window.scormdriver_content = window.frames['scormdriver_content'];
window.AICCComm = window.frames['AICCComm'];
window.rusticisoftware_aicc_results = window.frames['rusticisoftware_aicc_results'];
window.NothingFrame = window.frames['NothingFrame'];
</script>
</body>
</html>

View File

@ -0,0 +1,76 @@
function isTrue(val) {
return val && String(val).toLowerCase() === 'true';
}
function verifySuspendDataVersion(maxAttempts) {
var commitDataAndBook;
var launchData;
var manifestCPV;
var resetFlag;
var sentBookCheck;
var sentDataCheck;
var suspendData = GetDataChunk();
var suspendDataCPV;
if (!suspendData) {
return;
}
if (maxAttempts === 0) {
WriteToDebug('WARNING: ERROR WITH CLEARING SUSPEND OR BOOKMARKING DATA!');
WriteToDebug('NO MORE ATTEMPTS LEFT. COURSE BEHAVIOR MAY BE NEGATIVELY IMPACTED.');
return;
}
try {
launchData = JSON.parse(atob(GetLaunchData()));
} catch (e) {
WriteToDebug('WARNING: Issue with obtaining manifest launch data. Version reset behavior disabled.');
return;
}
try {
suspendData = JSON.parse(suspendData);
} catch (e) {
WriteToDebug('WARNING: Unable to parse suspend data. Version reset behavior disabled.');
return;
}
manifestCPV = launchData.cpv;
resetFlag = launchData.rld;
suspendDataCPV = suspendData.cpv;
if (suspendDataCPV !== manifestCPV) {
WriteToDebug('WARNING: Suspend data version mismatch against manifest version.');
if (resetFlag) {
if (REVIEW_MODE_IS_READ_ONLY && GetLessonMode() === 3) {
WriteToDebug('WARNING: Course is in review mode. Unable to clear suspend and bookmarking data!');
WriteToDebug('Resetting of learner data will not occur. Course behavior may be negatively impacted.');
return;
}
WriteToDebug('Okay to clear data. Now clearing suspend and bookmarking data...');
sentDataCheck = SetDataChunk("");
WriteToDebug('Suspend Data Cleared: ' + sentDataCheck );
sentBookCheck = SetBookmark("");
WriteToDebug('Bookmark Data Cleared: ' + sentBookCheck );
commitDataAndBook = CommitData();
WriteToDebug('Data Commited: ' + commitDataAndBook );
if (isTrue(sentDataCheck) && isTrue(sentBookCheck) && isTrue(commitDataAndBook)) {
WriteToDebug('Verified that all bookmarking and suspend data have been cleared. Data commited successfully.');
} else {
WriteToDebug('WARNING: ERROR WITH CLEARING SUSPEND OR BOOKMARKING DATA! RETRY ATTEMPTS LEFT: ' + (maxAttempts-1));
verifySuspendDataVersion(maxAttempts-1);
}
} else {
WriteToDebug('WARNING: Course reset flag not turned on. Resetting of learner data will not occur.');
WriteToDebug('Course behavior may be negatively impacted.');
}
}
}

View File

@ -15,7 +15,7 @@
<!-- Your stuff: Third-party CSS libraries go here -->
<!-- This file stores project-specific CSS -->
<link href="{% static 'css/project.min.css' %}" rel="stylesheet">
<link href="{% static 'css/tailwind-output.css' %}" rel="stylesheet">
<link href="{% static 'css/output.css' %}" rel="stylesheet">
{% endblock %}
<!-- Le javascript
================================================== -->
@ -27,6 +27,8 @@
<!-- Your stuff: Third-party javascript libraries go here -->
<!-- place project specific Javascript in this file -->
<script defer src="{% static 'js/project.js' %}"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
{% endblock javascript %}

View File

@ -0,0 +1,104 @@
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block body_class %}template-cicle{% endblock %}
{% block content %}
<div class="w-full flex mt-6">
<div class="bg-white flex flex-col w-1/3 m-5">
<div><a href="{% pageurl page.get_parent.get_parent %}">Zurück zum Lernpfad</a></div>
<div class="flex justify-center">
<svg width="500" height="500">
<defs>
<marker id="triangle"
viewBox="0 0 10 10"
refX="5"
refY="5"
markerUnits="strokeWidth"
markerWidth="10" markerHeight="10"
orient="auto" overflow="visible" fill='#99C7E7'>
<path d="M -2 5 L 8 0 L 8 10 z"/>
</marker>
</defs>
</svg>
</div>
<h1 class="font-bold text-5xl mt-6 mb-4">{{ page.title }}</h1>
<div class="mt4">{{ page.description }}</div>
<div class="mt-4">{{ page.goals }}</div>
</div>
<div class="bg-gray-50 flex-col w-2/3">
{% for learning_sequence in page.learning_sequences.all %}
<div class="p-6 max-w-sm mx-auto bg-white shadow-lg m-4">
<h2 class="font-bold">{{ learning_sequence.title }}</h2>
{% for learning_unit in learning_sequence.learning_units.all %}
<div>
<a target="_blank" href="{% pageurl learning_unit %}">{{ learning_unit.title }}</a>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
<script>
var data = [5, 5, 5, 5, 5];
var svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height"),
radius = Math.min(width, height) / 2.5,
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var color = '#99C7E7';
// Generate the pie
var pie = d3.pie();
// Generate the arcs
var arc = d3.arc()
.innerRadius(radius / 2.5)
.padAngle(12 / 360)
.outerRadius(radius);
// Generate the arrows
var arrow = d3.arc()
.innerRadius(radius * 1.15)
.padAngle(30 / 360)
.outerRadius(radius * 1.16);
//Generate groups
var arcs = g.selectAll("arc")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arc")
//Generate groups
var arrows = g.selectAll("arrow")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arrow")
.attr("marker-start", "url(#triangle)")
var markers = g.selectAll("arrow").attr("transform", "translate(60, 60) rotate(30)")
//Draw arc paths
arcs.append("path")
.attr("fill", color)
.attr("d", arc)
//Draw arrow paths
arrows.append("path")
.attr("fill", color)
.attr("d", arrow)
</script>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block body_class %}template-learningpath{% endblock %}
{% block content %}
<h1 class="font-bold text-5xl">{{ page.title }}</h1>
<div class="intro">{{ page.intro|richtext }}</div>
<div class="flex flex-row w-full h-max-80 bg-gray-50 m-4 divide-x-4">
{% for topic in page.topics.all %}
<div class="bg-gray-50-600 m-4">
<h2 class="font-bold">{{ topic.title }}</h2>
<div class="flex flex-row">
{% for circle in topic.circles.all %}
<div class="h-36 w-36 bg-gray-400 m-5">
<h2><a href="{% pageurl circle %}">{{ circle.title }}</a></h2>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% load static %}
{% block body_class %}template-learning_unit{% endblock %}
{% block content %}
<learning_unit>
{% if not page.contents %}
<div> O0ps, in dieser Lerneinheit wurde kein Inhalt erfasst.</div>
{% endif %}
{% for block in page.contents %}
{% if block.block_type == 'video' %}
<h1>{{ block.block_type }}</h1>
<div class="video">
<h1>{{ block.value.title }}</h1>
<div>{{ block.value.description }}</div>
<video width="50%" controls>
<source src={{ block.value.url }} type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
{% endif %}
{% if block.block_type == 'web_based_training' %}
<div> Loading web based training...</div>
<a class="font-bold" href="javascript:open_web_based_training('{{ block.value.url }}');">Click me!</a>
{% endif %}
{% endfor %}
</learning_unit>
<script>
function open_web_based_training(url){
var realUrl = {% get_media_prefix %} + url
console.log("Loading wbt", realUrl)
window.location.href = realUrl;
}
</script>
{% endblock %}