Merge branch 'develop' into feature/responsive-learnpfad

# Conflicts:
#	.dockerignore
This commit is contained in:
Lorenz Padberg 2022-08-10 10:44:57 +02:00
commit 1209a57e87
44 changed files with 875 additions and 270 deletions

View File

@ -13,4 +13,3 @@ venv
.git
.envrc

View File

@ -26,6 +26,8 @@
"@intlify/vite-plugin-vue-i18n": "^3.4.0",
"@rollup/plugin-alias": "^3.1.9",
"@rushstack/eslint-patch": "^1.1.0",
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.4",
"@testing-library/vue": "^6.6.0",
"@types/d3": "^7.4.0",
"@types/jsdom": "^16.2.14",

View File

@ -1,7 +1,11 @@
<template>
<div id="app" class="flex flex-col min-h-screen">
<MainNavigationBar class="flex-none" />
<RouterView class="flex-auto" />
<RouterView class="flex-auto" v-slot="{ Component }">
<Transition mode="out-in" name="app">
<component :is="Component"></component>
</Transition>
</RouterView>
<Footer class="flex-none" />
</div>
</template>
@ -11,6 +15,25 @@ import * as log from 'loglevel';
import MainNavigationBar from '@/components/MainNavigationBar.vue';
import Footer from '@/components/Footer.vue';
import {onMounted} from 'vue';
log.debug('App created');
onMounted(() => {
log.debug('App mounted');
});
</script>
<style lang="postcss" scoped>
.app-enter-active,
.app-leave-active {
transition: opacity 0.3s ease;
}
.app-enter-from,
.app-leave-to {
opacity: 0;
}
</style>

View File

@ -10,7 +10,7 @@ log.debug('Footer created');
class="
px-8
py-4
bg-gray-100
bg-gray-200
border-t border-gray-500
">
@2022 VBV

View File

@ -1,27 +1,29 @@
<script setup>
<script setup lang="ts">
import * as log from 'loglevel';
import { reactive } from 'vue';
import { onMounted, reactive} from 'vue';
import { useUserStore } from '@/stores/user';
import { useRoute } from 'vue-router';
import { useLearningPathStore } from '@/stores/learningPath';
import { useRoute, useRouter } from 'vue-router';
import { useAppStore } from '@/stores/app';
import IconLogout from "@/components/icons/IconLogout.vue";
import IconSettings from "@/components/icons/IconSettings.vue";
import ItDropdown from "@/components/ui/ItDropdown.vue";
import MobileMenu from "@/components/MobileMenu.vue"
log.debug('MainNavigationBar.vue created');
log.debug('MainNavigationBar created');
const route = useRoute()
const router = useRouter()
const userStore = useUserStore();
const appStore = useAppStore();
const learningPathStore = useLearningPathStore();
const state = reactive({showMenu: false});
function toggleNav() {
console.log(state.showMenu);
state.showMenu = !state.showMenu;
}
function calcNavigationMobileOpenClasses() {
return state.showMenu ? ['fixed', 'w-full', 'h-screen'] : [];
}
function menuActive(checkPath) {
return route.path.startsWith(checkPath);
}
@ -30,130 +32,207 @@ function inLearningPath() {
return route.path.startsWith('/learningpath/') || route.path.startsWith('/circle/');
}
function getLearningPathStringProp (prop: 'title' | 'slug'): string {
return inLearningPath() && learningPathStore.learningPath ? learningPathStore.learningPath[prop] : '';
}
function learningPathName (): string {
return getLearningPathStringProp('title')
}
function learninPathSlug (): string {
return getLearningPathStringProp('slug')
}
function backButtonUrl() {
if (route.path.startsWith('/circle/')) {
return '/learningpath/versicherungsvermittlerin';
}
return '/';
}
function handleDropdownSelect(data) {
log.debug('Selected action:', data.action)
switch (data.action) {
case 'settings':
router.push('/profile')
break
case 'logout':
router.push('/logout')
break
default:
console.log('no action')
}
}
onMounted(() => {
log.debug('MainNavigationBar mounted');
})
const profileDropdownData = [
[
{
title: 'Kontoeinstellungen',
icon: IconSettings,
data: {
action: 'settings'
}
}
],
[
{
title: 'Abmelden',
icon: IconLogout,
data: {
action: 'logout'
}
},
]
]
</script>
<template>
<div v-if="appStore.showMainNavigationBar" class="navigation bg-blue-900" :class="calcNavigationMobileOpenClasses()">
<nav
class="
px-8
py-4
mx-auto
lg:flex lg:justify-start lg:items-center
"
>
<div class="flex items-center justify-between">
<router-link
to="/" class="flex">
<it-icon-vbv class="h-8 w-16 -mt-3 -ml-3"/>
<div class="
text-white
text-2xl
pr-10
pl-3
ml-1
border-l border-white
<div>
<Teleport to="body">
<MobileMenu
:user="userStore"
:show="state.showMenu"
:learning-path-slug="learninPathSlug()"
:learning-path-name="learningPathName()"
@closemodal="state.showMenu = false"
/>
</Teleport>
<Transition name="nav">
<div v-if="appStore.showMainNavigationBar" class="navigation bg-blue-900">
<nav
class="
px-8
py-2
mx-auto
lg:flex lg:justify-start lg:items-center lg:py-4
"
>myVBV
</div>
</router-link>
>
<div class="flex items-center justify-between">
<div class="flex items-center">
<a
href="https://www.vbv.ch"
class="flex">
<it-icon-vbv class="h-8 w-16 mr-3 -mt-6 -ml-3"/>
</a>
<router-link
to="/"
class="flex">
<div class="
text-white
text-2xl
pr-10
pl-3
ml-1
border-l border-white
"
>
myVBV
</div>
</router-link>
</div>
<!-- Mobile menu button -->
<div @click="toggleNav" class="flex lg:hidden">
<button
type="button"
<div class="flex items-center lg:hidden">
<router-link
to="/messages"
class="nav-item flex flex-row items-center"
>
<it-icon-message class="w-8 h-8 mr-6"/>
</router-link>
<!-- Mobile menu button -->
<div @click="toggleNav" class="flex">
<button
type="button"
class="
w-8
h-8
text-white
hover:text-sky-500
focus:outline-none focus:text-sky-500
"
>
<it-icon-menu class="h-8 w-8"/>
</button>
</div>
</div>
</div>
<!-- Mobile Menu open: "block", Menu closed: "hidden" -->
<div
v-if="appStore.userLoaded && appStore.routingFinished && userStore.loggedIn "
:class="state.showMenu ? 'flex' : 'hidden'"
class="
w-8
text-white
hover:text-sky-500
focus:outline-none focus:text-sky-500
flex-auto
mt-8
lg:flex lg:space-y-0 lg:flex-row lg:items-center lg:space-x-10 lg:mt-0
"
>
<it-icon-menu class="h-8 w-8"/>
</button>
</div>
</div>
<router-link
v-if="inLearningPath()"
to="/learningpath/versicherungsvermittlerin"
class="nav-item"
:class="{'nav-item--active': menuActive('/learningpath/')}"
>
Lernpfad
</router-link>
<!-- Mobile Menu open: "block", Menu closed: "hidden" -->
<div
:class="state.showMenu ? 'flex' : 'hidden'"
class="
flex-auto
flex-col
mt-8
space-y-8
lg:flex lg:space-y-0 lg:flex-row lg:items-center lg:space-x-10 lg:mt-0
"
>
<router-link
v-if="userStore.loggedIn && !inLearningPath()"
to="/"
class="nav-item"
:class="{'nav-item--active': route.path === '/'}"
>
Cockpit
</router-link>
<router-link
v-if="userStore.loggedIn && !inLearningPath()"
to="/shop"
class="nav-item"
:class="{'nav-item--active': menuActive('/shop')}"
>
Shop
</router-link>
<router-link
v-if="inLearningPath()"
to="/competences/"
class="nav-item"
:class="{'nav-item--active': menuActive('/competences/')}"
>
Kompetenzprofil
</router-link>
<router-link
v-if="userStore.loggedIn && inLearningPath()"
to="/learningpath/versicherungsvermittlerin"
class="nav-item"
:class="{'nav-item--active': menuActive('/learningpath/')}"
>
Lernpfad
</router-link>
<hr class="text-white lg:hidden">
<div class="hidden lg:list-item flex-auto"></div>
<router-link
v-if="userStore.loggedIn"
to="/mediacenter"
class="nav-item"
:class="{'nav-item--active': menuActive('/mediacenter')}"
>
Mediathek
</router-link>
<router-link
v-if="userStore.loggedIn"
to="/messages"
class="nav-item flex flex-row items-center"
>
<it-icon-message class="w-8 h-8 mr-2"/>
</router-link>
<router-link
to="/profile"
class="nav-item flex items-center"
>
<div v-if="userStore.loggedIn">
<div v-if="userStore.avatar_url">
<img class="inline-block h-8 w-8 rounded-full"
:src="userStore.avatar_url"
alt=""/>
</div>
<div v-else>
{{ userStore.getFullName }}
<div class="hidden lg:block flex-auto"></div>
<router-link
to="/shop"
class="nav-item"
:class="{'nav-item--active': menuActive('/shop')}"
>
Shop
</router-link>
<router-link
to="/mediacenter"
class="nav-item"
:class="{'nav-item--active': menuActive('/mediacenter')}"
>
Mediathek
</router-link>
<router-link
to="/messages"
class="nav-item flex flex-row items-center"
>
<it-icon-message class="w-8 h-8 mr-6"/>
</router-link>
<div class="nav-item flex items-center" v-if="userStore.loggedIn">
<ItDropdown
:button-classes="[]"
:list-items="profileDropdownData"
:align="'right'"
@select="handleDropdownSelect"
>
<div v-if="userStore.avatar_url">
<img class="inline-block h-8 w-8 rounded-full"
:src="userStore.avatar_url"
alt=""/>
</div>
<div v-else>
{{ userStore.getFullName }}
</div>
</ItDropdown>
</div>
<div v-else><a class="" href="/login">Login</a></div>
</div>
<div v-else><a class="" href="/login">Login</a></div>
</router-link>
</nav>
</div>
</nav>
</Transition>
</div>
</template>
@ -165,4 +244,16 @@ function backButtonUrl() {
.nav-item--active {
@apply underline underline-offset-[21px] decoration-sky-500 decoration-4
}
.nav-enter-active,
.nav-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.nav-enter-from,
.nav-leave-to {
opacity: 0;
transform: translateY(-80px);
}
</style>

View File

@ -0,0 +1,70 @@
<script setup lang="ts">
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'
import IconLogout from '@/components/icons/IconLogout.vue'
import IconSettings from '@/components/icons/IconSettings.vue'
import {useRouter} from "vue-router";
const router = useRouter()
const props = defineProps<{
show: boolean,
user: object,
learningPathName: string,
learningPathSlug: string
}>()
const emits = defineEmits(['closemodal'])
const clickLink = (to: string) => {
router.push(to)
emits('closemodal')
}
</script>
<template>
<ItFullScreenModal
:show="show"
@closemodal="$emit('closemodal')"
>
<div>
<div>
<div class="flex border-b border-gray-500 -mx-8 px-8 pb-4">
<div v-if="user.avatar_url">
<img class="inline-block h-16 w-16 rounded-full"
:src="user.avatar_url"
alt=""/>
</div>
<div class="ml-6">
<h3>{{user.first_name}} {{user.last_name}}</h3>
<button
@click="clickLink('/profile')"
class="mt-2 inline-block items-center">
<IconSettings class="inline-block" /><span class="ml-3">Kontoeinstellungen</span>
</button>
</div>
</div>
<div>
<div
class="mt-6 pb-6 border-b border-gray-500"
v-if="learningPathName">
<h4 class="text-gray-900 text-sm">Kurs: {{learningPathName}}</h4>
<ul class="mt-6">
<li><button @click="clickLink('/learningpath')">Lernpfad</button></li>
<li class="mt-6">Kompetenzprofil</li>
</ul>
</div>
<div class="mt-6 pb-6 border-b border-gray-500">
<ul>
<li>Shop</li>
<li class="mt-6">Mediathek</li>
</ul>
</div>
<div class="mt-6 items-center">
<IconLogout class="inline-block" /><span class="ml-1">Abmelden</span>
</div>
</div>
</div>
</div>
</ItFullScreenModal>
</template>

View File

@ -5,20 +5,6 @@ import * as _ from 'underscore'
import {useCircleStore} from '@/stores/circle';
import * as log from 'loglevel';
const props = defineProps<{
width: {
default: 500,
type: number,
required: false
},
height: {
default: 500,
type: number,
required: false
},
}>()
const circleStore = useCircleStore();
function someFinished(learningSequence) {
@ -70,12 +56,12 @@ const blue900 = '#00224D',
sky400 = '#72CAFF',
sky500 = '#41B5FA'
function render() {
const width = 450, //props.width,
height = 450, //props.height,
radius: number = Math.min(width, height) / 2.4,
arrowStrokeWidth = 2
const width = 450
const height = 450
const radius = Math.min(width, height) / 2.4
function render() {
const arrowStrokeWidth = 2
const svg = d3.select('.circle-visualization')
.attr('viewBox', `0 0 ${width} ${height}`)
@ -224,6 +210,8 @@ function render() {
<pre hidden>{{ pieData }}</pre>
<pre hidden>{{render()}}</pre>
<svg class="circle-visualization h-full">
<circle v-if="!circleStore.circle" :cx="width / 2" :cy="height / 2" :r="radius" :color="gray300"/>
<circle v-if="!circleStore.circle" :cx="width / 2" :cy="height / 2" :r="radius / 2.5" color="white"/>
</svg>
</div>
</template>

View File

@ -1,22 +1,21 @@
<script setup lang="ts">
import {Circle} from '@/services/circle';
import ItFullScreenModal from '@/components/ui/ItFullScreenModal.vue'
const props = defineProps<{
circle: Circle
circle: Circle,
show: boolean
}>()
const emits = defineEmits(['closemodal'])
</script>
<template>
<div class="circle-overview px-4 py-16 lg:px-16 lg:py-24 relative">
<div
class="w-8 h-8 absolute right-4 top-4 cursor-pointer"
@click="$emit('close')"
>
<it-icon-close></it-icon-close>
</div>
<ItFullScreenModal
:show="show"
@closemodal="$emit('closemodal')"
>
<h1 class="">Überblick: Circle "{{ circle.title }}"</h1>
<p class="mt-8 text-xl">Hier zeigen wir dir, was du in diesem Circle lernen wirst.</p>
@ -45,8 +44,7 @@ const props = defineProps<{
{{jobSituation.value}}
</li>
</ul>
</div>
</ItFullScreenModal>
</template>
<style scoped>

View File

@ -82,5 +82,14 @@ const block = computed(() => {
</template>
<style scoped>
<style lang="scss" scoped>
$header-height: 77px;
$footer-height: 57px;
$content-height: $header-height + $footer-height;
.h-screen {
height: calc(100vh - $content-height);
}
</style>

View File

@ -0,0 +1,6 @@
<template>
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.3292 15.7779C26.3292 15.7625 26.3474 15.7474 26.3536 15.7321C26.3619 15.71 26.3691 15.6877 26.3749 15.6649V15.6222C26.39 15.5448 26.39 15.465 26.3749 15.3876V15.3449C26.3691 15.3221 26.3619 15.2998 26.3536 15.2777C26.3536 15.2623 26.3383 15.2472 26.3292 15.2319C26.32 15.2132 26.3098 15.1949 26.2985 15.1771L26.262 15.1313L26.2315 15.0918L20.9236 9.44226C20.8147 9.31721 20.6597 9.24193 20.494 9.23364C20.3283 9.22556 20.1665 9.2851 20.0457 9.39888C19.9249 9.51244 19.8558 9.67046 19.8541 9.83634C19.8522 10.002 19.9181 10.1615 20.0363 10.2776L24.3779 14.8906H7.47836C7.1417 14.8906 6.86865 15.1634 6.86865 15.5003C6.86865 15.8371 7.14172 16.11 7.47836 16.11H24.3779L20.0363 20.7229C19.8213 20.9698 19.8398 21.3426 20.0782 21.5672C20.3169 21.7916 20.6898 21.7877 20.9236 21.5583L26.2315 15.918L26.2619 15.8784L26.2985 15.8327C26.3098 15.8149 26.32 15.7966 26.3291 15.7779L26.3292 15.7779Z" fill="#0A0A0A"/>
<path d="M6.65855 27H11.5884C12.5588 27 13.4894 26.6146 14.1755 25.9286C14.8616 25.2423 15.247 24.3119 15.247 23.3414V21.2926C15.247 20.9559 14.9741 20.6829 14.6372 20.6829C14.3006 20.6829 14.0275 20.956 14.0275 21.2926V23.3506C14.0275 23.9975 13.7706 24.6179 13.3132 25.0753C12.8558 25.5328 12.2354 25.7897 11.5885 25.7897H6.65861C6.01166 25.7897 5.39134 25.5328 4.93386 25.0753C4.47642 24.6179 4.21952 23.9975 4.21952 23.3506V7.65855C4.21952 7.01161 4.47642 6.39129 4.93386 5.93381C5.39131 5.47636 6.01166 5.21946 6.65861 5.21946H11.5885C12.2354 5.21946 12.8557 5.47637 13.3132 5.93381C13.7706 6.39125 14.0275 7.01161 14.0275 7.65855V9.7074C14.0275 10.0441 14.3006 10.3171 14.6372 10.3171C14.9741 10.3171 15.247 10.044 15.247 9.7074V7.65855C15.247 6.68817 14.8616 5.75774 14.1755 5.07143C13.4894 4.38535 12.5588 4 11.5884 4H6.65855C5.68817 4 4.75774 4.38535 4.07143 5.07143C3.38535 5.75768 3 6.68811 3 7.65855V23.3413C3 24.3117 3.38535 25.2422 4.07143 25.9285C4.75768 26.6145 5.68811 26.9999 6.65855 26.9999V27Z" fill="#0A0A0A"/>
</svg>
</template>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import {reactive} from 'vue'
import {Menu, MenuButton, MenuItems, MenuItem} from '@headlessui/vue'
const props = defineProps<{
buttonClasses: [string],
listItems: [
[object]
],
align: 'left' | 'right'
}>()
const emit = defineEmits<{
(e: 'select', data: object): void
}>()
</script>
<template>
<Menu as="div" class="relative inline-block text-left">
<div>
<MenuButton
:class="buttonClasses"
>
<slot></slot>
</MenuButton>
</div>
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<MenuItems
class="absolute mt-2 px-6 w-56 origin-top-right divide-y divide-gray-500 bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
:class="[align === 'left' ? 'left-0' : 'right-0']"
>
<div class="" v-for="section in listItems" :key="section">
<div class="px-1 py-1" v-for="item in section" :key="item">
<MenuItem>
<button
@click="$emit('select', item.data)"
class="text-black group flex w-full items-center px-0 py-2 text-sm"
>
<span class="inline-block pr-2">
<component
v-if="item.icon"
:is="item.icon"
></component>
</span>
{{item.title}}
</button>
</MenuItem>
</div>
</div>
</MenuItems>
</transition>
</Menu>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
// inspiration https://vuejs.org/examples/#modal
const props = defineProps<{
show: boolean
}>()
const emits = defineEmits(['closemodal'])
</script>
<template>
<Transition mode="in-out">
<div
v-if="show"
class="circle-overview px-4 py-16 lg:px-16 lg:py-24 fixed top-0 overflow-y-scroll bg-white h-full w-full">
<button
type="button"
class="w-8 h-8 absolute right-4 top-4 cursor-pointer"
@click="$emit('closemodal')"
>
<it-icon-close></it-icon-close>
</button>
<slot></slot>
</div>
</Transition>
</template>

View File

@ -1,14 +1,13 @@
import type {NavigationGuardWithThis, RouteLocationNormalized} from 'vue-router';
import {useUserStore} from '@/stores/user';
import type { NavigationGuardWithThis, RouteLocationNormalized } from 'vue-router'
import { useUserStore } from '@/stores/user'
export const updateLoggedIn: NavigationGuardWithThis<undefined> = (_to) => {
const loggedIn = getCookieValue('loginStatus') === 'true'
const userStore = useUserStore()
userStore.$patch({loggedIn});
userStore.$patch({ loggedIn })
if (loggedIn && !userStore.email) {
userStore.fetchUser();
userStore.fetchUser()
}
}

View File

@ -1,7 +1,8 @@
import {createRouter, createWebHistory} from 'vue-router'
import CockpitView from '@/views/CockpitView.vue';
import LoginView from '@/views/LoginView.vue';
import {redirectToLoginIfRequired, updateLoggedIn} from '@/router/guards';
import { createRouter, createWebHistory } from 'vue-router'
import CockpitView from '@/views/CockpitView.vue'
import LoginView from '@/views/LoginView.vue'
import { redirectToLoginIfRequired, updateLoggedIn } from '@/router/guards'
import { useAppStore } from '@/stores/app'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -11,8 +12,8 @@ const router = createRouter({
component: LoginView,
meta: {
// no login required -> so `public === true`
public: true
}
public: true,
},
},
{
path: '/',
@ -38,28 +39,33 @@ const router = createRouter({
{
path: '/learningpath/:learningPathSlug',
component: () => import('../views/LearningPathView.vue'),
props: true
props: true,
},
{
path: '/circle/:circleSlug',
component: () => import('../views/CircleView.vue'),
props: true
props: true,
},
{
path: '/styleguide',
component: () => import('../views/StyelGuideView.vue'),
meta: {
public: true
}
public: true,
},
},
{
path: '/:pathMatch(.*)*',
component: () => import('../views/404View.vue'),
},
]
],
})
router.beforeEach(updateLoggedIn)
router.beforeEach(redirectToLoginIfRequired)
router.afterEach((to, from) => {
const appStore = useAppStore();
appStore.routingFinished = true;
});
export default router

View File

@ -1,13 +1,17 @@
import {defineStore} from 'pinia'
import { defineStore } from 'pinia'
export type AppState = {
showMainNavigationBar: boolean;
userLoaded: boolean
routingFinished: boolean
showMainNavigationBar: boolean
}
export const useAppStore = defineStore({
id: 'app',
state: () => ({
showMainNavigationBar: true,
userLoaded: false,
routingFinished: false,
} as AppState),
getters: {
},

View File

@ -25,7 +25,7 @@ export const useLearningPathStore = defineStore({
return this.learningPath;
}
try {
const learningPathData = await itGet(`/learnpath/api/learningpath/${slug}/`);
const learningPathData = await itGet(`/learnpath/api/page/${slug}/`);
const completionData = await itGet(`/api/completion/learning_path/${learningPathData.translation_key}/`);
this.learningPath = learningPathData;
@ -54,8 +54,6 @@ export const useLearningPathStore = defineStore({
})
this.learningPath.topics.push(topic);
console.log('#######################');
console.log(this.learningPath);
}
return this.learningPath;
} catch (error) {

View File

@ -2,6 +2,7 @@ import * as log from 'loglevel';
import {defineStore} from 'pinia'
import {itGet, itPost} from '@/fetchHelpers';
import {useAppStore} from '@/stores/app';
// typed state https://stackoverflow.com/questions/71012513/when-using-pinia-and-typescript-how-do-you-use-an-action-to-set-the-state
export type UserState = {
@ -46,11 +47,14 @@ export const useUserStore = defineStore({
}
},
fetchUser() {
const appStore = useAppStore();
itGet('/api/core/me/').then((data) => {
this.$state = data;
this.loggedIn = true;
appStore.userLoaded = true;
}).catch(() => {
this.loggedIn = false;
appStore.userLoaded = true;
})
}
}

View File

@ -2,12 +2,12 @@
import * as log from 'loglevel';
import LearningSequence from '@/components/circle/LearningSequence.vue';
import CircleOverview from '@/components/circle/CircleOverview.vue';
import CircleDiagram from '@/components/circle/CircleDiagram.vue';
import LearningContent from '@/components/circle/LearningContent.vue';
import {onMounted} from 'vue'
import {useCircleStore} from '@/stores/circle';
import SelfEvaluation from '@/components/circle/SelfEvaluation.vue';
import CircleDiagram from '@/components/circle/CircleDiagram.vue';
log.debug('CircleView.vue created');
@ -16,34 +16,39 @@ const props = defineProps<{
}>()
const circleStore = useCircleStore();
circleStore.loadCircle(props.circleSlug);
onMounted(async () => {
log.info('CircleView.vue mounted');
await circleStore.loadCircle(props.circleSlug);
});
</script>
<template>
<Transition>
<div v-if="circleStore.page === 'OVERVIEW'">
<CircleOverview :circle="circleStore.circle" @close="circleStore.page = 'INDEX'"/>
</div>
<div v-else-if="circleStore.page === 'LEARNING_CONTENT'">
<div>
<Teleport to="body">
<CircleOverview
:circle="circleStore.circle"
:show="circleStore.page === 'OVERVIEW'"
@closemodal="circleStore.page = 'INDEX'"
/>
</Teleport>
<Transition mode="out-in">
<div v-if="circleStore.page === 'LEARNING_CONTENT'">
<LearningContent :key="circleStore.currentLearningContent.translation_key"/>
</div>
<div v-else-if="circleStore.page === 'SELF_EVALUATION'">
<SelfEvaluation :key="circleStore.currentSelfEvaluation.translation_key"/>
</div>
<div v-else-if="circleStore.circle">
<div v-else>
<div class="circle">
<div class="flex flex-col lg:flex-row">
<div class="flex-initial lg:w-128 px-4 py-4 lg:px-8 lg:py-8">
<h1 class="text-blue-dark text-7xl">
{{ circleStore.circle.title }}
{{ circleStore.circle?.title }}
</h1>
<div v-if="circleStore.circle" class="w-full mt-8">
<div class="w-full mt-8">
<CircleDiagram></CircleDiagram>
</div>
@ -65,7 +70,7 @@ onMounted(async () => {
<div class="block border border-gray-500 mt-8 p-6">
<h3 class="text-blue-dark">Das lernst du in diesem Circle.</h3>
<div class="prose mt-4">
{{ circleStore.circle.description }}
{{ circleStore.circle?.description }}
</div>
<button class="btn-primary mt-4" @click="circleStore.page = 'OVERVIEW'">Erfahre mehr dazu</button>
@ -81,9 +86,9 @@ onMounted(async () => {
</div>
</div>
<div class="flex-auto bg-gray-100 px-4 py-8 lg:px-24">
<div class="flex-auto bg-gray-200 px-4 py-8 lg:px-24">
<div
v-for="learningSequence in circleStore.circle.learningSequences"
v-for="learningSequence in circleStore.circle?.learningSequences || []"
:key="learningSequence.translation_key"
>
<LearningSequence
@ -97,6 +102,7 @@ onMounted(async () => {
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
@ -111,7 +117,4 @@ onMounted(async () => {
opacity: 0;
}
.v-enter-active {
transition-delay: 0.3s;
}
</style>

View File

@ -9,7 +9,7 @@ const userStore = useUserStore();
</script>
<template>
<main class="px-8 py-8 lg:px-12 lg:py-12 bg-gray-100">
<main class="px-8 py-8 lg:px-12 lg:py-12 bg-gray-200">
<h1>Willkommen, {{userStore.first_name}}</h1>
<h2 class="mt-12">Deine Kurse</h2>

View File

@ -26,7 +26,7 @@ onMounted(async () => {
</script>
<template>
<div class="bg-gray-100" v-if="learningPathStore.learningPath">
<div class="bg-gray-200" v-if="learningPathStore.learningPath">
<div class="learningpath flex flex-col">
<div class="flex flex-col h-max">

View File

@ -3,6 +3,9 @@
import {reactive} from 'vue'
import {Listbox, ListboxButton, ListboxOption, ListboxOptions} from '@headlessui/vue'
import ItCheckbox from '@/components/ui/ItCheckbox.vue';
import ItDropdown from "@/components/ui/ItDropdown.vue";
import IconLogout from "@/components/icons/IconLogout.vue"
import IconSettings from "@/components/icons/IconSettings.vue"
const state = reactive({
@ -22,7 +25,35 @@ const state = reactive({
dropdownSelected: {id: 8},
})
const dropdownData = [
[
{
title: 'Option 1',
icon: IconLogout,
data: {}
},
{
title: 'Option 2',
icon: null,
data: {
test: 12
}
}
],
[
{
title: 'Option 3',
icon: IconSettings,
data: {
amount: 34
}
},
]
]
// TODO: die CSS-Klasse für die Farben wird hier in der StyleGuideView.vue generiert.
// deshalb muss man diese CSS-Klassen in tailwind.config.js "safelist"en, wenn diese sonst
// noch nirgendwo verwendet werden.
const colors = ['blue', 'sky', 'orange', 'green', 'red', 'gray',];
const colorValues = [100, 200, 300, 400, 500, 600, 700, 800, 900,];
@ -30,6 +61,10 @@ function colorBgClass(color: string, value: number) {
return `bg-${color}-${value}`;
}
function log(data: any) {
console.log(data);
}
</script>
<template>
@ -272,11 +307,11 @@ function colorBgClass(color: string, value: number) {
<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100"
leave-to-class="opacity-0">
<ListboxOptions
class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 py-1 text-base ring-1 ring-gray-900 ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
<ListboxOption as="template" v-for="person in state.dropdownValues" :key="person.id" :value="person"
v-slot="{ active, selected }">
<li
:class="[active ? 'text-white bg-blue-900' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']">
:class="[active ? 'text-white bg-blue-900' : 'text-black', 'cursor-default select-none relative py-2 pl-3 pr-9']">
<span :class="[state.dropdownSelected ? 'font-semibold' : 'font-normal', 'block truncate']">
{{ person.name }}
</span>
@ -298,6 +333,17 @@ function colorBgClass(color: string, value: number) {
<ItCheckbox disabled class="mt-4">Disabled</ItCheckbox>
<h2 class="mt-8 mb-8">Dropdown</h2>
<div class="h-60">
<ItDropdown
:button-classes="['btn-primary']"
:list-items="dropdownData"
:align="'left'"
@select="log"
>Click Me</ItDropdown>
</div>
</main>
</template>

View File

@ -4,7 +4,8 @@ module.exports = {
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
// '../server/vbv_lernwelt/**/*.{html,js}',
// TODO: wenn man den server-pfad auch angibt wird Tailwind langsamer?! (Startzeit erhöht sich stark...)
// '../server/vbv_lernwelt/**/*.{html,js,py}',
],
theme: {
fontFamily: {
@ -21,8 +22,9 @@ module.exports = {
transparent: 'transparent',
current: 'currentColor',
'white': '#ffffff',
'black': '#0A0A0A',
'blue': {
700: '#1A5197',
700: '#2957A6',
900: '#00224D',
},
'sky': {
@ -31,21 +33,21 @@ module.exports = {
},
'orange': {
500: '#FE955A',
600: '#F37F3E',
600: '#E68B4E',
},
'green': {
500: '#3EDF9C',
600: '#17D29A',
500: '#54CE8B',
600: '#5BB782',
},
'red': {
500: '#DE3618',
500: '#EF7D68',
},
'gray': {
100: '#EDF2F6',
200: '#EDF2F6',
300: '#E0E5EC',
500: '#B1C1CA',
700: '#6F787E',
900: '#0A0A0A',
900: '#585F63',
},
}
},

View File

@ -3,7 +3,7 @@
@tailwind utilities;
html {
@apply text-gray-900
@apply text-black
}
svg {
@ -24,24 +24,37 @@ svg {
}
h2 {
@apply text-3xl xl:text-4xl font-bold
@apply text-2xl md:text-3xl xl:text-4xl font-bold
}
.heading-2 {
@apply text-3xl xl:text-4xl font-bold
@apply text-2xl md:text-3xl xl:text-4xl font-bold
}
h3 {
@apply text-2xl font-bold
@apply text-xl xl:text-2xl font-bold
}
.heading-3 {
@apply text-3xl xl:text-4xl font-bold
@apply text-xl xl:text-2xl font-bold
}
.link {
@apply underline underline-offset-2
}
.link-large {
@apply text-lg underline xl:text-xl
}
.text-large {
@apply text-lg xl:text-xl
}
.text-bold {
@apply text-base font-bold
}
}
@layer components {
@ -59,7 +72,7 @@ svg {
.btn-secondary {
@apply font-bold py-2 px-4 align-middle inline-block
bg-white text-blue-900 border-2 border-blue-900
hover:bg-gray-100
hover:bg-gray-200
disabled:opacity-50 disabled:cursor-not-allowed
}

Binary file not shown.

33
scripts/count_queries.py Normal file
View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
import json
import os
import sys
import django
sys.path.append("../server")
os.environ.setdefault("IT_APP_ENVIRONMENT", "development")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.base")
django.setup()
from wagtail.models import Page
def main():
from django.conf import settings
settings.DEBUG = True
from django.db import connection
from django.db import reset_queries
reset_queries()
page = Page.objects.get(slug='versicherungsvermittlerin', locale__language_code='de-CH')
serializer = page.specific.get_serializer_class()(page.specific)
print(serializer.data)
print(len(json.dumps(serializer.data)))
print(len(connection.queries))
if __name__ == '__main__':
main()

View File

@ -486,32 +486,37 @@ ALLOWED_HOSTS = env.list(
# CACHES
CACHES = {
"default": {
"BACKEND": env(
"IT_DJANGO_CACHE_BACKEND",
default="django.core.cache.backends.db.DatabaseCache",
),
"BACKEND": env("IT_DJANGO_CACHE_BACKEND", default="django.core.cache.backends.db.DatabaseCache"),
"LOCATION": env("IT_DJANGO_CACHE_LOCATION", default="django_cache_table"),
}
},
}
if "django_redis.cache.RedisCache" in env("IT_DJANGO_CACHE_BACKEND", default=""):
CACHES = {
"default": {
"BACKEND": env(
"IT_DJANGO_CACHE_BACKEND",
default="django.core.cache.backends.db.DatabaseCache",
),
"LOCATION": env("IT_DJANGO_CACHE_LOCATION", default="django_cache_table"),
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": env("IT_DJANGO_CACHE_LOCATION"),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
# Mimicing memcache behavior.
# https://github.com/jazzband/django-redis#memcached-exceptions-behavior
"IGNORE_EXCEPTIONS": True,
},
}
},
}
CACHES["learning_path_cache"] = {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "django_cache_learning_path",
}
# OAuth/OpenId Connect
IT_OAUTH_TENANT_ID = env.str("IT_OAUTH_TENANT_ID", default=None)
if IT_OAUTH_TENANT_ID:
IT_OAUTH_AUTHORIZE_PARAMS = {
'tenant_id': IT_OAUTH_TENANT_ID
}
else:
IT_OAUTH_AUTHORIZE_PARAMS = {}
OAUTH = {
"client_name": env("IT_OAUTH_CLIENT_NAME", default="lernetz"),
@ -519,10 +524,11 @@ OAUTH = {
"client_secret": env("IT_OAUTH_CLIENT_SECRET", default=""),
"access_token_url": env("IT_OAUTH_ACCESS_TOKEN_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/token"),
"authorize_url": env("IT_OAUTH_AUTHORIZE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/auth"),
"authorize_params": IT_OAUTH_AUTHORIZE_PARAMS,
"api_base_url": env("IT_OAUTH_API_BASE_URL", default="https://sso.test.b.lernetz.host/auth/realms/vbv/protocol/openid-connect/"),
"local_redirect_uri": env("IT_OAUTH_LOCAL_DIRECT_URI", default="http://localhost:3000/sso/callback/"),
"local_redirect_uri": env("IT_OAUTH_LOCAL_DIRECT_URI", default="http://localhost:8000/sso/callback/"),
"client_kwargs": {
'scope': '',
'scope': env("IT_OAUTH_SCOPE", default=''),
'token_endpoint_auth_method': 'client_secret_post',
'token_placement': 'header',
}

View File

@ -9,12 +9,12 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
default_password = 'test'
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')
_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')
def _create_student_user(email, first_name, last_name, avatar_url=''):
def _create_student_user(email, first_name, last_name, avatar_url='', password=default_password):
student_user, created = _get_or_create_user(
user_model=user_model, username=email, password=default_password,
user_model=user_model, username=email, password=password,
)
student_user.first_name = first_name
student_user.last_name = last_name
@ -73,7 +73,24 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
email='christoph.bosshard@vbv-afa.ch',
first_name='Christoph',
last_name='Bosshard',
avatar_url='/static/avatars/avatar_christoph.jpg'
avatar_url='/static/avatars/avatar_christoph.jpg',
password='myvbv1234'
)
_create_student_user(
email='alexandra.vangelista@lernetz.ch',
first_name='Alexandra',
last_name='Vangelista',
avatar_url='/static/avatars/avatar_alexandra.png',
password='myvbv1234'
)
_create_student_user(
email='chantal.rosenberg@vbv-afa.ch',
first_name='Chantal',
last_name='Rosenberg',
avatar_url='/static/avatars/avatar_chantal.png',
password='myvbv1234'
)

View File

@ -2,6 +2,8 @@ from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models import JSONField
from vbv_lernwelt.core.managers import UserManager
class User(AbstractUser):
"""
@ -13,6 +15,8 @@ class User(AbstractUser):
avatar_url = models.CharField(max_length=254, blank=True, default='')
email = models.EmailField('email address', unique=True)
objects = UserManager()
class SecurityRequestResponseLog(models.Model):
label = models.CharField(max_length=255, blank=True, default="")

View File

@ -29,6 +29,12 @@ class LearningPath(Page):
def __str__(self):
return f"{self.title}"
@classmethod
def get_serializer_class(cls):
return get_it_serializer_class(
cls, ['id', 'title', 'slug', 'type', 'translation_key', 'children']
)
class Topic(Page):
# title = models.TextField(default='')
@ -55,8 +61,9 @@ class Topic(Page):
@classmethod
def get_serializer_class(cls):
return get_it_serializer_class(cls,
field_names=['id', 'title', 'slug', 'type', 'translation_key', 'is_visible', ])
return get_it_serializer_class(
cls, field_names=['id', 'title', 'slug', 'type', 'translation_key', 'is_visible', ]
)
class Meta:
verbose_name = "Topic"
@ -143,6 +150,13 @@ class LearningSequence(Page):
def get_admin_display_title(self):
return f'{self.icon} {self.draft_title}'
def get_admin_display_title_html(self):
return f'''
<span style="display: inline-flex; align-items: center; font-size: 1.25rem; font-weight: 700;">
<{self.icon} style="height: 32px; width: 32px;"></{self.icon}>
<span style="margin-left: 8px;">{self.draft_title}</span>
</span>'''
def full_clean(self, *args, **kwargs):
super(LearningSequence, self).full_clean(*args, **kwargs)
@ -161,6 +175,9 @@ class LearningUnit(Page):
def get_serializer_class(cls):
return get_it_serializer_class(cls, field_names=['id', 'title', 'slug', 'type', 'translation_key', 'children'])
def get_admin_display_title_html(self):
return f'<span style="font-weight: 700; font-size: 20px;">{self.draft_title}</span>'
class LearningUnitQuestion(Page):
parent_page_types = ['learnpath.LearningUnit']
@ -213,6 +230,13 @@ class LearningContent(Page):
return display_title
def get_admin_display_title_html(self):
return f'''
<span style="display: inline-flex; align-items: center;">
<it-icon-checkbox-unchecked style="height: 24px; width: 24px;"></it-icon-checkbox-unchecked>
<span style="margin-left: 8px;">{self.get_admin_display_title()}</span>
</span>'''
class Meta:
verbose_name = "Learning Content"

View File

@ -20,5 +20,20 @@ class ItBaseSerializer(wagtail_serializers.BaseSerializer):
meta_fields = []
def __init__(self, *args, **kwargs):
self.descendants = kwargs.pop('descendants', None)
super().__init__(*args, **kwargs)
def get_children(self, obj):
return [c.specific.get_serializer_class()(c.specific).data for c in obj.get_children()]
if not self.descendants:
self.descendants = [p for p in obj.get_descendants().specific()]
children = _get_children(self.descendants, obj)
return [c.specific.get_serializer_class()(c.specific, descendants=self.descendants).data for c in children]
def _get_descendants(pages, obj):
return [c for c in pages if c.path.startswith(obj.path) and c.depth >= obj.depth]
def _get_children(pages, obj):
return [c for c in pages if c.path.startswith(obj.path) and obj.depth + 1 == c.depth]

View File

@ -1,20 +0,0 @@
from rest_framework import serializers
from vbv_lernwelt.learnpath.models import Circle, LearningPath
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class
class LearningPathSerializer(get_it_serializer_class(LearningPath, [])):
children = serializers.SerializerMethodField()
meta_fields = []
def get_children(self, obj):
return [c.specific.get_serializer_class()(c.specific).data for c in obj.get_children()]
def get_meta_label(self, obj):
return obj._meta.label
class Meta:
model = Circle
fields = ['id', 'title', 'slug', 'type', 'translation_key', 'children']

View File

@ -0,0 +1,16 @@
import structlog
from django.core.cache import caches
from django.db.models.signals import post_delete, post_save
from wagtail.models import Page
logger = structlog.get_logger(__name__)
def invalidate_learning_path_cache(sender, **kwargs):
logger.debug('invalidate learning_path_cache', label='learning_path_cache')
caches['learning_path_cache'].clear()
for subclass in Page.__subclasses__():
post_save.connect(invalidate_learning_path_cache, subclass)
post_delete.connect(invalidate_learning_path_cache, subclass)

View File

@ -1,10 +1,8 @@
from django.urls import path, re_path
from .views import circle_view, generate_web_component_icons
from .views import learningpath_view
from .views import generate_web_component_icons, page_api_view
urlpatterns = [
path(r"api/circle/<slug:slug>/", circle_view, name="circle_view"),
path(r"api/learningpath/<slug:slug>/", learningpath_view, name="learningpath_view"),
path(r"api/page/<slug:slug>/", page_api_view, name="page_api_view"),
re_path(r"icons/$", generate_web_component_icons, name="generate_web_component_icons"),
]

View File

@ -4,18 +4,19 @@ from pathlib import Path
from django.conf import settings
from django.shortcuts import render
from django.views.decorators.cache import cache_page
from rest_framework.decorators import api_view
from rest_framework.response import Response
from wagtail.models import Page
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.learnpath.models import Circle, LearningPath
from vbv_lernwelt.learnpath.serializers import LearningPathSerializer
@api_view(['GET'])
def circle_view(request, slug):
circle = Circle.objects.get(slug=slug)
serializer = Circle.get_serializer_class()(circle)
@cache_page(60 * 60 * 8, cache="learning_path_cache")
def page_api_view(request, slug):
page = Page.objects.get(slug=slug, locale__language_code='de-CH')
serializer = page.specific.get_serializer_class()(page.specific)
return Response(serializer.data)
@ -38,9 +39,3 @@ def generate_web_component_icons(request):
context={'svg_files': svg_files},
content_type="application/javascript"
)
@api_view(['GET'])
def learningpath_view(request, slug):
learning_path = LearningPath.objects.get(slug=slug, locale__language_code='de-CH')
serializer = LearningPathSerializer(learning_path)
return Response(serializer.data)

View File

@ -23,7 +23,7 @@ oauth.register(
access_token_url=settings.OAUTH["access_token_url"],
access_token_params=None,
authorize_url=settings.OAUTH["authorize_url"],
authorize_params=None,
authorize_params=settings.OAUTH["authorize_params"],
api_base_url=settings.OAUTH["api_base_url"],
client_kwargs=settings.OAUTH["client_kwargs"]
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><circle cx="15" cy="15" r="14" fill="#3ede9b"/><g><path d="M11.69,11.04c0,.66-.54,1.2-1.2,1.2s-1.2-.54-1.2-1.2,.54-1.2,1.2-1.2,1.2,.54,1.2,1.2Z" fill="#0a0a0a"/><path d="M20.71,11.04c0,.66-.54,1.2-1.2,1.2s-1.2-.54-1.2-1.2,.54-1.2,1.2-1.2,1.2,.54,1.2,1.2Z" fill="#0a0a0a"/></g><path d="M9.39,17.13c1.57,1.16,3.51,1.86,5.61,1.86s4.04-.69,5.61-1.86" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25"/></svg>
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30 30"><defs><clipPath id="b"><circle cx="15" cy="15" r="14" fill="none"/></clipPath></defs><circle cx="15" cy="15" r="14" fill="#5bb782"/><g clip-path="url(#b)"><ellipse cx="15" cy="13.85" rx="12.85" ry="12.22" fill="#64d996"/></g><g><path d="M11.69,11.04c0,.66-.54,1.2-1.2,1.2s-1.2-.54-1.2-1.2,.54-1.2,1.2-1.2,1.2,.54,1.2,1.2Z" fill="#0a0a0a"/><path d="M20.71,11.04c0,.66-.54,1.2-1.2,1.2s-1.2-.54-1.2-1.2,.54-1.2,1.2-1.2,1.2,.54,1.2,1.2Z" fill="#0a0a0a"/></g><path d="M15,19.61c-2.16,0-4.23-.68-5.98-1.98-.28-.21-.34-.6-.13-.88,.21-.28,.6-.33,.87-.13,1.53,1.13,3.34,1.73,5.24,1.73s3.7-.6,5.24-1.73c.28-.21,.67-.15,.87,.13s.15,.67-.13,.87c-1.75,1.29-3.82,1.98-5.98,1.98Z"/><path d="M15,2c7.17,0,13,5.83,13,13s-5.83,13-13,13S2,22.17,2,15,7.83,2,15,2m0-1C7.27,1,1,7.27,1,15s6.27,14,14,14,14-6.27,14-14S22.73,1,15,1h0Z" fill="#5bb782"/></svg>

Before

Width:  |  Height:  |  Size: 552 B

After

Width:  |  Height:  |  Size: 981 B

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><circle cx="15" cy="15" r="14" fill="#fd945a"/><path d="M11.39,10.4c0,.66-.54,1.2-1.2,1.2s-1.2-.54-1.2-1.2,.54-1.2,1.2-1.2,1.2,.54,1.2,1.2Z" fill="#0a0a0a"/><path d="M19.96,11.48c0,.66-.54,1.2-1.2,1.2s-1.2-.54-1.2-1.2,.54-1.2,1.2-1.2,1.2,.54,1.2,1.2Z" fill="#0a0a0a"/><path d="M4.3,17.41c-.67,.65-.41,1.67-.29,2.48,.16,1.24-.08,2.53-.67,3.63-.71,1.51,.28,3.61,1.46,4.69,1.85,1.6,4.01,1.15,5.99,.61,.48-.13,.99-.29,1.31-.67s.3-1.08-.15-1.29c.67-.12,.83-1.1,.26-1.45,.54-.35,.55-1.24,.02-1.61,1.15-.2,2.43-.57,3.73-.99,2.07-.68,1.2-2.28,.07-2.13-2.57,.34-5.04,.62-7.63,.92-.52,.06-.98-.3-1.08-.81-.2-1.02,0-2.21-.71-3.05-.56-.66-1.68-.92-2.31-.34Z" fill="#fd945a" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25"/><path d="M11.62,16.76l2.35,.33" fill="#fd945a" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25"/></svg>
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30 30"><defs><clipPath id="b"><circle cx="15" cy="15" r="14" fill="none"/></clipPath></defs><circle cx="15" cy="15" r="14" fill="#df7f49"/><g clip-path="url(#b)"><ellipse cx="15" cy="13.85" rx="12.85" ry="12.22" fill="#fe955a"/></g><path d="M15,2c7.17,0,13,5.83,13,13s-5.83,13-13,13S2,22.17,2,15,7.83,2,15,2m0-1C7.27,1,1,7.27,1,15s6.27,14,14,14,14-6.27,14-14S22.73,1,15,1h0Z" fill="#df7f49"/><path d="M11.39,10.4c0,.66-.54,1.2-1.2,1.2s-1.2-.54-1.2-1.2,.54-1.2,1.2-1.2,1.2,.54,1.2,1.2Z" fill="#0a0a0a"/><path d="M19.96,11.48c0,.66-.54,1.2-1.2,1.2s-1.2-.54-1.2-1.2,.54-1.2,1.2-1.2,1.2,.54,1.2,1.2Z" fill="#0a0a0a"/><g><path d="M4.3,17.41c-.67,.65-.41,1.67-.29,2.48,.16,1.24-.08,2.53-.67,3.63-.71,1.51,.28,3.61,1.46,4.69,1.85,1.6,4.01,1.15,5.99,.61,.48-.13,.99-.29,1.31-.67s.3-1.08-.15-1.29c.67-.12,.83-1.1,.26-1.45,.54-.35,.55-1.24,.02-1.61,1.15-.2,2.43-.57,3.73-.99,2.07-.68,1.2-2.28,.07-2.13-2.57,.34-5.04,.62-7.63,.92-.52,.06-.98-.3-1.08-.81-.2-1.02,0-2.21-.71-3.05-.56-.66-1.68-.92-2.31-.34Z" fill="#ffb68c"/><path d="M7.86,29.96c-1.19,0-2.36-.32-3.46-1.27-1.44-1.32-2.44-3.7-1.62-5.43,.54-1.02,.76-2.17,.62-3.29l-.04-.28c-.13-.82-.31-1.94,.52-2.73h0c.38-.36,.89-.53,1.45-.5,.67,.04,1.33,.37,1.76,.88,.62,.74,.69,1.64,.75,2.44,.02,.3,.04,.61,.1,.89,.04,.2,.21,.34,.39,.31l.43-.05c2.43-.28,4.76-.55,7.19-.87,.86-.11,1.67,.43,1.86,1.23,.16,.71-.19,1.64-1.65,2.12-1.13,.37-2.1,.65-2.95,.84,.03,.12,.04,.25,.04,.38,0,.29-.08,.56-.21,.79,.14,.28,.19,.6,.12,.94-.05,.25-.15,.47-.3,.65,.02,.06,.04,.11,.05,.17,.11,.47-.01,.99-.32,1.37-.46,.56-1.16,.76-1.63,.88-1.02,.28-2.07,.52-3.11,.52Zm-3.12-12.09c-.33,.32-.27,.86-.15,1.63l.05,.31c.17,1.38-.09,2.8-.74,4.01-.53,1.13,.26,2.95,1.33,3.93,1.58,1.36,3.46,1,5.41,.47,.41-.11,.8-.23,.99-.47,.09-.1,.09-.29,.05-.34-.25-.12-.38-.37-.35-.64s.25-.48,.52-.53c.06-.01,.08-.12,.09-.13,0-.05,.01-.14-.05-.18-.18-.11-.29-.31-.3-.52s.1-.42,.28-.53c.09-.06,.13-.18,.13-.28,0-.1-.03-.23-.12-.29-.21-.14-.31-.4-.25-.65,.05-.25,.25-.44,.5-.48,.98-.17,2.17-.49,3.64-.97,.62-.2,.86-.48,.82-.65-.03-.15-.23-.29-.47-.26-2.43,.32-4.77,.59-7.21,.87l-.43,.05c-.83,.1-1.61-.48-1.76-1.32-.06-.33-.09-.68-.12-1.03-.05-.7-.09-1.3-.46-1.73-.21-.24-.55-.42-.88-.44-.16,0-.37,.01-.52,.15Z" fill="#df7f49"/></g><g><path d="M11.62,16.76l2.35,.33" fill="#fd945a"/><path d="M13.96,17.71s-.06,0-.09,0l-2.35-.33c-.34-.05-.58-.36-.53-.71s.37-.58,.71-.53l2.35,.33c.34,.05,.58,.36,.53,.71-.04,.31-.31,.54-.62,.54Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -2,13 +2,28 @@
{% load wagtailadmin_tags i18n %}
{% block extra_js %}
{{ block.super }}
{{ block.super }}
<script type="text/javascript">
if (document.location.href.endsWith('/')) {
// add ?ordering=ord as default ordering
console.log('redirect');
document.location.replace(document.location.href + '?ordering=ord');
}
<style>
.title_type_learningcontent {
border-left: 2px solid #41B5FA;
padding-left: 16px !important;
}
.title_type_learningunit {
border-left: 2px solid #41B5FA;
padding-left: 16px !important;
}
</style>
<script defer src="/learnpath/icons/"></script>
<script type="text/javascript">
if (document.location.href.endsWith('/')) {
// add ?ordering=ord as default ordering
console.log('redirect');
document.location.replace(document.location.href + '?ordering=ord');
}
</script>
{% endblock %}

View File

@ -0,0 +1,96 @@
{% load i18n %}
{% load l10n %}
{% load wagtailadmin_tags %}
<table class="listing {% if full_width %}full-width{% endif %} {% block table_classname %}{% endblock %}">
{% if show_ordering_column or show_bulk_actions %}
<col width="10px"/>
{% endif %}
<col/>
{% if show_parent %}
<col/>
{% endif %}
<col width="12%"/>
<col width="12%"/>
<col width="12%"/>
<col width="10%"/>
<thead>
{% block pre_parent_page_headers %}
{% endblock %}
{% if parent_page %}
{% page_permissions parent_page as parent_page_perms %}
<tr class="index {% if not parent_page.live %} unpublished{% endif %}
{% block parent_page_row_classname %}{% endblock %}">
<td class="title"{% if show_ordering_column or show_bulk_actions %} colspan="2"{% endif %}>
{% block parent_page_title %}
{% endblock %}
</td>
<td class="updated" valign="bottom">{% if parent_page.latest_revision_created_at %}
<div class="human-readable-date" title="{{ parent_page.latest_revision_created_at|date:"DATETIME_FORMAT" }}">
{% blocktrans trimmed with time_period=parent_page.latest_revision_created_at|timesince %}{{ time_period }}
ago{% endblocktrans %}</div>{% endif %}</td>
<td class="type" valign="bottom">
{% if not parent_page.is_root %}
{{ parent_page.content_type.model_class.get_verbose_name }}
{% endif %}
</td>
<td class="status" valign="bottom">
{% if not parent_page.is_root %}
{% include "wagtailadmin/shared/page_status_tag.html" with page=parent_page %}
{% endif %}
</td>
<td></td>
</tr>
{% endif %}
{% block post_parent_page_headers %}
{% endblock %}
</thead>
<tbody>
{% if pages %}
{% trans "Select page" as checkbox_aria_label %}
{% for page in pages %}
{% page_permissions page as page_perms %}
<tr {% if ordering == "ord" %}id="page_{{ page.id|unlocalize }}"
data-page-title="{{ page.get_admin_display_title }}"{% endif %}
class="{% if not page.live %}unpublished{% endif %} {% block page_row_classname %}{% endblock %}">
{% if show_ordering_column %}
<td class="ord">{% if orderable and ordering == "ord" %}
<div class="handle icon icon-grip text-replace">{% trans 'Drag' %}</div>{% endif %}</td>
{% elif show_bulk_actions %}
{% include "wagtailadmin/bulk_actions/listing_checkbox_cell.html" with obj_type="page" obj=page aria_labelledby_prefix="page_" aria_labelledby=page.pk|unlocalize aria_labelledby_suffix="_title" %}
{% endif %}
<td id="page_{{ page.pk|unlocalize }}_title" class="title title_type_{{ page.content_type.model }}" valign="top" data-listing-page-title>
{{ page.type }}
{% block page_title %}
{% endblock %}
</td>
{% if show_parent %}
<td class="parent" valign="top">
{% block page_parent_page_title %}
{% with page.get_parent as parent %}
{% if parent %}
<a
href="{% url 'wagtailadmin_explore' parent.id %}">{{ parent.specific_deferred.get_admin_display_title }}</a>
{% endif %}
{% endwith %}
{% endblock %}
</td>
{% endif %}
<td class="updated" valign="top">{% if page.latest_revision_created_at %}
<div class="human-readable-date" title="{{ page.latest_revision_created_at|date:"DATETIME_FORMAT" }}">
{% blocktrans trimmed with time_period=page.latest_revision_created_at|timesince %}{{ time_period }}
ago{% endblocktrans %}</div>{% endif %}</td>
<td class="type" valign="top">{{ page.content_type.model_class.get_verbose_name }}</td>
<td class="status" valign="top">
{% include "wagtailadmin/shared/page_status_tag.html" with page=page %}
</td>
{% block page_navigation %}
{% endblock %}
</tr>
{% endfor %}
{% else %}
{% block no_results %}{% endblock %}
{% endif %}
</tbody>
</table>

View File

@ -0,0 +1,8 @@
{% extends "wagtailadmin/pages/listing/_list_explore.html" %}
{% load i18n wagtailadmin_tags %}
{% block page_title %}
{% include "wagtailadmin/pages/listing/_page_title_explore.html" %}
{% endblock %}

View File

@ -0,0 +1,41 @@
{% load i18n wagtailadmin_tags %}
{# The title field for a page in the page listing, when in 'explore' mode #}
<div class="title-wrapper">
{% if page.is_site_root %}
{% if perms.wagtailcore.add_site or perms.wagtailcore.change_site or perms.wagtailcore.delete_site %}
<a href="{% url 'wagtailsites:index' %}" class="icon icon-site" title="{% trans 'Sites menu' %}"></a>
{% endif %}
{% endif %}
{% if page_perms.can_edit %}
<a href="{% url 'wagtailadmin_pages:edit' page.id %}"
title="{% trans 'Edit this page' %}">
{% if page.get_admin_display_title_html %}
{% autoescape off %}{{ page.get_admin_display_title_html }}{% endautoescape %}
{% else %}
{{ page.get_admin_display_title }}
{% endif %}
</a>
<a href="/cms/pages/124/edit/"
title="Edit this page">
</a>
{% else %}
{{ page.get_admin_display_title }}
{% endif %}
{% if show_locale_labels %}
<span class="status-tag status-tag--label">{{ page.locale.get_display_name }}</span>
{% endif %}
{% block pages_listing_title_extra %}{% endblock pages_listing_title_extra %}
{% include "wagtailadmin/pages/listing/_privacy_indicator.html" with page=page %}
{% include "wagtailadmin/pages/listing/_locked_indicator.html" with page=page %}
</div>
<ul class="actions">
{% page_listing_buttons page page_perms %}
</ul>