Merge branch 'feature/bitbucket-unit-tests' into develop

This commit is contained in:
Daniel Egger 2022-08-26 18:20:03 +02:00
commit 5875f13143
60 changed files with 341 additions and 1026 deletions

View File

@ -9,7 +9,7 @@ pipelines:
services: services:
- postgres - postgres
caches: caches:
- vbvpip - pip
script: script:
- source ./env/bitbucket/prepare_for_test.sh - source ./env/bitbucket/prepare_for_test.sh
- pip install -r server/requirements/requirements-dev.txt - pip install -r server/requirements/requirements-dev.txt
@ -27,14 +27,15 @@ pipelines:
- cypress/**/*.mp4 - cypress/**/*.mp4
caches: caches:
- node - node
- vbvpip - pip
- cypress - cypress
script: script:
- export IT_SERVE_VUE=false
- export IT_ALLOW_LOCAL_LOGIN=true
- source ./env/bitbucket/prepare_for_test.sh - source ./env/bitbucket/prepare_for_test.sh
- python -m venv vbvvenv
- source vbvvenv/bin/activate
- pip install -r server/requirements/requirements-dev.txt - pip install -r server/requirements/requirements-dev.txt
- npm install - npm install
- npm run build
- ./prepare_server_cypress.sh --start-background - ./prepare_server_cypress.sh --start-background
- npm run cypress:ci - npm run cypress:ci
# - npm run build # - npm run build
@ -52,20 +53,20 @@ pipelines:
deployment: prod deployment: prod
trigger: manual trigger: manual
script: script:
- ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://myservicecrm.swisscom.ch/deploy-iesc-bKVAkQguPDVi - ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://myservicecrm.swisscom.ch/deploy-iesc-xxx
custom: custom:
deploy-preprod: deploy-preprod:
- step: - step:
name: Deploy to PREPROD name: Deploy to PREPROD
deployment: preprod deployment: preprod
script: script:
- ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://preprod.myservicecrm.ch/deploy-iesc-bKVAkQguPDVi - ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://preprod.myservicecrm.ch/deploy-iesc-xxx
deploy-api: deploy-api:
- step: - step:
name: Deploy to API name: Deploy to API
deployment: api deployment: api
script: script:
- ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://api.myservicecrm.ch/deploy-iesc-bKVAkQguPDVi - ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://api.myservicecrm.ch/deploy-iesc-xxx
definitions: definitions:
caches: caches:

View File

@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import * as log from 'loglevel'; import * as log from 'loglevel';
import { onMounted, reactive} from 'vue'; import {onMounted, reactive} from 'vue';
import { useUserStore } from '@/stores/user'; import {useUserStore} from '@/stores/user';
import { useLearningPathStore } from '@/stores/learningPath'; import {useLearningPathStore} from '@/stores/learningPath';
import { useRoute, useRouter } from 'vue-router'; import {useRoute, useRouter} from 'vue-router';
import { useAppStore } from '@/stores/app'; import {useAppStore} from '@/stores/app';
import IconLogout from "@/components/icons/IconLogout.vue"; import IconLogout from "@/components/icons/IconLogout.vue";
import IconSettings from "@/components/icons/IconSettings.vue"; import IconSettings from "@/components/icons/IconSettings.vue";
import ItDropdown from "@/components/ui/ItDropdown.vue"; import ItDropdown from "@/components/ui/ItDropdown.vue";
@ -146,6 +146,7 @@ const profileDropdownData = [
v-if="userStore.loggedIn" v-if="userStore.loggedIn"
to="/messages" to="/messages"
class="nav-item flex flex-row items-center" class="nav-item flex flex-row items-center"
data-cy="messages-link"
> >
<it-icon-message class="w-8 h-8 mr-6"/> <it-icon-message class="w-8 h-8 mr-6"/>
</router-link> </router-link>
@ -213,6 +214,7 @@ const profileDropdownData = [
<router-link <router-link
to="/messages" to="/messages"
class="nav-item flex flex-row items-center" class="nav-item flex flex-row items-center"
data-cy="messages-link"
> >
<it-icon-message class="w-8 h-8 mr-6"/> <it-icon-message class="w-8 h-8 mr-6"/>
</router-link> </router-link>

View File

@ -1,13 +1,13 @@
import {createApp} from 'vue' import {createApp} from 'vue'
import {createPinia} from 'pinia' import {createPinia} from 'pinia'
import {setupI18n} from './i18n' // import {setupI18n} from './i18n'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import '../tailwind.css' import '../tailwind.css'
const i18n = setupI18n() // const i18n = setupI18n()
const app = createApp(App) const app = createApp(App)
// todo: define lang setup // todo: define lang setup
@ -15,6 +15,6 @@ const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.use(i18n) // app.use(i18n)
app.mount('#app') app.mount('#app')

View File

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

View File

@ -48,7 +48,7 @@ onMounted(async () => {
<LearningPathDiagram class="max-w-[1680px] w-full" identifier="mainVisualization" v-bind:vertical="false"></LearningPathDiagram> <LearningPathDiagram class="max-w-[1680px] w-full" identifier="mainVisualization" v-bind:vertical="false"></LearningPathDiagram>
</div> </div>
<h1 class="m-12">{{ learningPathStore.learningPath.title }}</h1> <h1 data-cy="learning-path-title" class="m-12">{{ learningPathStore.learningPath.title }}</h1>
<div <div
class="bg-white m-12 p-8 flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-gray-500 justify-start"> class="bg-white m-12 p-8 flex flex-col lg:flex-row divide-y lg:divide-y-0 lg:divide-x divide-gray-500 justify-start">

View File

@ -48,6 +48,7 @@ const userStore = useUserStore();
<div> <div>
<input <input
data-cy="login-button"
type="submit" type="submit"
value="Login" value="Login"
class="btn-primary" class="btn-primary"

24
cypress.config.js Normal file
View File

@ -0,0 +1,24 @@
const { defineConfig } = require('cypress')
module.exports = defineConfig({
watchForFileChanges: false,
video: false,
retries: {
runMode: 1,
openMode: 0,
},
reporter: 'junit',
reporterOptions: {
mochaFile: 'cypress/test-reports/cypress-results-[hash].xml',
toConsole: true,
},
e2e: {
// experimentalSessionAndOrigin: true,
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:8001',
},
})

View File

@ -1,14 +0,0 @@
{
"baseUrl": "http://localhost:8001",
"watchForFileChanges": false,
"video": false,
"retries": {
"runMode": 1,
"openMode": 0
},
"reporter": "junit",
"reporterOptions": {
"mochaFile": "cypress/test-reports/cypress-results-[hash].xml",
"toConsole": true
}
}

7
cypress/e2e/helpers.js Normal file
View File

@ -0,0 +1,7 @@
export const login = (username, password) => {
cy.request({
method: 'POST',
url: '/core/login/',
body: { username, password },
})
}

47
cypress/e2e/login.cy.js Normal file
View File

@ -0,0 +1,47 @@
import { login } from "./helpers";
describe("login", () => {
beforeEach(() => {
cy.manageCommand("cypress_reset");
});
it("can login to app with username/password", () => {
cy.visit("/");
cy.get("#username").type("admin");
cy.get("#password").type("test");
cy.get('[data-cy="login-button"]').click();
cy.request("/api/core/me").its("status").should("eq", 200);
cy.get('[data-cy="welcome-message"]').should(
"contain",
"Willkommen, Peter"
);
});
it("can login with helper function", () => {
login("admin", "test");
cy.visit("/");
cy.request("/api/core/me").its("status").should("eq", 200);
cy.get('[data-cy="welcome-message"]').should(
"contain",
"Willkommen, Peter"
);
});
it("login will redirect to requestet page", () => {
cy.visit("/learningpath/versicherungsvermittlerin");
cy.get("h1").should("contain", "Login");
cy.get("#username").type("admin");
cy.get("#password").type("test");
cy.get('[data-cy="login-button"]').click();
cy.get('[data-cy="learning-path-title"]').should(
"contain",
"Versicherungsvermittler"
);
});
});

View File

@ -1,148 +0,0 @@
/// <reference types="cypress" />
// Welcome to Cypress!
//
// This spec file contains a variety of sample tests
// for a todo list app that are designed to demonstrate
// the power of writing tests in Cypress.
//
// To learn more about how Cypress works and
// what makes it such an awesome testing tool,
// please read our getting started guide:
// https://on.cypress.io/introduction-to-cypress
describe('example to-do app', () => {
beforeEach(() => {
cy.manageCommand('cypress_reset');
cy.visit('/todo/');
cy.get("#username").type("cypress@example.com");
cy.get("#password").type("test");
cy.get('[data-cy="submit"]').click();
})
it.skip('can access simple todo page', () => {
cy.get('[data-cy="simple-list-title"]').should('contain', 'Todos');
});
// it('displays two todo items by default', () => {
// // We use the `cy.get()` command to get all elements that match the selector.
// // Then, we use `should` to assert that there are two matched items,
// // which are the two default items.
// cy.get('.todo-list li').should('have.length', 2)
//
// // We can go even further and check that the default todos each contain
// // the correct text. We use the `first` and `last` functions
// // to get just the first and last matched elements individually,
// // and then perform an assertion with `should`.
// cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
// cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
// })
// it('can add new todo items', () => {
// // We'll store our item text in a variable so we can reuse it
// const newItem = 'Feed the cat'
//
// // Let's get the input element and use the `type` command to
// // input our new list item. After typing the content of our item,
// // we need to type the enter key as well in order to submit the input.
// // This input has a data-test attribute so we'll use that to select the
// // element in accordance with best practices:
// // https://on.cypress.io/selecting-elements
// cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
//
// // Now that we've typed our new item, let's check that it actually was added to the list.
// // Since it's the newest item, it should exist as the last element in the list.
// // In addition, with the two default items, we should have a total of 3 elements in the list.
// // Since assertions yield the element that was asserted on,
// // we can chain both of these assertions together into a single statement.
// cy.get('.todo-list li')
// .should('have.length', 3)
// .last()
// .should('have.text', newItem)
// })
// it('can check off an item as completed', () => {
// // In addition to using the `get` command to get an element by selector,
// // we can also use the `contains` command to get an element by its contents.
// // However, this will yield the <label>, which is lowest-level element that contains the text.
// // In order to check the item, we'll find the <input> element for this <label>
// // by traversing up the dom to the parent element. From there, we can `find`
// // the child checkbox <input> element and use the `check` command to check it.
// cy.contains('Pay electric bill')
// .parent()
// .find('input[type=checkbox]')
// .check()
//
// // Now that we've checked the button, we can go ahead and make sure
// // that the list element is now marked as completed.
// // Again we'll use `contains` to find the <label> element and then use the `parents` command
// // to traverse multiple levels up the dom until we find the corresponding <li> element.
// // Once we get that element, we can assert that it has the completed class.
// cy.contains('Pay electric bill')
// .parents('li')
// .should('have.class', 'completed')
// })
//
// context('with a checked task', () => {
// beforeEach(() => {
// // We'll take the command we used above to check off an element
// // Since we want to perform multiple tests that start with checking
// // one element, we put it in the beforeEach hook
// // so that it runs at the start of every test.
// cy.contains('Pay electric bill')
// .parent()
// .find('input[type=checkbox]')
// .check()
// })
//
// it('can filter for uncompleted tasks', () => {
// // We'll click on the "active" button in order to
// // display only incomplete items
// cy.contains('Active').click()
//
// // After filtering, we can assert that there is only the one
// // incomplete item in the list.
// cy.get('.todo-list li')
// .should('have.length', 1)
// .first()
// .should('have.text', 'Walk the dog')
//
// // For good measure, let's also assert that the task we checked off
// // does not exist on the page.
// cy.contains('Pay electric bill').should('not.exist')
// })
//
// it('can filter for completed tasks', () => {
// // We can perform similar steps as the test above to ensure
// // that only completed tasks are shown
// cy.contains('Completed').click()
//
// cy.get('.todo-list li')
// .should('have.length', 1)
// .first()
// .should('have.text', 'Pay electric bill')
//
// cy.contains('Walk the dog').should('not.exist')
// })
//
// it('can delete all completed tasks', () => {
// // First, let's click the "Clear completed" button
// // `contains` is actually serving two purposes here.
// // First, it's ensuring that the button exists within the dom.
// // This button only appears when at least one task is checked
// // so this command is implicitly verifying that it does exist.
// // Second, it selects the button so we can click it.
// cy.contains('Clear completed').click()
//
// // Then we can make sure that there is only one element
// // in the list and our element does not exist
// cy.get('.todo-list li')
// .should('have.length', 1)
// .should('not.have.text', 'Pay electric bill')
//
// // Finally, make sure that the clear button no longer exists.
// cy.contains('Clear completed').should('not.exist')
// })
// })
})

View File

@ -53,9 +53,16 @@
const _ = Cypress._; const _ = Cypress._;
Cypress.Commands.add('manageCommand', (command, preCommand = '') => { Cypress.Commands.add('manageCommand', (command, preCommand = '') => {
const execCommand = `${preCommand} python3 server/manage.py ${command} --settings=config.settings.test_cypress`; const execCommand = `${preCommand} python server/manage.py ${command} --settings=config.settings.test_cypress`;
console.log(execCommand); console.log(execCommand);
return cy.exec(execCommand); return cy.exec(execCommand, { failOnNonZeroExit: false }).then(result => {
if(result.code) {
throw new Error(`Execution of "${command}" failed
Exit code: ${result.code}
Stdout:\n${result.stdout}
Stderr:\n${result.stderr}`);
}
});
}); });
Cypress.Commands.add('manageShellCommand', (command) => { Cypress.Commands.add('manageShellCommand', (command) => {

View File

@ -3,7 +3,7 @@
# push new version to Docker Hub # push new version to Docker Hub
# > docker push iterativ/vbv-lernwelt-bitbucket # > docker push iterativ/vbv-lernwelt-bitbucket
# run locally with directory mounted # run locally with directory mounted
# > docker run -v $(dirname "$(pwd)"):/src -it iterativ/vbv-lernwelt-bitbucket /bin/bash # > docker run -v "$(pwd)":/src -it iterativ/vbv-lernwelt-bitbucket /bin/bash
FROM python:3.10-bullseye FROM python:3.10-bullseye
MAINTAINER Daniel Egger <daniel.egger@iterativ.ch> MAINTAINER Daniel Egger <daniel.egger@iterativ.ch>

View File

@ -5,13 +5,9 @@
"build": "npm install --prefix client && npm run build --prefix client && npm run build:tailwind --prefix client", "build": "npm install --prefix client && npm run build --prefix client && npm run build:tailwind --prefix client",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:run": "cypress run", "cypress:ci": "cypress run"
"cypress:ci": "cypress run --config baseUrl=http://localhost:8001",
"cypress:ci:open": "cypress open --config baseUrl=http://localhost:8001"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.2", "cypress": "^10.6.0"
"@tailwindcss/typography": "^0.5.2",
"cypress": "^9.4.1"
} }
} }

View File

@ -103,7 +103,6 @@ THIRD_PARTY_APPS = [
LOCAL_APPS = [ LOCAL_APPS = [
"vbv_lernwelt.core", "vbv_lernwelt.core",
"vbv_lernwelt.simpletodo",
"vbv_lernwelt.sso", "vbv_lernwelt.sso",
"vbv_lernwelt.learnpath", "vbv_lernwelt.learnpath",
"vbv_lernwelt.completion", "vbv_lernwelt.completion",

View File

@ -4,7 +4,6 @@ import os
os.environ['IT_APP_ENVIRONMENT'] = 'development' os.environ['IT_APP_ENVIRONMENT'] = 'development'
from .base import * # noqa from .base import * # noqa
from .base import env
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
TEST_RUNNER = "django.test.runner.DiscoverRunner" TEST_RUNNER = "django.test.runner.DiscoverRunner"

View File

@ -1,4 +1,7 @@
# pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position # pylint: disable=unused-wildcard-import,wildcard-import,wrong-import-position
import os
os.environ['IT_APP_ENVIRONMENT'] = 'development'
from .base import * # noqa from .base import * # noqa

View File

@ -34,7 +34,6 @@ urlpatterns = [
path('admin/raise_error/', user_passes_test(lambda u: u.is_superuser, login_url='/login/')(raise_example_error), ), path('admin/raise_error/', user_passes_test(lambda u: u.is_superuser, login_url='/login/')(raise_example_error), ),
path(settings.ADMIN_URL, admin.site.urls), path(settings.ADMIN_URL, admin.site.urls),
path("checkratelimit/", check_rate_limit), path("checkratelimit/", check_rate_limit),
path("todo/", include("vbv_lernwelt.simpletodo.urls")),
path("sso/", include("vbv_lernwelt.sso.urls")), path("sso/", include("vbv_lernwelt.sso.urls")),
path('cms/', include(wagtailadmin_urls)), path('cms/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)), path('documents/', include(wagtaildocs_urls)),
@ -42,18 +41,14 @@ urlpatterns = [
path('learnpath/', include("vbv_lernwelt.learnpath.urls")), path('learnpath/', include("vbv_lernwelt.learnpath.urls")),
path('api/completion/', include("vbv_lernwelt.completion.urls")), path('api/completion/', include("vbv_lernwelt.completion.urls")),
re_path(r'api/core/me/$', me_user_view, name='me_user_view'), re_path(r'api/core/me/$', me_user_view, name='me_user_view'),
re_path(r'core/login/$', django_view_authentication_exempt(vue_login), name='vue_login'),
re_path(r'core/logout/$', vue_logout, name='vue_logout'), re_path(r'core/logout/$', vue_logout, name='vue_logout'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG: if settings.DEBUG:
# Static file serving when using Gunicorn + Uvicorn for local web socket development # Static file serving when using Gunicorn + Uvicorn for local web socket development
urlpatterns += staticfiles_urlpatterns() urlpatterns += staticfiles_urlpatterns()
if settings.ALLOW_LOCAL_LOGIN:
urlpatterns += [
re_path(r'core/login/$', django_view_authentication_exempt(vue_login), name='vue_login'),
]
# API URLS # API URLS
urlpatterns += [ urlpatterns += [
# API base url # API base url
@ -113,4 +108,3 @@ if settings.DEBUG:
# serve everything else via the vue app # serve everything else via the vue app
urlpatterns += [re_path(r'^.*$', vue_home, name='home')] urlpatterns += [re_path(r'^.*$', vue_home, name='home')]

View File

@ -14,8 +14,8 @@ class CircleCompletion(models.Model):
# Page can either be a LearningContent or a LearningUnitQuestion for now # Page can either be a LearningContent or a LearningUnitQuestion for now
page_key = models.UUIDField() page_key = models.UUIDField()
page_type = models.CharField(max_length=255, default='', blank=True) page_type = models.CharField(max_length=255, default='', blank=True)
circle_key = models.UUIDField() circle_key = models.UUIDField(blank=True, default='')
learning_path_key = models.UUIDField() learning_path_key = models.UUIDField(blank=True, default='')
completed = models.BooleanField(default=False) completed = models.BooleanField(default=False)
json_data = models.JSONField(default=dict, blank=True) json_data = models.JSONField(default=dict, blank=True)

View File

@ -4,25 +4,21 @@ from rest_framework.test import APITestCase
from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.learnpath.models import LearningContent from vbv_lernwelt.learnpath.models import LearningContent
from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path from vbv_lernwelt.learnpath.tests.create_simple_test_learning_path import create_simple_test_learning_path
from vbv_lernwelt.learnpath.tests.test_create_default_learning_path import create_locales_for_wagtail
class CompletionApiTestCase(APITestCase): class CompletionApiTestCase(APITestCase):
@classmethod def setUp(self) -> None:
def setUpClass(cls) -> None:
super(CompletionApiTestCase, cls).setUpClass()
create_locales_for_wagtail() create_locales_for_wagtail()
create_default_users() create_default_users()
create_default_learning_path() create_simple_test_learning_path()
def setUp(self) -> None:
self.user = User.objects.get(username='student') self.user = User.objects.get(username='student')
self.client.login(username='student', password='test') self.client.login(username='student', password='test')
def test_completeLearningContent_works(self): def test_completeLearningContent_works(self):
learning_content = LearningContent.objects.get(title='Einleitung Circle "Anlayse"') learning_content = LearningContent.objects.get(title='Einleitung Circle "Unit-Test Circle"')
learning_content_key = str(learning_content.translation_key) learning_content_key = str(learning_content.translation_key)
circle_key = str(learning_content.get_parent().translation_key) circle_key = str(learning_content.get_parent().translation_key)

View File

@ -1,13 +1,16 @@
import djclick as click import djclick as click
from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path, \ from vbv_lernwelt.learnpath.create_default_learning_path import create_default_learning_path, \
delete_default_learning_path delete_default_learning_path
@click.command() @click.command()
@click.option("--customer_language", default="de") @click.option('--reset-learning-path', default=False)
def command(customer_language): def command(reset_learning_path):
print("cypress reset data") print("cypress reset data")
delete_default_learning_path() if reset_learning_path:
create_default_learning_path(skip_locales=True) delete_default_learning_path()
create_default_learning_path(skip_locales=True)

View File

@ -0,0 +1,7 @@
from django.conf import settings
from wagtail.models import Locale
def create_locales_for_wagtail():
for language in settings.WAGTAIL_CONTENT_LANGUAGES:
Locale.objects.get_or_create(language_code=language[0])

View File

@ -1,16 +0,0 @@
from django.test import TestCase
from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.tests.factories import UserFactory
from vbv_lernwelt.simpletodo.models import SimpleList
class TestUserCreation(TestCase):
def test_create_user(self):
User(last_name='Sepp').save()
def test_simple(self):
# create_default_learning_path()
self.user = UserFactory()
SimpleList.objects.get_or_create(title='Default', user=self.user)
self.assertTrue(True)

View File

@ -48,20 +48,23 @@ def vue_home(request):
@api_view(['POST']) @api_view(['POST'])
@ensure_csrf_cookie @ensure_csrf_cookie
def vue_login(request): def vue_login(request):
try: if settings.ALLOW_LOCAL_LOGIN:
username = request.data.get('username') try:
password = request.data.get('password') username = request.data.get('username')
if username and password: password = request.data.get('password')
user = authenticate(request, username=username, password=password) if username and password:
if user: user = authenticate(request, username=username, password=password)
login(request, user) if user:
logger.debug('login successful', username=username, email=user.email, label='login') login(request, user)
return Response(UserSerializer(user).data) logger.debug('login successful', username=username, email=user.email, label='login')
except Exception as e: return Response(UserSerializer(user).data)
logger.exception(e) except Exception as e:
logger.exception(e)
logger.debug('login failed', username=username, label='login') logger.debug('login failed', username=username, label='login')
return Response({'success': False}, status=401) return Response({'success': False}, status=401)
else:
return Response({'success': False, 'message': 'ALLOW_LOCAL_LOGIN=false'}, status=403)
@api_view(['GET']) @api_view(['GET'])

View File

@ -1,8 +1,9 @@
import djclick as click import djclick as click
from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path from vbv_lernwelt.learnpath.create_default_learning_path import create_default_learning_path
@click.command() @click.command()
def command(): def command():
create_default_learning_path(skip_locales=True) create_default_learning_path(skip_locales=True)
# create_simple_test_learning_path(skip_locales=True)

View File

@ -1,6 +1,6 @@
import djclick as click import djclick as click
from vbv_lernwelt.learnpath.tests.create_default_learning_path import delete_default_learning_path from vbv_lernwelt.learnpath.create_default_learning_path import delete_default_learning_path
@click.command() @click.command()

View File

@ -1,13 +1,12 @@
# Create your models here. from django.db import models
from django.utils.text import slugify from django.utils.text import slugify
from wagtail import blocks from wagtail import blocks
from wagtail.admin.panels import FieldPanel, StreamFieldPanel
from wagtail.blocks import StreamBlock from wagtail.blocks import StreamBlock
from wagtail.fields import StreamField from wagtail.fields import StreamField
from wagtail.images.blocks import ImageChooserBlock from wagtail.images.blocks import ImageChooserBlock
from wagtail.models import Page, Orderable from wagtail.models import Page, Orderable
from vbv_lernwelt.learnpath.models_competences import *
from vbv_lernwelt.learnpath.models_learning_unit_content import WebBasedTrainingBlock, VideoBlock, PodcastBlock, \ from vbv_lernwelt.learnpath.models_learning_unit_content import WebBasedTrainingBlock, VideoBlock, PodcastBlock, \
CompetenceBlock, ExerciseBlock, DocumentBlock, KnowledgeBlock CompetenceBlock, ExerciseBlock, DocumentBlock, KnowledgeBlock
from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class from vbv_lernwelt.learnpath.serializer_helpers import get_it_serializer_class

View File

@ -1,66 +0,0 @@
from django.db import models
from wagtail.models import Page, Orderable
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import FieldPanel, StreamFieldPanel, InlinePanel
class CompetencePage(Page):
"""This is the page where the competences and Fullfillment criterias are manged
For one Learning Path"""
content_panels = Page.content_panels + [
InlinePanel('competences', label="Competences"),
]
subpage_types = ['learnpath.Circle']
parent_page_types = ['learnpath.LearningPath']
class Meta:
verbose_name = "Learning Path"
def __str__(self):
return f"{self.title}"
class Competence(Orderable):
""" In VBV Terms this is a "Handlungskompetenz"""
category_short = models.CharField(max_length=3, default='')
name = models.CharField(max_length=2048)
competence_page = ParentalKey('learnpath.CompetencePage',
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='competences',
)
def get_short_info(self):
return f"{self.category_short}{self.sort_order}"
def __str__(self):
return f"{self.get_short_info()}: {self.name}"
class Meta:
verbose_name = "Competence"
class FullfillmentCriteria(Orderable):
""" VBV Term Leistungskriterium"""
name = models.CharField(max_length=2048)
competence = models.ForeignKey(Competence, on_delete=models.CASCADE, null=True)
def get_short_info(self):
return f"{self.competence.get_short_info()}.{self.sort_order}"
def __str__(self):
return f"{self.get_short_info()}: {self.name}"
class Meta:
verbose_name = "Fullfillment Criteria"

View File

@ -1,78 +0,0 @@
{
"competences": [
{
"name": "Weiterempfehlung für Neukunden generieren",
"category_short": "A",
"fullfillment_criteria": [
{
"name": "bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden"
},
{
"name": "geeignete Personen wie z.B. Garagisten, Architekten, Treuhänder auf die Vermittlung/Zusammenarbeit anzusprechen"
},
{
"name": "verschiedene Datenquellen wie Internet, Telefonbuch, Handelszeitung, Baugesuche etc. gezielt für die Gewinnung von Neukunden zu benützen"
},
{
"name": "ein beliebiges Gespräch resp. einen bestehenden Kontakt in die Richtung «Versicherung» zu lenken"
},
{
"name": "das Thema Risiko und Sicherheit in einem Gespräch gezielt und auf die Situation des jeweiligen Gesprächspartners bezogen einfliessen zu lassen"
},
{
"name": "im täglichen Kontakt potentielle Kundinnen und Kunden zu erkennen"
}
]
},
{
"name": "Kundengespräche vereinbaren",
"category_short": "A",
"fullfillment_criteria": [
{
"name": "je nach (Neu-) Kunde Form und Ort für das Gespräch festzulegen"
},
{
"name": "sich intern und extern die nötigen Informationen über den (Neu-) Kunde zu beschaffen"
},
{
"name": "die Terminierung auf ein bestimmtes Thema wie z.B. Rechtsschutz, Vorsorge, Krankenversicherung etc. auszurichten"
},
{
"name": "für das zu führende Gespräch eine Agenda zu erstellen"
},
{
"name": "für das zu führende Gespräch geeignete Hilfsmittel und Unterlagen zusammenzustellen"
}, {
"name": "eine Kaltakquise durchzuführen und auf mögliche Einwände reagieren zu können"
}
]
},
{
"name": "Auftritt in den sozialen Medien zeitgemäss halten",
"category_short": "A",
"fullfillment_criteria": [ {
"name": "in Zusammenarbeit mit den IT-Spezialisten und der Marketingabteilung die Inhalte für den zu realisierenden Medienauftritt zielgruppengerecht festzulegen"
},
{
"name": "für die verschiedenen Kundensegmente die passenden sozialen Medien zu definieren"
},
{
"name": "die Inhalte compliant zu halten"
}
]
},
{
"name": "Kundendaten erfassen",
"category_short": "A",
"fullfillment_criteria": []
},
{
"name": "Wünsche, Ziele und Bedürfnisse der Kunden im Gespräch ermitteln",
"category_short": "B",
"fullfillment_criteria": []
}
]
}

View File

@ -1,30 +0,0 @@
import factory
import wagtail_factories
from vbv_lernwelt.learnpath.models_competences import Competence, FullfillmentCriteria, CompetencePage
from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFactory
class CompetencePageFactory(wagtail_factories.PageFactory):
# learning_path = factory.SubFactory(LearningPathFactory)
class Meta:
model = CompetencePage
class CompetenceFactory(factory.django.DjangoModelFactory):
category_short = 'A'
name = "Weiterempfehung für neukunden generieren"
competence_page = factory.SubFactory(CompetencePageFactory)
class Meta:
model = Competence
class FullfilmentCriteriaFactory(factory.django.DjangoModelFactory):
name = 'Bestehende Kunden so zu beraten, dass sie von diesen weiterempfohlen werden'
competence = factory.SubFactory(CompetenceFactory)
class Meta:
model = FullfillmentCriteria

View File

@ -1,23 +0,0 @@
import json
import os.path
from vbv_lernwelt.learnpath.tests.competences_factories import CompetenceFactory, FullfilmentCriteriaFactory
competences_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'competences.json')
def create_default_competences(competences_json=competences_file):
with open(competences_json) as f:
competences_json = json.load(f)
for index, compentence in enumerate(competences_json['competences']):
competence_model = CompetenceFactory(name=compentence['name'], category_short=compentence['category_short'], sort_order=index)
print(competence_model)
for criteria_index, criteria in enumerate(compentence['fullfillment_criteria']):
criteria_model = FullfilmentCriteriaFactory(name=criteria['name'], competence=competence_model, sort_order=criteria_index)
print(criteria_model)

View File

@ -0,0 +1,149 @@
import wagtail_factories
from django.conf import settings
from wagtail.models import Site, Page
from vbv_lernwelt.core.admin import User
from vbv_lernwelt.learnpath.tests.learning_path_factories import LearningPathFactory, TopicFactory, CircleFactory, \
LearningSequenceFactory, LearningContentFactory, VideoBlockFactory, PodcastBlockFactory, CompetenceBlockFactory, \
ExerciseBlockFactory, LearningUnitFactory, LearningUnitQuestionFactory
def create_circle(title, learning_path):
return CircleFactory(
title=title,
parent=learning_path,
description="Unit-Test Circle",
job_situations=[
('job_situation', 'Absicherung der Familie'),
('job_situation', 'Reisen'),
],
goals=[
('goal', '... die heutige Versicherungssituation von Privat- oder Geschäftskunden einzuschätzen.'),
('goal', '... deinem Kunden seine optimale Lösung aufzuzeigen'),
],
experts=[
('person', {'last_name': 'Huggel', 'first_name': 'Patrizia', 'email': 'patrizia.huggel@example.com'}),
]
)
def create_circle_children(circle, title):
LearningSequenceFactory(title='Starten', parent=circle, icon='it-icon-ls-start')
LearningContentFactory(
title=f'Einleitung Circle "{title}"',
parent=circle,
minutes=15,
contents=[('video', VideoBlockFactory(
url='https://www.youtube.com/embed/qhPIfxS2hvI',
description='In dieser Circle zeigt dir ein Fachexperte anhand von Kundensituationen, wie du erfolgreich'
'den Kundenbedarf ermitteln, analysieren, priorisieren und anschliessend zusammenfassen kannst.'
))]
)
LearningSequenceFactory(title='Beobachten', parent=circle, icon='it-icon-ls-watch')
lu = LearningUnitFactory(
title='Absicherung der Familie',
parent=circle,
)
LearningUnitQuestionFactory(
title="Ich bin in der Lage, mit geeigneten Fragestellungen die Deckung von Versicherungen zu erfassen.",
parent=lu
)
LearningUnitQuestionFactory(
title="Zweite passende Frage zu 'Absicherung der Familie'",
parent=lu
)
LearningContentFactory(
title='Ermittlung des Kundenbedarfs',
parent=circle,
minutes=30,
contents=[('podcast', PodcastBlockFactory(
description='Die Ermittlung des Kundenbedarfs muss in einem eingehenden Gespräch herausgefunden werden. Höre dazu auch diesen Podcast an.',
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',
))]
)
LearningContentFactory(
title='Kundenbedürfnisse erkennen',
parent=circle,
minutes=30,
contents=[('competence', CompetenceBlockFactory())]
)
LearningContentFactory(
title='Was braucht eine Familie?',
parent=circle,
minutes=60,
contents=[('exercise', ExerciseBlockFactory(url='/media/web_based_trainings/story-01-a-01-patrizia-marco-sichern-sich-ab-einstieg/scormcontent/index.html'
))]
)
lu = LearningUnitFactory(title='Reisen', parent=circle)
LearningUnitQuestionFactory(
title='Passende Frage zu "Reisen"',
parent=lu
)
LearningContentFactory(
title='Reiseversicherung',
parent=circle,
minutes=240,
contents=[('competence', CompetenceBlockFactory())]
)
LearningContentFactory(
title='Sorgenfrei reisen',
parent=circle,
minutes=120,
contents=[('exercise', ExerciseBlockFactory(
url='/media/web_based_trainings/story-06-a-01-emma-und-ayla-campen-durch-amerika-einstieg/scormcontent/index.html'))]
)
LearningSequenceFactory(title='Beenden', parent=circle, icon='it-icon-ls-end')
LearningContentFactory(
title='Kompetenzprofil anschauen',
parent=circle,
minutes=30,
contents=[('document', CompetenceBlockFactory())]
)
LearningContentFactory(
title='Circle "Analyse" abschliessen',
parent=circle,
minutes=30,
contents=[('document', CompetenceBlockFactory())]
)
def create_simple_test_learning_path(user=None, skip_locales=True):
if user is None:
user = User.objects.get(username='info@iterativ.ch')
site = Site.objects.filter(is_default_site=True).first()
if not site:
site = wagtail_factories.SiteFactory(is_default_site=True)
if settings.APP_ENVIRONMENT == 'development':
site.port = 8000
site.save()
lp = LearningPathFactory(title="Unit-Test Lernpfad", parent=site.root_page)
TopicFactory(title="Unit-Test Topic", is_visible=False, parent=lp)
circle_analyse = create_circle('Unit-Test Circle', lp)
create_circle_children(circle_analyse, 'Unit-Test Circle')
# 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)

View File

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

View File

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

View File

@ -2,44 +2,30 @@ from rest_framework.test import APITestCase
from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.admin import User
from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.learnpath.models import LearningPath, Circle from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path from vbv_lernwelt.learnpath.models import LearningPath
from vbv_lernwelt.learnpath.tests.test_create_default_learning_path import create_locales_for_wagtail from vbv_lernwelt.learnpath.tests.create_simple_test_learning_path import create_simple_test_learning_path
class TestRetrieveLearingPathContents(APITestCase): class TestRetrieveLearingPathContents(APITestCase):
@classmethod def setUp(self) -> None:
def setUpClass(cls) -> None:
super(TestRetrieveLearingPathContents, cls).setUpClass()
create_locales_for_wagtail() create_locales_for_wagtail()
create_default_users() create_default_users()
create_default_learning_path() create_simple_test_learning_path()
def setUp(self) -> None: self.user = User.objects.get(username='student')
qs = LearningPath.objects.filter(title="Versicherungsvermittler/in") self.client.login(username='student', password='test')
self.credentials = {
'username': 'admin',
'password': 'admin'}
user = User.objects.get(username='admin') def test_get_learnpathPage(self):
self.client.force_authenticate(user=user) learning_path = LearningPath.objects.get(slug='unit-test-lernpfad')
self.client.post('/login/', self.credentials, follow=True) response = self.client.get('/learnpath/api/page/unit-test-lernpfad/')
print(response)
def test_get_circle(self):
circle = Circle.objects.get(slug='analyse')
response = self.client.get(f'/wagtailapi/v2/pages/{circle.id}/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['title'], 'Analyse') data = response.json()
# print(data)
def test_get_circle_has_learning_sequences(self): self.assertEqual(learning_path.title, data['title'])
circle = Circle.objects.get(slug='analyse') # topic and circle
response = self.client.get(f'/wagtailapi/v2/pages/{circle.id}/') self.assertEqual(2, len(data['children']))
self.assertTrue(len(response.data['learning_sequences']) > 2) # circle "unit-test-circle" contents
self.assertEqual(response.data['learning_sequences'][0]['title'], 'Starten') self.assertEqual(13, len(data['children'][1]['children']))
def test_get_circle_has_learning_sequences_learningpackages(self):
circle = Circle.objects.get(slug='analyse')
response = self.client.get(f'/wagtailapi/v2/pages/{circle.id}/')
self.assertTrue(len(response.data['learning_sequences'][0]['learnging_packages']) > 1)
self.assertTrue(response.data['learning_sequences'][0]['learnging_packages'][0]['title'])

View File

@ -1,18 +0,0 @@
from django.test import TestCase
from vbv_lernwelt.learnpath.models_competences import Competence, FullfillmentCriteria
from vbv_lernwelt.learnpath.tests.competences_factories import CompetencePageFactory, CompetenceFactory, \
FullfilmentCriteriaFactory
class TestCompetencesFactories(TestCase):
def test_create_competences_page(self):
CompetencePageFactory()
def test_create_competence(self):
CompetenceFactory(name='Boogie Woogie')
self.assertEqual(Competence.objects.filter(name='Boogie Woogie').count(), 1)
def test_create_fullfillment_criteria(self):
FullfilmentCriteriaFactory(name='shuffle like ...')
self.assertEqual(FullfillmentCriteria.objects.filter(name='shuffle like ...').count(), 1)

View File

@ -1,12 +0,0 @@
from django.test import TestCase
from vbv_lernwelt.learnpath.tests.create_default_competences import create_default_competences
class TestCreateDefaultCompetences(TestCase):
def test_create_default_competeneces(self):
create_default_competences()

View File

@ -1,23 +0,0 @@
from django.conf import settings
from django.test import TestCase
from wagtail.models import Locale
from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.learnpath.models import LearningPath
from vbv_lernwelt.learnpath.tests.create_default_learning_path import create_default_learning_path
class TestCreateDefaultLearningPaths(TestCase):
def setUp(self) -> None:
create_default_users()
create_locales_for_wagtail()
def test_create_learning_path(self):
create_default_learning_path()
qs = LearningPath.objects.filter(title="Versicherungsvermittler/in")
self.assertTrue(qs.exists())
def create_locales_for_wagtail():
for language in settings.WAGTAIL_CONTENT_LANGUAGES:
Locale.objects.get_or_create(language_code=language[0])

View File

@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
#
# Iterativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2015 Iterativ GmbH. All rights reserved.
#
# Created on 2022-03-29
# @author: lorenz.padberg@iterativ.ch

View File

@ -2,6 +2,7 @@
import glob import glob
from pathlib import Path from pathlib import Path
import structlog
from django.conf import settings from django.conf import settings
from django.shortcuts import render from django.shortcuts import render
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
@ -11,13 +12,19 @@ from wagtail.models import Page
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
logger = structlog.get_logger(__name__)
@api_view(['GET']) @api_view(['GET'])
@cache_page(60 * 60 * 8, cache="learning_path_cache") @cache_page(60 * 60 * 8, cache="learning_path_cache")
def page_api_view(request, slug): def page_api_view(request, slug):
page = Page.objects.get(slug=slug, locale__language_code='de-CH') try:
serializer = page.specific.get_serializer_class()(page.specific) page = Page.objects.get(slug=slug, locale__language_code='de-CH')
return Response(serializer.data) serializer = page.specific.get_serializer_class()(page.specific)
return Response(serializer.data)
except Exception as e:
logger.error(e)
return Response({"error": str(e)}, status=404)
@django_view_authentication_exempt @django_view_authentication_exempt

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,9 +1,8 @@
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from wagtail.core.models import Collection from wagtail.core.models import Collection
from wagtail.models import Locale
from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.core.create_default_users import create_default_users
from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.media_library.create_default_documents import create_default_collections, create_default_documents from vbv_lernwelt.media_library.create_default_documents import create_default_collections, create_default_documents
from vbv_lernwelt.media_library.models import LibraryDocument from vbv_lernwelt.media_library.models import LibraryDocument
@ -24,8 +23,3 @@ class TestCreateDefaultDocuments(TestCase):
create_default_documents() create_default_documents()
qs = LibraryDocument.objects.all() qs = LibraryDocument.objects.all()
self.assertTrue(qs.exists()) self.assertTrue(qs.exists())
def create_locales_for_wagtail():
for language in settings.WAGTAIL_CONTENT_LANGUAGES:
Locale.objects.get_or_create(language_code=language[0])

View File

@ -1,39 +0,0 @@
# Register your models here.
from django.contrib import admin
from .models import SimpleTask, SimpleList
@admin.register(SimpleList)
class SimpleListAdmin(admin.ModelAdmin):
list_display = [
"title",
"user",
"created",
]
list_filter = [
"user",
]
search_fields = [
"title",
]
@admin.register(SimpleTask)
class SimpleTaskAdmin(admin.ModelAdmin):
date_hierarchy = "deadline"
list_display = [
"title",
"deadline",
"created",
"list",
"done",
]
list_filter = [
"list",
"done",
]
search_fields = [
"title",
"text",
]

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class SimpletodoConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "vbv_lernwelt.simpletodo"

View File

@ -1,105 +0,0 @@
# Generated by Django 3.2.12 on 2022-02-03 20:37
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="SimpleList",
fields=[
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="modified",
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("title", models.CharField(max_length=255)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="SimpleTask",
fields=[
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="created",
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now,
editable=False,
verbose_name="modified",
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("title", models.CharField(max_length=255)),
("text", models.TextField(blank=True, default="")),
("done", models.BooleanField(default=False)),
("deadline", models.DateTimeField(blank=True, null=True)),
(
"list",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="simpletodo.simplelist",
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -1,24 +0,0 @@
import uuid
from django.conf import settings
from django.db import models
from model_utils.models import TimeStampedModel
class SimpleList(TimeStampedModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=255)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
def __str__(self):
return f"{self.title} ({self.user})"
class SimpleTask(TimeStampedModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=255)
text = models.TextField(blank=True, default="")
done = models.BooleanField(default=False)
deadline = models.DateTimeField(blank=True, null=True)
list = models.ForeignKey(SimpleList, on_delete=models.CASCADE)

View File

@ -1,41 +0,0 @@
import structlog
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer
from vbv_lernwelt.simpletodo.models import SimpleTask, SimpleList
logger = structlog.get_logger(__name__)
class SimpleTaskSerializer(ModelSerializer):
list_title = serializers.CharField(max_length=100)
class Meta:
model = SimpleTask
fields = [
"id",
"title",
"text",
"done",
"deadline",
"list_title",
]
def create(self, validated_data):
user = validated_data.pop("user", None)
if user is None:
raise serializers.ValidationError("User is required")
list_title = validated_data.pop("list_title")
simple_list, _ = SimpleList.objects.get_or_create(title=list_title, user=user)
validated_data["list"] = simple_list
logger.debug(
"Creating task",
label="simpletodo",
dt={"s1": 3, "s2": 4},
title=validated_data.get("title"),
list_title=list_title,
)
return super().create(validated_data)

View File

@ -1,17 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mx-auto">
{% for list in simple_lists %}
{% include "simpletodo/partials/simple_list.html" with list=list%}
{% endfor %}
</div>
<it-icon-ls-start class="text-orange-500"></it-icon-ls-start>
<it-icon-arrow-up></it-icon-arrow-up>
<it-icon-arrow-down></it-icon-arrow-down>
{% endblock %}

View File

@ -1,21 +0,0 @@
<form class="flex mt-4" action="/todo/api/tasks/" method="POST">
{% csrf_token %}
<input class="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-gray-darker"
type="text"
name="title"
maxlength="100"
required
placeholder="Add Todo"
>
<input type="hidden" name="list_title" value="{{ list.title }}">
<input
type="submit"
value="Add"
hx-post="/todo/api/tasks/"
hx-trigger="submit"
hx-target="#parent-div"
hx-swap="outerHTML"
class="flex-no-shrink p-2 border-2 rounded text-blue-500 border-blue-500 hover:text-white hover:bg-blue-500"
>
</form>

View File

@ -1,18 +0,0 @@
<div class="h-100 w-full flex items-center justify-center bg-blue-100 font-sans">
<div class="bg-white rounded shadow p-6 m-4 w-full lg:w-3/5 md:w-3/4">
<div class="mb-4">
<h2
class="text-gray-darkest"
data-cy="simple-list-title"
>
{{ list.title }}
</h2>
{% include "simpletodo/partials/add_task_form.html" with task=task %}
</div>
{% for task in list.simpletask_set.all %}
{% include "simpletodo/partials/task.html" with task=task %}
{% endfor %}
</div>
</div>

View File

@ -1,35 +0,0 @@
<div class="task">
<div class="flex mb-4 items-center">
{% if task.done %}
<p class="flex-auto text-blue-500 line-through">
{% else %}
<p class="flex-auto text-blue-900">
{% endif %}
{{ task.title }}
</p>
<button
hx-post="/todo/api/tasks/{{ task.id }}/toggle_done/"
hx-swap="outerHTML swap:0.5s"
hx-target="closest .task"
{% if task.done %}
class="flex-no-shrink p-2 ml-4 mr-2 border-2 rounded hover:text-white text-gray-500 border-gray-500 hover:bg-gray-500"
{% else %}
class="flex-no-shrink p-2 ml-4 mr-2 border-2 rounded hover:text-white text-green-500 border-green-500 hover:bg-green-500"
{% endif %}
>
{% if task.done %}
Not Done
{% else %}
Done
{% endif %}
</button>
<button
hx-delete="/todo/api/tasks/{{ task.id }}/"
hx-swap="outerHTML swap:0.5s"
hx-target="closest .task"
class="flex-no-shrink p-2 ml-2 border-2 rounded text-red-500 border-red-500 hover:text-white hover:bg-red-500">
Remove
</button>
</div>
</div>

View File

@ -1,26 +0,0 @@
from django.test import TestCase
from vbv_lernwelt.core.tests.factories import UserFactory
from vbv_lernwelt.simpletodo.models import SimpleTask
from vbv_lernwelt.simpletodo.serializers import SimpleTaskSerializer
class SimpleTaskSerializerTestCase(TestCase):
def setUp(self) -> None:
self.user = UserFactory()
def test_serializer(self):
serializer = SimpleTaskSerializer(
data={
"title": "Test",
"list_title": "Todos",
}
)
serializer.is_valid(raise_exception=True)
serializer.save(user=self.user)
task = SimpleTask.objects.first()
self.assertEqual(task.title, "Test")
self.assertEqual(task.list.title, "Todos")

View File

@ -1,13 +0,0 @@
from django.conf.urls import url, include
from django.urls import path
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r"tasks", views.SimpleTaskViewSet, basename="tasks")
urlpatterns = [
path("", views.index, name="index"),
url(r"^api/", include(router.urls)),
]

View File

@ -1,92 +0,0 @@
from django.http import HttpResponse
from django.shortcuts import redirect, render
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.renderers import TemplateHTMLRenderer, JSONRenderer
from rest_framework.response import Response
from vbv_lernwelt.simpletodo.models import SimpleList, SimpleTask
from vbv_lernwelt.simpletodo.serializers import SimpleTaskSerializer
def index(request):
simple_lists = SimpleList.objects.filter(user=request.user)
if simple_lists.count() == 0:
simple_lists = [SimpleList.objects.create(user=request.user, title="Todos")]
return render(request, "simpletodo/index.html", {"simple_lists": simple_lists})
class SimpleTaskViewSet(viewsets.ModelViewSet):
serializer_class = SimpleTaskSerializer
renderer_classes = [TemplateHTMLRenderer, JSONRenderer]
def get_queryset(self):
user = self.request.user
return SimpleTask.objects.filter(list__user=user)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if request.accepted_renderer.format == "html":
serializer.is_valid(raise_exception=True)
else:
serializer.is_valid(raise_exception=True)
serializer.save(user=request.user)
if request.accepted_renderer.format == "html":
return redirect("/todo/")
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
instance.delete()
if request.htmx:
return HttpResponse(status=200, content="")
return Response(status=status.HTTP_204_NO_CONTENT)
@action(
detail=True,
methods=[
"post",
],
)
def toggle_done(self, request, pk=None):
task = self.get_object()
task.done = not task.done
task.save()
if request.htmx:
return render(request, "simpletodo/partials/task.html", {"task": task})
return Response(self.get_serializer(task), status=status.HTTP_200_OK)
#
# def get_category_from_request(self, request):
# cat_name = request.query_params.get('cat_name')
# category_obj = None
#
# if cat_name:
# category_obj = VideoCategory.objects.filter(category__iexact=cat_name).first()
# if not category_obj:
# category_obj = VideoCategory.objects.first()
#
# return category_obj
#
# @action(detail=False, methods=['get'])
# def form(self, request):
# category_obj = self.get_category_from_request(request)
# return Response(template_name='videos/partials/video_form.html', data={'category': category_obj})
#
# @action(detail=False, methods=['get'])
# def cancel(self, request):
# category_obj = self.get_category_from_request(request)
# return Response(template_name='videos/partials/show_add_form.html', data={'category': category_obj})

View File

@ -1,4 +1,8 @@
server/requirements/ server/requirements/
env_secrets/ env_secrets/
env/docker_local.env env/docker_local.env
server/vbv_lernwelt/media/
server/vbv_lernwelt/simpletodo/
supabase.md
scripts/supabase/init.sql
.envs/ .envs/