Merge branch 'develop' of bitbucket.org:iterativ/vbv_lernwelt into develop

This commit is contained in:
Lorenz Padberg 2022-06-30 15:20:08 +02:00
commit 30ef9c9c39
63 changed files with 684 additions and 324 deletions

View File

@ -5,14 +5,14 @@ Project setup is based on [cookiecutter-django](https://github.com/cookiecutter/
## Run for development
```bash
# run tailwind cli (on project root folder!)
npm run tailwind
# run vue vite dev server
cd client && npm run dev
# reset db and run django dev server
./prepare_server.sh
# run tailwind cli (for tailwind support on django templates)
cd client && npm run tailwind
```
## Installation
@ -61,7 +61,7 @@ npm run dev
### General part
Cypress and TailwindCSS ist installed for client and server, so there is this package.json on the project root directory
Cypress is installed for client and server, so there is this package.json on the project root directory
```bash
# in project root directory

View File

@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -1,8 +0,0 @@
// https://docs.cypress.io/api/introduction/api.html
describe('My First Test', () => {
it('visits the app root url', () => {
cy.visit('/')
cy.contains('h1', 'You did it!')
})
})

View File

@ -1,19 +0,0 @@
/* eslint-env node */
// ***********************************************************
// This example plugins/index.ts can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
export default ((on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
return config
}) as Cypress.PluginConfig

View File

@ -1,9 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["./**/*"],
"compilerOptions": {
"module": "CommonJS",
"preserveValueImports": false,
"types": ["node", "cypress/types/cypress"]
}
}

View File

@ -1,25 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

View File

@ -1,20 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -1,10 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["./integration/**/*", "./support/**/*"],
"compilerOptions": {
"isolatedModules": false,
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
}
}

View File

@ -4,10 +4,12 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build && cp ./dist/index.html ../server/vbv_lernwelt/templates/vue/index.html && cp -r ./dist/static/vue ../server/vbv_lernwelt/static/",
"build:tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --minify",
"test": "vitest run",
"coverage": "vitest run --coverage",
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch"
},
"dependencies": {
"@headlessui/vue": "^1.6.4",
@ -33,18 +35,19 @@
"@vue/eslint-config-typescript": "^10.0.0",
"@vue/test-utils": "^2.0.0-rc.18",
"@vue/tsconfig": "^0.1.3",
"autoprefixer": "^10.4.4",
"autoprefixer": "^10.4.7",
"cypress": "^9.5.3",
"eslint": "^8.5.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-vue": "^8.2.0",
"happy-dom": "^5.3.1",
"postcss": "^8.4.12",
"postcss": "^8.4.14",
"postcss-import": "^14.1.0",
"prettier": "^2.5.1",
"sass": "^1.50.1",
"sass-loader": "^12.6.0",
"start-server-and-test": "^1.14.0",
"tailwindcss": "^3.1.4",
"typescript": "~4.6.3",
"vite": "^2.9.1",
"vitest": "^0.15.1",

6
client/postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,8 +1,14 @@
<template>
<div id="app">
<MainNavigationBar />
<RouterView />
</div>
</template>
<script setup lang="ts">
import * as log from 'loglevel';
import MainNavigationBar from '@/components/MainNavigationBar.vue';
log.debug('App created');
</script>

View File

@ -1 +0,0 @@
@import "../../../../server/vbv_lernwelt/static/css/tailwind.css";

View File

@ -1,7 +1,16 @@
<script setup>
import * as log from 'loglevel';
import { reactive } from 'vue';
import { useUserStore } from '@/stores/user';
import { useRoute } from 'vue-router';
import { useAppStore } from '@/stores/app';
log.debug('MainNavigationBar.vue created');
const route = useRoute()
const userStore = useUserStore();
const appStore = useAppStore();
const state = reactive({showMenu: false});
function toggleNav() {
@ -13,10 +22,14 @@ function calcNavigationMobileOpenClasses() {
return state.showMenu ? ['fixed', 'w-full', 'h-screen'] : [];
}
function menuActive(checkPath) {
return route.path.startsWith(checkPath);
}
</script>
<template>
<div class="navigation bg-blue-900" :class="calcNavigationMobileOpenClasses()">
<div v-if="appStore.showMainNavigationBar" class="navigation bg-blue-900" :class="calcNavigationMobileOpenClasses()">
<nav
class="
px-8
@ -27,15 +40,18 @@ function calcNavigationMobileOpenClasses() {
>
<div class="flex items-center justify-between">
<router-link
to="/"
class="
font-bold
to="/" class="flex">
<it-icon-vbv class="h-8 w-16 -mt-3 -ml-3"/>
<div class="
text-white
text-2xl
hover:text-sky-500
pr-10
pl-3
ml-1
border-l border-white
"
>myVBV
>myVBV
</div>
</router-link>
<!-- Mobile menu button -->
@ -48,18 +64,13 @@ function calcNavigationMobileOpenClasses() {
focus:outline-none focus:text-sky-500
"
>
<svg viewBox="0 0 24 24" class="w-6 h-6 fill-current">
<path
fill-rule="evenodd"
d="M4 5h16a1 1 0 0 1 0 2H4a1 1 0 1 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2z"
></path>
</svg>
<it-icon-menu class="h-8 w-10"/>
</button>
</div>
</div>
<!-- Mobile Menu open: "block", Menu closed: "hidden" -->
<ul
<div
:class="state.showMenu ? 'flex' : 'hidden'"
class="
flex-auto
@ -69,17 +80,62 @@ function calcNavigationMobileOpenClasses() {
lg:flex lg:space-y-0 lg:flex-row lg:items-center lg:space-x-10 lg:mt-0
"
>
<li class="text-white hover:text-sky-500 text-2xl font-bold lg:text-base lg:font-normal">Lernpfad</li>
<li class="text-white hover:text-sky-500 text-2xl font-bold lg:text-base lg:font-normal">Kompetenzprofil</li>
<router-link
to="/dashboard"
class="nav-item"
:class="{'nav-item--active': menuActive('/dashboard')}"
>
Dashboard
</router-link>
<router-link
to="/shop"
class="nav-item"
:class="{'nav-item--active': menuActive('/shop')}"
>
Shop
</router-link>
<hr class="text-white lg:hidden">
<li class="hidden lg:list-item flex-auto"></li>
<li class="text-white hover:text-sky-500 text-2xl font-bold lg:text-base lg:font-normal">Mediathek</li>
<li class="text-white hover:text-sky-500 text-2xl font-bold lg:text-base lg:font-normal flex flex-row items-center">
<it-icon-message class="w-8 h-8 mr-2 lg:w-6 lg:h-6 lg:mr-1"/>
Netzwerk
</li>
<li class="text-white hover:text-sky-500 text-2xl font-bold lg:text-base lg:font-normal">Jan Baumgartner</li>
</ul>
<div class="hidden lg:list-item flex-auto"></div>
<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-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>
</div>
<div v-else><a class="" href="/login">Login</a></div>
</router-link>
</div>
</nav>
</div>
</template>
<style lang="postcss" scoped>
.nav-item {
@apply text-white hover:text-sky-500 text-2xl font-bold lg:text-base lg:font-normal;
}
.nav-item--active {
@apply underline underline-offset-[21px] decoration-sky-500 decoration-4
}
</style>

View File

@ -47,8 +47,16 @@ const block = computed(() => {
</button>
</nav>
<div v-if="block.type === 'exercise'" class="h-screen">
<iframe
width="100%"
height="100%"
scrolling="no"
src="/media/web_based_trainings/rise_cmi5_test_export/scormcontent/index.html"
/>
</div>
<div class="mx-auto max-w-5xl px-4 lg:px-8 py-4">
<div v-else class="mx-auto max-w-5xl px-4 lg:px-8 py-4">
<p>{{ block.value.description }}</p>
<div v-if="block.type === 'video'">

View File

@ -0,0 +1,22 @@
<template>
<svg viewBox="0 0 39 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M38.3507 28.111C38.3898 28.1686 38.4124 28.2358 38.4162 28.3053C38.4201 28.3748 38.4049 28.4441 38.3725 28.5057C38.34 28.5672 38.2914 28.6188 38.2319 28.655C38.1724 28.6911 38.1042 28.7104 38.0346 28.7108H36.3463C36.2839 28.7105 36.2224 28.6949 36.1673 28.6655C36.1122 28.636 36.0652 28.5936 36.0302 28.5419L32.0792 22.7066L28.1282 28.5419C28.0932 28.5936 28.0461 28.636 27.991 28.6654C27.9359 28.6949 27.8745 28.7104 27.812 28.7108H26.122C26.0523 28.7104 25.9841 28.6911 25.9246 28.655C25.8651 28.6188 25.8165 28.5672 25.7841 28.5057C25.7516 28.4441 25.7365 28.3748 25.7403 28.3053C25.7442 28.2358 25.7668 28.1686 25.8058 28.111L31.7648 19.312C31.8 19.26 31.8475 19.2175 31.9029 19.1881C31.9584 19.1586 32.0202 19.1432 32.083 19.1432C32.1458 19.1432 32.2076 19.1586 32.2631 19.1881C32.3186 19.2175 32.366 19.26 32.4012 19.312L38.3507 28.111Z"
fill="#007AC3"/>
<path
d="M12.6181 28.111C12.6572 28.1686 12.6798 28.2358 12.6836 28.3053C12.6874 28.3748 12.6723 28.4441 12.6399 28.5057C12.6074 28.5672 12.5588 28.6188 12.4993 28.655C12.4398 28.6911 12.3716 28.7104 12.302 28.7108H10.6119C10.5495 28.7104 10.488 28.6949 10.4329 28.6654C10.3779 28.636 10.3308 28.5936 10.2958 28.5419L6.34521 22.7066L2.3942 28.5419C2.35919 28.5936 2.31211 28.636 2.25702 28.6654C2.20193 28.6949 2.1405 28.7104 2.07805 28.7108H0.383918C0.314304 28.7104 0.246102 28.6911 0.186595 28.655C0.127088 28.6188 0.0785123 28.5672 0.0460511 28.5057C0.01359 28.4441 -0.00153491 28.3748 0.00228956 28.3053C0.00611403 28.2358 0.0287451 28.1686 0.0677665 28.111L6.0268 19.312C6.06202 19.26 6.10943 19.2175 6.1649 19.1881C6.22037 19.1586 6.2822 19.1432 6.34498 19.1432C6.40777 19.1432 6.4696 19.1586 6.52506 19.1881C6.58053 19.2175 6.62795 19.26 6.66317 19.312L12.6181 28.111Z"
fill="white"/>
<path
d="M0.0660575 7.0891C0.027036 7.03145 0.00440495 6.96429 0.000580477 6.89478C-0.003244 6.82527 0.0118844 6.75603 0.0443455 6.69444C0.0768067 6.63286 0.125382 6.58125 0.184889 6.54512C0.244396 6.50899 0.312599 6.48971 0.382213 6.48932H2.07047C2.13293 6.48967 2.19436 6.50523 2.24944 6.53466C2.30453 6.56409 2.35161 6.60651 2.38662 6.65824L6.33718 12.4935L10.2882 6.65824C10.3232 6.60651 10.3703 6.56409 10.4254 6.53466C10.4805 6.50523 10.5419 6.48967 10.6043 6.48932H12.2944C12.364 6.48971 12.4322 6.50899 12.4917 6.54512C12.5512 6.58125 12.5998 6.63286 12.6323 6.69444C12.6647 6.75603 12.6799 6.82527 12.676 6.89478C12.6722 6.96429 12.6496 7.03145 12.6106 7.0891L6.65966 15.8876C6.62451 15.9397 6.57711 15.9824 6.52164 16.0118C6.46616 16.0413 6.40429 16.0568 6.34147 16.0568C6.27865 16.0568 6.21678 16.0413 6.16131 16.0118C6.10583 15.9824 6.05844 15.9397 6.02328 15.8876L0.0660575 7.0891Z"
fill="#007AC3"/>
<path
d="M25.7982 7.0891C25.7592 7.03145 25.7366 6.96429 25.7328 6.89478C25.7289 6.82527 25.7441 6.75603 25.7765 6.69444C25.809 6.63286 25.8576 6.58125 25.9171 6.54512C25.9766 6.50899 26.0448 6.48971 26.1144 6.48932H27.8026C27.8651 6.48963 27.9265 6.50517 27.9816 6.53461C28.0367 6.56405 28.0838 6.60648 28.1188 6.65824L32.0698 12.4935L36.0208 6.65824C36.0558 6.60651 36.1029 6.56409 36.158 6.53466C36.2131 6.50523 36.2745 6.48967 36.337 6.48932H38.027C38.0966 6.48971 38.1648 6.50899 38.2244 6.54512C38.2839 6.58125 38.3324 6.63286 38.3649 6.69444C38.3974 6.75603 38.4125 6.82527 38.4087 6.89478C38.4048 6.96429 38.3822 7.03145 38.3432 7.0891L32.3841 15.8881C32.349 15.9402 32.3016 15.9828 32.2461 16.0123C32.1906 16.0418 32.1288 16.0572 32.066 16.0572C32.0031 16.0572 31.9413 16.0418 31.8858 16.0123C31.8303 15.9828 31.7829 15.9402 31.7478 15.8881L25.7982 7.0891Z"
fill="white"/>
<path
d="M24.2526 20.5202C23.1808 19.5633 21.7844 19.0514 20.3481 19.0889C17.1866 19.0889 15.1465 21.3851 15.1465 24.9395V34.1161C15.1466 34.218 15.1872 34.3156 15.2592 34.3876C15.3313 34.4596 15.429 34.5 15.5308 34.5H17.1035C17.2053 34.5 17.303 34.4596 17.3751 34.3876C17.4472 34.3156 17.4877 34.218 17.4878 34.1161V28.5739H22.0228C22.1247 28.5738 22.2223 28.5333 22.2943 28.4612C22.3663 28.3891 22.4067 28.2914 22.4067 28.1896V26.7895C22.4067 26.6876 22.3663 26.5899 22.2943 26.5179C22.2223 26.4458 22.1247 26.4052 22.0228 26.4051H17.4874V24.8722C17.4874 22.5431 18.5262 21.2591 20.4163 21.2591C21.3288 21.2362 22.2134 21.5752 22.8769 22.2021L23.0395 22.3471C23.0789 22.3821 23.125 22.4086 23.1751 22.4251C23.2252 22.4415 23.2782 22.4474 23.3307 22.4425C23.3831 22.4376 23.434 22.4219 23.4802 22.3965C23.5264 22.3711 23.5668 22.3364 23.5991 22.2947L24.4545 21.1859C24.5125 21.1105 24.5406 21.0163 24.5331 20.9214C24.5257 20.8265 24.4834 20.7378 24.4143 20.6724C24.3343 20.596 24.263 20.5283 24.2526 20.5202Z"
fill="white"/>
<path
d="M19.2443 6.13249C19.2444 6.08179 19.2546 6.03161 19.2742 5.98484C19.2938 5.93808 19.3224 5.89566 19.3585 5.86001C19.3945 5.82437 19.4373 5.79622 19.4843 5.77717C19.5313 5.75813 19.5816 5.74857 19.6323 5.74904C19.7226 5.74904 19.7989 5.74904 19.8129 5.7522C21.1089 5.881 22.3057 6.50307 23.1558 7.4897C24.0059 8.47632 24.4442 9.75198 24.38 11.0527C24.2917 12.3636 23.7038 13.5907 22.7374 14.4808C21.7709 15.3709 20.4998 15.8562 19.186 15.8366H15.5277C15.4258 15.8365 15.3282 15.7959 15.2562 15.7238C15.1842 15.6518 15.1438 15.5541 15.1438 15.4522V0.884351C15.1438 0.782493 15.1842 0.6848 15.2562 0.612733C15.3282 0.540666 15.4258 0.50012 15.5277 0.5H17.0976C17.1996 0.5 17.2973 0.540494 17.3694 0.612574C17.4415 0.684654 17.482 0.782414 17.482 0.884351V13.2405C17.482 13.3425 17.5225 13.4402 17.5946 13.5123C17.6666 13.5844 17.7644 13.6249 17.8663 13.6249H19.2326C19.9527 13.6327 20.6501 13.3736 21.1905 12.8975C21.7308 12.4215 22.0758 11.7622 22.1588 11.0469C22.2259 10.2951 21.9924 9.5474 21.5092 8.96758C21.0261 8.38775 20.3328 8.0231 19.5812 7.95353C19.4866 7.94417 19.3988 7.89972 19.3353 7.82892C19.2718 7.75812 19.2371 7.66611 19.238 7.57099L19.2443 6.13249Z"
fill="white"/>
</svg>
</template>

View File

@ -5,7 +5,7 @@ import {setupI18n} from './i18n'
import App from './App.vue'
import router from './router'
import '@/assets/styles/index.scss'
import '../tailwind.css'
const i18n = setupI18n()
const app = createApp(App)

View File

@ -1,24 +1,21 @@
import type {NavigationGuardWithThis, RouteLocationNormalized} from 'vue-router';
import type {UserState} from '@/stores/user'
import {useUserStore} from '@/stores/user'
import type {Store} from 'pinia';
const cookieName = 'loginStatus'
let userStore: Store | null = null
import {useUserStore} from '@/stores/user';
export const updateLoggedIn: NavigationGuardWithThis<undefined> = (_to) => {
const loggedIn = getCookieValue(cookieName) === 'true'
const store = getUserStore()
const loggedIn = getCookieValue('loginStatus') === 'true'
const userStore = useUserStore()
store.$patch({ loggedIn })
userStore.$patch({loggedIn});
if (loggedIn && !userStore.email) {
userStore.fetchUser();
}
}
export const redirectToLoginIfRequired: NavigationGuardWithThis<undefined> = (to, _from) => {
const store = getUserStore()
if(loginRequired(to) && !store.loggedIn) {
return { name: 'home' }
const userStore = useUserStore()
if(loginRequired(to) && !userStore.loggedIn) {
return `/login?next=${to.fullPath}`
}
}
@ -31,20 +28,6 @@ export const getCookieValue = (cookieName: string): string => {
return cookieValue.pop() || '';
}
// Pina is not ready when router is called the first time by app.use(), so we need to load it here
const getUserStore = (): UserState & Store => {
if (!userStore) {
userStore = useUserStore()
}
return userStore as unknown as UserState & Store
}
const loginRequired = (to: RouteLocationNormalized) => {
return !to.meta?.public
}
// export const unauthorizedAccess: NavigationGuardWithThis<undefined> = (to) => {
// r loginRequired(to) && getCookieValue('loginStatus') !== 'true';
// }

View File

@ -1,9 +1,8 @@
import {createRouter, createWebHistory} from 'vue-router'
import HomeView from '../views/HomeView.vue';
import HomeView from '@/views/HomeView.vue';
import LoginView from '@/views/LoginView.vue';
import {redirectToLoginIfRequired, updateLoggedIn} from '@/router/guards';
const loginUrl = '/sso/login/'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
@ -18,15 +17,32 @@ const router = createRouter({
},
{
path: '/login',
component: HomeView,
beforeEnter(_to, _from) {
window.location.href = loginUrl
},
component: LoginView,
meta: {
public: true
}
},
{
{
path: '/dashboard',
component: () => import('@/views/DashboardView.vue'),
},
{
path: '/shop',
component: () => import('@/views/ShopView.vue'),
},
{
path: '/mediacenter',
component: () => import('@/views/MediaView.vue'),
},
{
path: '/messages',
component: () => import('@/views/MessagesView.vue'),
},
{
path: '/profile',
component: () => import('@/views/ProfileView.vue'),
},
{
path: '/learningpath/:learningPathSlug',
component: () => import('../views/LearningPathView.vue'),
props: true
@ -36,10 +52,6 @@ const router = createRouter({
component: () => import('../views/CircleView.vue'),
props: true
},
{
path: '/profile',
component: () => import('../views/ProfileView.vue'),
},
{
path: '/styleguide',
component: () => import('../views/StyelGuideView.vue'),

View File

@ -1,7 +1,7 @@
import type {CircleChild, LearningContent, LearningSequence, LearningUnit} from '@/types';
function createEmptyLearningUnit(parentLearningSequence: LearningSequence): LearningUnit {
function _createEmptyLearningUnit(parentLearningSequence: LearningSequence): LearningUnit {
return {
id: 0,
title: '',
@ -35,7 +35,7 @@ export function parseLearningSequences (children: CircleChild[]): LearningSequen
learningSequence = Object.assign(child, {learningUnits: []});
// initialize empty learning unit if there will not come a learning unit next
learningUnit = createEmptyLearningUnit(learningSequence);
learningUnit = _createEmptyLearningUnit(learningSequence);
} else if (child.type === 'learnpath.LearningUnit') {
if (!learningSequence) {
throw new Error('LearningUnit found before LearningSequence');

View File

@ -1,8 +0,0 @@
import axios from 'axios';
export function getUserData () {
return axios({
method: 'get',
url: 'http://localhost:3000/api/me',
})
}

16
client/src/stores/app.ts Normal file
View File

@ -0,0 +1,16 @@
import {defineStore} from 'pinia'
export type AppState = {
showMainNavigationBar: boolean;
}
export const useAppStore = defineStore({
id: 'app',
state: () => ({
showMainNavigationBar: true,
} as AppState),
getters: {
},
actions: {
}
})

View File

@ -5,6 +5,7 @@ import {defineStore} from 'pinia'
import type {Circle, CircleChild, CircleCompletion, LearningContent, LearningUnit, LearningUnitQuestion} from '@/types'
import {itGet, itPost} from '@/fetchHelpers';
import {parseLearningSequences} from '@/services/circle';
import {useAppStore} from '@/stores/app';
export type CircleStoreState = {
circleData: Circle;
@ -80,19 +81,27 @@ export const useCircleStore = defineStore({
},
openLearningContent(learningContent: LearningContent) {
this.currentLearningContent = learningContent;
const appStore = useAppStore();
appStore.showMainNavigationBar = false;
this.page = 'LEARNING_CONTENT';
},
closeLearningContent() {
this.currentLearningContent = undefined;
const appStore = useAppStore();
appStore.showMainNavigationBar = true;
this.page = 'INDEX';
},
openSelfEvaluation(learningUnit: LearningUnit) {
this.page = 'SELF_EVALUATION';
const appStore = useAppStore();
appStore.showMainNavigationBar = false;
this.currentSelfEvaluation = learningUnit;
},
closeSelfEvaluation() {
this.page = 'INDEX';
this.currentSelfEvaluation = undefined;
const appStore = useAppStore();
appStore.showMainNavigationBar = true;
this.page = 'INDEX';
},
calcSelfEvaluationStatus(learningUnit: LearningUnit) {
if (learningUnit.children.length > 0) {

View File

@ -1,8 +1,15 @@
import * as log from 'loglevel';
import {defineStore} from 'pinia'
import {itGet, itPost} from '@/fetchHelpers';
// 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 = {
first_name: string,
last_name: string,
email: string;
username: string,
avatar_url: string,
loggedIn: boolean;
}
@ -10,16 +17,41 @@ export const useUserStore = defineStore({
id: 'user',
state: () => ({
email: '',
first_name: '',
last_name: '',
username: '',
avatar_url: '',
loggedIn: false
} as UserState),
getters: {
getFullName(): string {
return `${this.first_name} ${this.last_name}`.trim();
},
},
actions: {
setEmail (email: string) {
this.email = email
handleLogin(username: string, password: string, next='/') {
if (username && password) {
itPost('/core/login/', {
username,
password,
}).then((data) => {
this.$state = data;
this.loggedIn = true;
log.debug(`redirect to ${next}`);
window.location.href = next;
}).catch(() => {
this.loggedIn = false;
alert('Login failed');
});
}
},
setLoggedIn (isLoggedIn: boolean) {
this.loggedIn = isLoggedIn
fetchUser() {
itGet('/api/core/me/').then((data) => {
this.$state = data;
this.loggedIn = true;
}).catch(() => {
this.loggedIn = false;
})
}
}
})

View File

@ -1,7 +1,5 @@
<script setup lang="ts">
import * as log from 'loglevel';
import MainNavigationBar from '@/components/MainNavigationBar.vue';
import LearningSequence from '@/components/circle/LearningSequence.vue';
import CircleOverview from '@/components/circle/CircleOverview.vue';
import LearningContent from '@/components/circle/LearningContent.vue';
@ -11,6 +9,8 @@ import {useCircleStore} from '@/stores/circle';
import SelfEvaluation from '@/components/circle/SelfEvaluation.vue';
import CircleDiagram from '@/components/circle/CircleDiagram.vue';
log.debug('CircleView.vue created');
const props = defineProps<{
circleSlug: string
}>()
@ -36,8 +36,6 @@ onMounted(async () => {
<SelfEvaluation :key="circleStore.currentSelfEvaluation.translation_key"/>
</div>
<div v-else>
<MainNavigationBar/>
<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">

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import * as log from 'loglevel';
log.debug('DashboardView created');
</script>
<template>
<main class="px-8 py-8">
<h1>Dashboard</h1>
</main>
</template>
<style scoped>
</style>

View File

@ -1,17 +1,15 @@
<script setup lang="ts">
import MainNavigationBar from '@/components/MainNavigationBar.vue';</script>
<script setup lang="ts"></script>
<template>
<MainNavigationBar/>
<main class="px-8 py-8">
<h1>myVBV Start Page</h1>
<div class="mt-8 flex flex-col lg:flex-row justify-start gap-4">
<router-link class="link text-xl" to="/styleguide">Styelguide</router-link>
<a class="link text-xl" href="/login/">Login</a>
<router-link class="link text-xl" to="/learningpath/versicherungsvermittlerin">Lernpfad "Versicherungsvermittlerin" (Login benötigt)</router-link>
<router-link class="link text-xl" to="/circle/analyse">Circle "Analyse" (Login benötigt)</router-link>
<a class="link text-xl" href="/login">Login</a>
<router-link class="link text-xl" to="/learningpath/versicherungsvermittlerin">Lernpfad "Versicherungsvermittlerin"</router-link>
<router-link class="link text-xl" to="/circle/analyse">Circle "Analyse"</router-link>
</div>
</main>
</template>

View File

@ -5,7 +5,6 @@ import * as log from 'loglevel';
import MainNavigationBar from '../components/MainNavigationBar.vue';
import LearningPathDiagram from '../components/circle/LearningPathDiagram.vue';
export default {
@ -65,9 +64,6 @@ export default {
<template>
<div class="bg-gray-300 h-screen" v-if="learningPathContents">
<MainNavigationBar/>
<div class="learningpath flex flex-col">
<div class="flex flex-col h-max">
<div class="bg-white h-80 pt-8">

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import * as log from 'loglevel';
import {reactive} from 'vue';
import {useUserStore} from '@/stores/user';
import {useRoute} from 'vue-router';
const route = useRoute()
log.debug('LoginView.vue created');
log.debug(route.query);
const state = reactive({
username: '',
password: '',
});
const userStore = useUserStore();
</script>
<template>
<main class="px-8 py-8">
<h1>Login</h1>
<form
@submit.prevent="userStore.handleLogin(state.username, state.password, route.query.next)"
>
<div class="mt-8 mb-4">
<label class="block mb-1" for="email">Username</label>
<input
id="username"
type="text"
name="username"
v-model="state.username"
class="py-2 px-3 border border-gray-500 mt-1 block w-96"
/>
</div>
<div class="mb-4">
<label class="block mb-1" for="password">Password</label>
<input
id="password"
type="password"
name="password"
v-model="state.password"
class="py-2 px-3 border border-gray-500 mt-1 block w-96"
/>
</div>
<div>
<input
type="submit"
value="Login"
class="btn-primary"
/>
</div>
</form>
</main>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import * as log from 'loglevel';
log.debug('ShopView created');
</script>
<template>
<main class="px-8 py-8">
<h1>Mediathek</h1>
</main>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import * as log from 'loglevel';
log.debug('MessagesView created');
</script>
<template>
<main class="px-8 py-8">
<h1>Messages</h1>
</main>
</template>
<style scoped>
</style>

View File

@ -1,34 +1,15 @@
<template>
<div class="about">
<h1>This is a profile page</h1>
</div>
</template>
<script setup lang="ts">
import * as log from 'loglevel';
<script>
import { ref, onMounted } from 'vue'
import { getUserData } from '@/services/http'
log.debug('ProfileView created');
export default {
setup () {
onMounted(async () => {
// const res = await getUserData();
// users.value = res.data;
// console.log(res);
});
return {
}
},
}
</script>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
<template>
<main class="px-8 py-8">
<h1>Profil</h1>
</main>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import * as log from 'loglevel';
log.debug('ShopView created');
</script>
<template>
<main class="px-8 py-8">
<h1>Shop</h1>
</main>
</template>
<style scoped>
</style>

View File

@ -2,7 +2,6 @@
import {reactive} from 'vue'
import {Listbox, ListboxButton, ListboxOption, ListboxOptions} from '@headlessui/vue'
import MainNavigationBar from '@/components/MainNavigationBar.vue';
import ItCheckbox from '@/components/ui/ItCheckbox.vue';
@ -34,8 +33,6 @@ function colorBgClass(color: string, value: number) {
</script>
<template>
<MainNavigationBar/>
<main class="px-8 py-4">
<h1>Style Guide</h1>
@ -168,6 +165,16 @@ function colorBgClass(color: string, value: number) {
<it-icon-close class="w-6 h-6"/>
</div>
<div class="inline-flex flex-col text-white bg-blue-900">
vbv
<it-icon-vbv class="w-24 h-24"/>
</div>
<div class="inline-flex flex-col">
vbv-pos
<it-icon-vbv-pos class="w-24 h-24"/>
</div>
</div>

View File

@ -2,9 +2,9 @@ const colors = require('tailwindcss/colors')
module.exports = {
content: [
'./client/index.html',
'./client/src/**/*.{vue,js,ts,jsx,tsx}',
'./server/vbv_lernwelt/**/*.{html,js}',
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
'../server/vbv_lernwelt/**/*.{html,js}',
],
theme: {
fontFamily: {

View File

@ -4,15 +4,9 @@ set -o errexit
set -o pipefail
set -o nounset
python /app/manage.py collectstatic --noinput
# TODO remove after stabilisation
python /app/manage.py reset_schema
python /app/manage.py collectstatic --noinput
python /app/manage.py createcachetable
python /app/manage.py migrate
# TODO remove after stabilisation
python /app/manage.py create_default_users
python /app/manage.py create_default_learning_path
/usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:80 --chdir=/app -k uvicorn.workers.UvicornWorker

View File

@ -2,19 +2,16 @@
"name": "vbv_lernwelt_cypress",
"version": "1.0.0",
"scripts": {
"build": "npm install --prefix client && npm run build --prefix client && npm run build:tailwind",
"build:tailwind": "tailwindcss -i ./tailwind/input.css -o ./server/vbv_lernwelt/static/css/tailwind.css --minify",
"build": "npm install --prefix client && npm run build --prefix client && npm run build:tailwind --prefix client",
"test": "echo \"Error: no test specified\" && exit 1",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:ci": "cypress run --config baseUrl=http://localhost:8001",
"cypress:ci:open": "cypress open --config baseUrl=http://localhost:8001",
"tailwind": "tailwindcss -i ./tailwind/input.css -o ./server/vbv_lernwelt/static/css/tailwind.css --watch"
"cypress:ci:open": "cypress open --config baseUrl=http://localhost:8001"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.2",
"cypress": "^9.4.1",
"tailwindcss": "^3.0.24"
"cypress": "^9.4.1"
}
}

View File

@ -92,7 +92,9 @@ THIRD_PARTY_APPS = [
'wagtail.search',
'wagtail.admin',
'wagtail',
'wagtail.locales',
# 'wagtail.locales',
"wagtail_localize",
"wagtail_localize.locales",
'wagtail.api.v2',
'modelcluster',
@ -127,7 +129,7 @@ AUTH_USER_MODEL = "core.User"
# FIXME make configurable!?
# LOGIN_URL = "/sso/login/"
LOGIN_URL = "/login/"
LOGIN_URL = "/login"
LOGIN_REDIRECT_URL = "/"
ALLOW_LOCAL_LOGIN = env.bool("IT_ALLOW_LOCAL_LOGIN", default=False)
@ -168,7 +170,7 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.common.BrokenLinkEmailsMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_htmx.middleware.HtmxMiddleware",
"csp.middleware.CSPMiddleware",
"vbv_lernwelt.core.middleware.auth.AuthenticationRequiredMiddleware",
"vbv_lernwelt.core.middleware.security.SecurityRequestResponseLoggingMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
@ -448,6 +450,10 @@ REST_FRAMEWORK = {
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
CORS_URLS_REGEX = r"^/api/.*$"
# django-csp
CSP_DEFAULT_SRC = ("'self'", "'unsafe-inline'", 'ws://localhost:3000', 'localhost:8000', 'blob:', 'data:', 'http://*')
CSP_FRAME_ANCESTORS = ("'self'",)
# By Default swagger ui is available only to admin user. You can change permission classs to change that
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
SPECTACULAR_SETTINGS = {

View File

@ -1,7 +1,6 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.contrib.auth.decorators import user_passes_test
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path, re_path
@ -17,8 +16,7 @@ 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, cypress_reset_view, vue_home,
)
check_rate_limit, cypress_reset_view, vue_home, vue_login, me_user_view, )
from .wagtail_api import wagtail_api_router
@ -43,14 +41,16 @@ urlpatterns = [
path('pages/', include(wagtail_urls)),
path('learnpath/', include("vbv_lernwelt.learnpath.urls")),
path('api/completion/', include("vbv_lernwelt.completion.urls")),
re_path(r'api/core/me/$', me_user_view, name='me_user_view'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
# Static file serving when using Gunicorn + Uvicorn for local web socket development
urlpatterns += staticfiles_urlpatterns()
if settings.ALLOW_LOCAL_LOGIN:
urlpatterns += [path("login/", django_view_authentication_exempt(
auth_views.LoginView.as_view(template_name="core/login.html"))),]
urlpatterns += [
re_path(r'core/login/$', django_view_authentication_exempt(vue_login), name='vue_login'),
]
# API URLS
@ -67,7 +67,7 @@ urlpatterns += [
if settings.APP_ENVIRONMENT != 'production':
urlpatterns += [
re_path(r'cypressreset/$', cypress_reset_view, name='cypress_reset_view'),
re_path(r'core/cypressreset/$', cypress_reset_view, name='cypress_reset_view'),
]
# fmt: on

View File

@ -82,6 +82,7 @@ django==3.2.13
# via
# -r requirements.in
# django-cors-headers
# django-csp
# django-debug-toolbar
# django-extensions
# django-filter
@ -97,12 +98,15 @@ django==3.2.13
# djangorestframework
# drf-spectacular
# wagtail
# wagtail-localize
django-click==2.3.0
# via -r requirements.in
django-cors-headers==3.11.0
# via -r requirements.in
django-coverage-plugin==2.0.2
# via -r requirements-dev.in
django-csp==3.7
# via -r requirements.in
django-debug-toolbar==3.2.4
# via -r requirements-dev.in
django-extensions==3.1.5
@ -262,6 +266,8 @@ platformdirs==2.5.1
# virtualenv
pluggy==1.0.0
# via pytest
polib==1.1.1
# via wagtail-localize
portalocker==2.4.0
# via concurrent-log-handler
pre-commit==2.17.0
@ -400,6 +406,7 @@ typing-extensions==4.1.1
# django-stubs-ext
# djangorestframework-stubs
# mypy
# wagtail-localize
uritemplate==4.1.1
# via
# coreapi
@ -414,12 +421,15 @@ uvloop==0.16.0
# via uvicorn
virtualenv==20.14.0
# via pre-commit
wagtail==3.0.0
wagtail==3.0.1
# via
# -r requirements.in
# wagtail-factories
# wagtail-localize
wagtail-factories==2.0.1
# via -r requirements.in
wagtail-localize==1.2.1
# via -r requirements.in
watchdog==2.1.7
# via werkzeug
watchgod==0.8.1

View File

@ -24,6 +24,7 @@ dj-database-url
django-click
django-ratelimit
django-ipware
django-csp
psycopg2-binary
gunicorn
@ -35,3 +36,4 @@ concurrent-log-handler
wagtail>=3,<4
wagtail-factories
wagtail-localize

View File

@ -50,6 +50,7 @@ django==3.2.13
# via
# -r requirements.in
# django-cors-headers
# django-csp
# django-filter
# django-htmx
# django-model-utils
@ -61,10 +62,13 @@ django==3.2.13
# djangorestframework
# drf-spectacular
# wagtail
# wagtail-localize
django-click==2.3.0
# via -r requirements.in
django-cors-headers==3.11.0
# via -r requirements.in
django-csp==3.7
# via -r requirements.in
django-filter==21.1
# via wagtail
django-htmx==1.9.0
@ -134,6 +138,8 @@ pillow==9.0.1
# via
# -r requirements.in
# wagtail
polib==1.1.1
# via wagtail-localize
portalocker==2.4.0
# via concurrent-log-handler
psycopg2-binary==2.9.3
@ -192,6 +198,8 @@ telepath==0.2
# via wagtail
text-unidecode==1.3
# via python-slugify
typing-extensions==4.2.0
# via wagtail-localize
uritemplate==4.1.1
# via drf-spectacular
urllib3==1.26.9
@ -202,12 +210,15 @@ uvicorn[standard]==0.17.6
# via -r requirements.in
uvloop==0.16.0
# via uvicorn
wagtail==3.0.0
wagtail==3.0.1
# via
# -r requirements.in
# wagtail-factories
# wagtail-localize
wagtail-factories==2.0.1
# via -r requirements.in
wagtail-localize==1.2.1
# via -r requirements.in
watchgod==0.8.1
# via uvicorn
webencodings==0.5.1

View File

@ -19,7 +19,7 @@ class CompletionApiTestCase(APITestCase):
def setUp(self) -> None:
self.user = User.objects.get(username='student')
self.client.login(username='student', password='student')
self.client.login(username='student', password='test')
def test_completeLearningContent_works(self):
learning_content = LearningContent.objects.get(title='Einleitung Circle "Anlayse"')

View File

@ -26,7 +26,7 @@ def mark_circle_completion(request):
page_key = request.data.get('page_key')
completed = request.data.get('completed', True)
page = Page.objects.get(translation_key=page_key)
page = Page.objects.get(translation_key=page_key, locale__language_code='de-CH')
page_type = get_wagtail_type(page.specific)
circle = Circle.objects.ancestor_of(page).first()

View File

@ -5,25 +5,76 @@ from vbv_lernwelt.core.models import User
def create_default_users(user_model=User, group_model=Group, default_password=None):
if default_password is None:
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')
student_group, created = group_model.objects.get_or_create(name='student_group')
admin_password = default_password
if not admin_password:
admin_password = 'admin'
admin_user, created = _get_or_create_user(user_model=user_model, username='admin', password=admin_password)
admin_user.is_superuser = True
admin_user.is_staff = True
admin_user.groups.add(admin_group)
admin_user.save()
def _create_student_user(email, first_name, last_name, avatar_url=''):
student_user, created = _get_or_create_user(
user_model=user_model, username=email, password=default_password,
)
student_user.first_name = first_name
student_user.last_name = last_name
student_user.avatar_url = avatar_url
student_user.groups.add(student_group)
student_user.save()
student_user_password = default_password
if not student_user_password:
student_user_password = 'student'
student_user, created = _get_or_create_user(user_model=user_model, username='student', password=student_user_password)
student_user.groups.add(student_group)
student_user.save()
def _create_admin_user(email, first_name, last_name, avatar_url=''):
admin_user, created = _get_or_create_user(
user_model=user_model, username=email, password=default_password
)
admin_user.is_superuser = True
admin_user.is_staff = True
admin_user.first_name = first_name
admin_user.last_name = last_name
admin_user.avatar_url = avatar_url
admin_user.groups.add(admin_group)
admin_user.save()
_create_admin_user(
email='info@iterativ.ch',
first_name='Info',
last_name='Iterativ',
avatar_url='/static/avatars/avatar_iterativ.png'
)
_create_admin_user(
email='admin',
first_name='Peter',
last_name='Adminson',
avatar_url='/static/avatars/avatar_iterativ.png'
)
_create_student_user(
email='student',
first_name='Student',
last_name='Meier',
avatar_url='/static/avatars/avatar_iterativ.png'
)
_create_student_user(
email='daniel.egger@iterativ.ch',
first_name='Daniel',
last_name='Egger',
avatar_url='/static/avatars/avatar_iterativ.png'
)
_create_student_user(
email='axel.manderbach@lernetz.ch',
first_name='Axel',
last_name='Manderbach',
avatar_url='/static/avatars/avatar_axel.jpg'
)
_create_student_user(
email='christoph.bosshard@vbv-afa.ch',
first_name='Christoph',
last_name='Bosshard',
avatar_url='/static/avatars/avatar_christoph.jpg'
)
def _get_or_create_user(user_model, *args, **kwargs):
@ -34,6 +85,10 @@ def _get_or_create_user(user_model, *args, **kwargs):
user = user_model.objects.filter(username=username).first()
if not user:
user = user_model.objects.create(username=username, password=make_password(password))
user = user_model.objects.create(
username=username,
password=make_password(password),
email=username,
)
created = True
return user, created

View File

@ -10,4 +10,4 @@ def command(customer_language):
print("cypress reset data")
delete_default_learning_path()
create_default_learning_path()
create_default_learning_path(skip_locales=False)

View File

@ -1,5 +1,6 @@
import djclick as click
from django.conf import settings
from django.core.management import call_command
from django.db import transaction, connection
@ -24,3 +25,7 @@ def command():
print(user)
reset_schema(db_config_user=user)
call_command('createcachetable')
call_command('migrate')
call_command('create_default_users')
call_command('create_default_learning_path')

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.13 on 2022-06-28 12:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='avatar_url',
field=models.CharField(blank=True, default='', max_length=254),
),
migrations.AlterField(
model_name='user',
name='email',
field=models.EmailField(max_length=254, unique=True, verbose_name='email address'),
),
]

View File

@ -15,7 +15,7 @@ def create_users(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
("core", "0001_initial"),
("core", "0002_user_model"),
]
operations = [

View File

@ -10,6 +10,8 @@ class User(AbstractUser):
"""
# FIXME: look into it...
# objects = UserManager()
avatar_url = models.CharField(max_length=254, blank=True, default='')
email = models.EmailField('email address', unique=True)
class SecurityRequestResponseLog(models.Model):

View File

@ -0,0 +1,11 @@
from rest_framework import serializers
from vbv_lernwelt.core.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
'id', 'first_name', 'last_name', 'email', 'username', 'avatar_url',
]

View File

@ -1,35 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mx-auto">
<div class="w-full min-h-screen bg-gray-50 flex flex-col sm:justify-center items-center pt-6 sm:pt-0">
<div class="w-full sm:max-w-md p-5 mx-auto">
<h2 class="mb-12 text-center text-5xl font-extrabold">Login</h2>
<form method="POST">
{% csrf_token %}
<div class="mb-4">
<label class="block mb-1" for="email">Username</label>
<input id="username" type="text" name="username" class="py-2 px-3 border border-gray-300 focus:border-red-300 focus:outline-none focus:ring focus:ring-red-200 focus:ring-opacity-50 rounded-md shadow-sm disabled:bg-gray-100 mt-1 block w-full"/>
</div>
<div class="mb-4">
<label class="block mb-1" for="password">Password</label>
<input id="password" type="password" name="password" class="py-2 px-3 border border-gray-300 focus:border-red-300 focus:outline-none focus:ring focus:ring-red-200 focus:ring-opacity-50 rounded-md shadow-sm disabled:bg-gray-100 mt-1 block w-full"/>
</div>
<div class="mt-6">
<button
class="w-full inline-flex items-center justify-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold capitalize text-white hover:bg-red-700 active:bg-red-700 focus:outline-none focus:border-red-700 focus:ring focus:ring-red-200 disabled:opacity-25 transition"
data-cy="submit"
type="submit">
Login
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -6,8 +6,6 @@ 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
) -> EventDict:

View File

@ -1,7 +1,9 @@
# Create your views here.
import requests
import structlog
from django.conf import settings
from django.contrib.auth import authenticate, login
from django.core.management import call_command
from django.http import JsonResponse, HttpResponse, HttpResponseRedirect
from django.shortcuts import render
@ -11,8 +13,12 @@ from ratelimit.decorators import ratelimit
from rest_framework import authentication
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.core.serializers import UserSerializer
logger = structlog.get_logger(__name__)
@django_view_authentication_exempt
@ -37,6 +43,32 @@ def vue_home(request):
return HttpResponse(content)
@api_view(['POST'])
@ensure_csrf_cookie
def vue_login(request):
try:
username = request.data.get('username')
password = request.data.get('password')
if username and password:
user = authenticate(request, username=username, password=password)
if user:
login(request, user)
logger.debug('login successful', username=username, email=user.email, label='login')
return Response(UserSerializer(user).data)
except Exception as e:
logger.exception(e)
logger.debug('login failed', username=username, label='login')
return Response({'success': False}, status=401)
@api_view(['GET'])
def me_user_view(request):
if request.user.is_authenticated:
return Response(UserSerializer(request.user).data)
return Response(status=403)
def permission_denied_view(request, exception):
return render(request, "403.html", status=403)

View File

@ -5,4 +5,4 @@ from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_def
@click.command()
def command():
create_default_learning_path()
create_default_learning_path(skip_locales=False)

View File

@ -1,17 +1,20 @@
import wagtail_factories
from django.conf import settings
from wagtail.models import Site, Page
from django.core.management import call_command
from wagtail.models import Site, Page, Locale
from wagtail_localize.models import LocaleSynchronization
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningContent
from vbv_lernwelt.learnpath.models import LearningPath, Topic, Circle, LearningSequence, LearningContent, LearningUnit, \
LearningUnitQuestion
from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFactory, TopicFactory, CircleFactory, \
LearningSequenceFactory, LearningContentFactory, VideoBlockFactory, PodcastBlockFactory, CompetenceBlockFactory, \
ExerciseBlockFactory, DocumentBlockFactory, LearningUnitFactory, LearningUnitQuestionFactory
def create_default_learning_path(user=None):
def create_default_learning_path(user=None, skip_locales=True):
if user is None:
user = User.objects.get(username='admin')
user = User.objects.get(username='info@iterativ.ch')
site = Site.objects.filter(is_default_site=True).first()
@ -22,6 +25,7 @@ def create_default_learning_path(user=None):
site.port = 8000
site.save()
# create_default_competences()
lp = LearningPathFactory(title="Versicherungsvermittler/in", parent=site.root_page)
@ -404,12 +408,29 @@ Fachspezialisten bei.
# tp = TopicFactory.create(title="Prüfung", is_visible=False, learning_path=lp)
# circle_7 = CircleFactory.create(title="Prüfungsvorbereitung", parent=lp, topic=tp)
# locales
if not skip_locales:
locale_de = Locale.objects.get(language_code='de-CH')
locale_fr, _ = Locale.objects.get_or_create(language_code='fr-CH')
LocaleSynchronization.objects.get_or_create(
locale_id=locale_fr.id,
sync_from_id=locale_de.id
)
locale_it, _ = Locale.objects.get_or_create(language_code='it-CH')
LocaleSynchronization.objects.get_or_create(
locale_id=locale_it.id,
sync_from_id=locale_de.id
)
call_command('sync_locale_trees')
# all pages belong to 'admin' by default
Page.objects.update(owner=user)
def delete_default_learning_path():
LearningContent.objects.all().delete()
LearningUnitQuestion.objects.all().delete()
LearningUnit.objects.all().delete()
LearningSequence.objects.all().delete()
Circle.objects.all().delete()
Topic.objects.all().delete()

View File

@ -57,7 +57,7 @@ class LearningContentFactory(wagtail_factories.PageFactory):
class VideoBlockFactory(wagtail_factories.StructBlockFactory):
url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
url = "https://www.youtube.com/embed/qhPIfxS2hvI"
description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam"
class Meta:
@ -65,7 +65,7 @@ class VideoBlockFactory(wagtail_factories.StructBlockFactory):
class WebBasedTrainingBlockFactory(wagtail_factories.StructBlockFactory):
url = "https://www.example.com"
url = "/media/web_based_trainings/rise_cmi5_test_export/scormcontent/index.html"
description = "Beispiel Rise Modul"
class Meta:
@ -74,7 +74,7 @@ class WebBasedTrainingBlockFactory(wagtail_factories.StructBlockFactory):
class PodcastBlockFactory(wagtail_factories.StructBlockFactory):
description = "Beispiel Podcast"
url = "https://docs.wagtail.org/en/stable/topics/streamfield.html"
url = "https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/325190984&color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true"
class Meta:
model = PodcastBlock

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px"
y="0px"
viewBox="0 0 450.709 418.11" enable-background="new 0 0 450.709 418.11" xml:space="preserve">
<g>
<g>
<path d="M262.573,231.338c-8.74-7.379-17.496-10.559-28.81-10.559c-23.314,0-38.378,16.943-38.378,43.168v67.706
c0,1.566,1.269,2.835,2.835,2.835h11.603c1.566,0,2.835-1.269,2.835-2.835v-40.89h33.462c1.566,0,2.835-1.269,2.835-2.835v-10.34
c0-1.566-1.269-2.835-2.835-2.835h-33.462v-11.31c0-17.186,7.677-26.653,21.612-26.653c7.148,0,12.845,2.163,18.155,6.957
c0.069,0.063,0.586,0.523,1.2,1.07c1.228,1.093,3.126,0.917,4.13-0.385l6.31-8.181c0.888-1.151,0.76-2.788-0.295-3.789
C263.175,231.899,262.648,231.401,262.573,231.338z"/>
</g>
<g>
<path d="M225.62,125.182c0.003-1.573,1.289-2.843,2.863-2.829c0.675,0.006,1.231,0.014,1.333,0.024
c6.856,0.657,13.394,3.223,18.955,7.448c9.876,7.504,15.388,19.34,14.743,31.662c-1.041,19.793-17.869,35.296-38.312,35.296
h-26.977c-1.566,0-2.835-1.269-2.835-2.835V86.457c0-1.566,1.269-2.835,2.835-2.835h11.585c1.566,0,2.835,1.269,2.835,2.835
v91.169c0,1.566,1.269,2.835,2.835,2.835h10.081c11.139,0,20.622-8.355,21.591-19.019c1.042-11.523-7.505-21.809-19.019-22.824
c-1.445-0.127-2.537-1.369-2.534-2.82L225.62,125.182z"/>
</g>
</g>
<g>
<path fill="#0069B0" d="M351.43,291.772"/>
<path fill="#0079C3" d="M366.594,287.348c1.275,1.882-0.074,4.424-2.347,4.424h0.012H351.79c-0.941,0-1.82-0.467-2.347-1.245
l-29.151-43.055l-29.151,43.055c-0.527,0.779-1.406,1.245-2.347,1.245h-12.47c-2.273,0-3.622-2.542-2.347-4.424l43.967-64.92
c1.124-1.66,3.57-1.66,4.694,0L366.594,287.348z"/>
<path d="M176.731,287.348c1.275,1.882-0.074,4.424-2.347,4.424h0.012h-12.469c-0.941,0-1.82-0.467-2.347-1.245l-29.151-43.055
l-29.151,43.055c-0.527,0.779-1.406,1.245-2.347,1.245h-12.47c-2.273,0-3.622-2.542-2.347-4.424l43.967-64.92
c1.124-1.66,3.57-1.66,4.694,0L176.731,287.348z"/>
<path fill="#0079C3" d="M84.114,132.238c-1.275-1.882,0.074-4.424,2.347-4.424h-0.012h12.469c0.941,0,1.82,0.467,2.347,1.245
l29.151,43.055l29.151-43.055c0.527-0.779,1.406-1.245,2.347-1.245h12.47c2.273,0,3.622,2.542,2.347,4.424l-43.967,64.92
c-1.124,1.66-3.57,1.66-4.694,0L84.114,132.238z"/>
<path d="M273.978,132.238c-1.275-1.882,0.074-4.424,2.347-4.424h-0.012h12.469c0.941,0,1.82,0.467,2.347,1.245l29.151,43.055
l29.151-43.055c0.527-0.779,1.406-1.245,2.347-1.245h12.47c2.273,0,3.622,2.542,2.347,4.424l-43.967,64.92
c-1.124,1.66-3.57,1.66-4.694,0L273.978,132.238z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px"
y="0px"
viewBox="0 0 450.709 418.11" enable-background="new 0 0 450.709 418.11" xml:space="preserve">
<g>
<g>
<path fill="#FFFFFF" d="M262.573,231.338c-8.74-7.379-17.496-10.559-28.81-10.559c-23.314,0-38.378,16.943-38.378,43.168v67.706
c0,1.566,1.269,2.835,2.835,2.835h11.603c1.566,0,2.835-1.269,2.835-2.835v-40.89h33.462c1.566,0,2.835-1.269,2.835-2.835v-10.34
c0-1.566-1.269-2.835-2.835-2.835h-33.462v-11.31c0-17.186,7.677-26.653,21.612-26.653c7.148,0,12.845,2.163,18.155,6.957
c0.069,0.063,0.586,0.523,1.2,1.07c1.228,1.093,3.126,0.917,4.13-0.385l6.31-8.181c0.888-1.151,0.76-2.788-0.295-3.789
C263.175,231.899,262.648,231.401,262.573,231.338z"/>
</g>
<g>
<path fill="#FFFFFF" d="M225.62,125.182c0.003-1.573,1.289-2.843,2.863-2.829c0.675,0.006,1.231,0.014,1.333,0.024
c6.856,0.657,13.394,3.223,18.955,7.448c9.876,7.504,15.388,19.34,14.743,31.662c-1.041,19.793-17.869,35.296-38.312,35.296
h-26.977c-1.566,0-2.835-1.269-2.835-2.835V86.457c0-1.566,1.269-2.835,2.835-2.835h11.585c1.566,0,2.835,1.269,2.835,2.835
v91.169c0,1.566,1.269,2.835,2.835,2.835h10.081c11.139,0,20.622-8.355,21.591-19.019c1.042-11.523-7.505-21.809-19.019-22.824
c-1.445-0.127-2.537-1.369-2.534-2.82L225.62,125.182z"/>
</g>
</g>
<g>
<path fill="#0069B0" d="M351.43,291.772"/>
<path fill="#0079C3" d="M366.594,287.348c1.275,1.882-0.074,4.424-2.347,4.424h0.012H351.79c-0.941,0-1.82-0.467-2.347-1.245
l-29.151-43.055l-29.151,43.055c-0.527,0.779-1.406,1.245-2.347,1.245h-12.47c-2.273,0-3.622-2.542-2.347-4.424l43.967-64.92
c1.124-1.66,3.57-1.66,4.694,0L366.594,287.348z"/>
<path fill="#FFFFFF" d="M176.731,287.348c1.275,1.882-0.074,4.424-2.347,4.424h0.012h-12.469c-0.941,0-1.82-0.467-2.347-1.245
l-29.151-43.055l-29.151,43.055c-0.527,0.779-1.406,1.245-2.347,1.245h-12.47c-2.273,0-3.622-2.542-2.347-4.424l43.967-64.92
c1.124-1.66,3.57-1.66,4.694,0L176.731,287.348z"/>
<path fill="#0079C3" d="M84.114,132.238c-1.275-1.882,0.074-4.424,2.347-4.424h-0.012h12.469c0.941,0,1.82,0.467,2.347,1.245
l29.151,43.055l29.151-43.055c0.527-0.779,1.406-1.245,2.347-1.245h12.47c2.273,0,3.622,2.542,2.347,4.424l-43.967,64.92
c-1.124,1.66-3.57,1.66-4.694,0L84.114,132.238z"/>
<path fill="#FFFFFF" d="M273.978,132.238c-1.275-1.882,0.074-4.424,2.347-4.424h-0.012h12.469c0.941,0,1.82,0.467,2.347,1.245
l29.151,43.055l29.151-43.055c0.527-0.779,1.406-1.245,2.347-1.245h12.47c2.273,0,3.622,2.542,2.347,4.424l-43.967,64.92
c-1.124,1.66-3.57,1.66-4.694,0L273.978,132.238z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -5,9 +5,13 @@
{% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}
<div class="content">
<form action="/cypressreset/" method="post">
<form action="/core/cypressreset/" method="post">
{% csrf_token %}
<button class="" name="">Testdaten zurück setzen</button>
<button class="btn" name="">Testdaten zurück setzen</button>
</form>
<form action="/core/schemareset/" method="post">
{% csrf_token %}
<button class="btn" name="">Datenbank zurück setzen</button>
</form>
</div>
</div>