Branch Cleanup

This commit is contained in:
Jürgen Edelbluth 2024-04-13 11:35:38 +02:00
parent 3941e3f92e
commit d859e1952c
Signed by: jed
GPG Key ID: 6DEAEDD5CDB646DF
911 changed files with 154232 additions and 2 deletions

70
.ci/Jenkinsfile vendored Normal file
View File

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

155
.gitignore vendored Normal file
View File

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

6
.gitmodules vendored Normal file
View File

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

8
.idea/.gitignore vendored Normal file
View File

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

12
.idea/dataSources.xml Normal file
View File

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

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

28
.idea/jsonSchemas.xml Normal file
View File

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

7
.idea/misc.xml Normal file
View File

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

8
.idea/modules.xml Normal file
View File

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

23
.idea/platform.iml Normal file
View File

@ -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="&lt;map/&gt;" />
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

8
.idea/vcs.xml Normal file
View File

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

4
3rdparty/.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated

15
3rdparty/.gitignore vendored Normal file
View File

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

893
3rdparty/.yarn/releases/yarn-4.1.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

5
3rdparty/.yarnrc.yml vendored Normal file
View File

@ -0,0 +1,5 @@
enableTelemetry: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.1.0.cjs

3
3rdparty/README.md vendored Normal file
View File

@ -0,0 +1,3 @@
# 3rdparty
3rd Party Libs für das solawi-suite-System

11
3rdparty/package.json vendored Normal file
View File

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

64
3rdparty/yarn.lock vendored Normal file
View File

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

20
CHANGES.md Normal file
View File

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

View File

@ -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
1 0001a 2 hguerreau1@aboutads.info Harriott
2 0001b 2 iizon2@comcast.net Isaac
3 0002a 4 ngodspeede3@yandex.ru Nikolia
4 0002b 4 smorlon4@craigslist.org Staci
5 0002c 4 fdurtnal5@zdnet.com Florry
6 0002d 4 cbargery6@stumbleupon.com Curtis
7 0003 1 mrother7@ow.ly Marthena
8 0004 1 lcapinetti8@auda.org.au Liz
9 0005a 2 tkenset9@creativecommons.org Thorndike
10 0005b 2 kwillsheara@youtu.be Kassia
11 0006a 2 cconachieb@behance.net Crichton
12 0006b 2 yorhrtc@istockphoto.com Yankee
13 0007a 2 smacconachyd@wufoo.com Sonnnie
14 0007b 2 coharae@merriam-webster.com Chrystal
15 0008 1 jcarmelf@howstuffworks.com Jacquelynn
16 0009 1 bdaberg@cpanel.net Brucie
17 0010 1 ethirlawayh@slate.com Ellene
18 0011a 2 scarusi@accuweather.com Sharlene
19 0011b 2 apietrasikj@rakuten.co.jp Aguste
20 0012a 2 svegask@t-online.de Silvan
21 0012b 2 mboxilll@hexun.com Michail
22 0013a 2 mosgodbym@cocolog-nifty.com Mahala
23 0013b 2 cpauln@vistaprint.com Conni
24 0014a 2 cmatzeitiso@cpanel.net Carmelita
25 0014b 2 jdarlingtonp@hc360.com Jeff
26 0015a 2 dsmithq@tuttocitta.it Darcie
27 0015b 2 tclapsonr@uol.com.br Tanhya
28 0016a 2 cfinkers@photobucket.com Caitrin
29 0016b 2 bgommet@bravesites.com Burch
30 0017a 2 hbenetu@dyndns.org Heddie
31 0017b 2 rhighmanv@marketwatch.com Rebbecca
32 0018a 2 mgentnerw@pbs.org Masha
33 0018b 2 omacanultyx@tamu.edu Ottilie
34 0019a 2 ryankeevy@auda.org.au Raymund
35 0019b 2 mgrishankovz@npr.org Marlon
36 0020a 4 rsnoddin10@fotki.com Rosalie
37 0020b 4 tstucksbury11@tuttocitta.it Toiboid
38 0020c 4 eriseborough12@paypal.com Eberhard
39 0020d 4 jboas13@redcross.org Joelly
40 0021a 3 btaber14@home.pl Ban
41 0021b 3 emapples15@wikipedia.org Emanuel
42 0021c 3 jbleakman16@seesaa.net Joane
43 0022a 5 apossel17@cpanel.net Ansell
44 0022b 5 cbenettelli18@elpais.com Christiano
45 0022c 5 jeaseman19@odnoklassniki.ru Jacqui
46 0022d 5 ecrilley1a@harvard.edu Elyssa
47 0022e 5 akilalea1b@wix.com Agosto
48 0023a 2 sbardsley1c@cisco.com Sibylle
49 0023b 2 istanley1d@psu.edu Irving
50 0024a 2 ekhadir1e@taobao.com Eliza
51 0024b 2 challows1f@senate.gov Charline
52 0025a 2 sdran1g@spotify.com Sutherlan
53 0025b 2 zgarfirth1h@yahoo.com Zondra
54 0026a 2 jarkcoll1i@slideshare.net Josy
55 0026b 2 cbirch1j@geocities.jp Cherry
56 0027a 2 bsickling1k@privacy.gov.au Berte
57 0027b 2 edougliss1l@patch.com Eda
58 0028a 2 crosario1m@diigo.com Conroy
59 0028b 2 borto1n@irs.gov Beatrisa
60 0029a 2 amurcutt1o@list-manage.com Archer
61 0029b 2 umacclenan1p@guardian.co.uk Ulberto
62 0030a 2 csherrum1q@nsw.gov.au Cathy
63 0030b 2 srawsthorne1r@icio.us Shanon
64 0031a 3 wlipson1s@topsy.com Winna
65 0031b 3 jgarnar1t@jimdo.com Jaclyn
66 0031c 3 omerriday1u@vkontakte.ru Odelia
67 0032a 2 eloosemore1v@statcounter.com Earvin
68 0032b 2 bpickover1w@furl.net Bethena
69 0033 1 kchislett1x@angelfire.com Kenton
70 0034a 2 weuesden1y@xrea.com Welch
71 0034b 2 psimonds1z@de.vu Piggy
72 0035a 2 ebachmann20@ucla.edu Elianore
73 0035b 2 bsutherden21@princeton.edu Billy
74 0036a 2 asangster22@harvard.edu Abran
75 0036b 2 rbreckwell23@examiner.com Rickey
76 0037a 2 pmachon24@desdev.cn Page
77 0037b 2 jbraundt25@imgur.com Jackelyn
78 0038a 2 mrivers26@pagesperso-orange.fr Mervin
79 0038b 2 sgiacovelli27@hugedomains.com Sibylle
80 0039a 2 mbesant28@uol.com.br Muhammad
81 0039b 2 rcardoe29@technorati.com Rourke
82 0040a 8 vgandey2a@lulu.com Vale
83 0040b 8 eviall2b@bbc.co.uk Emmy
84 0040c 8 sswin2c@yellowpages.com Sonny
85 0040d 8 aportman2d@canalblog.com Ardeen
86 0040e 8 jchaudhry2e@imageshack.us Joye
87 0040f 8 rleidecker2f@fda.gov Ryann
88 0040g 8 mproske2g@answers.com Mathias
89 0040h 8 dpatry2h@gmpg.org Dalenna
90 0041a 2 ksharply2i@sbwire.com Kaitlin
91 0041b 2 cdulanty2j@harvard.edu Clem
92 0042a 2 phawes2k@jiathis.com Parrnell
93 0042b 2 hwackley2l@cornell.edu Harold
94 0043a 2 mmuccino2m@forbes.com Meridel
95 0043b 2 cbarkas2n@cnet.com Cicily
96 0044 1 nbowcher2o@mit.edu Nickola
97 0045 1 rpideon2p@google.ca Raynor
98 0046a 2 ccolling2q@sina.com.cn Clyve
99 0046b 2 rcornbill2r@oakley.com Robby
100 0047a 2 nbilston1@senate.gov Noble
101 0047b 2 dgooke2@dyndns.org Dixie
102 0048a 2 gmcpeake3@bbc.co.uk Ganny
103 0048b 2 mstairmand4@bizjournals.com Martguerita
104 0049 1 hsedgeworth5@vimeo.com Hale
105 0050a 8 abrignall6@cocolog-nifty.com Astrid
106 0050b 8 jcondon7@mayoclinic.com Joya
107 0050c 8 cledward8@ebay.co.uk Cristabel
108 0050d 8 aspino9@123-reg.co.uk Anita
109 0050e 8 krosenblada@opera.com Kaitlynn
110 0050f 8 blowndesb@illinois.edu Bambi
111 0050g 8 gbroadhurstc@oakley.com Giacopo
112 0050h 8 pcarnied@vkontakte.ru Pattie

View File

@ -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
1 0001a 2 hguerreau1@aboutads.info Harriott
2 0001b 2 iizon2@comcast.net Isaac
3 0002a 4 ngodspeede3@yandex.ru Nikolia
4 0002b 4 smorlon4@craigslist.org Staci
5 0002c 4 fdurtnal5@zdnet.com Florry
6 0002d 4 cbargery6@stumbleupon.com Curtis
7 0003 1 mrother7@ow.ly Marthena
8 0004 1 lcapinetti8@auda.org.au Liz
9 0005a 2 tkenset9@creativecommons.org Thorndike
10 0005b 2 kwillsheara@youtu.be Kassia
11 0006a 2 cconachieb@behance.net Crichton
12 0006b 2 yorhrtc@istockphoto.com Yankee
13 0007a 2 smacconachyd@wufoo.com Sonnnie
14 0007b 2 coharae@merriam-webster.com Chrystal
15 0008 1 jcarmelf@howstuffworks.com Jacquelynn
16 0009 1 bdaberg@cpanel.net Brucie
17 0010 1 ethirlawayh@slate.com Ellene
18 0011a 2 scarusi@accuweather.com Sharlene
19 0011b 2 apietrasikj@rakuten.co.jp Aguste
20 0012a 2 svegask@t-online.de Silvan
21 0012b 2 mboxilll@hexun.com Michail
22 0013a 2 mosgodbym@cocolog-nifty.com Mahala
23 0013b 2 cpauln@vistaprint.com Conni
24 0014a 2 cmatzeitiso@cpanel.net Carmelita
25 0014b 2 jdarlingtonp@hc360.com Jeff
26 0015a 2 dsmithq@tuttocitta.it Darcie
27 0015b 2 tclapsonr@uol.com.br Tanhya
28 0016a 2 cfinkers@photobucket.com Caitrin
29 0016b 2 bgommet@bravesites.com Burch
30 0017a 2 hbenetu@dyndns.org Heddie
31 0017b 2 rhighmanv@marketwatch.com Rebbecca
32 0018a 2 mgentnerw@pbs.org Masha
33 0018b 2 omacanultyx@tamu.edu Ottilie
34 0019a 2 ryankeevy@auda.org.au Raymund
35 0019b 2 mgrishankovz@npr.org Marlon
36 0020a 4 rsnoddin10@fotki.com Rosalie
37 0020b 4 tstucksbury11@tuttocitta.it Toiboid
38 0020c 4 eriseborough12@paypal.com Eberhard
39 0020d 4 jboas13@redcross.org Joelly
40 0021a 3 btaber14@home.pl Ban
41 0021b 3 emapples15@wikipedia.org Emanuel
42 0021c 3 jbleakman16@seesaa.net Joane
43 0022a 5 apossel17@cpanel.net Ansell
44 0022b 5 cbenettelli18@elpais.com Christiano
45 0022c 5 jeaseman19@odnoklassniki.ru Jacqui
46 0022d 5 ecrilley1a@harvard.edu Elyssa
47 0022e 5 akilalea1b@wix.com Agosto
48 0023a 2 sbardsley1c@cisco.com Sibylle
49 0023b 2 istanley1d@psu.edu Irving
50 0024a 2 ekhadir1e@taobao.com Eliza
51 0024b 2 challows1f@senate.gov Charline
52 0025a 2 sdran1g@spotify.com Sutherlan
53 0025b 2 zgarfirth1h@yahoo.com Zondra
54 0026a 2 jarkcoll1i@slideshare.net Josy
55 0026b 2 cbirch1j@geocities.jp Cherry
56 0027a 2 bsickling1k@privacy.gov.au Berte
57 0027b 2 edougliss1l@patch.com Eda
58 0028a 2 crosario1m@diigo.com Conroy
59 0028b 2 borto1n@irs.gov Beatrisa
60 0029a 2 amurcutt1o@list-manage.com Archer
61 0029b 2 umacclenan1p@guardian.co.uk Ulberto
62 0030a 2 csherrum1q@nsw.gov.au Cathy
63 0030b 2 srawsthorne1r@icio.us Shanon
64 0031a 3 wlipson1s@topsy.com Winna
65 0031b 3 jgarnar1t@jimdo.com Jaclyn
66 0031c 3 omerriday1u@vkontakte.ru Odelia
67 0032a 2 eloosemore1v@statcounter.com Earvin
68 0032b 2 bpickover1w@furl.net Bethena
69 0033 1 kchislett1x@angelfire.com Kenton
70 0034a 2 weuesden1y@xrea.com Welch
71 0034b 2 psimonds1z@de.vu Piggy
72 0035a 2 ebachmann20@ucla.edu Elianore
73 0035b 2 bsutherden21@princeton.edu Billy
74 0036a 2 asangster22@harvard.edu Abran
75 0036b 2 rbreckwell23@examiner.com Rickey
76 0037a 2 pmachon24@desdev.cn Page
77 0037b 2 jbraundt25@imgur.com Jackelyn
78 0038a 2 mrivers26@pagesperso-orange.fr Mervin
79 0038b 2 sgiacovelli27@hugedomains.com Sibylle
80 0039a 2 mbesant28@uol.com.br Muhammad
81 0039b 2 rcardoe29@technorati.com Rourke
82 0040a 8 vgandey2a@lulu.com Vale
83 0040b 8 eviall2b@bbc.co.uk Emmy
84 0040c 8 sswin2c@yellowpages.com Sonny
85 0040d 8 aportman2d@canalblog.com Ardeen
86 0040e 8 jchaudhry2e@imageshack.us Joye
87 0040f 8 rleidecker2f@fda.gov Ryann
88 0040g 8 mproske2g@answers.com Mathias
89 0040h 8 dpatry2h@gmpg.org Dalenna
90 0041a 2 ksharply2i@sbwire.com Kaitlin
91 0041b 2 cdulanty2j@harvard.edu Clem
92 0042a 2 phawes2k@jiathis.com Parrnell
93 0042b 2 hwackley2l@cornell.edu Harold
94 0043a 2 mmuccino2m@forbes.com Meridel
95 0043b 2 cbarkas2n@cnet.com Cicily
96 0044 1 nbowcher2o@mit.edu Nickola
97 0045 1 rpideon2p@google.ca Raynor
98 0046a 2 ccolling2q@sina.com.cn Clyve
99 0046b 2 rcornbill2r@oakley.com Robby
100 0047a 2 nbilston1@senate.gov Noble
101 0047b 2 dgooke2@dyndns.org Dixie
102 0048a 2 gmcpeake3@bbc.co.uk Ganny
103 0048b 2 mstairmand4@bizjournals.com Martguerita
104 0049 1 hsedgeworth5@vimeo.com Hale
105 0050a 8 abrignall6@cocolog-nifty.com Astrid
106 0050b 8 jcondon7@mayoclinic.com Joya
107 0050c 8 cledward8@ebay.co.uk Cristabel
108 0050d 8 aspino9@123-reg.co.uk Anita
109 0050e 8 krosenblada@opera.com Kaitlynn
110 0050f 8 blowndesb@illinois.edu Bambi
111 0050g 8 gbroadhurstc@oakley.com Giacopo
112 0050h 8 pcarnied@vkontakte.ru Pattie

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
*** Settings ***
Resource ../Keywords/django.resource
Suite Setup Start Solawi-Suite
Suite Teardown Stop Solawi-Suite

View File

@ -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
_email/.gitkeep Normal file
View File

1
documentation Submodule

@ -0,0 +1 @@
Subproject commit a520d21ddc0c4bd96b3d57bd012f2134150fffa8

View File

@ -0,0 +1 @@
0xf16a75d1926178c8c7a5e7895859d59c511c96b5c0f5d3d25053168861e5d1eafa451bdbe2061c140bc5e917b7238052ce02a505cff4c4210a6a01df96ef06e1

View File

@ -0,0 +1 @@
0x5cda238115d1e93aea954d12e1edabb635f1728e5c3bfad7fa7403be8ab454083be8db052b472f97a24a2914f67286256eecbf6ee8e006447795b628f1587e0a

22
manage.py Normal file
View File

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

4183
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

1
poetry.toml Normal file
View File

@ -0,0 +1 @@
virtualenvs.in-project = false

74
pyproject.toml Normal file
View File

@ -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
solawi_apps/__init__.py Normal file
View File

View File

8
solawi_apps/ahg/apps.py Normal file
View File

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

View File

View File

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

View File

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

View File

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

View File

6
solawi_apps/ahg/mixin.py Normal file
View File

@ -0,0 +1,6 @@
from solawi_apps.app_permission.mixin import AccessPermissionRequiredMixin
class LoginRequiredMixin(AccessPermissionRequiredMixin):
app_name = "solawi_apps.ahg"

133
solawi_apps/ahg/models.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

17
solawi_apps/ahg/urls.py Normal file
View File

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

View File

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
from django.views.generic import TemplateView
class NoAppPermission(TemplateView):
template_name = "solawi_apps/app_permissions/app-access-denied.html"

View File

View File

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

View File

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

View File

@ -0,0 +1 @@
CONNECTED_BEAMERS_SESSION_KEY = "__connected_beamers__"

View File

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

View File

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