Branch Cleanup
This commit is contained in:
parent
3941e3f92e
commit
d859e1952c
|
@ -0,0 +1,70 @@
|
|||
pipeline {
|
||||
|
||||
agent any
|
||||
|
||||
environment {
|
||||
DJANGO_SETTINGS_MODULE = "solawi_platform.settings"
|
||||
SOLAWI_SUITE_SECRET_KEY = credentials('SOLAWI_SUITE_SECRET_KEY')
|
||||
ROBOT_HEADLESS = "yes"
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Submodules') {
|
||||
steps {
|
||||
sh 'git submodule update --init --recursive --depth=1 documentation'
|
||||
}
|
||||
}
|
||||
stage('Install') {
|
||||
steps {
|
||||
sh 'poetry install --without prod --sync'
|
||||
}
|
||||
}
|
||||
stage('Prepare 3rd Party Libs') {
|
||||
steps {
|
||||
sh '(cd 3rdparty; yarn install)'
|
||||
}
|
||||
}
|
||||
stage('Prepare Documentation') {
|
||||
steps {
|
||||
sh '(cd documentation; poetry run mkdocs build)'
|
||||
}
|
||||
}
|
||||
stage('Prepare Test') {
|
||||
steps {
|
||||
sh 'poetry run manage.py migrate --no-input'
|
||||
sh 'poetry run manage.py collectstatic --no-input'
|
||||
}
|
||||
}
|
||||
stage('Unit Test') {
|
||||
steps {
|
||||
sh 'poetry run manage.py test'
|
||||
}
|
||||
}
|
||||
/* At this moment, the Build Server cannot execute browsers
|
||||
stage('Prepare Integration Test') {
|
||||
steps {
|
||||
sh 'poetry run rfbrowser init'
|
||||
}
|
||||
}
|
||||
stage('Integration Test') {
|
||||
steps {
|
||||
sh 'poetry run robot -A SystemTest/robot.args.conf -e manual SystemTest/TestCases'
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
deleteDir()
|
||||
}
|
||||
always {
|
||||
withCredentials([string(credentialsId: 'solawi-suite-notification-email', variable: 'EMAIL')]) {
|
||||
mail to: '$EMAIL',
|
||||
subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}",
|
||||
body: "Duration: ${currentBuild.durationString} / Jenkins URL: ${env.BUILD_URL}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
uat-test-results/
|
||||
.pabotsuitenames
|
||||
allure-report/
|
||||
|
||||
_compiled_static_files/
|
||||
!_compiled_static_files/.gitkeep
|
||||
|
||||
dumpdata.json
|
||||
|
||||
_email/
|
||||
!_email/.gitkeep
|
||||
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
.idea/**/aws.xml
|
||||
.idea/**/contentModel.xml
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
cmake-build-*/
|
||||
.idea/**/mongoSettings.xml
|
||||
*.iws
|
||||
out/
|
||||
.idea_modules/
|
||||
atlassian-ide-plugin.xml
|
||||
.idea/replstate.xml
|
||||
.idea/sonarlint/
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
.idea/httpRequests
|
||||
.idea/caches/build_file_checksums.ser
|
||||
*~
|
||||
.fuse_hidden*
|
||||
.directory
|
||||
.Trash-*
|
||||
.nfs*
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
__pycache__/
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
media
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
*.stackdump
|
||||
[Dd]esktop.ini
|
||||
$RECYCLE.BIN/
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
*.lnk
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon
|
||||
._*
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
*.manifest
|
||||
*.spec
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
*.mo
|
||||
instance/
|
||||
.webassets-cache
|
||||
.scrapy
|
||||
docs/_build/
|
||||
.pybuilder/
|
||||
target/
|
||||
.ipynb_checkpoints
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
.pdm.toml
|
||||
__pypackages__/
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
*.sage.py
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.spyderproject
|
||||
.spyproject
|
||||
.ropeproject
|
||||
/site
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
.pyre/
|
||||
.pytype/
|
||||
cython_debug/
|
|
@ -0,0 +1,6 @@
|
|||
[submodule "solawi_platform/production_settings"]
|
||||
path = solawi_platform/production_settings
|
||||
url = igit@git.codebau.dev:solawi-suite/solawi-suite-production-settings.git
|
||||
[submodule "documentation"]
|
||||
path = documentation
|
||||
url = igit@git.codebau.dev:solawi-suite/documentation.git
|
|
@ -0,0 +1,8 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="db.sqlite3" uuid="a71f7656-d5f2-45c5-a96b-d0ac533486fa">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db.sqlite3</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,6 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JsonSchemaMappingsProjectConfiguration">
|
||||
<state>
|
||||
<map>
|
||||
<entry key="MkDocs Configuration 1.0">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
<option name="name" value="MkDocs Configuration 1.0" />
|
||||
<option name="relativePathToSchema" value="https://json.schemastore.org/mkdocs-1.0.json" />
|
||||
<option name="applicationDefined" value="true" />
|
||||
<option name="patterns">
|
||||
<list>
|
||||
<Item>
|
||||
<option name="path" value="documentation/mkdocs.dev.yml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value="documentation/mkdocs.yml" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</state>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Poetry (platform)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (platform) (2)" project-jdk-type="Python SDK" />
|
||||
</project>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/platform.iml" filepath="$PROJECT_DIR$/.idea/platform.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="django" name="Django">
|
||||
<configuration>
|
||||
<option name="rootFolder" value="$MODULE_DIR$" />
|
||||
<option name="settingsModule" value="solawi_platform/settings.py" />
|
||||
<option name="manageScript" value="manage.py" />
|
||||
<option name="environment" value="<map/>" />
|
||||
<option name="doNotUseTestRunner" value="false" />
|
||||
<option name="trackFilePattern" value="" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Poetry (platform) (2)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||
</component>
|
||||
</module>
|
|
@ -0,0 +1,27 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="All Tests" type="DjangoTestsConfigurationType">
|
||||
<module name="platform" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="DJANGO_SELENIUM_HEADLESS" value="0" />
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
<env name="SE_AVOID_STATS" value="true" />
|
||||
<env name="SOLAWI_SUITE_SECRET_KEY" value="0x3739d20117be06f317d31bf30296b563e37a1a5ef6299a0e920675288f920627" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="$USER_HOME$/Library/Caches/pypoetry/virtualenvs/platform-SLbylcYK-py3.12/bin/python" />
|
||||
<option name="SDK_NAME" value="Poetry (platform)" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="TARGET" value="" />
|
||||
<option name="SETTINGS_FILE" value="" />
|
||||
<option name="CUSTOM_SETTINGS" value="false" />
|
||||
<option name="USE_OPTIONS" value="false" />
|
||||
<option name="OPTIONS" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Frontend" type="JavascriptDebugType" uri="http://127.0.0.1:8000/" useFirstLineBreakpoints="true">
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,30 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Solawi Platform" type="Python.DjangoServer" factoryName="Django server">
|
||||
<module name="platform" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
<env name="DJANGO_SETTINGS_MODULE" value="solawi_platform.settings" />
|
||||
<env name="SE_AVOID_STATS" value="true" />
|
||||
<env name="SOLAWI_SUITE_SECRET_KEY" value="0x3739d20117be06f317d31bf30296b563e37a1a5ef6299a0e920675288f920627" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="SDK_NAME" value="Poetry (platform) (2)" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<option name="launchJavascriptDebuger" value="false" />
|
||||
<option name="port" value="8000" />
|
||||
<option name="host" value="localhost" />
|
||||
<option name="additionalOptions" value="" />
|
||||
<option name="browserUrl" value="" />
|
||||
<option name="runTestServer" value="false" />
|
||||
<option name="runNoReload" value="false" />
|
||||
<option name="useCustomRunCommand" value="false" />
|
||||
<option name="customRunCommand" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,30 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Solawi Platform (no debug)" type="Python.DjangoServer" factoryName="Django server">
|
||||
<module name="platform" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
<env name="DJANGO_SETTINGS_MODULE" value="solawi_platform.settings" />
|
||||
<env name="SE_AVOID_STATS" value="true" />
|
||||
<env name="SOLAWI_SUITE_SECRET_KEY" value="0x3739d20117be06f317d31bf30296b563e37a1a5ef6299a0e920675288f920627" />
|
||||
<env name="SOLAWI_SUITE_DEBUG" value="0" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<option name="launchJavascriptDebuger" value="false" />
|
||||
<option name="port" value="8000" />
|
||||
<option name="host" value="localhost" />
|
||||
<option name="additionalOptions" value="" />
|
||||
<option name="browserUrl" value="" />
|
||||
<option name="runTestServer" value="false" />
|
||||
<option name="runNoReload" value="false" />
|
||||
<option name="useCustomRunCommand" value="false" />
|
||||
<option name="customRunCommand" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,27 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="true" type="DjangoTestsConfigurationType">
|
||||
<module name="platform" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
<env name="SE_AVOID_STATS" value="true" />
|
||||
<env name="DJANGO_SELENIUM_HEADLESS" value="0" />
|
||||
<env name="SOLAWI_SUITE_SECRET_KEY" value="0x3739d20117be06f317d31bf30296b563e37a1a5ef6299a0e920675288f920627" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="$USER_HOME$/AppData/Local/pypoetry/Cache/virtualenvs/platform-eQqOmOVw-py3.12/Scripts/python.exe" />
|
||||
<option name="SDK_NAME" value="Poetry (platform)" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="TARGET" value="" />
|
||||
<option name="SETTINGS_FILE" value="" />
|
||||
<option name="CUSTOM_SETTINGS" value="false" />
|
||||
<option name="USE_OPTIONS" value="false" />
|
||||
<option name="OPTIONS" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,25 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="true" type="RobotRunConfiguration" factoryName="RobotRunConfiguration">
|
||||
<module name="platform" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<envs>
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="$Projectpath$" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="SCRIPT_NAME" value="" />
|
||||
<option name="PARAMETERS" value="-A SystemTest/robot.args.conf" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="true" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
<option name="REDIRECT_INPUT" value="false" />
|
||||
<option name="INPUT_FILE" value="" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/documentation" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/solawi_platform/production_settings" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,4 @@
|
|||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
/.pnp.* binary linguist-generated
|
|
@ -0,0 +1,15 @@
|
|||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
node_modules/
|
||||
|
||||
# Swap the comments on the following lines if you wish to use zero-installs
|
||||
# In that case, don't forget to run `yarn config set enableGlobalCache false`!
|
||||
# Documentation here: https://yarnpkg.com/features/caching#zero-installs
|
||||
|
||||
#!.yarn/cache
|
||||
.pnp.*
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,5 @@
|
|||
enableTelemetry: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.1.0.cjs
|
|
@ -0,0 +1,3 @@
|
|||
# 3rdparty
|
||||
|
||||
3rd Party Libs für das solawi-suite-System
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "3rdparty",
|
||||
"packageManager": "yarn@4.1.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"bootstrap": "5.3.3",
|
||||
"moment": "^2.30.1",
|
||||
"reveal.js": "^5.0.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
# This file is generated by running "yarn install" inside your project.
|
||||
# Manual changes might be lost - proceed with caution!
|
||||
|
||||
__metadata:
|
||||
version: 8
|
||||
cacheKey: 10c0
|
||||
|
||||
"3rdparty@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "3rdparty@workspace:."
|
||||
dependencies:
|
||||
"@fortawesome/fontawesome-free": "npm:^6.5.1"
|
||||
"@simplewebauthn/browser": "npm:^9.0.1"
|
||||
bootstrap: "npm:5.3.3"
|
||||
moment: "npm:^2.30.1"
|
||||
reveal.js: "npm:^5.0.5"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@fortawesome/fontawesome-free@npm:^6.5.1":
|
||||
version: 6.5.1
|
||||
resolution: "@fortawesome/fontawesome-free@npm:6.5.1"
|
||||
checksum: 10c0/dc359ede1ddfc6d91915345143008420001c56b164a6fe95d81bcca631eecca41009bfacf794d55bdb9d927c112119ce90a11cb4f576a815ed69ec681e5eb824
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@simplewebauthn/browser@npm:^9.0.1":
|
||||
version: 9.0.1
|
||||
resolution: "@simplewebauthn/browser@npm:9.0.1"
|
||||
dependencies:
|
||||
"@simplewebauthn/types": "npm:^9.0.1"
|
||||
checksum: 10c0/141f3f55d99ad2b4dcf697026d2470fa10b65a36286b616ead71b56ddd5636c30eb001599d27c2509a87c93def6636a2c7081ae362910268b78733a676af3ebf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@simplewebauthn/types@npm:^9.0.1":
|
||||
version: 9.0.1
|
||||
resolution: "@simplewebauthn/types@npm:9.0.1"
|
||||
checksum: 10c0/397f079ac029ada1413d6001850e3b49f99297a9933822758bdca9c47c36d6558e1dc81682725e0b03c501551c94fea040cead76883dcd2bb3a34b32984900f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bootstrap@npm:5.3.3":
|
||||
version: 5.3.3
|
||||
resolution: "bootstrap@npm:5.3.3"
|
||||
peerDependencies:
|
||||
"@popperjs/core": ^2.11.8
|
||||
checksum: 10c0/bb68ca7b763977b9cce40cb5b8c676ae19a716d2f5d15009fa7bdbcec9dea426968e3cb748fbed7592fbf10edd7c749aea841c2920996a7c1aa5e0a6e2d4c2ad
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"moment@npm:^2.30.1":
|
||||
version: 2.30.1
|
||||
resolution: "moment@npm:2.30.1"
|
||||
checksum: 10c0/865e4279418c6de666fca7786607705fd0189d8a7b7624e2e56be99290ac846f90878a6f602e34b4e0455c549b85385b1baf9966845962b313699e7cb847543a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"reveal.js@npm:^5.0.5":
|
||||
version: 5.0.5
|
||||
resolution: "reveal.js@npm:5.0.5"
|
||||
checksum: 10c0/9ebe682eed3ca928f393e16490e1894af146ef23fa0b2be1a1bcfbf4f1c685b882ee3dbfab821b867f7290e2546627877cfa0871f7ecf69dff5719ffe8a53082
|
||||
languageName: node
|
||||
linkType: hard
|
|
@ -0,0 +1,20 @@
|
|||
# 0.5.0-dev: Continuous Integration, Continuous Delivery / 13.04.2024
|
||||
|
||||
Dieses Release enthält keinerlei neue Funktionalitäten, es bereitet jedoch das Projekt dahingehend vor, dass es nun
|
||||
vollautomatisiert kontinuierlich gebaut, getestet und deployed werden kann.
|
||||
|
||||
Dazu wird der "main"-Branch nun automatisiert deployed, während auf dem "develop"-Branch Änderungen fortlaufend getestet
|
||||
werden. Ein Merge von "develop" nach "main" stößt das Deployment an - entsprechende Deployment-Skripte gehören nun mit
|
||||
zum Code.
|
||||
|
||||
Insgesamt ist das ein Breaking Change, da sich die Deployment-Strategie vollkommen ändert.
|
||||
|
||||
## Feature: Pipeline-Skripte
|
||||
|
||||
Gebaut wird die [solawi-suite] fortan mit Jenkins, entsprechend ist ein Jenkins-File hinterlegt.
|
||||
|
||||
## Enhancement: Bearbeiten-Links in der Dokumentation
|
||||
|
||||
In Vorbereitung der Veröffentlichung des Quellcodes der [solawi-suite] wurden in der Dokumentation Links integriert, die
|
||||
den direkten Zugriff auf die Bearbeitung von Seiten des Handbuchs ermöglichen, natürlich nur für die, die über einen
|
||||
entsprechenden Account verfügen.
|
|
@ -0,0 +1,112 @@
|
|||
0001a;2;hguerreau1@aboutads.info;Harriott
|
||||
0001b;2;iizon2@comcast.net;Isaac
|
||||
0002a;4;ngodspeede3@yandex.ru;Nikolia
|
||||
0002b;4;smorlon4@craigslist.org;Staci
|
||||
0002c;4;fdurtnal5@zdnet.com;Florry
|
||||
0002d;4;cbargery6@stumbleupon.com;Curtis
|
||||
0003;1;mrother7@ow.ly;Marthena
|
||||
0004;1;lcapinetti8@auda.org.au;Liz
|
||||
0005a;2;tkenset9@creativecommons.org;Thorndike
|
||||
0005b;2;kwillsheara@youtu.be;Kassia
|
||||
0006a;2;cconachieb@behance.net;Crichton
|
||||
0006b;2;yorhrtc@istockphoto.com;Yankee
|
||||
0007a;2;smacconachyd@wufoo.com;Sonnnie
|
||||
0007b;2;coharae@merriam-webster.com;Chrystal
|
||||
0008;1;jcarmelf@howstuffworks.com;Jacquelynn
|
||||
0009;1;bdaberg@cpanel.net;Brucie
|
||||
0010;1;ethirlawayh@slate.com;Ellene
|
||||
0011a;2;scarusi@accuweather.com;Sharlene
|
||||
0011b;2;apietrasikj@rakuten.co.jp;Aguste
|
||||
0012a;2;svegask@t-online.de;Silvan
|
||||
0012b;2;mboxilll@hexun.com;Michail
|
||||
0013a;2;mosgodbym@cocolog-nifty.com;Mahala
|
||||
0013b;2;cpauln@vistaprint.com;Conni
|
||||
0014a;2;cmatzeitiso@cpanel.net;Carmelita
|
||||
0014b;2;jdarlingtonp@hc360.com;Jeff
|
||||
0015a;2;dsmithq@tuttocitta.it;Darcie
|
||||
0015b;2;tclapsonr@uol.com.br;Tanhya
|
||||
0016a;2;cfinkers@photobucket.com;Caitrin
|
||||
0016b;2;bgommet@bravesites.com;Burch
|
||||
0017a;2;hbenetu@dyndns.org;Heddie
|
||||
0017b;2;rhighmanv@marketwatch.com;Rebbecca
|
||||
0018a;2;mgentnerw@pbs.org;Masha
|
||||
0018b;2;omacanultyx@tamu.edu;Ottilie
|
||||
0019a;2;ryankeevy@auda.org.au;Raymund
|
||||
0019b;2;mgrishankovz@npr.org;Marlon
|
||||
0020a;4;rsnoddin10@fotki.com;Rosalie
|
||||
0020b;4;tstucksbury11@tuttocitta.it;Toiboid
|
||||
0020c;4;eriseborough12@paypal.com;Eberhard
|
||||
0020d;4;jboas13@redcross.org;Joelly
|
||||
0021a;3;btaber14@home.pl;Ban
|
||||
0021b;3;emapples15@wikipedia.org;Emanuel
|
||||
0021c;3;jbleakman16@seesaa.net;Joane
|
||||
0022a;5;apossel17@cpanel.net;Ansell
|
||||
0022b;5;cbenettelli18@elpais.com;Christiano
|
||||
0022c;5;jeaseman19@odnoklassniki.ru;Jacqui
|
||||
0022d;5;ecrilley1a@harvard.edu;Elyssa
|
||||
0022e;5;akilalea1b@wix.com;Agosto
|
||||
0023a;2;sbardsley1c@cisco.com;Sibylle
|
||||
0023b;2;istanley1d@psu.edu;Irving
|
||||
0024a;2;ekhadir1e@taobao.com;Eliza
|
||||
0024b;2;challows1f@senate.gov;Charline
|
||||
0025a;2;sdran1g@spotify.com;Sutherlan
|
||||
0025b;2;zgarfirth1h@yahoo.com;Zondra
|
||||
0026a;2;jarkcoll1i@slideshare.net;Josy
|
||||
0026b;2;cbirch1j@geocities.jp;Cherry
|
||||
0027a;2;bsickling1k@privacy.gov.au;Berte
|
||||
0027b;2;edougliss1l@patch.com;Eda
|
||||
0028a;2;crosario1m@diigo.com;Conroy
|
||||
0028b;2;borto1n@irs.gov;Beatrisa
|
||||
0029a;2;amurcutt1o@list-manage.com;Archer
|
||||
0029b;2;umacclenan1p@guardian.co.uk;Ulberto
|
||||
0030a;2;csherrum1q@nsw.gov.au;Cathy
|
||||
0030b;2;srawsthorne1r@icio.us;Shanon
|
||||
0031a;3;wlipson1s@topsy.com;Winna
|
||||
0031b;3;jgarnar1t@jimdo.com;Jaclyn
|
||||
0031c;3;omerriday1u@vkontakte.ru;Odelia
|
||||
0032a;2;eloosemore1v@statcounter.com;Earvin
|
||||
0032b;2;bpickover1w@furl.net;Bethena
|
||||
0033;1;kchislett1x@angelfire.com;Kenton
|
||||
0034a;2;weuesden1y@xrea.com;Welch
|
||||
0034b;2;psimonds1z@de.vu;Piggy
|
||||
0035a;2;ebachmann20@ucla.edu;Elianore
|
||||
0035b;2;bsutherden21@princeton.edu;Billy
|
||||
0036a;2;asangster22@harvard.edu;Abran
|
||||
0036b;2;rbreckwell23@examiner.com;Rickey
|
||||
0037a;2;pmachon24@desdev.cn;Page
|
||||
0037b;2;jbraundt25@imgur.com;Jackelyn
|
||||
0038a;2;mrivers26@pagesperso-orange.fr;Mervin
|
||||
0038b;2;sgiacovelli27@hugedomains.com;Sibylle
|
||||
0039a;2;mbesant28@uol.com.br;Muhammad
|
||||
0039b;2;rcardoe29@technorati.com;Rourke
|
||||
0040a;8;vgandey2a@lulu.com;Vale
|
||||
0040b;8;eviall2b@bbc.co.uk;Emmy
|
||||
0040c;8;sswin2c@yellowpages.com;Sonny
|
||||
0040d;8;aportman2d@canalblog.com;Ardeen
|
||||
0040e;8;jchaudhry2e@imageshack.us;Joye
|
||||
0040f;8;rleidecker2f@fda.gov;Ryann
|
||||
0040g;8;mproske2g@answers.com;Mathias
|
||||
0040h;8;dpatry2h@gmpg.org;Dalenna
|
||||
0041a;2;ksharply2i@sbwire.com;Kaitlin
|
||||
0041b;2;cdulanty2j@harvard.edu;Clem
|
||||
0042a;2;phawes2k@jiathis.com;Parrnell
|
||||
0042b;2;hwackley2l@cornell.edu;Harold
|
||||
0043a;2;mmuccino2m@forbes.com;Meridel
|
||||
0043b;2;cbarkas2n@cnet.com;Cicily
|
||||
0044;1;nbowcher2o@mit.edu;Nickola
|
||||
0045;1;rpideon2p@google.ca;Raynor
|
||||
0046a;2;ccolling2q@sina.com.cn;Clyve
|
||||
0046b;2;rcornbill2r@oakley.com;Robby
|
||||
0047a;2;nbilston1@senate.gov;Noble
|
||||
0047b;2;dgooke2@dyndns.org;Dixie
|
||||
0048a;2;gmcpeake3@bbc.co.uk;Ganny
|
||||
0048b;2;mstairmand4@bizjournals.com;Martguerita
|
||||
0049;1;hsedgeworth5@vimeo.com;Hale
|
||||
0050a;8;abrignall6@cocolog-nifty.com;Astrid
|
||||
0050b;8;jcondon7@mayoclinic.com;Joya
|
||||
0050c;8;cledward8@ebay.co.uk;Cristabel
|
||||
0050d;8;aspino9@123-reg.co.uk;Anita
|
||||
0050e;8;krosenblada@opera.com;Kaitlynn
|
||||
0050f;8;blowndesb@illinois.edu;Bambi
|
||||
0050g;8;gbroadhurstc@oakley.com;Giacopo
|
||||
0050h;8;pcarnied@vkontakte.ru;Pattie
|
|
|
@ -0,0 +1,112 @@
|
|||
0001a;2;hguerreau1@aboutads.info;Harriott
|
||||
0001b;2;iizon2@comcast.net;Isaac
|
||||
0002a;4;ngodspeede3@yandex.ru;Nikolia
|
||||
0002b;4;smorlon4@craigslist.org;Staci
|
||||
0002c;4;fdurtnal5@zdnet.com;Florry
|
||||
0002d;4;cbargery6@stumbleupon.com;Curtis
|
||||
0003;1;mrother7@ow.ly;Marthena
|
||||
0004;1;lcapinetti8@auda.org.au;Liz
|
||||
0005a;2;tkenset9@creativecommons.org;Thorndike
|
||||
0005b;2;kwillsheara@youtu.be;Kassia
|
||||
0006a;2;cconachieb@behance.net;Crichton
|
||||
0006b;2;yorhrtc@istockphoto.com;Yankee
|
||||
0007a;2;smacconachyd@wufoo.com;Sonnnie
|
||||
0007b;2;coharae@merriam-webster.com;Chrystal
|
||||
0008;1;jcarmelf@howstuffworks.com;Jacquelynn
|
||||
0009;1;bdaberg@cpanel.net;Brucie
|
||||
0010;1;ethirlawayh@slate.com;Ellene
|
||||
0011a;2;scarusi@accuweather.com;Sharlene
|
||||
0011b;2;apietrasikj@rakuten.co.jp;Aguste
|
||||
0012a;2;svegask@t-online.de;Silvan
|
||||
0012b;2;mboxilll@hexun.com;Michail
|
||||
0013a;2;mosgodbym@cocolog-nifty.com;Mahala
|
||||
0013b;2;cpauln@vistaprint.com;Conni
|
||||
0014a;2;cmatzeitiso@cpanel.net;Carmelita
|
||||
0014b;2;jdarlingtonp@hc360.com;Jeff
|
||||
0015a;2;dsmithq@tuttocitta.it;Darcie
|
||||
0015b;2;tclapsonr@uol.com.br;Tanhya
|
||||
0016a;2;cfinkers@photobucket.com;Caitrin
|
||||
0016b;2;bgommet@bravesites.com;Burch
|
||||
0017a;2;hbenetu@dyndns.org;Heddie
|
||||
0017b;2;rhighmanv@marketwatch.com;Rebbecca
|
||||
0018a;2;mgentnerw@pbs.org;Masha
|
||||
0018b;2;omacanultyx@tamu.edu;Ottilie
|
||||
0019a;2;ryankeevy@auda.org.au;Raymund
|
||||
0019b;2;mgrishankovz@npr.org;Marlon
|
||||
0020a;4;rsnoddin10@fotki.com;Rosalie
|
||||
0020b;4;tstucksbury11@tuttocitta.it;Toiboid
|
||||
0020c;4;eriseborough12@paypal.com;Eberhard
|
||||
0020d;4;jboas13@redcross.org;Joelly
|
||||
0021a;3;btaber14@home.pl;Ban
|
||||
0021b;3;emapples15@wikipedia.org;Emanuel
|
||||
0021c;3;jbleakman16@seesaa.net;Joane
|
||||
0022a;5;apossel17@cpanel.net;Ansell
|
||||
0022b;5;cbenettelli18@elpais.com;Christiano
|
||||
0022c;5;jeaseman19@odnoklassniki.ru;Jacqui
|
||||
0022d;5;ecrilley1a@harvard.edu;Elyssa
|
||||
0022e;5;akilalea1b@wix.com;Agosto
|
||||
0023a;2;sbardsley1c@cisco.com;Sibylle
|
||||
0023b;2;istanley1d@psu.edu;Irving
|
||||
0024a;2;ekhadir1e@taobao.com;Eliza
|
||||
0024b;2;challows1f@senate.gov;Charline
|
||||
0025a;2;sdran1g@spotify.com;Sutherlan
|
||||
0025b;2;zgarfirth1h@yahoo.com;Zondra
|
||||
0026a;2;jarkcoll1i@slideshare.net;Josy
|
||||
0026b;2;cbirch1j@geocities.jp;Cherry
|
||||
0027a;2;bsickling1k@privacy.gov.au;Berte
|
||||
0027b;2;edougliss1l@patch.com;Eda
|
||||
0028a;2;crosario1m@diigo.com;Conroy
|
||||
0028b;2;borto1n@irs.gov;Beatrisa
|
||||
0029a;2;amurcutt1o@list-manage.com;Archer
|
||||
0029b;2;umacclenan1p@guardian.co.uk;Ulberto
|
||||
0030a;2;csherrum1q@nsw.gov.au;Cathy
|
||||
0030b;2;srawsthorne1r@icio.us;Shanon
|
||||
0031a;3;wlipson1s@topsy.com;Winna
|
||||
0031b;3;jgarnar1t@jimdo.com;Jaclyn
|
||||
0031c;3;omerriday1u@vkontakte.ru;Odelia
|
||||
0032a;2;eloosemore1v@statcounter.com;Earvin
|
||||
0032b;2;bpickover1w@furl.net;Bethena
|
||||
0033;1;kchislett1x@angelfire.com;Kenton
|
||||
0034a;2;weuesden1y@xrea.com;Welch
|
||||
0034b;2;psimonds1z@de.vu;Piggy
|
||||
0035a;2;ebachmann20@ucla.edu;Elianore
|
||||
0035b;2;bsutherden21@princeton.edu;Billy
|
||||
0036a;2;asangster22@harvard.edu;Abran
|
||||
0036b;2;rbreckwell23@examiner.com;Rickey
|
||||
0037a;2;pmachon24@desdev.cn;Page
|
||||
0037b;2;jbraundt25@imgur.com;Jackelyn
|
||||
0038a;2;mrivers26@pagesperso-orange.fr;Mervin
|
||||
0038b;2;sgiacovelli27@hugedomains.com;Sibylle
|
||||
0039a;2;mbesant28@uol.com.br;Muhammad
|
||||
0039b;2;rcardoe29@technorati.com;Rourke
|
||||
0040a;8;vgandey2a@lulu.com;Vale
|
||||
0040b;8;eviall2b@bbc.co.uk;Emmy
|
||||
0040c;8;sswin2c@yellowpages.com;Sonny
|
||||
0040d;8;aportman2d@canalblog.com;Ardeen
|
||||
0040e;8;jchaudhry2e@imageshack.us;Joye
|
||||
0040f;8;rleidecker2f@fda.gov;Ryann
|
||||
0040g;8;mproske2g@answers.com;Mathias
|
||||
0040h;8;dpatry2h@gmpg.org;Dalenna
|
||||
0041a;2;ksharply2i@sbwire.com;Kaitlin
|
||||
0041b;2;cdulanty2j@harvard.edu;Clem
|
||||
0042a;2;phawes2k@jiathis.com;Parrnell
|
||||
0042b;2;hwackley2l@cornell.edu;Harold
|
||||
0043a;2;mmuccino2m@forbes.com;Meridel
|
||||
0043b;2;cbarkas2n@cnet.com;Cicily
|
||||
0044;1;nbowcher2o@mit.edu;Nickola
|
||||
0045;1;rpideon2p@google.ca;Raynor
|
||||
0046a;2;ccolling2q@sina.com.cn;Clyve
|
||||
0046b;2;rcornbill2r@oakley.com;Robby
|
||||
0047a;2;nbilston1@senate.gov;Noble
|
||||
0047b;2;dgooke2@dyndns.org;Dixie
|
||||
0048a;2;gmcpeake3@bbc.co.uk;Ganny
|
||||
0048b;2;mstairmand4@bizjournals.com;Martguerita
|
||||
0049;1;hsedgeworth5@vimeo.com;Hale
|
||||
0050a;8;abrignall6@cocolog-nifty.com;Astrid
|
||||
0050b;8;jcondon7@mayoclinic.com;Joya
|
||||
0050c;8;cledward8@ebay.co.uk;Cristabel
|
||||
0050d;8;aspino9@123-reg.co.uk;Anita
|
||||
0050e;8;krosenblada@opera.com;Kaitlynn
|
||||
0050f;8;blowndesb@illinois.edu;Bambi
|
||||
0050g;8;gbroadhurstc@oakley.com;Giacopo
|
||||
0050h;8;pcarnied@vkontakte.ru;Pattie
|
|
92
README.md
92
README.md
|
@ -1,5 +1,93 @@
|
|||
# Solawi Suite Platform
|
||||
|
||||
Plattform-Basis für die Solawi-Apps.
|
||||
## Development Key
|
||||
|
||||
Work in progress.
|
||||
**DO NOT USE IN PRODUCTION!**
|
||||
|
||||
```text
|
||||
0x3739d20117be06f317d31bf30296b563e37a1a5ef6299a0e920675288f920627
|
||||
```
|
||||
|
||||
Use as environment variable during development:
|
||||
|
||||
```
|
||||
SOLAWI_SUITE_SECRET_KEY=0x3739d20117be06f317d31bf30296b563e37a1a5ef6299a0e920675288f920627
|
||||
```
|
||||
|
||||
## Deployment steps
|
||||
|
||||
```bash
|
||||
git submodule update
|
||||
git submodule foreach --recursive git clean -id
|
||||
poetry install --without dev
|
||||
poetry run manage.py migrate
|
||||
(cd 3rdparty; yarn install)
|
||||
(cd documentation; poetry run mkdocs build)
|
||||
poetry run manage.py collectstatic
|
||||
supervisorctl restart solawi_suite
|
||||
```
|
||||
|
||||
## Important Cron Jobs
|
||||
|
||||
```cronexp
|
||||
MAILTO="administrator@solawi-system"
|
||||
DJANGO_SETTINGS_MODULE="solawi_platform.production_settings"
|
||||
SOLAWI_SUITE_SECRET_KEY=file:///path/to/secret_key.hex
|
||||
PATH=/usr/local/bin
|
||||
|
||||
SOLAWI_SUITE_HOME=/path/to/solawi-suite/platform
|
||||
|
||||
# .---------------- minute (0 - 59)
|
||||
# | .------------- hour (0 - 23)
|
||||
# | | .---------- day of month (1 - 31)
|
||||
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
|
||||
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
|
||||
# | | | | |
|
||||
# * * * * * command to be executed
|
||||
# * * * * * command --arg1 --arg2 file1 file2 2>&1
|
||||
11 4 * * * (cd $SOLAWI_SUITE_HOME; poetry run manage.py cleanup_burned_otp)
|
||||
*/7 * * * * (cd $SOLAWI_SUITE_HOME; poetry run manage.py cleanup_forgot_password)
|
||||
*/9 * * * * (cd $SOLAWI_SUITE_HOME; poetry run manage.py cleanup_pending_registrations)
|
||||
*/13 * * * * (cd $SOLAWI_SUITE_HOME; poetry run manage.py cleanup_rate_limiter)
|
||||
33 3 * * * (cd $SOLAWI_SUITE_HOME; poetry run manage.py clearsessions)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
poetry install --without prod
|
||||
poetry run rfbrowser init
|
||||
(cd 3rdparty; yarn install)
|
||||
(cd documentation; poetry run mkdocs build)
|
||||
poetry run manage.py collectstatic
|
||||
```
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```bash
|
||||
poetry run manage.py test
|
||||
```
|
||||
|
||||
### System Test
|
||||
|
||||
```bash
|
||||
poetry run robot -A SystemTest/robot.args.conf SystemTest/TestCases
|
||||
yarn dlx allure-commandline generate uat-test-results/allure
|
||||
# or
|
||||
yarn dlx allure-commandline serve uat-test-results/allure
|
||||
```
|
||||
|
||||
### System Test (parallel)
|
||||
|
||||
```bash
|
||||
poetry run pabot --processes 4 --command robot -A SystemTest/robot.args.conf --end-command --output-dir uat-test-results --xunit xunit.xml SystemTest/TestCases
|
||||
yarn dlx allure-commandline generate uat-test-results/allure
|
||||
# or
|
||||
yarn dlx allure-commandline serve uat-test-results/allure
|
||||
```
|
||||
|
||||
### Load Test
|
||||
|
||||
TODO: locust
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
*** Settings ***
|
||||
Documentation Keyword Library for the [solawi-suite] application
|
||||
... Handles starting and shutdown of the application
|
||||
Library solawi_uat.environment.instance.SolawiSuiteInstanceLibrary
|
||||
Library Browser jsextension=${CURDIR}/webauthn.js
|
||||
|
||||
*** Keywords ***
|
||||
Solawi-Suite Inner Setup
|
||||
${vars} = Get Variables
|
||||
IF "\${HOST}" in $vars
|
||||
IF $HOST != ${None}
|
||||
RETURN
|
||||
END
|
||||
END
|
||||
Set Suite Variable ${solawi_suite_inner_setup} ${True}
|
||||
Start Solawi-Suite
|
||||
|
||||
Solawi-Suite Inner Teardown
|
||||
${vars} = Get Variables
|
||||
IF "\${solawi_suite_inner_setup}" in $vars
|
||||
IF ${solawi_suite_inner_setup} == ${True}
|
||||
Stop Solawi-Suite
|
||||
Set Global Variable ${HOST} ${None}
|
||||
END
|
||||
END
|
||||
|
||||
Start Solawi-Suite
|
||||
Create Test Database
|
||||
Start Daphne Server
|
||||
${daphne_host} = Get Test Host
|
||||
Set Global Variable $HOST ${daphne_host}
|
||||
Wait For Application Startup
|
||||
|
||||
Stop Solawi-Suite
|
||||
Shutdown Daphne Server
|
||||
Clean Temporary Folder
|
||||
|
||||
Get Browser
|
||||
${browser} = New Browser chromium headless=%{ROBOT_HEADLESS=no}
|
||||
RETURN ${browser}
|
||||
|
||||
Get Context
|
||||
${context} = New Context
|
||||
RETURN ${context}
|
||||
|
||||
Open Page
|
||||
[Arguments] ${url}
|
||||
${browser} = Get Browser
|
||||
${context} = Get Context
|
||||
${page} = New Page ${HOST}${url}
|
||||
Set Viewport Size 1600 800
|
||||
upgradeBrowserForWebAuthnVirtualAuthenticator
|
||||
RETURN ${page}
|
||||
|
||||
Logout
|
||||
Go To ${HOST}/users/auth/logout
|
||||
Click css=form[method="post"]:not([action]) button[type="submit"].btn.btn-warning
|
|
@ -0,0 +1,56 @@
|
|||
*** Settings ***
|
||||
Resource django.resource
|
||||
Library solawi_uat.helper.DataLib
|
||||
Library solawi_uat.helper.RandomTestDataLib
|
||||
Library String
|
||||
|
||||
|
||||
*** Keywords ***
|
||||
Get Registration Link From Mail
|
||||
[Arguments] ${registration_mail}
|
||||
@{links} = Extract All Links ${registration_mail}
|
||||
FOR ${link} IN @{links}
|
||||
${res} = Fetch From Left ${link} /register/
|
||||
IF "${res}" != "${link}"
|
||||
RETURN ${link}
|
||||
END
|
||||
END
|
||||
Fail No registration link found
|
||||
|
||||
Transform Registration Link From "${mail_link}"
|
||||
${res} = Replace String ${mail_link} http://127.0.0.1:8000 ${EMPTY}
|
||||
RETURN ${res}
|
||||
|
||||
Do Registration And Login As User "${username}"
|
||||
[Arguments] ${registration_mail} ${check_mail}=${True} ${password}=${None}
|
||||
IF ${check_mail}
|
||||
Should Contain ${registration_mail} To: ${username}
|
||||
Should Contain ${registration_mail} From: solawi-suite@solawi.me
|
||||
END
|
||||
${reg_url} = Get Registration Link From Mail ${registration_mail}
|
||||
${reg_url} = Transform Registration Link From "${reg_url}"
|
||||
${page} = Open Page ${reg_url}
|
||||
${first_name} = Get Random First Name
|
||||
${last_name} = Get Random Last Name
|
||||
IF $password == ${None}
|
||||
${password} = Get Random Password
|
||||
END
|
||||
${otp} = Get TOTP For User ${username}
|
||||
Fill Text css=input[name="first_name"] ${first_name}
|
||||
Fill Text css=input[name="last_name"] ${last_name}
|
||||
Fill Text css=input[name="otp"] ${otp}
|
||||
Fill Text css=input[name="password1"] ${password}
|
||||
Fill Text css=input[name="password2"] ${password}
|
||||
Click css=button[type="submit"]
|
||||
Get Element css=div[role="alert"].alert.alert-success
|
||||
Click css=a[href^="/users/auth/login"].btn.btn-primary
|
||||
Fill Text css=input[name="username"] ${username}
|
||||
Fill Text css=input[name="password"] ${password}
|
||||
Click css=button[type="submit"]
|
||||
Get Element css=div[role="alert"].alert.alert-success
|
||||
${otp} = Get TOTP For User ${username} ${otp}
|
||||
Fill Text css=input[name="otp"] ${otp}
|
||||
Click css=button[type="submit"]
|
||||
Get Element css=div[role="alert"].alert.alert-success
|
||||
Get Url $= /ui/welcome
|
||||
RETURN ${page}
|
|
@ -0,0 +1,16 @@
|
|||
async function upgradeBrowserForWebAuthnVirtualAuthenticator(page, logger) {
|
||||
const client = await page.context().newCDPSession(page);
|
||||
await client.send("WebAuthn.enable");
|
||||
const options = {
|
||||
transport: "internal",
|
||||
protocol: "ctap2",
|
||||
isUserVerified: true,
|
||||
hasUserVerification: true,
|
||||
hasPersistentKey: false,
|
||||
};
|
||||
return await client.send("WebAuthn.addVirtualAuthenticator", { options: options });
|
||||
}
|
||||
|
||||
upgradeBrowserForWebAuthnVirtualAuthenticator.rfdoc = "Upgrade Browser to use Virtual Authenticator"
|
||||
exports.__esModule = true;
|
||||
exports.upgradeBrowserForWebAuthnVirtualAuthenticator = upgradeBrowserForWebAuthnVirtualAuthenticator;
|
|
@ -0,0 +1,71 @@
|
|||
*** Settings ***
|
||||
Documentation Basic Tests for some special pages
|
||||
Resource ../Keywords/django.resource
|
||||
Suite Setup Solawi-Suite Inner Setup
|
||||
Suite Teardown Solawi-Suite Inner Teardown
|
||||
|
||||
*** Keywords ***
|
||||
Find Link To Special Page
|
||||
[Arguments] ${special_page_url}
|
||||
Get Element Count css=a[href="${special_page_url}"] > 0
|
||||
|
||||
Check Pages For Special Page "${page_url}"
|
||||
[Arguments] @{pages}
|
||||
FOR ${page} IN @{pages}
|
||||
Open Page ${page}
|
||||
Find Link To Special Page ${page_url}
|
||||
END
|
||||
|
||||
Can Successfully Open "${url}"
|
||||
Open Page /
|
||||
&{res} = HTTP ${url}
|
||||
Should Be Equal As Integers ${res.status} 200
|
||||
|
||||
*** Test Cases ***
|
||||
Redirect To Welcome Page - User Site
|
||||
[Documentation] Tests the redirect from the base URL to the welcome page
|
||||
Open Page /
|
||||
Get Url $= /ui/welcome
|
||||
|
||||
Check Imprint Page
|
||||
[Documentation] Is there an Imprint Page containing "Impressum" and "Datenschutz"
|
||||
Open Page /ui/impressum
|
||||
Get Element By Role heading name=Impressum
|
||||
Get Element By Role heading name=Datenschutz
|
||||
|
||||
Check Contact Page
|
||||
[Documentation] Is there a Contact Page containing contact information
|
||||
Open Page /ui/contact
|
||||
Get Element By Role heading name=Kontakt exact=True
|
||||
Get Element By Role heading name=Kontaktmöglichkeiten exact=True
|
||||
Get Element Count css=a[href="mailto:solawi-suite@solawi.me"] > 0
|
||||
|
||||
Has Contact Page Link
|
||||
[Documentation] Tests the reachability of the contact page from a bunch of pages
|
||||
Check Pages For Special Page "/ui/contact" /ui/welcome /ui/contact /ui/impressum /handbook/ /users/auth/login
|
||||
|
||||
Has Imprint Page Link
|
||||
[Documentation] Tests the reachability of the imprint/data protection page from a bunch of pages
|
||||
Check Pages For Special Page "/ui/impressum" /ui/welcome /ui/contact /ui/impressum /handbook/ /users/auth/login
|
||||
|
||||
Has About Page Link
|
||||
[Documentation] Tests the reachability of the about page from a bunch of pages
|
||||
Check Pages For Special Page "/ui/about" /ui/welcome /ui/contact /ui/impressum /users/auth/login
|
||||
|
||||
Has favicon.ico
|
||||
Can Successfully Open "/favicon.ico"
|
||||
|
||||
Has favicon.png
|
||||
Can Successfully Open "/favicon.png"
|
||||
|
||||
Has robots.txt
|
||||
Can Successfully Open "/robots.txt"
|
||||
|
||||
Has ads.txt
|
||||
Can Successfully Open "/ads.txt"
|
||||
|
||||
Has .well-known/security.txt
|
||||
Can Successfully Open "/.well-known/security.txt"
|
||||
|
||||
Has ui/matomo.js
|
||||
Can Successfully Open "/ui/matomo.js"
|
|
@ -0,0 +1,60 @@
|
|||
*** Settings ***
|
||||
Documentation Check Beamer Page
|
||||
|
||||
Resource ../Keywords/django.resource
|
||||
Resource ../Keywords/registration_and_login.resource
|
||||
Library solawi_uat.helper.RandomTestDataLib
|
||||
Library solawi_uat.helper.TimingLib
|
||||
Library String
|
||||
|
||||
Suite Setup Solawi-Suite Inner Setup
|
||||
Suite Teardown Solawi-Suite Inner Teardown
|
||||
|
||||
|
||||
*** Keywords ***
|
||||
Get Screen Code
|
||||
Open Page /beamer/
|
||||
Get Element css=header
|
||||
Hover css=header
|
||||
${code} = Get Text css=header b
|
||||
Length Should Be ${code} 16
|
||||
Should Match Regexp ${code} ^\\d{16}$
|
||||
RETURN ${code}
|
||||
|
||||
*** Test Cases ***
|
||||
Beamer Page Has Screen Code
|
||||
${code1} = Get Screen Code
|
||||
Delete All Cookies
|
||||
${code2} = Get Screen Code
|
||||
Delete All Cookies
|
||||
${code3} = Get Screen Code
|
||||
Should Not Be Equal As Strings ${code1} ${code2}
|
||||
Should Not Be Equal As Strings ${code2} ${code3}
|
||||
Should Not Be Equal As Strings ${code3} ${code1}
|
||||
${code4} = Get Screen Code
|
||||
Should Be Equal As Strings ${code3} ${code4}
|
||||
${code5} = Get Screen Code
|
||||
Should Be Equal As Strings ${code3} ${code5}
|
||||
|
||||
Communication To Beamer Works
|
||||
Get Browser
|
||||
${now} = Now
|
||||
Get Context
|
||||
${username} = Get Random E-Mail Address
|
||||
Run Manage.Py Command invite_user ${username} --module beamer
|
||||
${mail_content} = Get Latest Mail ${now}
|
||||
${user_page} = Do Registration And Login As User "${username}" ${mail_content}
|
||||
Get Context
|
||||
${beamer_page} = New Page ${HOST}/beamer/
|
||||
${code} = Get Screen Code
|
||||
Switch Page ${user_page} ALL
|
||||
Click css=header ul.nav li.nav-item a[href="/beamer/connections"]
|
||||
Fill Text css=input[name="screen_code"] ${code}
|
||||
Press Keys css=input[name="screen_code"] Enter
|
||||
Click css=a[href="/beamer/connection/${code}"].btn.btn-sm
|
||||
${test_text} = Get Random Last Name
|
||||
Fill Text css=input[name="message"] ${test_text}
|
||||
Press Keys css=input[name="message"] Enter
|
||||
Switch Page ${beamer_page} ALL
|
||||
Get Text css=section.present *= Test
|
||||
Get Text css=section.present *= ${test_text}
|
|
@ -0,0 +1,38 @@
|
|||
*** Settings ***
|
||||
Documentation Tests for using Emergency Codes for Login
|
||||
Resource ../Keywords/django.resource
|
||||
Resource ../Keywords/registration_and_login.resource
|
||||
Library solawi_uat.helper.RandomTestDataLib
|
||||
Library solawi_uat.helper.TimingLib
|
||||
Library String
|
||||
|
||||
Suite Setup Solawi-Suite Inner Setup
|
||||
Suite Teardown Solawi-Suite Inner Teardown
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
Check For Notification, Create New Codes, Login With Code
|
||||
${now} = Now
|
||||
${username} = Get Random E-Mail Address
|
||||
Run Manage.Py Command invite_user ${username}
|
||||
${mail_content} = Get Latest Mail ${now}
|
||||
${password} = Get Random Password
|
||||
Do Registration And Login As User "${username}" ${mail_content} ${True} ${password}
|
||||
Get Element css=header div[role="alert"].alert-danger.alert-dismissible a[href^="/users/self-service/emergency-codes"].btn.btn-sm
|
||||
Click css=header div[role="alert"].alert-danger.alert-dismissible a[href^="/users/self-service/emergency-codes"].btn.btn-sm
|
||||
Get Url *= /users/self-service/emergency-codes
|
||||
Click css=form[method="post"]:not([action]) button[type="submit"].btn.btn-danger
|
||||
${codes} = Get Text css=main div.card div.card-body pre.text-center
|
||||
Logout
|
||||
Go To ${HOST}/users/auth/login
|
||||
Fill Text css=input[name="username"] ${username}
|
||||
Fill Text css=input[name="password"] ${password}
|
||||
Click css=button[type="submit"]
|
||||
Get Element css=div[role="alert"].alert.alert-success
|
||||
Click css=form[method="post"]:not([action]) a[href^="/users/auth/emergency-code"]
|
||||
${code} = Get Line ${codes} 0
|
||||
Fill Text css=form[method="post"]:not([action]) input[name="emergency_code"] ${code}
|
||||
Click css=form[method="post"]:not([action]) button[type="submit"]
|
||||
Get Url $= /ui/welcome
|
||||
Get Element css=div[role="alert"].alert.alert-success
|
||||
Get Element css=footer a[href^="/users/auth/logout"].btn.btn-warning
|
|
@ -0,0 +1,32 @@
|
|||
*** Settings ***
|
||||
Documentation Test Using Passkeys
|
||||
Resource ../Keywords/django.resource
|
||||
Resource ../Keywords/registration_and_login.resource
|
||||
Library solawi_uat.helper.TimingLib
|
||||
Library Dialogs
|
||||
|
||||
Suite Setup Solawi-Suite Inner Setup
|
||||
Suite Teardown Solawi-Suite Inner Teardown
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
Create Passkey And Use For Login
|
||||
[Tags] manual
|
||||
${now} = Now
|
||||
${username} = Get Random E-Mail Address
|
||||
Run Manage.Py Command invite_user ${username}
|
||||
${mail_content} = Get Latest Mail ${now}
|
||||
${password} = Get Random Password
|
||||
Do Registration And Login As User "${username}" ${mail_content} ${True} ${password}
|
||||
Get Element css=header div[role="alert"].alert-warning.alert-dismissible a[href^="/passkey/"].btn.btn-sm
|
||||
Click css=header div[role="alert"].alert-warning.alert-dismissible a[href^="/passkey/"].btn.btn-sm
|
||||
Get Url *= /passkey/
|
||||
Click css=a[href="/passkey/add"].btn.btn-success
|
||||
Execute Manual Step Please create a passkey now
|
||||
Logout
|
||||
Click css=a[href^="/users/auth/login"].btn
|
||||
Click css=div.card div.card-body a[href^="/passkey/auth"].btn.btn-lg
|
||||
Execute Manual Step Please authenticate with Passkey now
|
||||
Get Url $= /ui/welcome
|
||||
Get Element css=div[role="alert"].alert.alert-success
|
||||
Get Element css=footer a[href^="/users/auth/logout"].btn.btn-warning
|
|
@ -0,0 +1,33 @@
|
|||
*** Settings ***
|
||||
Documentation Check the registration and login procedures
|
||||
Resource ../Keywords/django.resource
|
||||
Resource ../Keywords/registration_and_login.resource
|
||||
Library solawi_uat.helper.RandomTestDataLib
|
||||
Library solawi_uat.helper.TimingLib
|
||||
Library String
|
||||
|
||||
Suite Setup Solawi-Suite Inner Setup
|
||||
Suite Teardown Solawi-Suite Inner Teardown
|
||||
|
||||
|
||||
*** Test Cases ***
|
||||
Registration As Normal User And Login With Username/Password/TOTP
|
||||
${now} = Now
|
||||
${email} = Get Random E-Mail Address
|
||||
Run Manage.Py Command invite_user ${email}
|
||||
${mail_content} = Get Latest Mail ${now}
|
||||
Do Registration And Login As User "${email}" ${mail_content}
|
||||
Click css=footer button[data-bs-toggle="dropdown"]
|
||||
Get Element css=footer ul.dropdown-menu li a[href="/users/self-service"].dropdown-item
|
||||
|
||||
Registration As Super User And Login With Username/Password/TOTP
|
||||
${now} = Now
|
||||
${email} = Get Random E-Mail Address
|
||||
${data} = Run Manage.Py Command createsuperuser ${email}
|
||||
${link_code} = Decode Bytes To String ${data.stdout} "utf-8"
|
||||
@{link_code} = Split String ${link_code} : 1
|
||||
${link_code} = Strip String ${link_code[1]}
|
||||
Do Registration And Login As User "${email}" ${link_code} ${False}
|
||||
Get Element css=footer ul.dropdown-menu li a[href="/admin/"].dropdown-item
|
||||
Click css=footer button[data-bs-toggle="dropdown"]
|
||||
Get Element css=footer ul.dropdown-menu li a[href="/users/self-service"].dropdown-item
|
|
@ -0,0 +1,4 @@
|
|||
*** Settings ***
|
||||
Resource ../Keywords/django.resource
|
||||
Suite Setup Start Solawi-Suite
|
||||
Suite Teardown Stop Solawi-Suite
|
|
@ -0,0 +1,6 @@
|
|||
--listener allure_robotframework:uat-test-results/allure
|
||||
--prerunmodifier allure_robotframework.testplan
|
||||
--logtitle [solawi-suite] Sytem Test Log
|
||||
--reporttitle [solawi-suite] System Test Report
|
||||
--outputdir uat-test-results
|
||||
--xunit xunit.xml
|
|
@ -0,0 +1 @@
|
|||
Subproject commit a520d21ddc0c4bd96b3d57bd012f2134150fffa8
|
|
@ -0,0 +1 @@
|
|||
0xf16a75d1926178c8c7a5e7895859d59c511c96b5c0f5d3d25053168861e5d1eafa451bdbe2061c140bc5e917b7238052ce02a505cff4c4210a6a01df96ef06e1
|
|
@ -0,0 +1 @@
|
|||
0x5cda238115d1e93aea954d12e1edabb635f1728e5c3bfad7fa7403be8ab454083be8db052b472f97a24a2914f67286256eecbf6ee8e006447795b628f1587e0a
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'solawi_platform.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
virtualenvs.in-project = false
|
|
@ -0,0 +1,74 @@
|
|||
[tool.poetry]
|
||||
name = "platform"
|
||||
version = "0.4.3-dev"
|
||||
description = "Plattform für die Apps der Solawi."
|
||||
authors = ["Juergen Edelbluth <solawi@jued.de>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
packages = [
|
||||
{ include = "solawi_platform", from = ".", format = ["sdist", "wheel"] },
|
||||
{ include = "solawi_apps", from = ".", format = ["sdist", "wheel"] },
|
||||
{ include = "solawi_uat", from = ".", format = ["sdist", "wheel"] },
|
||||
]
|
||||
include = [
|
||||
{ path = "manage.py", format = ["sdist", "wheel"] },
|
||||
{ path = "keys", format = ["sdist", "wheel"] },
|
||||
{ path = "SystemTest", format = ["sdist", "wheel"] },
|
||||
]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
"manage.py" = "manage:main"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
eciespy = "^0.4.1"
|
||||
django = "^5.0.2"
|
||||
nanoid = "^2.0.0"
|
||||
cryptography = "^42.0.5"
|
||||
whitenoise = "^6.6.0"
|
||||
brotli = "^1.1.0"
|
||||
pyotp = "^2.9.0"
|
||||
qrcode = {extras = ["pil"], version = "^7.4.2"}
|
||||
django-ipware = "^6.0.4"
|
||||
docutils = "^0.20.1"
|
||||
django-eventstream = "^5.1.0"
|
||||
daphne = "^4.1.0"
|
||||
pydantic = "^2.6.3"
|
||||
xhtml2pdf = {extras = ["pycairo"], version = "^0.2.15"}
|
||||
markdown = "^3.6"
|
||||
django-csp = "^3.8"
|
||||
django-cors-headers = "^4.3.1"
|
||||
webauthn = "^2.1.0"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
django-debug-toolbar = "^4.3.0"
|
||||
beautifulsoup4 = "^4.12.3"
|
||||
selenium = "^4.18.1"
|
||||
playwright = "^1.42.0"
|
||||
requests-tracker = "^0.3.3"
|
||||
locust = "^2.24.1"
|
||||
robotframework = "^7.0"
|
||||
robotframework-browser = "^18.3.0"
|
||||
requests = "^2.31.0"
|
||||
faker = "^24.7.1"
|
||||
robotframework-pabot = "^2.18.0"
|
||||
allure-robotframework = "^2.13.5"
|
||||
|
||||
|
||||
[tool.poetry.group.prod.dependencies]
|
||||
mysqlclient = "^2.2.4"
|
||||
twisted = {version = "^24.3.0", extras = ["http2"]}
|
||||
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs-material = "^9.5.15"
|
||||
mkdocs-minify-plugin = "^0.8.0"
|
||||
mkdocs-redirects = "^1.2.1"
|
||||
mkdocs = {extras = ["i18n"], version = "^1.5.3"}
|
||||
mkdocs-glightbox = "^0.3.7"
|
||||
mkdocs-mermaid2-plugin = "^1.1.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
|
@ -0,0 +1,8 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class AhgConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'solawi_apps.ahg'
|
||||
verbose_name = _("[solawi-suite] Abholgemeinschaft")
|
|
@ -0,0 +1,10 @@
|
|||
from django import forms
|
||||
|
||||
from solawi_apps.ahg.models import Ahg
|
||||
|
||||
|
||||
class AhgForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Ahg
|
||||
fields = ("name", "note")
|
|
@ -0,0 +1,156 @@
|
|||
import django.db.models.deletion
|
||||
import solawi_apps.db.dbid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Ahg',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('note', models.TextField(blank=True, default=None, max_length=100000, null=True, verbose_name='Notiz')),
|
||||
('owner_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Beisitzer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Abholgemeinschaft',
|
||||
'verbose_name_plural': 'Abholgemeinschaften',
|
||||
'default_permissions': (),
|
||||
'unique_together': {('owner_fk', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HarvestYear',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('year', models.CharField(max_length=255, verbose_name='Jahr')),
|
||||
('note', models.TextField(blank=True, default=None, max_length=100000, null=True, verbose_name='Notiz')),
|
||||
('ahg_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ahg.ahg', verbose_name='Abholgemeinschaft')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Erntejahr',
|
||||
'verbose_name_plural': 'Erntejahre',
|
||||
'default_permissions': (),
|
||||
'unique_together': {('year', 'ahg_fk')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Date',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Aktiviert')),
|
||||
('date', models.DateField(verbose_name='Datum')),
|
||||
('note', models.TextField(blank=True, default=None, max_length=100000, null=True, verbose_name='Notiz')),
|
||||
('needs_pickup', models.BooleanField(default=True, verbose_name='Benötigt Abholung')),
|
||||
('harvest_year_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ahg.harvestyear', verbose_name='Erntejahr')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Datum',
|
||||
'verbose_name_plural': 'Daten',
|
||||
'default_permissions': (),
|
||||
'unique_together': {('harvest_year_fk', 'date')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Member',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Aktiviert')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('email', models.EmailField(default=None, max_length=254, null=True, verbose_name='E-Mail')),
|
||||
('requires_team', models.BooleanField(default=False, verbose_name='Benötigt Team')),
|
||||
('shares', models.DecimalField(blank=True, decimal_places=2, default=1.0, max_digits=3, null=True, verbose_name='Anteile')),
|
||||
('note', models.TextField(blank=True, default=None, max_length=100000, null=True, verbose_name='Notiz')),
|
||||
('ahg_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ahg.ahg', verbose_name='Abholgemeinschaft')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Mitglied',
|
||||
'verbose_name_plural': 'Mitglieder',
|
||||
'default_permissions': (),
|
||||
'unique_together': {('ahg_fk', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Aktiviert')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Team-Name')),
|
||||
('note', models.TextField(blank=True, default=None, max_length=100000, null=True, verbose_name='Notiz')),
|
||||
('ahg_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ahg.ahg', verbose_name='Abholgemeinschaft')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Abhol-Team',
|
||||
'verbose_name_plural': 'Abhol-Teams',
|
||||
'default_permissions': (),
|
||||
'unique_together': {('ahg_fk', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PlanMemberLink',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('date_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ahg.date', verbose_name='Datum')),
|
||||
('member_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ahg.member', verbose_name='Mitglied')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Einplanung Mitglied',
|
||||
'verbose_name_plural': 'Einplanungen Mitglied',
|
||||
'default_permissions': (),
|
||||
'unique_together': {('date_fk', 'member_fk')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PlanTeamLink',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('date_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ahg.date', verbose_name='Datum')),
|
||||
('team_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ahg.team', verbose_name='Team')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Einplanung Team',
|
||||
'verbose_name_plural': 'Einplanungen Team',
|
||||
'default_permissions': (),
|
||||
'unique_together': {('date_fk', 'team_fk')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamMember',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('note', models.TextField(blank=True, default=None, max_length=100000, null=True, verbose_name='Notiz')),
|
||||
('member_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ahg.member', verbose_name='Mitglied')),
|
||||
('team_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ahg.team', verbose_name='Abhol-Team')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Team-Mitglied',
|
||||
'verbose_name_plural': 'Team-Mitglieder',
|
||||
'default_permissions': (),
|
||||
'unique_together': {('team_fk', 'member_fk')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ahg', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ahg',
|
||||
name='owner_fk',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to=settings.AUTH_USER_MODEL, verbose_name='Besitzer'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,6 @@
|
|||
from solawi_apps.app_permission.mixin import AccessPermissionRequiredMixin
|
||||
|
||||
|
||||
class LoginRequiredMixin(AccessPermissionRequiredMixin):
|
||||
|
||||
app_name = "solawi_apps.ahg"
|
|
@ -0,0 +1,133 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from solawi_apps.db.models import NanoIdPkMixin, CreatedMixin, LastModifiedMixin, EnabledMixin, OwnerMixin
|
||||
|
||||
|
||||
class Ahg(NanoIdPkMixin, OwnerMixin, CreatedMixin, LastModifiedMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Abholgemeinschaft")
|
||||
verbose_name_plural = _("Abholgemeinschaften")
|
||||
default_permissions = ()
|
||||
unique_together = (
|
||||
("owner_fk", "name"),
|
||||
)
|
||||
|
||||
name = models.CharField(_("Name"), max_length=255, null=False, blank=False)
|
||||
note = models.TextField(_("Notiz"), null=True, blank=True, default=None, max_length=100_000)
|
||||
|
||||
|
||||
class HarvestYear(NanoIdPkMixin, CreatedMixin, LastModifiedMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Erntejahr")
|
||||
verbose_name_plural = _("Erntejahre")
|
||||
default_permissions = ()
|
||||
unique_together = (
|
||||
("year", "ahg_fk"),
|
||||
)
|
||||
|
||||
ahg_fk = models.ForeignKey(
|
||||
Ahg, verbose_name=_("Abholgemeinschaft"), null=False, blank=False, on_delete=models.CASCADE
|
||||
)
|
||||
year = models.CharField(_("Jahr"), null=False, blank=False, max_length=255)
|
||||
note = models.TextField(_("Notiz"), null=True, blank=True, default=None, max_length=100_000)
|
||||
|
||||
|
||||
class Date(NanoIdPkMixin, CreatedMixin, LastModifiedMixin, EnabledMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Datum")
|
||||
verbose_name_plural = _("Daten")
|
||||
default_permissions = ()
|
||||
unique_together = (
|
||||
("harvest_year_fk", "date"),
|
||||
)
|
||||
|
||||
harvest_year_fk = models.ForeignKey(
|
||||
HarvestYear, verbose_name=_("Erntejahr"), null=False, blank=False, on_delete=models.CASCADE
|
||||
)
|
||||
date = models.DateField(_("Datum"), null=False, blank=False)
|
||||
note = models.TextField(_("Notiz"), null=True, blank=True, default=None, max_length=100_000)
|
||||
needs_pickup = models.BooleanField(_("Benötigt Abholung"), null=False, blank=False, default=True)
|
||||
|
||||
|
||||
class Member(NanoIdPkMixin, CreatedMixin, LastModifiedMixin, EnabledMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Mitglied")
|
||||
verbose_name_plural = _("Mitglieder")
|
||||
default_permissions = ()
|
||||
unique_together = (
|
||||
("ahg_fk", "name"),
|
||||
)
|
||||
|
||||
ahg_fk = models.ForeignKey(
|
||||
Ahg, verbose_name=_("Abholgemeinschaft"), null=False, blank=False, on_delete=models.CASCADE
|
||||
)
|
||||
name = models.CharField(_("Name"), max_length=255, null=False, blank=False)
|
||||
email = models.EmailField(_("E-Mail"), null=True, blank=False, default=None)
|
||||
requires_team = models.BooleanField(_("Benötigt Team"), null=False, blank=False, default=False)
|
||||
shares = models.DecimalField(_("Anteile"), decimal_places=2, max_digits=3, null=True, blank=True, default=1.0)
|
||||
note = models.TextField(_("Notiz"), null=True, blank=True, default=None, max_length=100_000)
|
||||
|
||||
|
||||
class Team(NanoIdPkMixin, CreatedMixin, LastModifiedMixin, EnabledMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Abhol-Team")
|
||||
verbose_name_plural = _("Abhol-Teams")
|
||||
default_permissions = ()
|
||||
unique_together = (
|
||||
("ahg_fk", "name"),
|
||||
)
|
||||
|
||||
ahg_fk = models.ForeignKey(
|
||||
Ahg, verbose_name=_("Abholgemeinschaft"), null=False, blank=False, on_delete=models.CASCADE
|
||||
)
|
||||
name = models.CharField(_("Team-Name"), null=False, blank=False, max_length=255)
|
||||
note = models.TextField(_("Notiz"), null=True, blank=True, default=None, max_length=100_000)
|
||||
|
||||
|
||||
class TeamMember(NanoIdPkMixin, CreatedMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Team-Mitglied")
|
||||
verbose_name_plural = _("Team-Mitglieder")
|
||||
default_permissions = ()
|
||||
unique_together = (
|
||||
("team_fk", "member_fk"),
|
||||
)
|
||||
|
||||
note = models.TextField(_("Notiz"), null=True, blank=True, default=None, max_length=100_000)
|
||||
team_fk = models.ForeignKey(Team, verbose_name=_("Abhol-Team"), null=False, blank=False, on_delete=models.CASCADE)
|
||||
member_fk = models.ForeignKey(Member, verbose_name=_("Mitglied"), null=False, blank=False, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class PlanMemberLink(NanoIdPkMixin, CreatedMixin, LastModifiedMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Einplanung Mitglied")
|
||||
verbose_name_plural = _("Einplanungen Mitglied")
|
||||
default_permissions = ()
|
||||
unique_together = (
|
||||
("date_fk", "member_fk"),
|
||||
)
|
||||
|
||||
date_fk = models.ForeignKey(Date, verbose_name=_("Datum"), null=False, blank=False, on_delete=models.CASCADE)
|
||||
member_fk = models.ForeignKey(Member, verbose_name=_("Mitglied"), null=False, blank=False, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class PlanTeamLink(NanoIdPkMixin, CreatedMixin, LastModifiedMixin, models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Einplanung Team")
|
||||
verbose_name_plural = _("Einplanungen Team")
|
||||
default_permissions = ()
|
||||
unique_together = (
|
||||
("date_fk", "team_fk"),
|
||||
)
|
||||
|
||||
date_fk = models.ForeignKey(Date, verbose_name=_("Datum"), null=False, blank=False, on_delete=models.CASCADE)
|
||||
team_fk = models.ForeignKey(Team, verbose_name=_("Team"), null=False, blank=False, on_delete=models.CASCADE)
|
|
@ -0,0 +1,15 @@
|
|||
{% extends "solawi_apps/ui/base/app.html" %}
|
||||
|
||||
{% load i18n solawi_ui %}
|
||||
|
||||
{% block title %}{% translate "Abholgemeinschaft" %} {% title_sep %} {{ block.super }}{% endblock %}
|
||||
|
||||
{% block container %}
|
||||
<h1 class="text-end mb-4">{% translate "Abholgemeinschaft" %} <i class="fa fa-fw fa-people-carry-box"></i></h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
{% block breadcrumbs %}{% endblock %}
|
||||
</ol>
|
||||
</nav>
|
||||
{% block content %}{% endblock %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,36 @@
|
|||
{% extends "solawi_apps/ahg/ahg.html" %}
|
||||
|
||||
{% load i18n solawi_ui %}
|
||||
|
||||
{% block title %}{{ object.name }} {% title_sep %} {{ block.super }}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'solawi_apps.ahg:list' %}">{% translate "Abholgemeinschaften" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'solawi_apps.ahg:detail' object.id %}">{{ object.name }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{% translate "AHG löschen" %}</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-danger-subtle mt-4">
|
||||
<h2 class="h4 card-header m-0"><i class="fa fa-fw fa-solid fa-trash-alt"></i> {% translate "Abholgemeinschaft löschen" %}</h2>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card-text">{% blocktranslate with ahg=object.name %}Soll die Abholgemeinschaft <b>{{ ahg }}</b> wirklich gelöscht werden?{% endblocktranslate %}</div>
|
||||
<div class="card-text mt-3">{% blocktranslate %}Der Vorgang kann nicht rückgängig gemacht werden!{% endblocktranslate %}</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-danger"><i class="fa fa-fw fa-solid fa-trash-alt"></i> {% translate "Abholgemeinschaft löschen" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="d-grid">
|
||||
<a class="btn btn-secondary" href="{% url 'solawi_apps.ahg:detail' object.id %}"><i class="fa fa-fw fa-solid fa-cancel"></i> {% translate "Nicht löschen" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,48 @@
|
|||
{% extends "solawi_apps/ahg/_ahg.html" %}
|
||||
|
||||
{% load i18n solawi_ui solawi_forms %}
|
||||
|
||||
{% block title %}{% if object %}{% translate "Abholgemeinschaft bearbeiten" %}{% else %}{% translate "Neue Abholgemeinschaft" %}{% endif %} {% title_sep %} {{ block.super }}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'solawi_apps.ahg:list' %}">{% translate "Abholgemeinschaften" %}</a></li>
|
||||
{% if object %}<li class="breadcrumb-item"><a href="{% url 'solawi_apps.ahg:detail' object.id %}">{{ object.name }}</a></li>{% endif %}
|
||||
<li class="breadcrumb-item active" aria-current="page">{% spaceless %}
|
||||
{% if object %}
|
||||
{% translate "Abholgemeinschaft bearbeiten" %}
|
||||
{% else %}
|
||||
{% translate "Neue Abholgemeinschaft" %}
|
||||
{% endif %}
|
||||
{% endspaceless %}</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-light">
|
||||
<h2 class="h4 card-header m-0">{% spaceless %}
|
||||
{% if object %}
|
||||
{% translate "Abholgemeinschaft bearbeiten" %}
|
||||
{% else %}
|
||||
{% translate "Neue Abholgemeinschaft" %}
|
||||
{% endif %}
|
||||
{% endspaceless %}</h2>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'solawi_apps/ui/form/non-field-errors.html' %}
|
||||
<div class="mb-4">
|
||||
{% include 'solawi_apps/ui/form/form-field-floating-label.html' with field=form.name|form_control|autofocus %}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
{% include 'solawi_apps/ui/form/form-field.html' with field=form.note|form_control|monospace|textarea_rows %}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mt-4">{% spaceless %}
|
||||
{% if object %}
|
||||
<i class="fa fa-fw fa-solid fa-floppy-disk"></i> {% translate "Änderung speichern" %}
|
||||
{% else %}
|
||||
<i class="fa fa-fw fa-solid fa-circle-plus"></i> {% translate "Abholgemeinschaft erstellen" %}
|
||||
{% endif %}
|
||||
{% endspaceless %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,30 @@
|
|||
{% extends "solawi_apps/ahg/_ahg.html" %}
|
||||
|
||||
{% load i18n solawi_ui %}
|
||||
|
||||
{% block title %}{% translate "Abholgemeinschaften" %} {% title_sep %} {{ block.super }}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item active" aria-current="page">{% translate "Abholgemeinschaften" %}</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="text-end"><a href="{% url "solawi_apps.ahg:new" %}" class="btn btn-sm btn-success"><i class="fa fa-fw fa-circle-plus"></i> {% translate "Neue Abholgemeinschaft" %}</a></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Abholgemeinschaft" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for obj in object_list %}
|
||||
<tr>
|
||||
<td><a href="{% url 'solawi_apps.ahg:detail' obj.id %}">{{ obj.name }}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'solawi_apps/ui/bits/paginator.html' %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "solawi_apps/ahg/ahg-list.html" %}
|
||||
|
||||
{% load i18n solawi_ui %}
|
||||
|
||||
{% block title %}{{ object.name }} {% title_sep %} {{ block.super }}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'solawi_apps.ahg:list' %}">{% translate "Abholgemeinschaften" %}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ object.name }}</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="text-end mb-4">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="{% url 'solawi_apps.ahg:edit' object.id %}" class="btn btn-sm btn-primary"><i class="fa fa-fw fa-solid fa-pen"></i> {% translate "Bearbeiten" %}</a>
|
||||
<a href="{% url 'solawi_apps.ahg:delete' object.id %}" class="btn btn-sm btn-danger">{% translate "Löschen" %} <i class="fa fa-fw fa-solid fa-trash-alt"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<h2>{{ object.name }}</h2>
|
||||
{% if object.note %}
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">{{ object.note|markdown }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,17 @@
|
|||
from django.urls import path, register_converter
|
||||
|
||||
from solawi_apps.ahg.views.ahgs import AhgListView, AhgCreateView, AhgEditView, AhgDetailView, AhgDeleteView
|
||||
from solawi_apps.db.dbid import NanoIdConverter
|
||||
|
||||
|
||||
register_converter(NanoIdConverter, "nanoid")
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("", AhgListView.as_view(), name="list"),
|
||||
path("new", AhgCreateView.as_view(), name="new"),
|
||||
path("<nanoid:pk>/edit", AhgEditView.as_view(), name="edit"),
|
||||
path("<nanoid:pk>", AhgDetailView.as_view(), name="detail"),
|
||||
path("<nanoid:pk>/delete", AhgDeleteView.as_view(), name="delete"),
|
||||
]
|
||||
app_name = 'solawi_apps.ahg'
|
|
@ -0,0 +1,71 @@
|
|||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db import IntegrityError
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import ListView, UpdateView, CreateView, DetailView, DeleteView
|
||||
|
||||
from solawi_apps.ahg.forms.ahgs import AhgForm
|
||||
from solawi_apps.ahg.mixin import LoginRequiredMixin
|
||||
from solawi_apps.ahg.models import Ahg
|
||||
|
||||
|
||||
class AhgListView(LoginRequiredMixin, ListView):
|
||||
|
||||
template_name = "solawi_apps/ahg/ahg-list.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return Ahg.objects.filter(owner_fk=self.request.user).order_by('name')
|
||||
|
||||
|
||||
class AhgEditView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
|
||||
|
||||
template_name = "solawi_apps/ahg/ahg-form.html"
|
||||
form_class = AhgForm
|
||||
success_message = _("Abholgemeinschaft angepasst.")
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("solawi_apps.ahg:detail", kwargs={"pk": self.object.id})
|
||||
|
||||
def get_queryset(self):
|
||||
return Ahg.objects.filter(owner_fk=self.request.user)
|
||||
|
||||
|
||||
class AhgCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
|
||||
|
||||
template_name = "solawi_apps/ahg/ahg-form.html"
|
||||
form_class = AhgForm
|
||||
success_message = _("Abholgemeinschaft erstellt.")
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("solawi_apps.ahg:detail", kwargs={"pk": self.object.id})
|
||||
|
||||
def form_valid(self, form):
|
||||
instance = form.instance
|
||||
instance.owner_fk = self.request.user
|
||||
try:
|
||||
return super().form_valid(form)
|
||||
except IntegrityError:
|
||||
form.add_error("name", _("Diese Abholgemeinschaft existiert bereits"))
|
||||
except Exception as err:
|
||||
form.add_error(None, str(err))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class AhgDetailView(LoginRequiredMixin, DetailView):
|
||||
|
||||
template_name = "solawi_apps/ahg/ahg.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return Ahg.objects.filter(owner_fk=self.request.user)
|
||||
|
||||
|
||||
class AhgDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
|
||||
template_name = "solawi_apps/ahg/ahg-delete.html"
|
||||
success_url = reverse_lazy("solawi_apps.ahg:list")
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return _("Abholgemeinschaft %s gelöscht") % self.object.name
|
||||
|
||||
def get_queryset(self):
|
||||
return Ahg.objects.filter(owner_fk=self.request.user)
|
|
@ -0,0 +1,8 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class AnnounceConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'solawi_apps.announce'
|
||||
verbose_name = _("[solawi-suite] Ankündigungen")
|
|
@ -0,0 +1,29 @@
|
|||
from typing import Optional, List
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.utils.safestring import SafeString
|
||||
|
||||
|
||||
CONTEXT_SESSION_KEY = "__solawi-suite__announce__context__"
|
||||
|
||||
|
||||
def add_announcement_to_context(
|
||||
request: HttpRequest,
|
||||
level: int,
|
||||
announcement: SafeString,
|
||||
announce_id: Optional[str] = None,
|
||||
url_filter: Optional[List[str]] = None,
|
||||
):
|
||||
if (context := request.session.get(CONTEXT_SESSION_KEY, None)) is None:
|
||||
context = []
|
||||
if announce_id is not None:
|
||||
already_in = False
|
||||
for a_id, _, _, _ in context:
|
||||
if a_id == announce_id:
|
||||
already_in = True
|
||||
break
|
||||
if not already_in:
|
||||
context.append((announce_id, level, announcement, url_filter))
|
||||
else:
|
||||
context.append((None, level, announcement, url_filter))
|
||||
request.session[CONTEXT_SESSION_KEY] = context
|
|
@ -0,0 +1,54 @@
|
|||
import datetime
|
||||
import solawi_apps.db.dbid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MemberAnnouncement',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Aktiviert')),
|
||||
('active_from', models.DateTimeField(default=datetime.datetime(2000, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), verbose_name='Aktiv ab')),
|
||||
('active_to', models.DateTimeField(default=datetime.datetime(2999, 12, 31, 23, 59, 59, 999999, tzinfo=datetime.timezone.utc), verbose_name='Aktiv bis')),
|
||||
('level', models.PositiveSmallIntegerField(choices=[(20, 'info'), (30, 'warning'), (40, 'error'), (25, 'success')], default=20, verbose_name='Level')),
|
||||
('content', models.TextField(max_length=100000, verbose_name='Inhalt')),
|
||||
('link', models.TextField(blank=True, max_length=1000, null=True, verbose_name='Link')),
|
||||
('link_title', models.TextField(blank=True, max_length=10000, null=True, verbose_name='Link-Titel')),
|
||||
('dismissible', models.BooleanField(default=True, verbose_name='Kann geschlossen werden')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Ankündigung für Mitglieder',
|
||||
'verbose_name_plural': 'Ankündigungen für Mitglieder',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PublicAnnouncement',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Aktiviert')),
|
||||
('active_from', models.DateTimeField(default=datetime.datetime(2000, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), verbose_name='Aktiv ab')),
|
||||
('active_to', models.DateTimeField(default=datetime.datetime(2999, 12, 31, 23, 59, 59, 999999, tzinfo=datetime.timezone.utc), verbose_name='Aktiv bis')),
|
||||
('level', models.PositiveSmallIntegerField(choices=[(20, 'info'), (30, 'warning'), (40, 'error'), (25, 'success')], default=20, verbose_name='Level')),
|
||||
('content', models.TextField(max_length=100000, verbose_name='Inhalt')),
|
||||
('link', models.TextField(blank=True, max_length=1000, null=True, verbose_name='Link')),
|
||||
('link_title', models.TextField(blank=True, max_length=10000, null=True, verbose_name='Link-Titel')),
|
||||
('dismissible', models.BooleanField(default=True, verbose_name='Kann geschlossen werden')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Öffentliche Ankündigung',
|
||||
'verbose_name_plural': 'Öffentliche Ankündigungen',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,54 @@
|
|||
from django.contrib import messages
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from solawi_apps.db.models import NanoIdPkMixin, EnabledMixin, ActiveFromMixin, ActiveToMixin, CreatedMixin, \
|
||||
LastModifiedMixin
|
||||
|
||||
|
||||
LEVELS = (
|
||||
(messages.INFO, "info"),
|
||||
(messages.WARNING, "warning"),
|
||||
(messages.ERROR, "error"),
|
||||
(messages.SUCCESS, "success"),
|
||||
)
|
||||
|
||||
|
||||
class PublicAnnouncement(
|
||||
NanoIdPkMixin, EnabledMixin, ActiveFromMixin, ActiveToMixin, CreatedMixin, LastModifiedMixin, models.Model
|
||||
):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Öffentliche Ankündigung")
|
||||
verbose_name_plural = _("Öffentliche Ankündigungen")
|
||||
|
||||
level = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("Level"), choices=LEVELS, null=False, blank=False, default=messages.INFO,
|
||||
)
|
||||
content = models.TextField(verbose_name=_("Inhalt"), null=False, blank=False, max_length=100_000)
|
||||
link = models.TextField(verbose_name=_("Link"), null=True, blank=True, max_length=1_000)
|
||||
link_title = models.TextField(verbose_name=_("Link-Titel"), null=True, blank=True, max_length=10_000)
|
||||
dismissible = models.BooleanField(verbose_name=_("Kann geschlossen werden"), null=False, blank=False, default=True)
|
||||
|
||||
def __str__(self):
|
||||
return _("Öffentliche Ankündigung %s") % self.id
|
||||
|
||||
|
||||
class MemberAnnouncement(
|
||||
NanoIdPkMixin, EnabledMixin, ActiveFromMixin, ActiveToMixin, CreatedMixin, LastModifiedMixin, models.Model
|
||||
):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Ankündigung für Mitglieder")
|
||||
verbose_name_plural = _("Ankündigungen für Mitglieder")
|
||||
|
||||
level = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("Level"), choices=LEVELS, null=False, blank=False, default=messages.INFO,
|
||||
)
|
||||
content = models.TextField(verbose_name=_("Inhalt"), null=False, blank=False, max_length=100_000)
|
||||
link = models.TextField(verbose_name=_("Link"), null=True, blank=True, max_length=1_000)
|
||||
link_title = models.TextField(verbose_name=_("Link-Titel"), null=True, blank=True, max_length=10_000)
|
||||
dismissible = models.BooleanField(verbose_name=_("Kann geschlossen werden"), null=False, blank=False, default=True)
|
||||
|
||||
def __str__(self):
|
||||
return _("Mitglieder-Ankündigung %s") % self.id
|
|
@ -0,0 +1,13 @@
|
|||
{% load i18n %}{% if a_obj_list and a_obj_list|length > 0 %}{% spaceless %}
|
||||
{% for a in a_obj_list %}
|
||||
<div class="alert {{ a.level }}{% if a.dismissible %} alert-dismissible fade show{% endif %} m-0" role="alert">
|
||||
{{ a.content }}
|
||||
{% if a.link %}
|
||||
<br><a href="{{ a.link }}">{{ a.link_title|default:a.link }}</a>
|
||||
{% endif %}
|
||||
{% if a.dismissible %}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% translate 'Schließen' %}"></button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endspaceless %}{% endif %}
|
|
@ -0,0 +1,51 @@
|
|||
from django import template
|
||||
from django.conf import settings
|
||||
from django.template import RequestContext
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
|
||||
from solawi_apps.announce.context import CONTEXT_SESSION_KEY
|
||||
from solawi_apps.announce.models import PublicAnnouncement, MemberAnnouncement
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag("solawi_apps/announce/tag.html", name="announcements", takes_context=True)
|
||||
def get_announcement_tag(context: RequestContext) -> dict:
|
||||
announcements = []
|
||||
t_now = now()
|
||||
announcement_orm_classes = [PublicAnnouncement]
|
||||
if context.request.user.is_authenticated:
|
||||
announcement_orm_classes.append(MemberAnnouncement)
|
||||
for Announcement in announcement_orm_classes:
|
||||
for a in Announcement.objects.filter(
|
||||
enabled=True, active_from__lte=t_now, active_to__gte=t_now,
|
||||
).order_by("created").all():
|
||||
announcements.append({
|
||||
"level": settings.MESSAGE_TAGS.get(a.level),
|
||||
"content": mark_safe(a.content),
|
||||
"link": a.link,
|
||||
"link_title": a.link_title,
|
||||
"dismissible": a.dismissible,
|
||||
})
|
||||
if (context_announcements := context.request.session.get(CONTEXT_SESSION_KEY, None)) is not None:
|
||||
for a in context_announcements:
|
||||
url_filter = a[3]
|
||||
filtered = False
|
||||
if url_filter is not None and len(url_filter) > 0:
|
||||
for url in url_filter:
|
||||
if context.request.get_full_path().startswith(url):
|
||||
filtered = True
|
||||
if not filtered:
|
||||
announcements.append({
|
||||
"level": settings.MESSAGE_TAGS.get(a[1]),
|
||||
"content": mark_safe(a[2]),
|
||||
"link": None,
|
||||
"link_title": None,
|
||||
"dismissible": True,
|
||||
})
|
||||
if len(context_announcements) > 0:
|
||||
context.request.session[CONTEXT_SESSION_KEY] = []
|
||||
return {
|
||||
"a_obj_list": announcements,
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from solawi_apps.announce.models import PublicAnnouncement, MemberAnnouncement
|
||||
|
||||
|
||||
class SessionBasedAnnouncementTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
PublicAnnouncement.objects.create(
|
||||
enabled=True,
|
||||
level=messages.INFO,
|
||||
content="PUBLIC ANNOUNCEMENT",
|
||||
)
|
||||
MemberAnnouncement.objects.create(
|
||||
enabled=True,
|
||||
level=messages.INFO,
|
||||
content="MEMBER ANNOUNCEMENT",
|
||||
)
|
||||
User = get_user_model()
|
||||
User.objects.create_user(username='test', password='passwd', is_active=True)
|
||||
|
||||
def test_announcement_playout_for_anonymous_user(self):
|
||||
welcome = reverse("solawi_apps.ui:welcome")
|
||||
response = self.client.get(welcome)
|
||||
self.assertIn(b"PUBLIC ANNOUNCEMENT", response.content)
|
||||
self.assertNotIn(b"MEMBER ANNOUNCEMENT", response.content)
|
||||
|
||||
def test_announcement_playout_for_logged_in_user(self):
|
||||
welcome = reverse("solawi_apps.ui:welcome")
|
||||
self.client.login(username="test", password="passwd")
|
||||
response = self.client.get(welcome)
|
||||
self.assertIn(b"PUBLIC ANNOUNCEMENT", response.content)
|
||||
self.assertIn(b"MEMBER ANNOUNCEMENT", response.content)
|
|
@ -0,0 +1,8 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class AppPermissionConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'solawi_apps.app_permission'
|
||||
verbose_name = _("[solawi-suite] Berechtigungen für die App-Nutzung")
|
|
@ -0,0 +1,9 @@
|
|||
from typing import List, Tuple
|
||||
from django.apps import apps
|
||||
|
||||
|
||||
def get_all_installed_app_labels() -> List[Tuple[str, str]]:
|
||||
list_of_app_labels = [
|
||||
(ac.name, ac.verbose_name) for ac in apps.get_app_configs() if ac.name.startswith('solawi_apps.')
|
||||
]
|
||||
return list_of_app_labels
|
|
@ -0,0 +1,36 @@
|
|||
import datetime
|
||||
import django.db.models.deletion
|
||||
import solawi_apps.app_permission.integration
|
||||
import solawi_apps.db.dbid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AccessPermission',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Aktiviert')),
|
||||
('active_from', models.DateTimeField(default=datetime.datetime(2000, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), verbose_name='Aktiv ab')),
|
||||
('active_to', models.DateTimeField(default=datetime.datetime(2999, 12, 31, 23, 59, 59, 999999, tzinfo=datetime.timezone.utc), verbose_name='Aktiv bis')),
|
||||
('app_name', models.CharField(choices=solawi_apps.app_permission.integration.get_all_installed_app_labels, max_length=200, verbose_name='App Name')),
|
||||
('user_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Zugangsberechtigung',
|
||||
'verbose_name_plural': 'Zugangsberechtigungen',
|
||||
'unique_together': {('app_name', 'user_fk')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
import datetime
|
||||
import solawi_apps.app_permission.integration
|
||||
import solawi_apps.db.dbid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app_permission', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DefaultPermission',
|
||||
fields=[
|
||||
('id', models.CharField(default=solawi_apps.db.dbid.generate_id, editable=False, max_length=32, primary_key=True, serialize=False, validators=[solawi_apps.db.dbid.validate_nanoid], verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt')),
|
||||
('last_modified', models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Aktiviert')),
|
||||
('active_from', models.DateTimeField(default=datetime.datetime(2000, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), verbose_name='Aktiv ab')),
|
||||
('active_to', models.DateTimeField(default=datetime.datetime(2999, 12, 31, 23, 59, 59, 999999, tzinfo=datetime.timezone.utc), verbose_name='Aktiv bis')),
|
||||
('app_name', models.CharField(choices=solawi_apps.app_permission.integration.get_all_installed_app_labels, max_length=200, unique=True, verbose_name='App Name')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Standard Zugangsberechtigung',
|
||||
'verbose_name_plural': 'Standard Zugangsberechtigungen',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
DEFAULT_PERMISSIONS = (
|
||||
'solawi_apps.beamer',
|
||||
'solawi_apps.changelog',
|
||||
'solawi_apps.passkeys',
|
||||
)
|
||||
|
||||
|
||||
def add_default_permissions(apps, schema_editor):
|
||||
DefaultPermission = apps.get_model('app_permission', 'DefaultPermission')
|
||||
DefaultPermission.objects.bulk_create([
|
||||
DefaultPermission(app_name=d, enabled=True) for d in DEFAULT_PERMISSIONS
|
||||
], ignore_conflicts=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app_permission', '0002_defaultpermission'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_default_permissions)
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def add_passkey_permission_to_existing_users(apps, schema_editor):
|
||||
User = get_user_model()
|
||||
user_ids = [t.id for t in User.objects.all()]
|
||||
AccessPermission = apps.get_model('app_permission', 'AccessPermission')
|
||||
AccessPermission.objects.bulk_create([
|
||||
AccessPermission(
|
||||
user_fk_id=t,
|
||||
app_name='solawi_apps.passkeys',
|
||||
enabled=True,
|
||||
) for t in user_ids
|
||||
], ignore_conflicts=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app_permission', '0003_default_permissions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_passkey_permission_to_existing_users),
|
||||
]
|
|
@ -0,0 +1,36 @@
|
|||
from django.contrib.auth.mixins import AccessMixin
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.timezone import now
|
||||
|
||||
from solawi_apps.app_permission.models import AccessPermission
|
||||
|
||||
|
||||
class AccessPermissionRequiredMixin(AccessMixin):
|
||||
|
||||
app_name = None
|
||||
app_access_denied_url = reverse_lazy("solawi_apps.app_permission:permission-denied")
|
||||
|
||||
def get_app_name(self):
|
||||
return self.app_name
|
||||
|
||||
def get_app_access_denied_url(self):
|
||||
return self.app_access_denied_url
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
app_name = self.get_app_name()
|
||||
if request.user.is_superuser or app_name is None:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
t_now = now()
|
||||
if AccessPermission.objects.filter(
|
||||
user_fk=request.user,
|
||||
enabled=True,
|
||||
active_from__lte=t_now,
|
||||
active_to__gte=t_now,
|
||||
app_name=app_name,
|
||||
).exists():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
return redirect(self.get_app_access_denied_url(), permanent=False)
|
|
@ -0,0 +1,50 @@
|
|||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from solawi_apps.app_permission.integration import get_all_installed_app_labels
|
||||
from solawi_apps.db.models import NanoIdPkMixin, EnabledMixin, ActiveFromMixin, ActiveToMixin, CreatedMixin, \
|
||||
LastModifiedMixin
|
||||
|
||||
|
||||
class AccessPermission(
|
||||
NanoIdPkMixin, EnabledMixin, ActiveFromMixin, ActiveToMixin, CreatedMixin, LastModifiedMixin, models.Model
|
||||
):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Zugangsberechtigung")
|
||||
verbose_name_plural = _("Zugangsberechtigungen")
|
||||
unique_together = (
|
||||
("app_name", "user_fk"),
|
||||
)
|
||||
|
||||
app_name = models.CharField(
|
||||
verbose_name=_("App Name"), null=False, blank=False, max_length=200, choices=get_all_installed_app_labels
|
||||
)
|
||||
user_fk = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, verbose_name=_("User"), on_delete=models.CASCADE, null=False, blank=False
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.app_name} => {self.user_fk.email}"
|
||||
|
||||
|
||||
class DefaultPermission(
|
||||
NanoIdPkMixin, EnabledMixin, ActiveFromMixin, ActiveToMixin, CreatedMixin, LastModifiedMixin, models.Model
|
||||
):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Standard Zugangsberechtigung")
|
||||
verbose_name_plural = _("Standard Zugangsberechtigungen")
|
||||
|
||||
app_name = models.CharField(
|
||||
verbose_name=_("App Name"),
|
||||
null=False,
|
||||
blank=False,
|
||||
max_length=200,
|
||||
choices=get_all_installed_app_labels,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.app_name
|
|
@ -0,0 +1,11 @@
|
|||
{% extends 'solawi_apps/ui/base/app.html' %}
|
||||
|
||||
{% load i18n solawi_ui %}
|
||||
|
||||
{% block title %}{% translate "Zugang verweigert" %} {% title_sep %} {{ block.super }}{% endblock %}
|
||||
|
||||
{% block container %}
|
||||
<h1>{% translate "Zugang verweigert" %}</h1>
|
||||
<p>{% blocktranslate %}Du hast leider keinen Zugang zu dieser Applikation aus dem [solawi-suite] Werkzeugkasten.{% endblocktranslate %}</p>
|
||||
<p>{% blocktranslate %}Bitte wende dich an das Team, um wenn du Zugang benötigst.{% endblocktranslate %}</p>
|
||||
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from solawi_apps.app_permission.integration import get_all_installed_app_labels
|
||||
|
||||
|
||||
class InstalledAppsTest(TestCase):
|
||||
|
||||
def test_identifier_of_installed_apps(self):
|
||||
cases = [a[0] for a in get_all_installed_app_labels()]
|
||||
for case in cases:
|
||||
with self.subTest(case=case):
|
||||
self.assertTrue(case.startswith("solawi_apps."))
|
||||
|
||||
def test_label_of_installed_apps(self):
|
||||
cases = [a[1] for a in get_all_installed_app_labels()]
|
||||
for case in cases:
|
||||
with self.subTest(case=case):
|
||||
self.assertTrue(case.startswith("[solawi-suite] "))
|
|
@ -0,0 +1,65 @@
|
|||
from urllib.parse import quote
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.test import SimpleTestCase, RequestFactory
|
||||
|
||||
from solawi_apps.app_permission.mixin import AccessPermissionRequiredMixin
|
||||
|
||||
|
||||
class AccessMixinInUseWithRaiseTest(SimpleTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
class ConfiguredAccessPermissionRequiredMixin(AccessPermissionRequiredMixin):
|
||||
app_name = 'test'
|
||||
raise_exception = True
|
||||
|
||||
cls.mixin = ConfiguredAccessPermissionRequiredMixin()
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.request = self.factory.get("/")
|
||||
setattr(self.mixin, 'request', self.request)
|
||||
|
||||
def tearDown(self):
|
||||
delattr(self.mixin, 'request')
|
||||
|
||||
def test_get_app_name(self):
|
||||
self.assertEqual(self.mixin.get_app_name(), 'test')
|
||||
|
||||
def test_anon_request(self):
|
||||
self.request.user = AnonymousUser()
|
||||
with self.assertRaises(PermissionDenied):
|
||||
self.mixin.dispatch(self.request)
|
||||
|
||||
|
||||
class AccessMixinInUseWithoutRaiseTest(SimpleTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
class ConfiguredAccessPermissionRequiredMixin(AccessPermissionRequiredMixin):
|
||||
app_name = 'test'
|
||||
raise_exception = False
|
||||
|
||||
cls.mixin = ConfiguredAccessPermissionRequiredMixin()
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.request = self.factory.get("/")
|
||||
setattr(self.mixin, 'request', self.request)
|
||||
|
||||
def tearDown(self):
|
||||
delattr(self.mixin, 'request')
|
||||
|
||||
def test_get_app_name(self):
|
||||
self.assertEqual(self.mixin.get_app_name(), 'test')
|
||||
|
||||
def test_anon_request(self):
|
||||
self.request.user = AnonymousUser()
|
||||
response = self.mixin.dispatch(self.request)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertRedirects(
|
||||
response, f"{settings.LOGIN_URL}?next={quote('/')}", fetch_redirect_response=False
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
from django.urls import path
|
||||
|
||||
from solawi_apps.app_permission.views import NoAppPermission
|
||||
|
||||
urlpatterns = [
|
||||
path("app-access-permission-denied", NoAppPermission.as_view(), name="permission-denied"),
|
||||
]
|
||||
app_name = "solawi_apps.app_permission"
|
|
@ -0,0 +1,6 @@
|
|||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class NoAppPermission(TemplateView):
|
||||
|
||||
template_name = "solawi_apps/app_permissions/app-access-denied.html"
|
|
@ -0,0 +1,8 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class SolawiAppsBeamerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'solawi_apps.beamer'
|
||||
verbose_name = _("[solawi-suite] Beamer-Steuerung")
|
|
@ -0,0 +1,22 @@
|
|||
from django_eventstream.channelmanager import DefaultChannelManager
|
||||
|
||||
|
||||
SCREEN_CODE_SESSION_KEY = "__beamer_screen_code__"
|
||||
|
||||
|
||||
class BeamerChannelManager(DefaultChannelManager):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.__allowed_channels = []
|
||||
|
||||
def is_channel_reliable(self, channel):
|
||||
return False
|
||||
|
||||
def get_channels_for_request(self, request, view_kwargs):
|
||||
if (d := request.session.get(SCREEN_CODE_SESSION_KEY, None)) is not None:
|
||||
self.__allowed_channels.append(f"beamer-{d}")
|
||||
return self.__allowed_channels
|
||||
|
||||
def can_read_channel(self, user, channel):
|
||||
return channel in self.__allowed_channels
|
|
@ -0,0 +1 @@
|
|||
CONNECTED_BEAMERS_SESSION_KEY = "__connected_beamers__"
|
|
@ -0,0 +1,16 @@
|
|||
from django import forms
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class CodeForm(forms.Form):
|
||||
|
||||
screen_code = forms.CharField(label=_("Screen Code"), min_length=16, max_length=16, required=True, validators=[
|
||||
RegexValidator(regex=r"^\d{16}$"),
|
||||
])
|
||||
|
||||
|
||||
class MessageForm(forms.Form):
|
||||
|
||||
message = forms.CharField(label=_("Nachricht"), min_length=1, max_length=200, required=True)
|
||||
message.widget.attrs.update({'placeholder': _("Test-Nachricht")})
|
|
@ -0,0 +1,6 @@
|
|||
from solawi_apps.app_permission.mixin import AccessPermissionRequiredMixin
|
||||
|
||||
|
||||
class LoginRequiredMixin(AccessPermissionRequiredMixin):
|
||||
|
||||
app_name = "solawi_apps.beamer"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue