Compare commits

..

146 Commits

Author SHA1 Message Date
Oliver Zander 8abc051a59 full width related labels for all product change forms 2025-11-21 13:01:25 +00:00
Oliver Zander b156ee0067 fixed form data for project funding 2025-11-21 13:01:15 +00:00
Oliver Zander 85cb2368cc fixed validation 2025-11-21 13:01:15 +00:00
Oliver Zander f9898ee9b5 removed unnecessary code 2025-11-21 13:01:15 +00:00
Oliver Zander d8b8f28442 improved literature form validation 2025-11-21 13:01:15 +00:00
Oliver Zander 73cd0ff906 properly clean selfbuy data 2025-11-21 13:01:15 +00:00
Oliver Zander 20e5be26c0 added test for failed extern redirect 2025-11-21 13:01:15 +00:00
Oliver Zander 803c94137d fixed typing for help texts in application type 2025-11-21 13:01:15 +00:00
Oliver Zander c0a98d09cd added test for unknown applicant name 2025-11-21 13:01:15 +00:00
Oliver Zander f5c9b0c76c WM-18: improved mails & added full form data 2025-11-21 13:01:15 +00:00
Oliver Zander 35627d2764 WM-13: made denied projects re-grantable 2025-11-21 13:01:15 +00:00
Oliver Zander ba56ca21b4 WM-12: bring back opacity for admin 2025-11-21 13:01:15 +00:00
Oliver Zander 771819af0e WM-13: removed decision mails 2025-11-21 13:01:15 +00:00
Oliver Zander ee987d73c2 WM-12: use textarea for other fields 2025-11-21 13:01:15 +00:00
Oliver Zander f18b97a213 WM-11: left align applications 2025-11-21 13:01:15 +00:00
Oliver Zander a55b5f26d9 renamed blocks to applications 2025-11-21 13:01:15 +00:00
Oliver Zander d527a99a6d only use german 2025-11-21 13:01:15 +00:00
Oliver Zander 638f6cd5ed WM-12: hide other value row when not checked 2025-11-21 13:01:15 +00:00
Oliver Zander 714d249608 WM-16: reorder fields in admin 2025-11-21 13:01:15 +00:00
Oliver Zander 0a2bcac684 WM-19: added username to projects 2025-11-21 13:01:15 +00:00
Oliver Zander 48b922834a WM-17: added verbose names for models 2025-11-21 13:01:15 +00:00
Oliver Zander b4b35aa486 removed duplicate trailing colons in labels 2025-11-21 13:01:15 +00:00
Oliver Zander 4187d218b4 WM-12: added back button 2025-11-21 13:01:15 +00:00
Oliver Zander 2815780c27 added titles 2025-11-21 13:01:15 +00:00
Oliver Zander 0bd7bb0672 validate checkin & checkout 2025-11-21 13:01:15 +00:00
Oliver Zander 9b1fb39d8c removed initial value for travel cost 2025-11-21 13:01:15 +00:00
Oliver Zander ce1baef528 WM-12: changed some labels & verbose names 2025-11-21 13:01:15 +00:00
Oliver Zander 864eb031ed WM-11: switch service list to radios with info links 2025-11-21 13:01:15 +00:00
Oliver Zander 4d7058f460 fixed outdated import in oauth middleware 2025-11-21 13:01:09 +00:00
Oliver Zander ebd7ebd3fd added admin tests 2025-10-21 15:35:24 +02:00
Oliver Zander a750f0d7d2 fixed template name 2025-10-21 15:35:12 +02:00
Oliver Zander 864df9613a cleaned up forms & fixed missing terms field 2025-10-20 15:39:33 +02:00
Oliver Zander 1dbd38dc4a remove other option before saving 2025-10-20 14:45:25 +02:00
Oliver Zander 2c79732200 use base template for project funding info page 2025-10-20 14:45:25 +02:00
Andreas Gohr e8848b0c97 README improvements
* removed dependency infos - those are in requirements.txt
* added info on mail attachments
* removed obsolete /intern endpoint
2025-10-20 11:39:21 +02:00
Oliver Zander 7365218adb added attachment helper 2025-10-17 17:31:51 +02:00
Oliver Zander c751a9fc37 Merge branch 'WM-4' into 'cosmocode'
[WM-4] (3) E-Mail Versand

Closes WM-4

See merge request wikimedia/foerderbarometer!9
2025-10-17 17:25:21 +02:00
Oliver Zander 1c98092473 improved attachment download code 2025-10-17 17:22:52 +02:00
Oliver Zander 7fcde34897 clean up mail attachment code 2025-10-17 16:04:14 +02:00
Oliver Zander b3484965b3 unified approval mails 2025-10-17 15:14:16 +02:00
Oliver Zander 5a5962b619 unified mailing 2025-10-17 14:51:18 +02:00
Oliver Zander 1e075fda68 made declined projects read only 2025-10-17 14:18:20 +02:00
Oliver Zander e9b60d7205 send decision mails on grant 2025-10-17 14:15:56 +02:00
Oliver Zander 76ba63002d clean up mail code 2025-10-17 12:06:23 +02:00
Oliver Zander 5d107dab96 split form processing in multiple methods 2025-10-17 11:42:42 +02:00
Roman 621941c6e4 Update and add email templates for applicant and staff notifications 2025-10-17 11:25:34 +02:00
Roman 319a06833e Add mail attachment settings with cache config, TTL, and URL-based attachment mapping 2025-10-17 11:18:45 +02:00
Roman f98894b250 Enhance ApplicationView to send applicant/staff emails with attachments and improved subjects 2025-10-17 11:05:24 +02:00
Roman 1ad4970cbc Add decision mailers for applicants and staff with approval/denial templates 2025-10-17 10:54:53 +02:00
Roman 5f2d99c037 Add robust mail attachment caching and file attachment support with TTL and MIME detection 2025-10-17 10:53:29 +02:00
Oliver Zander ad3ab2e993 made tests robust against changing order 2025-10-17 10:28:08 +02:00
Oliver Zander b963db5731 informal error message 2025-10-16 17:00:40 +02:00
Oliver Zander c5bdc80c54 allow empty grant fields in project 2025-10-16 16:44:26 +02:00
Oliver Zander 6a36b293f4 Merge branch 'feature/add-request-url-to-services' of gitlab.cosmocode.de:wikimedia/foerderbarometer into cosmocode 2025-10-16 16:31:35 +02:00
Oliver Zander 28a42b1709 fixed pid test 2025-10-16 15:46:49 +02:00
Oliver Zander 22aec6fdde fixed pid scheme 2025-10-16 15:41:22 +02:00
Oliver Zander 7e4d197384 added proxies for declined & requested projects 2025-10-16 15:38:41 +02:00
Oliver Zander 4efab724ca fixed test for proj 2025-10-16 12:26:52 +02:00
Oliver Zander 89cb50ddd7 improved datetime shortcuts 2025-10-16 12:16:52 +02:00
Oliver Zander 592b4dff74 use local timezone 2025-10-16 12:16:27 +02:00
Oliver Zander a8731a4195 use existing project model & added categories and wikimedia projects 2025-10-16 12:16:08 +02:00
Oliver Zander cf81a45231 fixed spacing & imports 2025-10-15 17:32:28 +02:00
Oliver Zander 7d1511bb93 added projektförderung to extern types test 2025-10-15 12:26:57 +02:00
Oliver Zander ec062df5f4 fixed and unified project funding & services 2025-10-15 12:24:14 +02:00
Oliver Zander c27e4c5183 cleaned up project forms 2025-10-15 12:20:11 +02:00
Oliver Zander d1cda4c1a9 simplified project request model 2025-10-15 12:19:25 +02:00
Oliver Zander dcd9a3d213 reordered migrations 2025-10-15 12:14:42 +02:00
Oliver Zander 4dbf3749d7 Merge branch 'feature/project-funding-under-1000' of gitlab.cosmocode.de:wikimedia/foerderbarometer into cosmocode 2025-10-15 11:26:46 +02:00
Oliver Zander 55cb4f2ad8 removed formtools 2025-10-14 17:00:46 +02:00
Oliver Zander 77d7ff2ce3 integrated negative test 2025-10-14 16:59:36 +02:00
Oliver Zander ce4aeb0721 fixed tests 2025-10-14 16:48:39 +02:00
Oliver Zander a7d3df7b39 simplified application views 2025-10-14 11:39:58 +02:00
Oliver Zander 84ef809705 do not show internal address default 2025-10-14 11:16:13 +02:00
Oliver Zander 01267e6939 simplified field ordering 2025-10-14 11:15:50 +02:00
Oliver Zander 29c4cdd8ac simplified migration name 2025-10-14 11:15:19 +02:00
Roman e4797b9bcd Squash duplicate migrations 0100 and 0101 into a single cleaned-up migration 2025-10-08 12:38:53 +02:00
Roman 48e0d1d2b1 Add mixin to auto-insert before and remove redundant fields definitions 2025-10-08 12:34:17 +02:00
Roman f646561136 Add migrations for request_url field in service models 2025-09-30 15:48:04 +02:00
Roman b23cf3adb8 Add request_url field and otrs_link.js integration to service admins with ordered fields 2025-09-30 15:46:23 +02:00
Roman 0db5cb7fd9 Add 'Antrag (URL)' request_url field to service models via RequestUrlMixin 2025-09-30 15:44:03 +02:00
Oliver Zander 38f6deee51 Merge branch 'feature/replace-radios-with-links' into 'cosmocode'
refactor(forms): Split Views and Template Restructure

See merge request wikimedia/foerderbarometer!5
2025-09-29 16:31:43 +02:00
Oliver Zander aa5c981872 Merge branch 'feature/terms-consent' into 'cosmocode'
Store user agreement to terms

See merge request wikimedia/foerderbarometer!4
2025-09-29 16:31:06 +02:00
Roman cda7d7a79d Switch to single quotes across models.py for consistent string formatting 2025-09-29 00:51:13 +02:00
Roman c9959988cb Fix: use Django URL tag for project funding link instead of hardcoded path 2025-09-29 00:40:20 +02:00
Roman 46240fedc1 Add public route, view, and navigation link for project funding requests under 1000 EUR 2025-09-29 00:33:42 +02:00
Roman 98f8414576 Add migrations for ProjectRequest and ProjectsDeclined models with updated options and field constraints 2025-09-29 00:32:09 +02:00
Roman cc55b17cff Add service layer for approving and declining ProjectRequest with transactional logic 2025-09-29 00:27:09 +02:00
Roman 14717c8318 Add admin integration for ProjectRequest with approve/decline actions and custom ApproveActionForm 2025-09-29 00:25:16 +02:00
Roman 4e6906e318 Add ProjectRequestForm and ProjectRequestAdminForm with JSON-backed multi-selects and help texts 2025-09-29 00:22:47 +02:00
Roman 011e262df6 Add ProjectRequest and ProjectsDeclined models for project funding under 1000 EUR 2025-09-29 00:18:40 +02:00
Roman a239922e14 use walrus operator for user session check 2025-09-01 13:33:17 +02:00
Roman 238c674517 change all string literals to single quotes 2025-09-01 13:24:59 +02:00
Roman 63b07bdf22 set default LANGUAGE_CODE to 'de' instead of 'en-us' 2025-09-01 12:47:24 +02:00
Roman 6585eeaf78 replace regex assertion and remove status code check in test_extern_lit 2025-09-01 12:04:52 +02:00
Roman ed2db04309 add negative test for Literature form submission without terms_accepted 2025-09-01 01:45:50 +02:00
Roman 8ba54bdca9 add CSS styles for form layout 2025-08-31 23:55:50 +02:00
Roman a2ec4071b4 restructure templates: introduce base with header/footer, replace old extern template, add generic form layout, and update info page 2025-08-31 23:47:27 +02:00
Roman 35ae9ab0e5 update URL routing to use new class-based application views and static extern page 2025-08-31 23:43:29 +02:00
Roman 7cfd477d40 refactor ExternView into separate class-based views with BaseApplicationView 2025-08-31 23:41:11 +02:00
Roman 98d1ae9284 refactor(forms): unify forms with BaseApplicationForm and CommonOrderMixin for consistent fields and order 2025-08-31 23:38:22 +02:00
Roman eea08e6075 Merge branch 'cosmocode' into feature/replace-radios-with-links 2025-08-27 17:53:54 +02:00
Andreas Gohr 0bb7f8680c Merge branch 'refactor/button-styles' into 'cosmocode'
moved inline styles to base.css and button.css and adjusted button styles

See merge request wikimedia/foerderbarometer!2
2025-08-27 12:18:58 +02:00
Roman Fen 1159cd3394 moved inline styles to base.css and button.css and adjusted button styles 2025-08-27 12:18:58 +02:00
Roman 8d57909a17 Add static info page for project funding ≥ 1,000 € 2025-08-27 11:58:40 +02:00
Roman 0d033e7e25 Refactor CheckForm to use model field 'terms_accepted' instead of separate 'check' field 2025-08-26 22:48:13 +02:00
Roman e10ec7e691 Add migration 0099: add terms_accepted field to BusinessCard, Email, List, and Literature 2025-08-26 15:39:43 +02:00
Roman c187bf2e9e Add custom save() in CheckForm to map 'check' field to model's 'terms_accepted' 2025-08-26 15:37:33 +02:00
Roman 9ef2af0a2f add terms_accepted to admin list display for BusinessCard, Literature, Email and List 2025-08-26 13:53:39 +02:00
Roman 7816ac4237 add terms_accepted field to Email, List, Literature and BusinessCard via mixin 2025-08-26 12:54:29 +02:00
Oliver Zander 94882d9039 fixed literature test 2025-08-21 10:47:17 +02:00
Oliver Zander 2973741696 added tests for internal views 2025-08-21 10:43:38 +02:00
Oliver Zander 860205cac9 skip finance id test 2025-08-21 10:11:59 +02:00
Oliver Zander 257b10310d readded test module file 2025-08-21 10:10:17 +02:00
Oliver Zander 656cfe8cc9 renamed model test file accordingly 2025-08-21 10:09:15 +02:00
Oliver Zander dff9ef858d added whitespace 2025-08-21 10:08:38 +02:00
Oliver Zander c329c84661 clean up tests 2025-08-21 10:08:24 +02:00
Oliver Zander 7698eb0648 removed intern view 2025-08-21 10:02:19 +02:00
Oliver Zander fa84fc6bd0 use a table form renderer 2025-08-20 14:28:52 +02:00
Andreas Gohr 126790d57f Merge branch 'docker' into 'cosmocode'
add docker compose setup for development

See merge request wikimedia/foerderbarometer!1
2025-08-20 12:53:33 +02:00
Andreas Gohr 2e883ba58b add docker compose setup for development 2025-08-20 12:45:06 +02:00
Oliver Zander 6dc1b22eb8 separate library, e-literature and software by using proxy models 2025-08-20 12:06:43 +02:00
Oliver Zander df8a9708ea set explicit library type choices 2025-08-20 11:19:05 +02:00
Oliver Zander b3c181bacc added missing migration for real- & username fields 2025-08-20 11:17:20 +02:00
Oliver Zander f5134e491f prettify type choices 2025-08-20 11:02:31 +02:00
Oliver Zander 1be12ff88e use mariadb for testing 2025-08-20 10:00:50 +02:00
Oliver Zander edefb6232c added gitlab ci 2025-08-19 16:43:48 +02:00
Oliver Zander 8259be4e7d removed duplicate admin path 2025-08-19 16:11:34 +02:00
Oliver Zander ee672a4f2f replaced format_html with mark_safe where no formatting is applied 2025-08-19 16:08:48 +02:00
Oliver Zander 6421fd64d2 updated deps, removed third party & unused deps 2025-08-19 16:03:36 +02:00
Oliver Zander 147398f3f4 simplified code 2025-08-19 16:02:57 +02:00
Oliver Zander b88c024645 toggle intern view and tests by setting 2025-08-19 16:02:18 +02:00
Oliver Zander 56a5d5b103 added tests for sendmails command 2025-08-19 16:00:57 +02:00
Oliver Zander f0cf6b37d9 added coveragerc 2025-08-19 14:52:31 +02:00
Oliver Zander febebec67c added more tests for extern view 2025-08-19 14:52:18 +02:00
Oliver Zander 4370292c55 fixed read only fields of travel admin 2025-08-19 14:10:36 +02:00
Oliver Zander 452fda2cb5 auto formatting: removed whitespace 2025-08-19 14:10:15 +02:00
Oliver Zander 5f0394afb5 merged settings & use .env files to configure differences 2025-08-19 09:45:14 +02:00
Oliver Zander bbfaddcc4a added basic tests for extern view 2025-08-18 16:42:09 +02:00
Oliver Zander 45bbe23560 conditionally set username so it can work local and in tests 2025-08-18 16:33:04 +02:00
Oliver Zander 2ae9a37083 auto formatting: removed whitespace 2025-08-18 16:32:31 +02:00
Oliver Zander 6c1c13cba8 moved tests 2025-08-18 12:55:24 +02:00
Oliver Zander 39cd2af768 fixed test 2025-08-18 12:54:14 +02:00
Oliver Zander b55b9aada0 fixed and improved quarter calculation 2025-08-18 12:53:40 +02:00
Oliver Zander 1ed6862a42 ignore idea config files & mac stuff 2025-08-18 12:52:54 +02:00
Oliver Zander dd4c50f70e ignore linked settings file 2025-08-18 12:52:32 +02:00
Oliver Zander bf7e1131db auto formatting: removed whitespace 2025-08-18 12:11:44 +02:00
93 changed files with 3578 additions and 2231 deletions

23
.coveragerc Normal file
View File

@ -0,0 +1,23 @@
[run]
branch = on
source = input
omit =
# ignore the tests itself
*/test*.py
*/tests/*.py
# ignore wsgi & asgi
*/?sgi.py
[report]
include =
input/*
exclude_lines =
pragma: no cover
raise NotImplementedError
omit =
# they are excluded in run, so
# doesn't need to be reported
*/test*.py
*/tests/*.py
*/?sgi.py

5
.env.develop.example Normal file
View File

@ -0,0 +1,5 @@
ENVIRONMENT = develop
DEBUG = yes
SECRET_KEY = not-a-secret-key
DATABASE_ENGINE = sqlite3
EMAIL_BACKEND = console

13
.env.production.example Normal file
View File

@ -0,0 +1,13 @@
ENVIRONMENT = production
DEBUG = no
SECRET_KEY = <enter a secret key>
HOST = https://foerderung.wikimedia.de
DATABASE_ENGINE = mysql
DATABASE_PASSWORD = <enter a database password>
EMAIL_BACKEND = smtp
EMAIL_HOST_USER = <enter an email host user>
EMAIL_HOST_PASSWORD = <enter an email host password>
OAUTH_ENABLED = yes
OAUTH_CLIENT_NAME = <enter a client name>
OAUTH_CLIENT_ID = <enter a client id>
OAUTH_CLIENT_SECRET = <enter a client secret>

10
.gitignore vendored
View File

@ -1,9 +1,6 @@
# secret passwords and so # secret passwords and so
/secrets.json /secrets.json
/staticfiles /staticfiles
# /foerderbarometer/settings.py
# /foerderbarometer/*settings*
/input/settings.py
/nohup.out /nohup.out
/logfile /logfile
*~ *~
@ -93,6 +90,10 @@ target/
profile_default/ profile_default/
ipython_config.py ipython_config.py
# IDEA
/*.iml
/.idea
# pyenv # pyenv
.python-version .python-version
@ -139,3 +140,6 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# MacOS
.DS_Store

28
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,28 @@
stages:
- test
test:
stage: test
image: python:3.11-bookworm
variables:
DJANGO_SETTINGS_MODULE: foerderbarometer.settings
ENVIRONMENT: test
SECRET_KEY: this-is-not-a-secret-key
DATABASE_ENGINE: mysql
DATABASE_HOST: mariadb
DATABASE_USER: root
DATABASE_PASSWORD: fdb
OAUTH_ENABLED: no
services:
- name: mariadb:10.6
variables:
MARIADB_ROOT_PASSWORD: fdb
tags:
- docker
coverage: /(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/
before_script:
- pip install -r requirements.txt coverage
script:
- coverage run manage.py test --noinput
after_script:
- coverage report

View File

@ -2,9 +2,13 @@
purpose: gather data from intern(WMDE) and extern(volunteers) forms to create a database ('förderdatenbank') and send emails with links for a questionary. purpose: gather data from intern(WMDE) and extern(volunteers) forms to create a database ('förderdatenbank') and send emails with links for a questionary.
## installation and development setup ## manual development setup
ln -sr foerderbarometer/settings_development.py foerderbarometer/settings.py cp .env.develop.example .env
By default a SQLite database is used for development.
To use a MariaDB change `DATABASE_ENGINE` in .env to `mysql` and amend `DATABASE_*` variables according to your setup.
For further information see the production setup below.
build the database with build the database with
@ -21,9 +25,26 @@ run the development server with
access via access via
http://localhost:8000/ http://localhost:8000/
http://localhost:8000/intern/ (login required)
http://localhost:8000/admin/ (login reqiured) http://localhost:8000/admin/ (login reqiured)
## docker compose development setup
The project comes with a `docker-compose.yml` file to run the project in a containerized environment.
cp .env.develop.example .env
docker compose up
The setup will use a containerized MariaDB database.
Create your superuser account with
docker compose exec django python3 manage.py createsuperuser
You can access the application via
http://localhost:8000/
http://localhost:8000/admin/ (login required)
## additional admin functionality ## additional admin functionality
The admin page is the standard admin page delivered by django but with two additional functionalities: The admin page is the standard admin page delivered by django but with two additional functionalities:
@ -33,29 +54,24 @@ entries to a csv file
- There is a new button in the bottom of every Project to "save as new" - There is a new button in the bottom of every Project to "save as new"
## versions used in development ## mail attachments
asgiref==3.2.10 For all mails, attachments can be defined as URLs. These URLs are fetched and chached when sending the mail and attached to the mail.
Django==3.1.2
django-formtools==2.4 Configuration is done via the `ATTACHMENT_URLS` setting. Attachments can be set for user (`RECIPIENT_APPLICANT`) and staff (`RECIPIENT_STAFF`) mails. The following mail types exist:
gunicorn==20.0.4
mysqlclient==2.1.1 * `TYPE_BIB` Bibliotheksstipendium
sqlparse==0.4.3 * `TYPE_ELIT` eLiteraturstipendium
whitenoise==6.2.0 * `TYPE_SOFT` Softwarestipendium
asgiref==3.2.10 * `TYPE_MAIL` E-Mail-Adresse
Authlib==1.2.1 * `TYPE_IFG` Kostenübernahme IFG-Anfrage
certifi==2023.7.22 * `TYPE_LIT` Literaturstipendium
cffi==1.16.0 * `TYPE_LIST` Mailingliste
chardet==5.2.0 * `TYPE_TRAV` Reisekosten
charset-normalizer==3.3.0 * `TYPE_VIS` Visitenkarten
cryptography==41.0.4 * `TYPE_PROJ` Projektförderung
idna==3.4
pycparser==2.21 For further details see `foerderbarometer/settings.py`
pytz==2023.3.post1
requests==2.31.0
six==1.16.0
typing_extensions==4.8.0
urllib3==2.0.6
## testing ## testing
@ -65,16 +81,26 @@ run some tests with
## production setup ## production setup
ln -sr foerderbarometer/settings_production.py foerderbarometer/settings.py cp .env.production.example .env
edit /secrets.json to contain something similar to edit .env and fill in the missing secrets
{ SECRET_KEY
"DATABASE_PASSWORD": "THIS IS TOP SECRET!", DATABASE_PASSWORD
"SECRET_KEY": "THIS IS ANOTHER SECRET!" EMAIL_HOST_USER
} EMAIL_HOST_PASSWORD
OAUTH_CLIENT_NAME
OAUTH_CLIENT_ID
OAUTH_CLIENT_SECRET
edit foerderbarometer/settings_production.py according to your database setup (tested with MariaDB 10.0.36) amend database variables to .env according to your database setup (tested with MariaDB 10.0.36), e.g.
DATABASE_NAME
DATABASE_USER
DATABASE_HOST
DATABASE_PORT
for a full set of all possible env vars have a look at foerderbarometer/settings.py
run the following commands: run the following commands:

33
docker-compose.yaml Normal file
View File

@ -0,0 +1,33 @@
services:
django:
image: python:3-alpine
working_dir: /app
volumes:
- .:/app
environment:
PYTHONUNBUFFERED: 1
DATABASE_ENGINE: mysql
DATABASE_HOST: mariadb
DATABASE_USER: fdb
DATABASE_PASSWORD: fdb
ports:
- 8000:8000
command:
- sh
- -c
- |
apk update
apk add gcc
apk add mariadb-dev
apk add musl-dev
pip install -Ur requirements.txt
python manage.py migrate
python manage.py runserver 0.0.0.0:8000
mariadb:
image: mariadb
environment:
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes
MARIADB_DATABASE: fdb
MARIADB_USER: fdb
MARIADB_PASSWORD: fdb

View File

@ -0,0 +1,33 @@
TYPE_ALL = 'ALL'
TYPE_BIB = 'BIB' # Bibliotheksstipendium
TYPE_ELIT = 'ELIT' # eLiteraturstipendium
TYPE_SOFT = 'SOFT' # Softwarestipendium
TYPE_MAIL = 'MAIL' # E-Mail-Adresse
TYPE_IFG = 'IFG' # Kostenübernahme IFG-Anfrage
TYPE_LIT = 'LIT' # Literaturstipendium
TYPE_LIST = 'LIST' # Mailingliste
TYPE_TRAV = 'TRAV' # Reisekosten
TYPE_VIS = 'VIS' # Visitenkarten
TYPE_PROJ = 'PROJ' # Projektförderung
TYPES = [
TYPE_BIB,
TYPE_ELIT,
TYPE_SOFT,
TYPE_MAIL,
TYPE_IFG,
TYPE_LIT,
TYPE_LIST,
TYPE_TRAV,
TYPE_VIS,
TYPE_PROJ,
]
RECIPIENT_APPLICANT = 'applicant'
RECIPIENT_STAFF = 'staff'
RECIPIENTS = [
RECIPIENT_APPLICANT,
RECIPIENT_STAFF,
]

View File

@ -0,0 +1,195 @@
import os
from pathlib import Path
from dotenv import load_dotenv
from input.utils.settings import env, password_validators
from .constants import *
BASE_DIR = Path(__file__).parents[1]
load_dotenv(BASE_DIR / '.env')
DEBUG = env('DEBUG', False)
SECRET_KEY = env('SECRET_KEY')
ALLOWED_HOSTS = ['*']
HOST = env('HOST', 'http://localhost:8000')
INSTALLED_APPS = [
'input.apps.InputConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'foerderbarometer.urls'
DJANGO_TEMPLATES = {
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
}
TEMPLATES = [DJANGO_TEMPLATES]
WSGI_APPLICATION = 'foerderbarometer.wsgi.application'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
DATABASE_ENGINE = env('DATABASE_ENGINE', 'mysql')
DATABASE_SQLITE = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
DATABASE_MYSQL = {
'ENGINE': 'django.db.backends.mysql',
'NAME': env('DATABASE_NAME', 'fdb'),
'USER': env('DATABASE_USER', 'fdb'),
'PASSWORD': env('DATABASE_PASSWORD'),
'HOST': env('DATABASE_HOST', 'localhost'),
'PORT': env('DATABASE_PORT', 3306),
'OPTIONS': {
'charset' : 'utf8',
'use_unicode' : True,
'init_command': 'SET '
'storage_engine=INNODB,'
'character_set_connection=utf8,'
'collation_connection=utf8_bin'
},
'TEST_CHARSET': 'utf8',
'TEST_COLLATION': 'utf8_general_ci',
}
if DATABASE_ENGINE == 'mysql':
DATABASE_DEFAULT = DATABASE_MYSQL
else:
DATABASE_DEFAULT = DATABASE_SQLITE
DATABASES = {
'default': DATABASE_DEFAULT,
}
EMAIL_BACKEND = env('EMAIL_BACKEND', 'console')
if EMAIL_BACKEND == 'smtp':
EMAIL_HOST = env('EMAIL_HOST', 'email.wikimedia.de')
EMAIL_PORT = env('EMAIL_PORT', 587)
EMAIL_USE_TLS = env('EMAIL_USE_TLS', True)
EMAIL_HOST_USER = env('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD')
EMAIL_BACKEND = f'django.core.mail.backends.{EMAIL_BACKEND}.EmailBackend'
EMAIL_URL_PREFIX = env('EMAIL_URL_PREFIX', HOST)
AUTH_PASSWORD_VALIDATORS = password_validators(
'UserAttributeSimilarityValidator',
'MinimumLengthValidator',
'CommonPasswordValidator',
'NumericPasswordValidator',
)
USE_I18N = True
USE_L10N = True
LANGUAGE_CODE = env('LANGUAGE_CODE', 'de')
LANGUAGES = [
('de', 'Deutsch'),
]
USE_TZ = True
TIME_ZONE = env('TIME_ZONE', 'Europe/Berlin')
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATIC_URL = '/static/'
FORM_RENDERER = 'input.forms.TableFormRenderer'
if OAUTH_ENABLED := env('OAUTH_ENABLED', not DEBUG):
MIDDLEWARE += ['input.middleware.oauth.OAuthMiddleware']
OAUTH_CLIENT_NAME = env('OAUTH_CLIENT_NAME')
OAUTH_CLIENT = {
'client_id': env('OAUTH_CLIENT_ID'),
'client_secret': env('OAUTH_CLIENT_SECRET'),
'access_token_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/access_token',
'authorize_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/authorize',
'api_base_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/resource',
'redirect_uri': env('OAUTH_REDIRECT_URI', f'{HOST}/oauth/callback'),
'client_kwargs': {
'scope': 'basic',
'token_placement': 'header'
},
'userinfo_endpoint': 'resource/profile',
}
OAUTH_URL_WHITELISTS = ['/admin']
OAUTH_COOKIE_SESSION_ID = 'sso_session_id'
IF_EMAIL = env('IF_EMAIL', 'community@wikimedia.de')
SURVEY_EMAIL = env('SURVEY_EMAIL', 'sandro.halank@wikimedia.de')
SURVEY_PREFIX = env('SURVEY_PREFIX', 'https://wikimedia.sslsurvey.de/Foerderbarometer/?')
DATAPROTECTION = 'https://www.wikimedia.de/datenschutz/#datenerfassung'
FOERDERRICHTLINIEN = 'https://de.wikipedia.org/wiki/Wikipedia:Wikimedia_Deutschland/Richtlinie_zur_Förderung_der_Communitys'
NUTZUNGSBEDINGUNGEN = 'static/input/nutzungsbedingungen.html'
NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE = 'static/input/nutzungsbedingungen-mail.pdf'
NUTZUNGSBEDINGUNGEN_MAILINGLISTEN = 'static/input/nutzungsbedingungen-mailinglisten.pdf'
NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM = 'static/input/nutzungsbedingungen-literaturstipendium.pdf'
NUTZUNGSBEDINGUNGEN_OTRS = 'static/input/2025_Nutzungsvereinbarung_OTRS.docx.pdf'
NUTZUNGSBEDINGUNGEN_VISITENKARTEN = 'static/input/nutzungsbedingungen-visitenkarten.pdf'
MAIL_ATTACHMENT_CACHE_DIR = env('MAIL_ATTACHMENT_CACHE_DIR', BASE_DIR / 'var' / 'mail-attachments')
MAIL_ATTACHMENT_TTL_SECONDS = env('MAIL_ATTACHMENT_TTL_SECONDS', 24 * 60 * 60)
MAIL_ATTACHMENT_URLS = {
RECIPIENT_APPLICANT: {
TYPE_ALL: [],
TYPE_VIS: [
'https://foerderung.wikimedia.de/static/input/nutzungsbedingungen-visitenkarten.pdf',
],
TYPE_MAIL: [
'https://foerderung.wikimedia.de/static/input/nutzungsbedingungen-mail.pdf',
],
TYPE_LIST: [
'https://foerderung.wikimedia.de/static/input/nutzungsbedingungen-mailinglisten.pdf',
],
TYPE_LIT: [
'https://foerderung.wikimedia.de/static/input/nutzungsbedingungen-literaturstipendium.pdf',
],
},
RECIPIENT_STAFF: {
TYPE_ALL: [],
},
}

View File

@ -1,203 +0,0 @@
"""
Django settings for foerderbarometer project.
Generated by 'django-admin startproject' using Django 3.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import json
import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
# prefix for urls in mails
URLPREFIX = 'https://fdb-devel.wikimedia.de'
# mails in development go to stdout
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
CSRF_TRUSTED_ORIGINS = ['https://fdb-devel.wikimedia.de']
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'xemail.wikimedia.de'
EMAIL_PORT = '587'
EMAIL_USE_TLS = True
#EMAIL_HOST_USER = get_secret('EMAIL_HOST_USER')
#EMAIL_HOST_PASSWORD = get_secret('EMAIL_HOST_PASSWORD')
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# get secrets
with open(os.path.join(BASE_DIR, 'secrets.json')) as secrets_file:
secrets = json.load(secrets_file)
def get_secret(setting, secrets=secrets):
"""Get secret setting or fail with ImproperlyConfigured"""
try:
return secrets[setting]
except KeyError:
raise ImproperlyConfigured("Set the {} setting".format(setting))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_secret('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
STATIC_ROOT = BASE_DIR / 'staticfiles'
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'input.apps.InputConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'formtools',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'input.middleware.oauth.OAuthMiddleware'
]
ROOT_URLCONF = 'foerderbarometer.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'foerderbarometer.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'fdbdevel',
'USER': 'fdbdevel',
'PASSWORD': get_secret('DATABASE_PASSWORD'),
'HOST': '10.0.6.224', # Or an IP Address that your database is hosted on
# 'PORT': '3306',
#optional:
'OPTIONS': {
'charset' : 'utf8',
'use_unicode' : True,
'init_command': 'SET '
'storage_engine=INNODB,'
'character_set_connection=utf8,'
'collation_connection=utf8_bin'
#'sql_mode=STRICT_TRANS_TABLES,' # see note below
#'SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED',
},
'TEST_CHARSET': 'utf8',
'TEST_COLLATION': 'utf8_general_ci',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'
# needed since django 3.2
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# OAuth Settings
OAUTH_URL_WHITELISTS = ['/admin']
OAUTH_CLIENT_NAME = '<name-of-the-configured-wikimedia-app>'
OAUTH_CLIENT_NAME = get_secret('OAUTH_CLIENT_NAME')
OAUTH_CLIENT = {
'client_id': get_secret('OAUTH_CLIENT_ID'),
'client_secret': get_secret('OAUTH_CLIENT_SECRET'),
'access_token_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/access_token',
'authorize_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/authorize',
'api_base_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/resource',
'redirect_uri': 'https://fdb-devel.wikimedia.de/oauth/callback',
'client_kwargs': {
'scope': 'basic',
'token_placement': 'header'
},
'userinfo_endpoint': 'resource/profile',
}
OAUTH_COOKIE_SESSION_ID = 'sso_session_id'

View File

@ -1,151 +0,0 @@
"""
Django settings for foerderbarometer project.
Generated by 'django-admin startproject' using Django 3.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import json
import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
# prefix for urls in mails
URLPREFIX = 'http://localhost:8000'
# mails in development go to stdout
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# get secrets
with open(os.path.join(BASE_DIR, 'secrets.json')) as secrets_file:
secrets = json.load(secrets_file)
def get_secret(setting, secrets=secrets):
"""Get secret setting or fail with ImproperlyConfigured"""
try:
return secrets[setting]
except KeyError:
raise ImproperlyConfigured("Set the {} setting".format(setting))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '*&7p9#_n$@^%0z49s+7jpy@+j1rw_hqh05knyd6y2*!0)r&b6h'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
STATIC_ROOT = BASE_DIR / 'staticfiles'
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'input.apps.InputConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'formtools',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'foerderbarometer.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'foerderbarometer.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'PASSWORD': get_secret('DATABASE_PASSWORD')
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'
# needed since django 3.2
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

View File

@ -1,158 +0,0 @@
"""
Django settings for foerderbarometer project.
Generated by 'django-admin startproject' using Django 3.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import json
import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
# prefix for urls in mails
URLPREFIX = 'http://localhost:8000'
# mails in development go to stdout
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'email.wikimedia.de'
EMAIL_PORT = '587'
EMAIL_USE_TLS = True
EMAIL_HOST_USER = '636ea784dd6ec43'
EMAIL_HOST_PASSWORD = 'wsgqp4ZaVRZZEpRJ'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# get secrets
with open(os.path.join(BASE_DIR, 'secrets.json')) as secrets_file:
secrets = json.load(secrets_file)
def get_secret(setting, secrets=secrets):
"""Get secret setting or fail with ImproperlyConfigured"""
try:
return secrets[setting]
except KeyError:
raise ImproperlyConfigured("Set the {} setting".format(setting))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '*&7p9#_n$@^%0z49s+7jpy@+j1rw_hqh05knyd6y2*!0)r&b6h'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
STATIC_ROOT = BASE_DIR / 'staticfiles'
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'input.apps.InputConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'formtools',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'foerderbarometer.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'foerderbarometer.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'PASSWORD': get_secret('DATABASE_PASSWORD')
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'
# needed since django 3.2
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

View File

@ -1,175 +0,0 @@
"""
Django settings for foerderbarometer project.
Generated by 'django-admin startproject' using Django 3.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import json
import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
# prefix for urls in mails
URLPREFIX = 'http://localhost:8000'
# mails in development go to stdout
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# get secrets
with open(os.path.join(BASE_DIR, 'secrets.json')) as secrets_file:
secrets = json.load(secrets_file)
def get_secret(setting, secrets=secrets):
"""Get secret setting or fail with ImproperlyConfigured"""
try:
return secrets[setting]
except KeyError:
raise ImproperlyConfigured("Set the {} setting".format(setting))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '*&7p9#_n$@^%0z49s+7jpy@+j1rw_hqh05knyd6y2*!0)r&b6h'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
STATIC_ROOT = BASE_DIR / 'staticfiles'
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'input.apps.InputConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'formtools',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'foerderbarometer.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'foerderbarometer.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
#
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': BASE_DIR / 'db.sqlite3',
# 'PASSWORD': get_secret('DATABASE_PASSWORD')
# }
# }
#
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'fdb',
'USER': 'fdb',
'PASSWORD': get_secret('DATABASE_PASSWORD'),
'HOST': 'localhost', # Or an IP Address that your database is hosted on
# 'PORT': '3306',
#optional:
'OPTIONS': {
'charset' : 'utf8',
'use_unicode' : True,
'init_command': 'SET '
'storage_engine=INNODB,'
'character_set_connection=utf8,'
'collation_connection=utf8_bin'
#'sql_mode=STRICT_TRANS_TABLES,' # see note below
#'SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED',
},
'TEST_CHARSET': 'utf8',
'TEST_COLLATION': 'utf8_general_ci',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'
# needed since django 3.2
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

View File

@ -1,171 +0,0 @@
"""
Django settings for foerderbarometer project.
Generated by 'django-admin startproject' using Django 3.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import json
import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
# mails in development go to stdout
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'email.wikimedia.de'
EMAIL_PORT = '587'
EMAIL_USE_TLS = True
EMAIL_HOST_USER = '636ea784dd6ec43'
EMAIL_HOST_PASSWORD = 'wsgqp4ZaVRZZEpRJ'
# prefix for urls in mails
URLPREFIX = 'http://foerderung.wikimedia.de'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# get secrets
with open(os.path.join(BASE_DIR, 'secrets.json')) as secrets_file:
secrets = json.load(secrets_file)
def get_secret(setting, secrets=secrets):
"""Get secret setting or fail with ImproperlyConfigured"""
try:
return secrets[setting]
except KeyError:
raise ImproperlyConfigured("Set the {} setting".format(setting))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_secret('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
STATIC_ROOT = BASE_DIR / 'staticfiles'
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'input.apps.InputConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'formtools',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'foerderbarometer.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'foerderbarometer.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'fdbdevel',
'USER': 'fdbdevel',
'PASSWORD': get_secret('DATABASE_PASSWORD'),
'HOST': '10.0.6.7', # Or an IP Address that your database is hosted on
# 'PORT': '3306',
#optional:
'OPTIONS': {
'charset' : 'utf8',
'use_unicode' : True,
'init_command': 'SET '
'storage_engine=INNODB,'
'character_set_connection=utf8,'
'collation_connection=utf8_bin'
#'sql_mode=STRICT_TRANS_TABLES,' # see note below
#'SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED',
},
'TEST_CHARSET': 'utf8',
'TEST_COLLATION': 'utf8_general_ci',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'

View File

@ -1,14 +1,49 @@
import csv import csv
from django.contrib import admin from django.contrib import admin
from django.db import models
from django.http import HttpResponse from django.http import HttpResponse
from .models import Account, Project, HonoraryCertificate, Library, IFG, Travel,\ from input.utils.list import reorder_value
Email, BusinessCard, List, Literature
from .forms import BaseProjectForm
from .models import (
Account,
Project,
ProjectCategory,
ProjectRequest,
ProjectDeclined,
WikimediaProject,
HonoraryCertificate,
Library,
ELiterature,
Software,
IFG,
Travel,
Email,
BusinessCard,
List,
Literature,
)
class WMDEAdmin(admin.ModelAdmin):
def get_fields(self, request, obj=None):
fields = super().get_fields(request, obj=obj)
if 'username' in fields:
fields = reorder_value(fields, 'username', after='email')
fields = reorder_value(fields, 'request_url', before='intern_notes')
if 'terms_accepted' in fields:
fields = reorder_value(fields, 'terms_accepted', before='request_url')
return fields
def export_as_csv(self, request, queryset): def export_as_csv(self, request, queryset):
meta = self.model._meta meta = self.model._meta
field_names = [field.name for field in meta.fields] field_names = [field.name for field in meta.fields]
@ -22,46 +57,160 @@ def export_as_csv(self, request, queryset):
return response return response
export_as_csv.short_description = "Ausgewähltes zu CSV exportieren" export_as_csv.short_description = "Ausgewähltes zu CSV exportieren"
admin.site.add_action(export_as_csv) admin.site.add_action(export_as_csv)
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin): @admin.register(ProjectCategory, WikimediaProject)
class ProjectCategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'order', 'project_count']
def get_queryset(self, request):
return super().get_queryset(request).annotate(
project_count=models.Count('projects'),
)
@admin.display(description='# Projekte', ordering='project_count')
def project_count(self, obj):
return obj.project_count
class ProjectAdminForm(BaseProjectForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field, model in self.categories.items():
if self.initial.get(f'{field}_other'):
self.initial[field] = [*self.initial[field], model.other]
def clean(self):
cleaned_data = BaseProjectForm.clean(self)
if self.errors:
return cleaned_data
if cleaned_data['granted']:
for field in 'granted_date', 'granted_from', 'account':
if not cleaned_data[field]:
self.add_error(field, 'Dieses Feld ist erforderlich, um dieses Projekt zu bewilligen.')
return cleaned_data
class BaseProjectAdmin(admin.ModelAdmin):
save_as = True save_as = True
form = ProjectAdminForm
search_fields = ('name', 'pid','finance_id', 'realname', 'start', 'end', 'participants_estimated', 'participants_real', 'cost', 'status', 'end_quartal') search_fields = ('name', 'pid','finance_id', 'realname', 'start', 'end', 'participants_estimated', 'participants_real', 'cost', 'status', 'end_quartal')
list_display = ('name', 'pid','finance_id', 'realname', 'start', 'end', 'participants_estimated', 'participants_real', 'cost', 'status', 'end_quartal') list_display = ('name', 'pid','finance_id', 'realname', 'start', 'end', 'participants_estimated', 'participants_real', 'cost', 'status', 'end_quartal')
fields = ('realname', 'email', 'granted', 'granted_date', 'mail_state', 'end_mail_send', 'survey_mail_send', 'survey_mail_date', 'name', 'description', 'pid', 'finance_id', 'start', 'end', 'otrs', 'plan', 'page', 'urls', 'group', 'location', 'participants_estimated', 'participants_real', 'insurance', 'insurance_technic', 'support', 'cost', 'account', 'granted_from', 'notes', 'intern_notes', 'status', 'project_of_year', 'end_quartal')
# action = ['export_as_csv']
date_hierarchy = 'end' date_hierarchy = 'end'
readonly_fields = ('end_quartal', 'project_of_year', 'pid', 'finance_id') readonly_fields = ('end_quartal', 'project_of_year', 'pid', 'finance_id')
fieldsets = [
('Kontakt', {'fields': (
'realname',
'email',
'username',
)}),
('Projekt', {'fields': (
'name',
'description',
'start',
'end',
'otrs',
'plan',
'page',
'urls',
'group',
'location',
'participants_estimated',
'participants_real',
'insurance',
'insurance_technic',
'support',
'cost',
'categories',
'categories_other',
'wikimedia_projects',
'wikimedia_projects_other',
'notes',
)}),
('Mailing', {'fields': (
'mail_state',
'end_mail_send',
'survey_mail_send',
'survey_mail_date',
)}),
('Bewilligung', {'fields': (
'granted',
'granted_date',
'granted_from',
'intern_notes',
)}),
('Accounting', {'fields': (
'account',
'status',
*readonly_fields,
)}),
]
class Media: class Media:
js = ('dropdown/js/otrs_link.js',) js = ('dropdown/js/otrs_link.js',)
css = {
'all': ['css/full-width-related-labels.css'],
}
granted: bool
def get_queryset(self, request):
return super().get_queryset(request).filter(granted=self.granted)
@admin.register(Project)
class ProjectAdmin(BaseProjectAdmin):
granted = True
@admin.register(ProjectRequest)
class ProjectRequestAdmin(BaseProjectAdmin):
granted = None
@admin.register(ProjectDeclined)
class ProjectDeclinedAdmin(BaseProjectAdmin):
granted = False
def has_add_permission(self, request):
return False
@admin.register(BusinessCard) @admin.register(BusinessCard)
class BusinessCardAdmin(admin.ModelAdmin): class BusinessCardAdmin(WMDEAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project') search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
# action = ['export_as_csv'] # action = ['export_as_csv']
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
class Media: class Media:
js = ('dropdown/js/base.js',) js = ('dropdown/js/base.js', 'dropdown/js/otrs_link.js')
@admin.register(Literature) @admin.register(Literature)
class LiteratureAdmin(admin.ModelAdmin): class LiteratureAdmin(WMDEAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
class Media:
js = ('dropdown/js/otrs_link.js',)
@admin.register(Account) @admin.register(Account)
class AccountAdmin(admin.ModelAdmin): class AccountAdmin(admin.ModelAdmin):
@ -72,23 +221,43 @@ class AccountAdmin(admin.ModelAdmin):
class HonoraryCertificateAdmin(admin.ModelAdmin): class HonoraryCertificateAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ['realname', 'granted', 'project__name', 'project__pid'] search_fields = ['realname', 'granted', 'project__name', 'project__pid']
list_display = ('realname', 'granted','project') list_display = ('realname', 'granted', 'project')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
autocomplete_fields = ['project'] autocomplete_fields = ['project']
class Media: class Media:
js = ('dropdown/js/otrs_link.js',) js = ('dropdown/js/otrs_link.js',)
@admin.register(Library)
class LibraryAdmin(admin.ModelAdmin): @admin.register(Library, ELiterature, Software)
class LibraryAdmin(WMDEAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
exclude = ['type']
class Media:
js = ('dropdown/js/otrs_link.js',)
def get_queryset(self, request):
return super().get_queryset(request).filter(type=self.model.TYPE)
def formfield_for_dbfield(self, db_field, request, **kwargs):
if db_field.name == 'library':
kwargs['label'] = self.model.LIBRARY_LABEL
kwargs['help_text'] = self.model.LIBRARY_HELP_TEXT
elif db_field.name == 'duration':
kwargs['help_text'] = self.model.DURATION_HELP_TEXT
return super().formfield_for_dbfield(db_field, request, **kwargs)
@admin.register(IFG) @admin.register(IFG)
class IFGAdmin(admin.ModelAdmin): class IFGAdmin(WMDEAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date')
@ -96,51 +265,44 @@ class IFGAdmin(admin.ModelAdmin):
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
class Media:
js = ('dropdown/js/otrs_link.js',)
@admin.register(Travel) @admin.register(Travel)
class TravelAdmin(admin.ModelAdmin): class TravelAdmin(WMDEAdmin):
save_as = True save_as = True
search_fields = ['realname', 'service_id', 'granted_date', 'project__name', 'project__pid'] search_fields = ['realname', 'service_id', 'granted_date', 'project__name', 'project__pid']
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project_end', 'project', 'project_end_quartal') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project_end', 'project',
'project_end_quartal')
list_display_links = ('realname', 'project') list_display_links = ('realname', 'project')
date_hierarchy = 'project_end' date_hierarchy = 'project_end'
readonly_fields = ('project_end_quartal', 'project_end')
autocomplete_fields = ['project'] autocomplete_fields = ['project']
readonly_fields = ['service_id'] readonly_fields = ['service_id', 'project_end', 'project_end_quartal']
class Media: class Media:
js = ('dropdown/js/otrs_link.js',) js = ('dropdown/js/otrs_link.js',)
@admin.register(Email) @admin.register(Email)
class EmailAdmin(admin.ModelAdmin): class EmailAdmin(WMDEAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
radio_fields = {'adult': admin.VERTICAL} radio_fields = {'adult': admin.VERTICAL}
readonly_fields = ['service_id'] readonly_fields = ['service_id']
class Media: class Media:
js = ('dropdown/js/base.js',) js = ('dropdown/js/base.js', 'dropdown/js/otrs_link.js')
@admin.register(List) @admin.register(List)
class ListAdmin(admin.ModelAdmin): class ListAdmin(WMDEAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
# commented out because of the individual registering to control displays in admin panel
#admin.site.register([
# Account,
# HonoraryCertificate,
# Library,
# IFG,
# Travel,
# Email,
# List,
# ])

View File

@ -1,16 +0,0 @@
"""
ASGI config for oauth_demo project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oauth_demo.settings')
application = get_asgi_application()

View File

@ -1,89 +1,198 @@
from django.db import models from django import forms
from django.forms import ModelForm, DateField, ChoiceField, RadioSelect, BooleanField from django.conf import settings
from django.contrib.admin.widgets import AdminDateWidget from django.contrib.admin.widgets import AdminDateWidget
from django.forms import ModelForm
from django.forms.renderers import DjangoTemplates
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as trans
from .models import Project, Volunteer, ConcreteVolunteer, Extern, ConcreteExtern, IFG, Library, TYPE_CHOICES,\ from .models import (
HonoraryCertificate, Travel, Email, Literature, List,\ Project,
BusinessCard ProjectCategory,
from .settings import DATAPROTECTION, FOERDERRICHTLINIEN, NUTZUNGSBEDINGUNGEN WikimediaProject,
IFG,
from . import settings Library,
ELiterature,
Software,
Travel,
Email,
Literature,
List,
BusinessCard,
)
class TableFormRenderer(DjangoTemplates):
"""
Set in settings as the default form renderer.
"""
form_template_name = 'django/forms/table.html'
class RadioField(forms.ChoiceField):
widget = forms.RadioSelect
class BaseApplicationForm(ModelForm):
"""
Base form for all external applications.
"""
class FdbForm(ModelForm):
'''this base class provides the required css class for all forms'''
required_css_class = 'required' required_css_class = 'required'
check = forms.BooleanField(
required=True,
label=format_html(
"""Ich stimme den <a href="{}" target="_blank" rel="noopener">Datenschutzbestimmungen</a> und der<br>
<a href="{}" target="_blank" rel="noopener">Richtlinie zur Förderung der Communitys</a> zu.""",
settings.DATAPROTECTION,
settings.FOERDERRICHTLINIEN
),
)
class ProjectForm(FdbForm):
# start = DateField(widget=AdminDateWidget()) PROJECT_COST_GT_1000_MESSAGE = format_html(
"""Bitte beachte, dass für Projektkosten über 1.000 € ein öffentlicher Projektplan erforderlich
ist (siehe <a href="{0}" target="blank_">Wikipedia:Förderung/Projektplanung)</a>.""",
'https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/Projektplanung'
)
class BaseProjectForm(ModelForm):
categories = {
'categories': ProjectCategory,
'wikimedia_projects': WikimediaProject,
}
class Media:
js = ('dropdown/js/otrs_link.js', 'js/project-categories.js')
def clean(self):
cleaned_data = ModelForm.clean(self)
if self.errors:
return cleaned_data
for field, model in self.categories.items():
field_other = f'{field}_other'
values = cleaned_data[field]
if model.other in values:
if not cleaned_data[field_other]:
self.add_error(field_other, f'Bitte gib einen Wert an oder deselektiere "{model.OTHER}".')
else:
cleaned_data[field_other] = ''
return cleaned_data
class ProjectForm(BaseProjectForm, BaseApplicationForm):
OPTIONAL_FIELDS = {
'categories_other',
'wikimedia_projects_other',
'page',
'group',
'location',
'insurance',
'notes',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in set(self.fields) - self.OPTIONAL_FIELDS:
self.fields[field].required = True
class Meta: class Meta:
model = Project model = Project
exclude = ('pid', 'project_of_year', 'finance_id','granted', 'granted_date', 'realname', 'email',\ fields = [
'end_mail_send', 'status', 'persons', 'survey_mail_date', 'mail_state') 'realname',
widgets = {'start': AdminDateWidget(), 'email',
'end': AdminDateWidget(),} 'name',
'description',
class Media: 'categories',
js = ('dropdown/js/otrs_link.js',) 'categories_other',
'wikimedia_projects',
'wikimedia_projects_other',
class ExternForm(FdbForm): 'start',
'end',
choice = ChoiceField(choices=TYPE_CHOICES.items(), widget=RadioSelect, 'participants_estimated',
label='Was möchtest Du beantragen?') 'page',
'group',
check = BooleanField(required=True, 'location',
label=format_html("Ich stimme den <a href='{}' target='_blank' rel='noopener'>Datenschutzbestimmungen</a> und der<br> <a href='{}' target='_blank' rel='noopener'>Richtlinie zur Förderung der Communitys</a> zu", 'cost',
DATAPROTECTION, FOERDERRICHTLINIEN)) 'insurance',
'notes',
class Meta: ]
model = ConcreteExtern labels = {
exclude = ('username', 'granted', 'granted_date', 'survey_mail_send', 'service_id', 'survey_mail_date', 'mail_state') 'cost': 'Kosten in Euro',
'insurance': 'Haftpflicht- und Unfallversicherung gewünscht',
'participants_estimated': 'Voraussichtliche Zahl der Teilnehmenden',
INTERN_CHOICES = {'PRO': 'Projektsteckbrief', }
'HON': 'Ehrenamtsbescheinigung, Akkreditierung oder Redaktionsbestätigung', widgets = {
'TRAV': 'Reisekostenerstattung'} 'start': AdminDateWidget,
'end': AdminDateWidget,
class InternForm(FdbForm):
choice = ChoiceField(choices = INTERN_CHOICES.items(), widget=RadioSelect,
label = 'Was möchtest Du eingeben?')
class Meta:
model = ConcreteVolunteer
exclude = ('granted', 'granted_date', 'survey_mail_send', 'survey_mail_date', 'mail_state')
HOTEL_CHOICES = {'TRUE': format_html('Hotelzimmer benötigt'),
'FALSE': format_html('Kein Hotelzimmer benötigt')
} }
class Media:
css = {
'all': ('css/dateFieldNoNowShortcutInTravels.css',)
}
class TravelForm(FdbForm): def clean_cost(self):
cost = self.cleaned_data['cost']
if cost > 1000:
raise forms.ValidationError(PROJECT_COST_GT_1000_MESSAGE, code='cost-gt-1000')
return cost
HOTEL_CHOICES = {
'TRUE': mark_safe('Hotelzimmer benötigt'),
'FALSE': mark_safe('Kein Hotelzimmer benötigt'),
}
class TravelForm(BaseApplicationForm):
# TODO: add some javascript to show/hide other-field # TODO: add some javascript to show/hide other-field
hotel = RadioField(label='Hotelzimmer benötigt', choices=HOTEL_CHOICES)
# this is the code, to change required to false if needed # this is the code, to change required to false if needed
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['project_name'].required = True self.fields['project_name'].required = True
self.fields['transport'].required = True self.fields['transport'].required = True
self.fields['travelcost'].required = True self.fields['travelcost'].required = True
self.fields['travelcost'].initial = None
self.fields['checkin'].required = True self.fields['checkin'].required = True
self.fields['checkout'].required = True self.fields['checkout'].required = True
self.fields['hotel'].required = True self.fields['hotel'].required = True
class Meta: class Meta:
model = Travel model = Travel
exclude = ('granted', 'granted_date', 'survey_mail_send', 'realname', 'email', 'survey_mail_date', 'project', 'request_url', 'payed_for_hotel_by', 'payed_for_travel_by', 'intern_notes', 'mail_state' ) fields = [
widgets = {'checkin': AdminDateWidget(), 'realname',
'checkout': AdminDateWidget(),} 'email',
fields = ['project_name', 'transport', 'travelcost', 'checkin', 'checkout', 'hotel', 'notes'] 'project_name',
hotel = ChoiceField(label='Hotelzimmer benötigt:', choices=HOTEL_CHOICES.items(), widget=RadioSelect()) 'transport',
'travelcost',
'checkin',
'checkout',
'hotel',
'notes',
]
labels = {
'checkin': 'Datum der Anreise',
'checkout': 'Datum der Abreise',
}
widgets = {
'checkin': AdminDateWidget,
'checkout': AdminDateWidget,
}
class Media: class Media:
js = ('dropdown/js/otrs_link.js',) js = ('dropdown/js/otrs_link.js',)
@ -91,74 +200,115 @@ class TravelForm(FdbForm):
'all': ('css/dateFieldNoNowShortcutInTravels.css',) 'all': ('css/dateFieldNoNowShortcutInTravels.css',)
} }
class LibraryForm(FdbForm):
class LibraryForm(BaseApplicationForm):
class Meta: class Meta:
model = Library model = Library
fields = ['cost', 'library', 'duration', 'notes', 'survey_mail_send'] fields = [
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] 'realname',
'email',
'cost',
'library',
'duration',
'notes',
]
labels = {
'cost': 'Kosten in Euro',
}
class HonoraryCertificateForm(FdbForm): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class Meta: self.fields['library'].label = self._meta.model.LIBRARY_LABEL
model = HonoraryCertificate self.fields['library'].help_text = self._meta.model.LIBRARY_HELP_TEXT
fields = ['request_url', 'project'] self.fields['duration'].help_text = self._meta.model.DURATION_HELP_TEXT
exclude = ['intern_notes']
class Media:
js = ('dropdown/js/otrs_link.js',)
class IFGForm(FdbForm): class ELiteratureForm(LibraryForm):
class Meta(LibraryForm.Meta):
model = ELiterature
class SoftwareForm(LibraryForm):
class Meta(LibraryForm.Meta):
model = Software
class IFGForm(BaseApplicationForm):
class Meta: class Meta:
model = IFG model = IFG
fields = ['cost', 'url', 'notes'] fields = [
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] 'realname',
'email',
'cost',
'url',
'notes',
]
class CheckForm(FdbForm): class TermsForm(BaseApplicationForm):
termstoaccept = NUTZUNGSBEDINGUNGEN terms_accepted_label = 'Ich stimme den <a href="{}">Nutzungsbedingungen</a> zu.'
terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['check'] = BooleanField(
required=True, self.fields['terms_accepted'].required = True
label=format_html( self.fields['terms_accepted'].label = format_html(self.terms_accepted_label, self.terms_accepted_url)
"Ich stimme den <a href='{}'>Nutzungsbedingungen</a> zu",
self.termstoaccept
)
)
class LiteratureForm(TermsForm):
terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM
"""Baseclass for all classes which need a check for Nutzungsbedingungen"""
# def __init__(self, *args, **kwargs):
# check = BooleanField(required=True,
# label=format_html("Ich stimme den <a href='{}'>Nutzungsbedingungen</a> zu",
# termstoaccept))
# NUTZUNGSBEDINGUNGEN))
class LiteratureForm(CheckForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['selfbuy_give_data'].required = True
class Meta: class Meta:
model = Literature model = Literature
fields = ['cost', 'info', 'source', 'notes', 'selfbuy', 'selfbuy_data', 'selfbuy_give_data'] fields = [
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] 'realname',
'email',
'cost',
'info',
'source',
'notes',
'selfbuy',
'selfbuy_data',
'selfbuy_give_data',
'terms_accepted',
]
class Media: class Media:
js = ('dropdown/js/literature.js',) js = ('dropdown/js/literature.js',)
ADULT_CHOICES = {'TRUE': format_html('Ich bin volljährig.'), def clean(self):
'FALSE': format_html('Ich bin noch nicht volljährig.') cleaned_data = TermsForm.clean(self)
}
if self.errors:
return cleaned_data
if cleaned_data['selfbuy'] == 'TRUE':
cleaned_data['selfbuy_data'] = ''
cleaned_data['selfbuy_give_data'] = False
return cleaned_data
for field in 'selfbuy_data', 'selfbuy_give_data':
if not cleaned_data.get(field):
self.add_error(field, trans('This field is required.'))
return cleaned_data
class EmailForm(CheckForm): ADULT_CHOICES = {
'TRUE': mark_safe('Ich bin volljährig.'),
'FALSE': mark_safe('Ich bin noch nicht volljährig.'),
}
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE
class EmailForm(TermsForm):
terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE
# this is the code, to change required to false if needed # this is the code, to change required to false if needed
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -166,22 +316,28 @@ class EmailForm(CheckForm):
self.fields['adult'].required = True self.fields['adult'].required = True
self.fields['other'].required = True self.fields['other'].required = True
adult = ChoiceField(label='Volljährigkeit', choices=ADULT_CHOICES.items(), widget=RadioSelect()) adult = RadioField(label='Volljährigkeit', choices=ADULT_CHOICES)
# TODO: add some javascript to show/hide other-field # TODO: add some javascript to show/hide other-field
class Meta: class Meta:
model = Email model = Email
fields = ['domain', 'address', 'other', 'adult'] fields = [
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] 'realname',
'email',
'domain',
'address',
'other',
'adult',
'terms_accepted',
]
class Media: class Media:
js = ('dropdown/js/mail.js',) js = ('dropdown/js/mail.js',)
class BusinessCardForm(TermsForm):
terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_VISITENKARTEN
class BusinessCardForm(CheckForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_VISITENKARTEN
# this is the code, to change required to false if needed # this is the code, to change required to false if needed
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -190,16 +346,36 @@ class BusinessCardForm(CheckForm):
class Meta: class Meta:
model = BusinessCard model = BusinessCard
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] fields = [
fields = ['project', 'data', 'variant', 'url_of_pic', 'send_data_to_print', 'sent_to'] 'realname',
'email',
'project',
'data',
'variant',
'url_of_pic',
'send_data_to_print',
'sent_to',
'terms_accepted',
]
class Media: class Media:
js = ('dropdown/js/businessCard.js',) js = ('dropdown/js/businessCard.js',)
class ListForm(CheckForm): class ListForm(TermsForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN
class Meta: class Meta:
model = List model = List
fields = ['domain', 'address'] fields = [
exclude = ['intern_notes', 'survey_mail_send','mail_state'] 'realname',
'email',
'domain',
'address',
'terms_accepted',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['address'].initial = ''

View File

View File

View File

@ -1,18 +1,17 @@
from datetime import date, timedelta from datetime import date, timedelta
import sys
from django.core.management.base import BaseCommand, CommandError from django.core.management import CommandError
from django.template.loader import get_template from django.core.management.base import BaseCommand
from django.core.mail import send_mail, BadHeaderError, EmailMessage from django.core.mail import BadHeaderError
from django.core.mail import EmailMultiAlternatives
from django.conf import settings from django.conf import settings
from input.models import Project, Library, HonoraryCertificate, Travel, Email,\ from input.models import Project, Library, HonoraryCertificate, Travel, Email,\
BusinessCard, List, IFG, Literature BusinessCard, List, IFG, Literature
from input.settings import IF_EMAIL, SURVEYPREFIX, SURVEY_EMAIL from input.utils.mail import send_email
class Command(BaseCommand): class Command(BaseCommand):
''' mails will be send here: ''' mails will be sent here:
- two weeks after confirmation of support for volunteer (/extern) send link - two weeks after confirmation of support for volunteer (/extern) send link
with surveylink with surveylink
@ -34,16 +33,12 @@ class Command(BaseCommand):
'type': type, 'type': type,
'name': name, 'name': name,
'pid': pid, 'pid': pid,
'SURVEYPREFIX': SURVEYPREFIX, } 'SURVEY_PREFIX': settings.SURVEY_PREFIX, }
txt_mail_template = get_template('input/survey_mail.txt')
html_mail_template = get_template('input/survey_mail.html') subject = 'Dein Feedback zur Förderung durch Wikimedia Deutschland'
try: try:
subject, from_email, to = 'Dein Feedback zur Förderung durch Wikimedia Deutschland', IF_EMAIL, email send_email('survey_mail', context, subject, email, bcc=[settings.SURVEY_EMAIL])
text_content = txt_mail_template.render(context)
html_content = html_mail_template.render(context)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to], bcc=[SURVEY_EMAIL])
msg.attach_alternative(html_content, "text/html")
msg.send()
#print('survey mail would have been send') #print('survey mail would have been send')
#survey_mail = EmailMessage('Dein Feedback zur Förderung durch Wikimedia Deutschland', #survey_mail = EmailMessage('Dein Feedback zur Förderung durch Wikimedia Deutschland',
@ -52,8 +47,8 @@ class Command(BaseCommand):
# [email], # [email],
# bcc=[SURVEY_EMAIL]) # bcc=[SURVEY_EMAIL])
#survey_mail.send(fail_silently=False) #survey_mail.send(fail_silently=False)
except BadHeaderError: except BadHeaderError as error:
return HttpResponse('Invalid header found.') raise CommandError(f'Invalid header found: {error}')
print(f'send surveylinkemail to {email}...') print(f'send surveylinkemail to {email}...')
@ -71,21 +66,14 @@ class Command(BaseCommand):
.exclude(end_mail_send = True)\ .exclude(end_mail_send = True)\
.filter(mail_state = 'NONE') .filter(mail_state = 'NONE')
txt_mail_template = get_template('input/if_end_of_project.txt') subject = 'Projektende erreicht'
html_mail_template = get_template('input/if_end_of_project.html') recipient = settings.IF_EMAIL
for project in old: for project in old:
context = {'project': project} context = {'project': project, 'URL_PREFIX': settings.EMAIL_URL_PREFIX}
context['URLPREFIX'] = settings.URLPREFIX
try: try:
subject, from_email, to = 'Projektende erreicht', IF_EMAIL, IF_EMAIL send_email('if_end_of_project', context, subject, recipient)
text_content = txt_mail_template.render(context)
html_content = html_mail_template.render(context)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
msg.attach_alternative(html_content, "text/html")
msg.send()
#print('end of project mail would have been sent') #print('end of project mail would have been sent')
#send_mail('Projektende erreicht', #send_mail('Projektende erreicht',
@ -111,33 +99,19 @@ class Command(BaseCommand):
approved_end = Project.objects.filter(status = 'END')\ approved_end = Project.objects.filter(status = 'END')\
.exclude(end_mail_send = True)\ .exclude(end_mail_send = True)\
.filter(mail_state = 'INF') .filter(mail_state = 'INF')
txt_mail_template = get_template('input/if_end_of_project_approved.txt')
html_mail_template = get_template('input/if_end_of_project_approved.html')
txt_informMail_template = get_template('input/if_end_of_project_orginformed.txt')
html_informMail_template = get_template('input/if_end_of_project_orginformed.html')
# send the mail to project.email, which would be the mail of the volunteer filling out the form # send the mail to project.email, which would be the mail of the volunteer filling out the form
for project in approved_end: for project in approved_end:
context = {'project': project} context = {'project': project, 'URL_PREFIX': settings.EMAIL_URL_PREFIX}
context['URLPREFIX'] = settings.URLPREFIX
try: try:
subject, from_email, to = 'Projektende erreicht', IF_EMAIL, project.email send_email('if_end_of_project_approved', context, 'Projektende erreicht', project.email)
text_content = txt_mail_template.render(context)
html_content = html_mail_template.render(context)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
msg.attach_alternative(html_content, "text/html")
msg.send()
#print('if and of project approved mail would have been sent') #print('if and of project approved mail would have been sent')
inform_subject, inform_from_email, inform_to = 'Projektorganisator*in wurde informiert', IF_EMAIL, IF_EMAIL send_email('if_end_of_project_orginformed', context, 'Projektorganisator*in wurde informiert', settings.IF_EMAIL)
inform_text_content = txt_informMail_template.render(context)
inform_html_content = html_informMail_template.render(context)
inform_msg = EmailMultiAlternatives(inform_subject, inform_text_content, inform_from_email, [inform_to])
inform_msg.attach_alternative(html_content, "text/html")
inform_msg.send()
#print('if end of project orginformed mail would have been sent') #print('if end of project orginformed mail would have been sent')
#send_mail('Projektende erreicht', #send_mail('Projektende erreicht',
@ -169,25 +143,15 @@ class Command(BaseCommand):
.exclude(end_mail_send = True)\ .exclude(end_mail_send = True)\
.filter(mail_state = 'INF') .filter(mail_state = 'INF')
html_mail_template = get_template('input/if_not_of_project_approved.html')
txt_mail_template = get_template('input/if_not_of_project_approved.txt')
txt_informMail_template = get_template('input/if_end_of_project_orginformed.txt')
html_informMail_template = get_template('input/if_end_of_project_orginformed.html')
# send the mail to project.email, which would be the mail of the volunteer that filled out the form # send the mail to project.email, which would be the mail of the volunteer that filled out the form
for project in approved_notHappened: for project in approved_notHappened:
context = {'project': project} context = {'project': project, 'URL_PREFIX': settings.EMAIL_URL_PREFIX}
context['URLPREFIX'] = settings.URLPREFIX
try:
subject, from_email, to = 'Projektende erreicht', IF_EMAIL, project.email
text_content = txt_mail_template.render(context)
html_content = html_mail_template.render(context)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
msg.attach_alternative(html_content, "text/html")
msg.send()
#print('if not of project approved end mail would have been sent')
try:
send_email('if_not_of_project_approved', context, 'Projektende erreicht', project.email)
#print('if not of project approved end mail would have been sent')
#send_mail('Projektende erreicht', #send_mail('Projektende erreicht',
# mail_template.render(context), # mail_template.render(context),
@ -195,12 +159,8 @@ class Command(BaseCommand):
# [project.email], # [project.email],
# fail_silently=False) # fail_silently=False)
inform_subject, inform_from_email, inform_to = 'Projektorganisator*in wurde informiert', IF_EMAIL, IF_EMAIL send_email('if_end_of_project_orginformed', context, 'Projektorganisator*in wurde informiert', settings.IF_EMAIL)
inform_text_content = txt_informMail_template.render(context)
inform_html_content = html_informMail_template.render(context)
inform_msg = EmailMultiAlternatives(inform_subject, inform_text_content, inform_from_email, [inform_to])
inform_msg.attach_alternative(html_content, "text/html")
inform_msg.send()
#print('if not of project approved end mail orginformed would have been sent') #print('if not of project approved end mail orginformed would have been sent')
#send_mail('Projektorganisator*in wurde informiert', #send_mail('Projektorganisator*in wurde informiert',
@ -297,16 +257,15 @@ class Command(BaseCommand):
'''send survey link 2 weeks after mailadresss, mailinglist or businesscards are granted''' '''send survey link 2 weeks after mailadresss, mailinglist or businesscards are granted'''
lastdate = date.today() - timedelta(days=14) lastdate = date.today() - timedelta(days=14)
typefield = ('MAIL','VIS','LIST') models = Email, BusinessCard, List
count = 0 types = 'MAIL', 'VIS', 'LIST'
for c in ('Email', 'BusinessCard', 'List'):
# get class via string for model, typ in zip(models, types):
supported = getattr(sys.modules[__name__], c).objects.filter(granted=True)\ supported = model.objects.filter(granted=True)\
.filter(granted_date__lt = lastdate)\ .filter(granted_date__lt = lastdate)\
.exclude(survey_mail_send=True)\ .exclude(survey_mail_send=True)\
.exclude(mail_state = 'END') .exclude(mail_state = 'END')
self.surveymails_to_object(supported, type=typefield[count]) self.surveymails_to_object(supported, type=typ)
count += 1
def handle(self, *args, **options): def handle(self, *args, **options):

57
input/management/tests.py Normal file
View File

@ -0,0 +1,57 @@
import datetime
from django.core.management import call_command
from django.test import TestCase
from django.utils.timezone import localdate
from input.models import Project, Account, Email, Library, HonoraryCertificate
class ManagementCommandTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.account = Account.objects.create(code='test')
def test_sendmails(self):
today = localdate(None)
start = today - datetime.timedelta(days=2)
end = today - datetime.timedelta(days=1)
granted = today - datetime.timedelta(days=15)
def create_project(name, **kwargs):
kwargs.setdefault('account', self.account)
kwargs.setdefault('start', start)
kwargs.setdefault('end', end)
return Project.objects.create(name=name, **kwargs)
create_project('Test end_of_projects_reached')
create_project('Test end_of_projects_approved', status='END', mail_state='INF')
create_project('Test notHappened_of_projects_approved', status='NOT', mail_state='INF')
Email.objects.create(
domain='SOURCE',
address='cosmocode',
adult='TRUE',
granted=True,
granted_date=granted,
)
Library.objects.create(
type='BIB',
library='Test',
duration='1 Jahr',
cost=100,
granted=True,
granted_date=granted,
)
HonoraryCertificate.objects.create(
request_url='https://example.com',
granted=True,
granted_date=granted,
project=create_project('Test surveymails_to_hon'),
)
call_command('sendmails')

View File

@ -1,11 +1,13 @@
from authlib.integrations.base_client import OAuthError from authlib.integrations.base_client import OAuthError
from authlib.integrations.django_client import OAuth from authlib.integrations.django_client import OAuth
from authlib.oauth2.rfc6749 import OAuth2Token from authlib.oauth2.rfc6749 import OAuth2Token
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from foerderbarometer import settings from django.conf import settings
from input import views
from input import models from input.models import Extern
class OAuthMiddleware(MiddlewareMixin): class OAuthMiddleware(MiddlewareMixin):
@ -14,7 +16,7 @@ class OAuthMiddleware(MiddlewareMixin):
self.oauth = OAuth() self.oauth = OAuth()
def process_request(self, request): def process_request(self, request):
# added this if clause to get the landing page before oauth # added this if-clause to get the landing page before oauth
if request.path == '/': if request.path == '/':
return self.get_response(request) return self.get_response(request)
if settings.OAUTH_URL_WHITELISTS is not None: if settings.OAUTH_URL_WHITELISTS is not None:
@ -37,12 +39,12 @@ class OAuthMiddleware(MiddlewareMixin):
self.clear_session(request) self.clear_session(request)
request.session['token'] = sso_client.authorize_access_token(request) request.session['token'] = sso_client.authorize_access_token(request)
# print('blub', request.session['token']) # print('blub', request.session['token'])
models.Extern.username = self.get_current_user(sso_client, request)['username'] Extern.username = self.get_current_user(sso_client, request)['username']
if self.get_current_user(sso_client, request) is not None: if self.get_current_user(sso_client, request) is not None:
redirect_uri = request.session.pop('redirect_uri', None) redirect_uri = request.session.pop('redirect_uri', None)
if redirect_uri is not None: if redirect_uri is not None:
return redirect(redirect_uri) return redirect(redirect_uri)
return redirect(views.ExternView) return redirect('extern')
if request.session.get('token', None) is not None: if request.session.get('token', None) is not None:
current_user = self.get_current_user(sso_client, request) current_user = self.get_current_user(sso_client, request)

View File

@ -38,7 +38,7 @@ class Migration(migrations.Migration):
('survey_mail_send', models.BooleanField(null=True)), ('survey_mail_send', models.BooleanField(null=True)),
('username', models.CharField(max_length=200, null=True)), ('username', models.CharField(max_length=200, null=True)),
('domain', models.CharField(choices=[('PEDIA', '@wikipedia.de'), ('BOOKS', '@wikibooks.de'), ('QUOTE', '@wikiquote.de'), ('SOURCE', '@wikisource.de'), ('VERSITY', '@wikiversity.de')], default='PEDIA', max_length=10)), ('domain', models.CharField(choices=[('PEDIA', '@wikipedia.de'), ('BOOKS', '@wikibooks.de'), ('QUOTE', '@wikiquote.de'), ('SOURCE', '@wikisource.de'), ('VERSITY', '@wikiversity.de')], default='PEDIA', max_length=10)),
('adress', models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges:')], default='USERNAME', max_length=50)), ('adress', models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges')], default='USERNAME', max_length=50)),
('other', models.CharField(blank=True, max_length=50, null=True)), ('other', models.CharField(blank=True, max_length=50, null=True)),
], ],
options={ options={

View File

@ -45,7 +45,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='email', model_name='email',
name='address', name='address',
field=models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges:')], default='USERNAME', max_length=50, verbose_name='Adressbestandteil'), field=models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges')], default='USERNAME', max_length=50, verbose_name='Adressbestandteil'),
), ),
migrations.AddField( migrations.AddField(
model_name='list', model_name='list',

View File

@ -13,7 +13,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='email', model_name='email',
name='address', name='address',
field=models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges:')], default='USERNAME', help_text='Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.', max_length=50, verbose_name='Adressbestandteil'), field=models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges')], default='USERNAME', help_text='Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.', max_length=50, verbose_name='Adressbestandteil'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='email', model_name='email',

View File

@ -18,12 +18,12 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='ifg', model_name='ifg',
name='notes', name='notes',
field=models.TextField(blank=True, help_text='Bitte gib an wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'), field=models.TextField(blank=True, help_text='Bitte gib an, wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='library', model_name='library',
name='notes', name='notes',
field=models.TextField(blank=True, help_text='Bitte gib an wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'), field=models.TextField(blank=True, help_text='Bitte gib an, wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='library', model_name='library',
@ -33,7 +33,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='literature', model_name='literature',
name='notes', name='notes',
field=models.TextField(blank=True, help_text='Bitte gib an wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'), field=models.TextField(blank=True, help_text='Bitte gib an, wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='project', model_name='project',

View File

@ -13,7 +13,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='travel', model_name='travel',
name='project_name', name='project_name',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Projektname:'), field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Projektname'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='library', model_name='library',

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='travel', model_name='travel',
name='hotel', name='hotel',
field=models.BooleanField(default=False, verbose_name='Hotelzimmer benötigt:'), field=models.BooleanField(default=False, verbose_name='Hotelzimmer benötigt'),
), ),
] ]

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='travel', model_name='travel',
name='transport', name='transport',
field=models.CharField(choices=[('BAHN', 'Bahn'), ('NONE', 'Keine Fahrtkosten'), ('OTHER', 'Sonstiges (mit Begründung)')], default='BAHN', max_length=5, verbose_name='Transportmittel:'), field=models.CharField(choices=[('BAHN', 'Bahn'), ('NONE', 'Keine Fahrtkosten'), ('OTHER', 'Sonstiges (mit Begründung)')], default='BAHN', max_length=5, verbose_name='Transportmittel'),
), ),
] ]

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='travel', model_name='travel',
name='hotel', name='hotel',
field=models.CharField(choices=[('TRUE', 'Hotelzimmer benötigt'), ('FALSE', 'Kein Hotelzimmer benötigt')], max_length=10, verbose_name='Hotelzimmer benötigt:'), field=models.CharField(choices=[('TRUE', 'Hotelzimmer benötigt'), ('FALSE', 'Kein Hotelzimmer benötigt')], max_length=10, verbose_name='Hotelzimmer benötigt'),
), ),
] ]

View File

@ -0,0 +1,113 @@
# Generated by Django 5.2.5 on 2025-08-20 09:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0096_auto_20230106_1338'),
]
operations = [
migrations.AlterField(
model_name='businesscard',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='businesscard',
name='username',
field=models.CharField(help_text='Wikimedia Benutzer_innenname', max_length=200, null=True, verbose_name='Benutzer_innenname'),
),
migrations.AlterField(
model_name='concreteextern',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='concreteextern',
name='username',
field=models.CharField(help_text='Wikimedia Benutzer_innenname', max_length=200, null=True, verbose_name='Benutzer_innenname'),
),
migrations.AlterField(
model_name='concretevolunteer',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='email',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='email',
name='username',
field=models.CharField(help_text='Wikimedia Benutzer_innenname', max_length=200, null=True, verbose_name='Benutzer_innenname'),
),
migrations.AlterField(
model_name='honorarycertificate',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='ifg',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='ifg',
name='username',
field=models.CharField(help_text='Wikimedia Benutzer_innenname', max_length=200, null=True, verbose_name='Benutzer_innenname'),
),
migrations.AlterField(
model_name='library',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='library',
name='type',
field=models.CharField(choices=[('BIB', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#Bibliotheksstipendium" target="_blank" rel="noopener">Bibliotheksstipendium</a>'), ('ELIT', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#eLiteraturstipendium" target="_blank" rel="noopener">eLiteraturstipendium</a>'), ('MAIL', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/E-Mail-Adressen_und_Visitenkarten#E-Mail-Adressen" target="_blank" rel="noopener">E-Mail-Adresse</a>'), ('IFG', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Gebührenerstattungen_für_Behördenanfragen" target="_blank" rel="noopener">Kostenübernahme IFG-Anfrage</a>'), ('LIT', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#Literaturstipendium" target="_blank" rel="noopener">Literaturstipendium</a>'), ('LIST', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/E-Mail-Adressen_und_Visitenkarten#Mailinglisten" target="_blank" rel="noopener">Mailingliste</a>'), ('TRAV', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Reisekostenerstattungen" target="_blank" rel="noopener">Reisekosten</a>'), ('SOFT', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Software-Stipendien" target="_blank" rel="noopener">Softwarestipendium</a>'), ('VIS', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/E-Mail-Adressen_und_Visitenkarten#Visitenkarten" target="_blank" rel="noopener">Visitenkarten</a>')], default='BIB', max_length=4),
),
migrations.AlterField(
model_name='library',
name='username',
field=models.CharField(help_text='Wikimedia Benutzer_innenname', max_length=200, null=True, verbose_name='Benutzer_innenname'),
),
migrations.AlterField(
model_name='list',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='list',
name='username',
field=models.CharField(help_text='Wikimedia Benutzer_innenname', max_length=200, null=True, verbose_name='Benutzer_innenname'),
),
migrations.AlterField(
model_name='literature',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='literature',
name='username',
field=models.CharField(help_text='Wikimedia Benutzer_innenname', max_length=200, null=True, verbose_name='Benutzer_innenname'),
),
migrations.AlterField(
model_name='project',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='travel',
name='realname',
field=models.CharField(default='', help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', max_length=200, null=True, verbose_name='Realname'),
),
migrations.AlterField(
model_name='travel',
name='username',
field=models.CharField(help_text='Wikimedia Benutzer_innenname', max_length=200, null=True, verbose_name='Benutzer_innenname'),
),
]

View File

@ -0,0 +1,40 @@
# Generated by Django 5.2.5 on 2025-08-20 10:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0097_alter_realname_and_username'),
]
operations = [
migrations.CreateModel(
name='ELiterature',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('input.library',),
),
migrations.CreateModel(
name='Software',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('input.library',),
),
migrations.AlterField(
model_name='library',
name='type',
field=models.CharField(choices=[('BIB', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#Bibliotheksstipendium" target="_blank" rel="noopener">Bibliotheksstipendium</a>'), ('ELIT', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#eLiteraturstipendium" target="_blank" rel="noopener">eLiteraturstipendium</a>'), ('SOFT', '<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Software-Stipendien" target="_blank" rel="noopener">Softwarestipendium</a>')], default='BIB', max_length=4),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.2.5 on 2025-08-26 11:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0098_add_eliterature_and_software_proxies'),
]
operations = [
migrations.AddField(
model_name='businesscard',
name='terms_accepted',
field=models.BooleanField(default=False, verbose_name='Nutzungsbedingungen zugestimmt'),
),
migrations.AddField(
model_name='email',
name='terms_accepted',
field=models.BooleanField(default=False, verbose_name='Nutzungsbedingungen zugestimmt'),
),
migrations.AddField(
model_name='list',
name='terms_accepted',
field=models.BooleanField(default=False, verbose_name='Nutzungsbedingungen zugestimmt'),
),
migrations.AddField(
model_name='literature',
name='terms_accepted',
field=models.BooleanField(default=False, verbose_name='Nutzungsbedingungen zugestimmt'),
),
]

View File

@ -0,0 +1,77 @@
# Generated by Django 5.2.5 on 2025-10-15 13:02
from functools import partial
from django.db import migrations, models
from input.utils.migrations import get_queryset
DEFAULT_PROJECT_CATEGORIES = [
'Erstellung und Weiterentwicklung von Inhalten für die Wikimedia-Projekte',
'Aufklärung über die Wikimedia-Projekte',
'Formate zur Ansprache, Gewinnung und Bindung von Ehrenamtlichen für die Wikimedia-Projekte',
'Beteiligung von Menschen, die einen erschwerten Zugang zum Engagement in den Wikimedia-Projekten haben',
'Vernetzung und Austausch innerhalb der Communitys oder zwischen den Communitys und externen Partner*innen',
'Vermittlung von Kompetenzen, die die ehrenamtliche Arbeit stärken',
'Stärkung einer respektvollen, konstruktiven Kommunikationskultur und der Wertschätzung in den Wikimedia-Projekten',
'Verbesserung der Selbstorganisation in Bezug auf interne Regeln, Strukturen und Prozesse der Wikimedia-Projektcommunitys',
'Ehrenamtliche Aktivitäten, die der Erstellung, Pflege und Weiterentwicklung von Tools oder sonstigen technischen Verbesserungen dienen',
]
DEFAULT_WIKIMEDIA_PROJECTS = [
'Wikipedia',
'Wikimedia Commons',
'Wikidata',
]
def create_default_objs(model, defaults, apps, schema_editor):
queryset = get_queryset(apps, schema_editor, 'input', model)
queryset.bulk_create([
queryset.model(name=name, order=order * 10)
for order, name in enumerate(defaults, 1)
])
class Migration(migrations.Migration):
dependencies = [
('input', '0099_add_terms_accepted'),
]
operations = [
migrations.CreateModel(
name='ProjectCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('order', models.PositiveIntegerField(verbose_name='Reihenfolge')),
],
options={
'verbose_name': 'Projektkategorie',
'verbose_name_plural': 'Projektkategorien',
'ordering': ['order'],
},
),
migrations.RunPython(
code=partial(create_default_objs, 'ProjectCategory', DEFAULT_PROJECT_CATEGORIES),
reverse_code=migrations.RunPython.noop,
),
migrations.CreateModel(
name='WikimediaProject',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('order', models.PositiveIntegerField(verbose_name='Reihenfolge')),
],
options={
'verbose_name': 'Wikimedia-Projekt',
'verbose_name_plural': 'Wikimedia-Projekte',
'ordering': ['order'],
},
),
migrations.RunPython(
code=partial(create_default_objs, 'WikimediaProject', DEFAULT_WIKIMEDIA_PROJECTS),
reverse_code=migrations.RunPython.noop,
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 5.2.5 on 2025-10-15 15:12
import input.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0100_projectcategory_wikimedia_project'),
]
operations = [
migrations.AddField(
model_name='project',
name='categories',
field=input.models.ProjectCategoryField(related_name='projects', to='input.projectcategory', verbose_name='Projektkategorien'),
),
migrations.AddField(
model_name='project',
name='categories_other',
field=models.CharField(blank=True, max_length=200, verbose_name='Projektkategorien (Sonstiges)'),
),
migrations.AddField(
model_name='project',
name='wikimedia_projects',
field=input.models.ProjectCategoryField(related_name='projects', to='input.wikimediaproject', verbose_name='Wikimedia-Projekte'),
),
migrations.AddField(
model_name='project',
name='wikimedia_projects_other',
field=models.CharField(blank=True, max_length=200, verbose_name='Wikimedia-Projekte (Anderes)'),
),
]

View File

@ -0,0 +1,43 @@
# Generated by Django 5.2.5 on 2025-10-16 13:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0101_wikimedia_project_categories_and_other'),
]
operations = [
migrations.CreateModel(
name='ProjectDeclined',
fields=[
],
options={
'verbose_name': 'Projekt (abgelehnt)',
'verbose_name_plural': 'Projekte (abgelehnt)',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('input.project',),
),
migrations.CreateModel(
name='ProjectRequest',
fields=[
],
options={
'verbose_name': 'Projekt (beantragt)',
'verbose_name_plural': 'Projekte (beantragt)',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('input.project',),
),
migrations.AlterModelOptions(
name='project',
options={'verbose_name': 'Projekt', 'verbose_name_plural': 'Projekte'},
),
]

View File

@ -0,0 +1,43 @@
# Generated by Django 5.2.5 on 2025-10-08 10:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0102_project_request_declined'),
]
operations = [
migrations.AddField(
model_name='businesscard',
name='request_url',
field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'),
),
migrations.AddField(
model_name='email',
name='request_url',
field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'),
),
migrations.AddField(
model_name='ifg',
name='request_url',
field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'),
),
migrations.AddField(
model_name='library',
name='request_url',
field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'),
),
migrations.AddField(
model_name='list',
name='request_url',
field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'),
),
migrations.AddField(
model_name='literature',
name='request_url',
field=models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.5 on 2025-10-16 14:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0103_add_request_url'),
]
operations = [
migrations.AlterField(
model_name='project',
name='account',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=models.CASCADE, to='input.account'),
),
migrations.AlterField(
model_name='project',
name='granted_date',
field=models.DateField(blank=True, null=True, verbose_name='Bewilligt am'),
),
migrations.AlterField(
model_name='project',
name='granted_from',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Bewilligt von'),
),
]

View File

@ -0,0 +1,57 @@
# Generated by Django 5.2.5 on 2025-11-07 15:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('input', '0104_alter_project_required_fields'),
]
operations = [
migrations.AlterModelOptions(
name='account',
options={'verbose_name': 'Kostenstelle', 'verbose_name_plural': 'Kostenstellen'},
),
migrations.AlterModelOptions(
name='businesscard',
options={'verbose_name': 'Visitenkarte', 'verbose_name_plural': 'Visitenkarten'},
),
migrations.AlterModelOptions(
name='eliterature',
options={'verbose_name': 'eLiteraturstipendium', 'verbose_name_plural': 'eLiteraturstipendien'},
),
migrations.AlterModelOptions(
name='email',
options={'verbose_name': 'E-Mail-Adresse', 'verbose_name_plural': 'E-Mail-Adressen'},
),
migrations.AlterModelOptions(
name='honorarycertificate',
options={'verbose_name': 'Bescheinigung', 'verbose_name_plural': 'Bescheinigungen'},
),
migrations.AlterModelOptions(
name='ifg',
options={'verbose_name': 'IFG-Anfrage', 'verbose_name_plural': 'IFG-Anfragen'},
),
migrations.AlterModelOptions(
name='library',
options={'verbose_name': 'Bibliotheksstipendium', 'verbose_name_plural': 'Bibliotheksstipendien'},
),
migrations.AlterModelOptions(
name='list',
options={'verbose_name': 'Mailingliste', 'verbose_name_plural': 'Mailinglisten'},
),
migrations.AlterModelOptions(
name='literature',
options={'verbose_name': 'Literaturstipendium', 'verbose_name_plural': 'Literaturstipendien'},
),
migrations.AlterModelOptions(
name='software',
options={'verbose_name': 'Softwarestipendium', 'verbose_name_plural': 'Softwarestipendien'},
),
migrations.AlterModelOptions(
name='travel',
options={'verbose_name': 'Reisekosten', 'verbose_name_plural': 'Reisekosten'},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.5 on 2025-11-07 15:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0105_add_verbose_names'),
]
operations = [
migrations.AddField(
model_name='project',
name='username',
field=models.CharField(help_text='Wikimedia Benutzer_innenname', max_length=200, blank=True, verbose_name='Benutzer_innenname'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.5 on 2025-11-10 10:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0106_project_username'),
]
operations = [
migrations.AlterField(
model_name='project',
name='categories_other',
field=models.TextField(blank=True, verbose_name='Projektkategorien (Sonstiges)'),
),
migrations.AlterField(
model_name='project',
name='wikimedia_projects_other',
field=models.TextField(blank=True, verbose_name='Wikimedia-Projekte (Anderes)'),
),
]

View File

@ -1,21 +1,55 @@
from contextlib import suppress
from datetime import date from datetime import date
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.forms import ModelMultipleChoiceField, CheckboxSelectMultiple
from django.utils.functional import cached_property, classproperty
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .settings import ACCOUNTS from foerderbarometer.constants import *
EMAIL_STATES = {'NONE': 'noch keine Mail versendet', EMAIL_STATES = {
'NONE': 'noch keine Mail versendet',
'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet', 'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet',
'CLOSE': 'die Projektabschlussmail wurde versendet', 'CLOSE': 'die Projektabschlussmail wurde versendet',
'END': 'alle automatischen Mails, auch surveyMail, wurden versendet'} 'END': 'alle automatischen Mails, auch surveyMail, wurden versendet',
}
class TermsConsentMixin(models.Model):
"""Abstract mixin to add a terms_accepted field for documenting user consent."""
terms_accepted = models.BooleanField(default=False, verbose_name='Nutzungsbedingungen zugestimmt')
class Meta:
abstract = True
class RequestUrlMixin(models.Model):
"""
Abstract mixin for adding an OTRS request URL field to admin models.
This field stores a direct link to the related OTRS ticket.
Note: OTRS links may contain semicolons, which must not be URL-encoded.
"""
request_url = models.URLField(max_length=2000, null=True, verbose_name='Antrag (URL)')
class Meta:
abstract = True
class Volunteer(models.Model): class Volunteer(models.Model):
realname = models.CharField(max_length=200, null=True, verbose_name="Realname", realname = models.CharField(max_length=200, null=True, verbose_name='Realname',
help_text="Bitte gib deinen Vornamen und deinen Nachnamen ein.", default='') help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', default='')
email = models.EmailField(max_length=200, null=True, verbose_name='E-Mail-Adresse', email = models.EmailField(max_length=200, null=True, verbose_name='E-Mail-Adresse',
help_text=format_html('Bitte gib deine E-Mail-Adresse ein, damit dich<br>Wikimedia Deutschland bei Rückfragen oder für<br>die Zusage kontaktieren kann.')) help_text=mark_safe('Bitte gib deine E-Mail-Adresse ein, damit dich<br>Wikimedia Deutschland bei Rückfragen oder für<br>die Zusage kontaktieren kann.'))
# the following Fields are not supposed to be edited by users # the following Fields are not supposed to be edited by users
@ -25,7 +59,6 @@ class Volunteer(models.Model):
mail_state = models.CharField(max_length=6, choices=EMAIL_STATES.items(), default='NONE') mail_state = models.CharField(max_length=6, choices=EMAIL_STATES.items(), default='NONE')
survey_mail_send = models.BooleanField(default=False, verbose_name='Keine Umfragemail schicken') survey_mail_send = models.BooleanField(default=False, verbose_name='Keine Umfragemail schicken')
@classmethod @classmethod
def set_granted(cl, key, b): def set_granted(cl, key, b):
obj = cl.objects.get(pk=key) obj = cl.objects.get(pk=key)
@ -37,17 +70,16 @@ class Volunteer(models.Model):
abstract = True abstract = True
class Extern(Volunteer): class Extern(Volunteer):
''' abstract basis class for all data entered by extern volunteers ''' ''' abstract basis class for all data entered by extern volunteers '''
username = models.CharField(max_length=200, null=True, verbose_name='Benutzer_innenname', username = models.CharField(max_length=200, null=True, verbose_name='Benutzer_innenname',
help_text=format_html("Wikimedia Benutzer_innenname")) help_text=mark_safe('Wikimedia Benutzer_innenname'))
# the following Fields are not supposed to be edited by users # the following Fields are not supposed to be edited by users
service_id = models.CharField(max_length=15, null=True, blank=True) service_id = models.CharField(max_length=15, null=True, blank=True)
def save(self,*args,**kwargs): def save(self, *args, **kwargs):
# we don't call save with args/kwargs to avoid UNIQUE CONSTRAINT errors # we don't call save with args/kwargs to avoid UNIQUE CONSTRAINT errors
# but maybe there is a better solution? # but maybe there is a better solution?
super().save() super().save()
@ -57,263 +89,470 @@ class Extern(Volunteer):
class Meta: class Meta:
abstract = True abstract = True
class ConcreteExtern(Extern): class ConcreteExtern(Extern):
''' needed because we can't initiate abstract base classes in the view''' ''' needed because we can't initiate abstract base classes in the view'''
pass pass
class Account(models.Model): class Account(models.Model):
code = models.CharField('Kostenstelle', max_length=5, default="DEF", code = models.CharField('Kostenstelle', max_length=5, default='DEF', null=False, primary_key=True)
null=False, primary_key = True)
description = models.CharField('Beschreibung', max_length=60, default='NO DESCRIPTION') description = models.CharField('Beschreibung', max_length=60, default='NO DESCRIPTION')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Kostenstelle'
verbose_name_plural = 'Kostenstellen'
def __str__(self): def __str__(self):
return f"{self.code} {self.description}" return f'{self.code} {self.description}'
@property
def has_subaccounts(self):
return self.code == '21111'
class BaseProjectCategory(models.Model):
OTHER: str
name = models.CharField('Name', max_length=200)
order = models.PositiveIntegerField('Reihenfolge')
class Meta:
abstract = True
ordering = ['order']
def __str__(self):
return self.name
@cached_property
def project_count(self):
return self.projects.count()
@classproperty
def other(cls):
return cls(id=0, name=cls.OTHER)
class ProjectCategory(BaseProjectCategory):
OTHER = 'Sonstiges'
class Meta(BaseProjectCategory.Meta):
verbose_name = 'Projektkategorie'
verbose_name_plural = 'Projektkategorien'
class WikimediaProject(BaseProjectCategory):
OTHER = 'Anderes'
class Meta(BaseProjectCategory.Meta):
verbose_name = 'Wikimedia-Projekt'
verbose_name_plural = 'Wikimedia-Projekte'
class ProductCategoryChoiceIterator(ModelMultipleChoiceField.iterator):
def __iter__(self):
yield from ModelMultipleChoiceField.iterator.__iter__(self)
yield f'{self.field.other.id}', self.field.other.name
class ProductCategoryFormField(ModelMultipleChoiceField):
widget = CheckboxSelectMultiple
iterator = ProductCategoryChoiceIterator
def __init__(self, *, other, **kwargs):
super().__init__(**kwargs)
self.other = other
def _check_values(self, value, *, other=False):
with suppress(TypeError):
value = set(value)
if other := f'{self.other.id}' in value:
value.remove(f'{self.other.id}')
queryset = super()._check_values(value)
if other:
return [*queryset, self.other]
return list(queryset)
class ProjectCategoryField(models.ManyToManyField):
def __init__(self, to, **kwargs):
kwargs['to'] = to
kwargs['related_name'] = 'projects'
super().__init__(**kwargs)
self.other_field = models.TextField(blank=True)
def contribute_to_class(self, cls, name, **kwargs):
super().contribute_to_class(cls, name, **kwargs)
model, other_field = self.remote_field.model, self.other_field
if not isinstance(model, str):
self.verbose_name = self._verbose_name = verbose_name = model._meta.verbose_name_plural
other_field.verbose_name = other_field._verbose_name = f'{verbose_name} ({model.OTHER})'
other_field.contribute_to_class(cls, f'{name}_other')
def formfield(self, **kwargs):
kwargs['form_class'] = ProductCategoryFormField
kwargs['other'] = self.remote_field.model.other
return super().formfield(**kwargs)
def save_form_data(self, instance, data):
data = list(data)
with suppress(ValueError):
data.remove(self.remote_field.model.other)
return super().save_form_data(instance, data)
class Project(Volunteer): class Project(Volunteer):
username = models.CharField(max_length=200, blank=True, verbose_name='Benutzer_innenname',
help_text=mark_safe('Wikimedia Benutzer_innenname'))
end_mail_send = models.BooleanField(default=False, verbose_name='Keine Projektabschlussmail schicken') end_mail_send = models.BooleanField(default=False, verbose_name='Keine Projektabschlussmail schicken')
name = models.CharField(max_length=200, verbose_name='Name des Projekts') name = models.CharField(max_length=200, verbose_name='Name des Projekts')
description = models.CharField(max_length=500, verbose_name="Kurzbeschreibung", null=True) description = models.CharField(max_length=500, verbose_name='Kurzbeschreibung', null=True)
start = models.DateField('Startdatum', null=True) start = models.DateField('Startdatum', null=True)
end = models.DateField('Erwartetes Projektende', null=True) end = models.DateField('Erwartetes Projektende', null=True)
otrs = models.URLField(max_length=300, null=True, verbose_name='OTRS-Link') otrs = models.URLField(max_length=300, null=True, verbose_name='OTRS-Link')
plan = models.URLField(max_length=2000, null=True, blank=True, verbose_name="Link zum Förderplan") plan = models.URLField(max_length=2000, null=True, blank=True, verbose_name='Link zum Förderplan')
page = models.URLField(max_length=2000, null=True, blank=True, verbose_name="Link zur Projektseite") page = models.URLField(max_length=2000, null=True, blank=True, verbose_name='Link zur Projektseite')
urls = models.CharField(max_length=2000, null=True, blank=True, verbose_name="Weitere Links") urls = models.CharField(max_length=2000, null=True, blank=True, verbose_name='Weitere Links')
group = models.CharField(max_length=2000, null=True, blank=True, verbose_name="Mitorganisierende") group = models.CharField(max_length=2000, null=True, blank=True, verbose_name='Mitorganisierende')
location = models.CharField(max_length=2000, null=True, blank=True, verbose_name="Ort/Adresse/Location") location = models.CharField(max_length=2000, null=True, blank=True, verbose_name='Ort/Adresse/Location')
participants_estimated = models.IntegerField(blank=True, null=True, verbose_name='Teilnehmende angefragt') participants_estimated = models.IntegerField(blank=True, null=True, verbose_name='Teilnehmende angefragt')
participants_real = models.IntegerField(blank=True, null=True, verbose_name='Teilnehmende ausgezählt') participants_real = models.IntegerField(blank=True, null=True, verbose_name='Teilnehmende ausgezählt')
insurance = models.BooleanField(default=False, verbose_name='Haftpflichtversicherung') insurance = models.BooleanField(default=False, verbose_name='Haftpflichtversicherung')
insurance_technic = models.BooleanField(default=False, verbose_name='Technikversicherung Ausland') insurance_technic = models.BooleanField(default=False, verbose_name='Technikversicherung Ausland')
support = models.CharField(max_length=300, blank=True, null=True, verbose_name='Betreuungsperson und Vertretung') support = models.CharField(max_length=300, blank=True, null=True, verbose_name='Betreuungsperson und Vertretung')
cost = models.IntegerField(blank=True, null=True, verbose_name='Kosten') cost = models.IntegerField(blank=True, null=True, verbose_name='Kosten')
account = models.ForeignKey('Account', on_delete=models.CASCADE, null=True, to_field='code', db_constraint = False) account = models.ForeignKey('Account', on_delete=models.CASCADE, blank=True, null=True, to_field='code', db_constraint=False)
granted_from = models.CharField(max_length=100,null=True,verbose_name='Bewilligt von') granted_date = models.DateField(blank=True, null=True, verbose_name='Bewilligt am')
notes = models.TextField(max_length=1000,null=True,blank=True,verbose_name='Anmerkungen') granted_from = models.CharField(max_length=100, blank=True, null=True, verbose_name='Bewilligt von')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") notes = models.TextField(max_length=1000, null=True, blank=True, verbose_name='Anmerkungen')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
categories = ProjectCategoryField(ProjectCategory)
wikimedia_projects = ProjectCategoryField(WikimediaProject)
# the following Fields are not supposed to be edited by users # the following Fields are not supposed to be edited by users
pid = models.CharField(max_length=15, null=True, blank=True) pid = models.CharField(max_length=15, null=True, blank=True)
status = models.CharField(max_length=3,choices=(('RUN', 'läuft'),('END','beendet'),('NOT','nicht stattgefunden')),default='RUN') status = models.CharField(max_length=3,choices=(('RUN', 'läuft'),('END','beendet'),('NOT','nicht stattgefunden')),default='RUN')
finance_id = models.CharField(max_length=15, null= True, blank=True) finance_id = models.CharField(max_length=15, null= True, blank=True)
project_of_year = models.IntegerField(default=0) project_of_year = models.IntegerField(default=0)
end_quartal = models.CharField(max_length=15, null=True, blank=True, verbose_name="Quartal Projekt Ende") end_quartal = models.CharField(max_length=15, null=True, blank=True, verbose_name='Quartal Projekt Ende')
def save(self,*args,**kwargs):
generate_finance_id=False
'''we generate the autogenerated fields here'''
# we don't call save with args/kwargs to avoid UNIQUE CONSTRAINT errors
# but maybe there is a better solution?
if not self.pk:
print ("NO PK THERE");
generate_finance_id=True
super().save()
else:
orig = type(self).objects.get(pk=self.pk) # Originaldaten aus der DB abrufen
if orig.start.year != self.start.year:
generate_finance_id=True
if orig.account.code != self.account.code:
if str(self.account.code) == '21111':
generate_finance_id=True
else:
self.finance_id = str(self.account.code)
if generate_finance_id:
print ("MUST GENERATE FINANCE ID")
year = self.start.year
projects = Project.objects.filter(start__year=year)
if not projects:
self.project_of_year = 1
#self.pid = str(self.start.year) + '-' + str(self.account.code) + str(self.project_of_year).zfill(3)
else:
# get the project of year number of latest entry
projects = projects.order_by("-project_of_year")[0]
# add one to value of latest entry
self.project_of_year = int(projects.project_of_year) + 1
# self.pid = str(self.start.year) + '-' + str(self.account.code) + str(self.project_of_year).zfill(3)
if str(self.account.code) == '21111':
self.finance_id = str(self.account.code) + '-' + str(self.project_of_year).zfill(3)
else:
self.finance_id = str(self.account.code)
# print (("Current PID",self.pid))
if not self.pid:
self.pid = str(self.account.code) + str(self.pk).zfill(8)
# self.pid = str(self.account.code) + str(self.pk).zfill(3)
print (("Hallo Leute! Ich save jetzt mal MIT PID DANN!!!",self.pid))
# generation of field quartals
if self.end.month in [1, 2, 3]:
self.end_quartal = 'Q1'
if self.end.month in [4, 5, 6]:
self.end_quartal = 'Q2'
if self.end.month in [7, 8, 9]:
self.end_quartal = 'Q3'
if self.end.month in [10, 11, 12]:
self.end_quartal = 'Q4'
super().save()
class Meta:
verbose_name = 'Projekt'
verbose_name_plural = 'Projekte'
def __str__(self): def __str__(self):
return f"{self.pid} {self.name}" return f'{self.pid or self.id} {self.name}'
def save(self, *, using=None, **kwargs):
kwargs['using'] = using
if self.end:
self.end_quartal = f'Q{self.end.month // 4 + 1}'
else:
self.end_quartal = ''
if not self.account:
self.finance_id = ''
self.project_of_year = 0
return super().save(**kwargs)
if self.should_generate_finance_id():
self.generate_finance_id()
super().save(**kwargs)
if not self.pid:
self.pid = f'{self.account.code}{self.id:08d}'
super().save(update_fields=['pid'], using=using)
def should_generate_finance_id(self):
if self.id is None:
return True
if not self.finance_id:
return True
start, account_id = type(self).objects.values_list('start', 'account').get(id=self.id)
return not (self.start.year == start.year and self.account_id == account_id)
def generate_finance_id(self):
"""
This is an improved version of the old code for generating a finance id.
There is still no protection by constraints against duplicate finance ids!
"""
queryset = Project.objects.exclude(id=self.id).filter(start__year=self.start.year)
max_project_of_year = queryset.aggregate(max=models.Max('project_of_year')).get('max') or 0
self.project_of_year = project_of_year = max_project_of_year + 1
if self.account.has_subaccounts:
self.finance_id = f'{self.account.code}-{project_of_year:03d}'
else:
self.finance_id = self.account.code
def clean(self):
if (self.start and self.end) and (self.end < self.start):
raise forms.ValidationError({
'end': [
forms.ValidationError('Das erwartete Projektende muss nach dem Startdatum liegen.'),
],
})
class ProjectRequest(Project):
class Meta:
proxy = True
verbose_name = 'Projekt (beantragt)'
verbose_name_plural = 'Projekte (beantragt)'
class ProjectDeclined(Project):
class Meta:
proxy = True
verbose_name = 'Projekt (abgelehnt)'
verbose_name_plural = 'Projekte (abgelehnt)'
class Intern(Volunteer): class Intern(Volunteer):
'''abstract base class for data entry from /intern (except Project)''' '''abstract base class for data entry from /intern (except Project)'''
request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)') request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen') intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta: class Meta:
abstract = True abstract = True
class ConcreteVolunteer(Volunteer): class ConcreteVolunteer(Volunteer):
''' needed because we can't initiate abstract base classes in the view''' ''' needed because we can't initiate abstract base classes in the view'''
pass pass
class HonoraryCertificate(Intern): class HonoraryCertificate(Intern):
''' this class is also used for accreditations ''' ''' this class is also used for accreditations '''
project = models.ForeignKey(Project, null=True, blank=True, on_delete=models.SET_NULL) project = models.ForeignKey(Project, null=True, blank=True, on_delete=models.SET_NULL)
class Meta:
verbose_name = 'Bescheinigung'
verbose_name_plural = 'Bescheinigungen'
def __str__(self): def __str__(self):
return "Certificate for " + self.realname return f'Bescheinigung für {self.realname}'
TRANSPORT_CHOICES = {'BAHN': 'Bahn', TRANSPORT_CHOICES = {
'BAHN': 'Bahn',
'NONE': 'Keine Fahrtkosten', 'NONE': 'Keine Fahrtkosten',
'OTHER': 'Sonstiges (mit Begründung)'} 'OTHER': 'Sonstiges (mit Begründung)',
}
PAYEDBY_CHOICES = {'WMDE': 'WMDE', PAYEDBY_CHOICES = {
'REQU': 'Antragstellender Mensch'} 'WMDE': 'WMDE',
'REQU': 'Antragstellender Mensch',
}
HOTEL_CHOICES = {'TRUE': format_html('Hotelzimmer benötigt'), HOTEL_CHOICES = {
'FALSE': format_html('Kein Hotelzimmer benötigt') 'TRUE': mark_safe('Hotelzimmer benötigt'),
} 'FALSE': mark_safe('Kein Hotelzimmer benötigt'),
}
from django.contrib.contenttypes.models import ContentType
class Travel(Extern): class Travel(Extern):
# project variable is now null true and blank true, which means it can be saved without project id to be later on filled out by admins # project variable is now null true and blank true, which means it can be saved without project id to be later on filled out by admins
project = models.ForeignKey(Project, on_delete=models.CASCADE, null=True, blank=True) project = models.ForeignKey(Project, on_delete=models.CASCADE, null=True, blank=True)
project_name = models.CharField(max_length=50, null=True, blank=True, verbose_name='Projektname:') project_name = models.CharField(max_length=50, null=True, blank=True, verbose_name='Projektname')
transport = models.CharField(max_length=5, choices=TRANSPORT_CHOICES.items(), default='BAHN', verbose_name='Transportmittel:') transport = models.CharField(max_length=5, choices=TRANSPORT_CHOICES.items(), default='BAHN', verbose_name='Transportmittel')
other_transport = models.CharField(max_length=200, null=True, blank=True, verbose_name='Sonstige Transportmittel (mit Begründung)') other_transport = models.CharField(max_length=200, null=True, blank=True, verbose_name='Sonstige Transportmittel (mit Begründung)')
travelcost = models.CharField(max_length=10, default="0", verbose_name='Fahrtkosten') travelcost = models.CharField(max_length=10, default='0', verbose_name='Fahrtkosten')
checkin = models.DateField(blank=True, null=True, verbose_name='Anreise') checkin = models.DateField(blank=True, null=True, verbose_name='Anreise')
checkout = models.DateField(blank=True, null=True, verbose_name='Abreise') checkout = models.DateField(blank=True, null=True, verbose_name='Abreise')
payed_for_hotel_by = models.CharField(max_length=4, choices=PAYEDBY_CHOICES.items(), blank=True, null=True, verbose_name='Kostenauslage Hotel durch') payed_for_hotel_by = models.CharField(max_length=4, choices=PAYEDBY_CHOICES.items(), blank=True, null=True, verbose_name='Kostenauslage Hotel durch')
payed_for_travel_by = models.CharField(max_length=4, choices=PAYEDBY_CHOICES.items(), blank=True, null=True, verbose_name='Kostenauslage Fahrt durch') payed_for_travel_by = models.CharField(max_length=4, choices=PAYEDBY_CHOICES.items(), blank=True, null=True, verbose_name='Kostenauslage Fahrt durch')
hotel = models.CharField(max_length=10, choices=HOTEL_CHOICES.items(), verbose_name='Hotelzimmer benötigt:') hotel = models.CharField(max_length=10, choices=HOTEL_CHOICES.items(), verbose_name='Hotelzimmer benötigt')
notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen') notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen')
request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)') request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen') intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
project_end = models.DateField(blank=True, null=True, verbose_name='Projektende') project_end = models.DateField(blank=True, null=True, verbose_name='Projektende')
# use content type model to get the end date for the project foreign key # use content type model to get the end date for the project foreign key
project_end_quartal = models.CharField(max_length=15, null=True, blank=True, verbose_name="Quartal Projekt Ende") project_end_quartal = models.CharField(max_length=15, null=True, blank=True, verbose_name='Quartal Projekt Ende')
from django.db.models.signals import pre_save class Meta:
from django.dispatch import receiver verbose_name = 'Reisekosten'
verbose_name_plural = 'Reisekosten'
@receiver(pre_save, sender=Travel, dispatch_uid="get_project_end") def __str__(self):
def getProjectEnd(sender, instance, **kwargs): return f'Reisekosten für {self.realname}'
#instance.project_end = instance.project.end
def clean(self):
if (self.checkin and self.checkout) and (self.checkout < self.checkin):
raise forms.ValidationError({
'checkout': [
forms.ValidationError('Das Datum der Abreise muss nach dem Datum der Anreise liegen.'),
],
})
@receiver(pre_save, sender=Travel, dispatch_uid='get_project_end')
def get_project_end(sender, instance, **kwargs):
if instance.project: if instance.project:
instance.project_end = instance.project.end instance.project_end = instance.project.end
instance.project_end_quartal = instance.project.end_quartal instance.project_end_quartal = instance.project.end_quartal
# using pre save instead
# def save(self,*args,**kwargs):
# '''we generate the autogenerated fields here'''
# # we don't call save with args/kwargs to avoid UNIQUE CONSTRAINT errors
# # but maybe there is a better solution?
# intern_notes
# project_end = self.checkout
# super(Travel, self).save(*args,**kwargs)
#abstract base class for Library and IFG # abstract base class for Library and IFG
class Grant(Extern): class Grant(RequestUrlMixin, Extern):
cost = models.CharField(max_length=10, verbose_name='Kosten', cost = models.CharField(max_length=10, verbose_name='Kosten',
help_text="Bitte gib die ungefähr zu erwartenden Kosten in Euro an.") help_text='Bitte gib die ungefähr zu erwartenden Kosten in Euro an.')
notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen', notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen',
help_text="Bitte gib an wofür Du das Stipendium verwenden willst.") help_text='Bitte gib an, wofür Du das Stipendium verwenden willst.')
class Meta: class Meta:
abstract = True abstract = True
TYPE_CHOICES = {'BIB': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#Bibliotheksstipendium" target="_blank" rel="noopener">Bibliotheksstipendium</a>'), def type_link(path, label):
'ELIT': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#eLiteraturstipendium" target="_blank" rel="noopener">eLiteraturstipendium</a>'), return format_html(
'MAIL': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/E-Mail-Adressen_und_Visitenkarten#E-Mail-Adressen" target="_blank" rel="noopener">E-Mail-Adresse</a>'), format_string='<a href="{href}" target="_blank" rel="noopener">{label}</a>',
'IFG': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Gebührenerstattungen_für_Behördenanfragen" target="_blank" rel="noopener">Kostenübernahme IFG-Anfrage</a>'), href=f'https://de.wikipedia.org/wiki/Wikipedia:Förderung/{path}',
'LIT': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#Literaturstipendium" target="_blank" rel="noopener">Literaturstipendium</a>'), label=label,
'LIST': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/E-Mail-Adressen_und_Visitenkarten#Mailinglisten" target="_blank" rel="noopener">Mailingliste</a>'), )
'TRAV': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/Reisekostenerstattungen" target="_blank" rel="noopener">Reisekosten</a>'),
'SOFT': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Software-Stipendien" target="_blank" rel="noopener">Softwarestipendium</a>'),
'VIS': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/E-Mail-Adressen_und_Visitenkarten#Visitenkarten" target="_blank" rel="noopener">Visitenkarten</a>'), TYPE_CHOICES = {
} TYPE_BIB: type_link('Zugang_zu_Fachliteratur#Bibliotheksstipendium', 'Bibliotheksstipendium'),
TYPE_ELIT: type_link('Zugang_zu_Fachliteratur#eLiteraturstipendium', 'eLiteraturstipendium'),
TYPE_MAIL: type_link('E-Mail-Adressen_und_Visitenkarten#E-Mail-Adressen', 'E-Mail-Adresse'),
TYPE_IFG: type_link('Gebührenerstattungen_für_Behördenanfragen', 'Kostenübernahme IFG-Anfrage'),
TYPE_LIT: type_link('Zugang_zu_Fachliteratur#Literaturstipendium', 'Literaturstipendium'),
TYPE_LIST: type_link('E-Mail-Adressen_und_Visitenkarten#Mailinglisten', 'Mailingliste'),
TYPE_TRAV: type_link('Reisekostenerstattungen', 'Reisekosten'),
TYPE_SOFT: type_link('Software-Stipendien', 'Softwarestipendium'),
TYPE_VIS: type_link('E-Mail-Adressen_und_Visitenkarten#Visitenkarten', 'Visitenkarten'),
TYPE_PROJ: type_link('Projektplanung', 'Projektförderung unter 1000 EUR'),
}
LIBRARY_TYPES = TYPE_BIB, TYPE_ELIT, TYPE_SOFT
LIBRARY_TYPE_CHOICES = [(choice, TYPE_CHOICES[choice]) for choice in LIBRARY_TYPES]
# same model is used for Library, ELitStip and Software! # same model is used for Library, ELitStip and Software!
class Library(Grant): class Library(Grant):
TYPE = TYPE_BIB
LIBRARY_LABEL = 'Bibliothek'
LIBRARY_HELP_TEXT = 'Für welche Bibliothek gilt das Stipendium?'
DURATION_HELP_TEXT = mark_safe('In welchem Zeitraum möchtest du recherchieren oder<br>wie lange ist der Bibliotheksausweis gültig?')
type = models.CharField( type = models.CharField(max_length=4, choices=LIBRARY_TYPE_CHOICES, default=TYPE_BIB)
max_length=4,
choices=TYPE_CHOICES.items(), #attention: actually only BIB, ELIT, SOFT should be used here
default='BIB',
)
library = models.CharField(max_length=200) library = models.CharField(max_length=200)
duration = models.CharField(max_length=100, verbose_name="Dauer") duration = models.CharField(max_length=100, verbose_name='Dauer')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Bibliotheksstipendium'
verbose_name_plural = 'Bibliotheksstipendien'
def __str__(self): def __str__(self):
return self.library return self.library
def save(self, **kwargs):
self.type = self.TYPE
SELFBUY_CHOICES = {'TRUE': format_html('Ich möchte das Werk selbst kaufen und per Kostenerstattung bei Wikimedia Deutschland abrechnen.'), return super().save(**kwargs)
'FALSE': format_html('Ich möchte, dass Wikimedia Deutschland das Werk für mich kauft'),
}
class Literature(Grant): class ELiterature(Library):
TYPE = TYPE_ELIT
LIBRARY_LABEL = 'Datenbank/Online-Ressource'
LIBRARY_HELP_TEXT = 'Für welche Datenbank/Online-Ressource gilt das Stipendium?'
DURATION_HELP_TEXT = 'Wie lange gilt der Zugang?'
class Meta:
proxy = True
verbose_name = 'eLiteraturstipendium'
verbose_name_plural = 'eLiteraturstipendien'
class Software(Library):
TYPE = TYPE_SOFT
LIBRARY_LABEL = 'Software'
LIBRARY_HELP_TEXT = 'Für welche Software gilt das Stipendium?'
DURATION_HELP_TEXT = 'Wie lange gilt die Lizenz?'
class Meta:
proxy = True
verbose_name = 'Softwarestipendium'
verbose_name_plural = 'Softwarestipendien'
SELFBUY_CHOICES = {
'TRUE': mark_safe('Ich möchte das Werk selbst kaufen und per Kostenerstattung bei Wikimedia Deutschland abrechnen.'),
'FALSE': mark_safe('Ich möchte, dass Wikimedia Deutschland das Werk für mich kauft'),
}
class Literature(TermsConsentMixin, Grant):
info = models.CharField(max_length=500, verbose_name='Informationen zum Werk', info = models.CharField(max_length=500, verbose_name='Informationen zum Werk',
help_text=format_html("Bitte gib alle Informationen zum benötigten Werk an,<br>\ help_text=mark_safe('Bitte gib alle Informationen zum benötigten Werk an,<br>\
die eine eindeutige Identifizierung ermöglichen (Autor, Titel, Verlag, ISBN, ...)")) die eine eindeutige Identifizierung ermöglichen (Autor, Titel, Verlag, ISBN, ...)'))
source = models.CharField(max_length=200, verbose_name='Bezugsquelle', source = models.CharField(max_length=200, verbose_name='Bezugsquelle',
help_text="Bitte gib an, wo du das Werk kaufen möchtest.") help_text='Bitte gib an, wo du das Werk kaufen möchtest.')
selfbuy = models.CharField( max_length=10, verbose_name='Selbstkauf?', choices=SELFBUY_CHOICES.items(), default='TRUE') selfbuy = models.CharField( max_length=10, verbose_name='Selbstkauf?', choices=SELFBUY_CHOICES.items(), default='TRUE')
selfbuy_give_data = models.BooleanField(verbose_name=format_html('Datenweitergabe erlauben'), help_text=format_html('Ich stimme der Weitergabe meiner Daten (Name, Postadresse) an den von mir angegebenen Anbieter/Dienstleister zu.')) selfbuy_give_data = models.BooleanField(verbose_name=mark_safe('Datenweitergabe erlauben'), help_text=mark_safe('Ich stimme der Weitergabe meiner Daten (Name, Postadresse) an den von mir angegebenen Anbieter/Dienstleister zu.'))
selfbuy_data = models.TextField(max_length=1000, verbose_name='Persönliche Daten sowie Adresse', default='',\ selfbuy_data = models.TextField(max_length=1000, verbose_name='Persönliche Daten sowie Adresse', default='',\
help_text=format_html("Bitte gib hier alle persönlichen Daten an, die wir benötigen, um das Werk<br>\ help_text=mark_safe('Bitte gib hier alle persönlichen Daten an, die wir benötigen, um das Werk<br>\
für dich zu kaufen und es dir anschließend zu schicken (z.B. Vorname Nachname, Anschrift, <br>\ für dich zu kaufen und es dir anschließend zu schicken (z.B. Vorname Nachname, Anschrift, <br>\
Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.")) Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.'))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Literaturstipendium'
verbose_name_plural = 'Literaturstipendien'
class IFG(Grant): class IFG(Grant):
url = models.URLField(max_length=2000, verbose_name="URL", url = models.URLField(max_length=2000, verbose_name='URL',
help_text="Bitte gib den Link zu deiner Anfrage bei Frag den Staat an.") help_text='Bitte gib den Link zu deiner Anfrage bei Frag den Staat an.')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'IFG-Anfrage'
verbose_name_plural = 'IFG-Anfragen'
def __str__(self): def __str__(self):
return "IFG-Anfrage von " + self.realname return f'IFG-Anfrage von {self.realname}'
DOMAIN_CHOICES = {'PEDIA': '@wikipedia.de',
DOMAIN_CHOICES = {
'PEDIA': '@wikipedia.de',
'BOOKS': '@wikibooks.de', 'BOOKS': '@wikibooks.de',
'QUOTE': '@wikiquote.de', 'QUOTE': '@wikiquote.de',
'SOURCE': '@wikisource.de', 'SOURCE': '@wikisource.de',
'VERSITY': '@wikiversity.de',} 'VERSITY': '@wikiversity.de',
}
class Domain(Extern):
class Domain(RequestUrlMixin, Extern):
domain = models.CharField(max_length=10, domain = models.CharField(max_length=10,
choices=DOMAIN_CHOICES.items(), choices=DOMAIN_CHOICES.items(),
default='PEDIA') default='PEDIA')
@ -321,31 +560,47 @@ class Domain(Extern):
class Meta: class Meta:
abstract = True abstract = True
MAIL_CHOICES = {'REALNAME': 'Vorname.Nachname',
MAIL_CHOICES = {
'REALNAME': 'Vorname.Nachname',
'USERNAME': 'Username', 'USERNAME': 'Username',
'OTHER': 'Sonstiges:'} 'OTHER': 'Sonstiges',
}
ADULT_CHOICES = {'TRUE': format_html('Ich bin volljährig.'), ADULT_CHOICES = {
'FALSE': format_html('Ich bin noch nicht volljährig.') 'TRUE': mark_safe('Ich bin volljährig.'),
} 'FALSE': mark_safe('Ich bin noch nicht volljährig.'),
}
class Email(Domain):
class Email(TermsConsentMixin, Domain):
address = models.CharField(max_length=50, address = models.CharField(max_length=50,
choices=MAIL_CHOICES.items(), choices=MAIL_CHOICES.items(),
default='USERNAME', verbose_name='Adressbestandteil', default='USERNAME', verbose_name='Adressbestandteil',
help_text=format_html("Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.")) help_text=mark_safe('Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.'))
other = models.CharField(max_length=50,blank=True,null=True, verbose_name="Sonstiges") other = models.CharField(max_length=50, blank=True, null=True, verbose_name='Sonstiges')
adult = models.CharField( max_length=10, verbose_name='Volljährigkeit', choices=ADULT_CHOICES.items(), default='FALSE') adult = models.CharField(max_length=10, verbose_name='Volljährigkeit', choices=ADULT_CHOICES.items(), default='FALSE')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class List(Domain): class Meta:
verbose_name = 'E-Mail-Adresse'
verbose_name_plural = 'E-Mail-Adressen'
class List(TermsConsentMixin, Domain):
address = models.CharField(max_length=50, default='NO_ADDRESS', address = models.CharField(max_length=50, default='NO_ADDRESS',
verbose_name="Adressbestandteil für Projektmailingliste", verbose_name='Adressbestandteil für Projektmailingliste',
help_text=format_html("Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.")) help_text=mark_safe('Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.'))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
PROJECT_CHOICE = {'PEDIA': 'Wikipedia', class Meta:
verbose_name = 'Mailingliste'
verbose_name_plural = 'Mailinglisten'
PROJECT_CHOICE = {
'PEDIA': 'Wikipedia',
'SOURCE': 'Wikisource', 'SOURCE': 'Wikisource',
'BOOKS': 'Wikibooks', 'BOOKS': 'Wikibooks',
'QUOTE': 'Wikiquote', 'QUOTE': 'Wikiquote',
@ -353,32 +608,53 @@ PROJECT_CHOICE = {'PEDIA': 'Wikipedia',
'VOYAGE': 'Wikivoyage', 'VOYAGE': 'Wikivoyage',
'DATA': 'Wikidata', 'DATA': 'Wikidata',
'NEWS': 'Wikinews', 'NEWS': 'Wikinews',
'COMMONS': 'Wikimedia Commons'} 'COMMONS': 'Wikimedia Commons',
}
BC_VARIANT = {'PIC': 'mit Bild', BC_VARIANT = {
'NOPIC': 'ohne Bild'} 'PIC': 'mit Bild',
'NOPIC': 'ohne Bild',
}
class BusinessCard(Extern):
class BusinessCard(RequestUrlMixin, TermsConsentMixin, Extern):
project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(), project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(),
default='PEDIA', verbose_name='Wikimedia-Projekt', default='PEDIA', verbose_name='Wikimedia-Projekt',
help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?') help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?')
data = models.TextField(max_length=1000, verbose_name='Persönliche Daten für die Visitenkarten', default='', data = models.TextField(max_length=1000, verbose_name='Persönliche Daten für die Visitenkarten', default='',
help_text=format_html("Bitte gib hier alle persönlichen Daten an, und zwar genau so,<br>\ help_text=mark_safe('Bitte gib hier alle persönlichen Daten an, und zwar genau so,<br>\
wie sie (auch in der entsprechenden Reihenfolge) auf den Visitenkarten stehen sollen<br>\ wie sie (auch in der entsprechenden Reihenfolge) auf den Visitenkarten stehen sollen<br>\
(z.B. Vorname Nachname, Benutzer:/Benutzerin:, Benutzer-/-innenname, Anschrift,<br>\ (z.B. Vorname Nachname, Benutzer:/Benutzerin:, Benutzer-/-innenname, Anschrift,<br>\
Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.<br>\ Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.<br>\
Hinweis: Telefonnummern bilden wir üblicherweise im internationalen Format gemäß<br>\ Hinweis: Telefonnummern bilden wir üblicherweise im internationalen Format gemäß<br>\
DIN 5008 ab. Als anzugebende E-Mail-Adresse empfehlen wir dir eine Wikimedia-Projekt-<br>\ DIN 5008 ab. Als anzugebende E-Mail-Adresse empfehlen wir dir eine Wikimedia-Projekt-<br>\
Adresse, die du ebenfalls beantragen kannst, sofern du nicht bereits eine besitzt.")) Adresse, die du ebenfalls beantragen kannst, sofern du nicht bereits eine besitzt.'))
variant = models.CharField(max_length=5, choices=BC_VARIANT.items(), variant = models.CharField(max_length=5, choices=BC_VARIANT.items(),
default='NOPIC', verbose_name='Variante', default='NOPIC', verbose_name='Variante',
help_text=format_html('so sehen die Varianten aus: <a href="https://upload.wikimedia.org/wikipedia/commons/c/cd/Muster_Visitenkarten_WMDE_2018.jpg">\ help_text=mark_safe('so sehen die Varianten aus: <a href="https://upload.wikimedia.org/wikipedia/commons/c/cd/Muster_Visitenkarten_WMDE_2018.jpg">\
mit Bild</a> <a href="https://upload.wikimedia.org/wikipedia/commons/d/d3/Muster_Visitenkarte_WMDE.png">ohne Bild</a>' )) mit Bild</a> <a href="https://upload.wikimedia.org/wikipedia/commons/d/d3/Muster_Visitenkarte_WMDE.png">ohne Bild</a>'))
url_of_pic = models.CharField(max_length=200, verbose_name='Url des Bildes', default='', help_text="Bitte gib die Wikimedia-Commons-URL des Bildes an.") url_of_pic = models.CharField(max_length=200, verbose_name='Url des Bildes', default='', help_text='Bitte gib die Wikimedia-Commons-URL des Bildes an.')
sent_to = models.TextField(max_length=1000, verbose_name='Versandadresse', sent_to = models.TextField(max_length=1000, verbose_name='Versandadresse',
default='', help_text="Bitte gib den Namen und die vollständige Adresse ein, an welche die Visitenkarten geschickt werden sollen.") default='', help_text='Bitte gib den Namen und die vollständige Adresse ein, an welche die Visitenkarten geschickt werden sollen.')
send_data_to_print = models.BooleanField(default=False, verbose_name=format_html('Datenweitergabe erlauben'), help_text=format_html('Hiermit erlaube ich die Weitergabe meiner Daten (Name, Postadresse) an den von Wikimedia<br> Deutschland ausgewählten Dienstleister (z. B. <a href="wir-machen-druck.de">wir-machen-druck.de</a>) zum Zwecke des direkten <br> Versands der Druckerzeugnisse an mich.')) send_data_to_print = models.BooleanField(default=False, verbose_name=mark_safe('Datenweitergabe erlauben'), help_text=mark_safe('Hiermit erlaube ich die Weitergabe meiner Daten (Name, Postadresse) an den von Wikimedia<br> Deutschland ausgewählten Dienstleister (z. B. <a href="wir-machen-druck.de">wir-machen-druck.de</a>) zum Zwecke des direkten <br> Versands der Druckerzeugnisse an mich.'))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Visitenkarte'
verbose_name_plural = 'Visitenkarten'
MODELS = {
TYPE_BIB: Library,
TYPE_ELIT: ELiterature,
TYPE_MAIL: Email,
TYPE_IFG: IFG,
TYPE_LIT: Literature,
TYPE_LIST: List,
TYPE_TRAV: Travel,
TYPE_SOFT: Software,
TYPE_VIS: BusinessCard,
}

View File

@ -1,78 +0,0 @@
# mail for IF-OTRS
IF_EMAIL = 'community@wikimedia.de'
#IF_EMAIL = 'test-luca-ext@wikimedia.de'
#SURVEY_EMAIL = 'christof.pins@wikimedia.de'
#SURVEY_EMAIL = 'luca.wulf@cannabinieri.de'
SURVEY_EMAIL = 'sandro.halank@wikimedia.de'
# prefix for urls
SURVEYPREFIX = 'https://wikimedia.sslsurvey.de/Foerderbarometer/?'
# some links
DATAPROTECTION = "https://www.wikimedia.de/datenschutz/#datenerfassung"
#FOERDERRICHTLINIEN = "https://de.wikipedia.org/wiki/Wikipedia:Wikimedia_Deutschland/F%C3%B6rderrichtlinien"
FOERDERRICHTLINIEN = "https://de.wikipedia.org/wiki/Wikipedia:Wikimedia_Deutschland/Richtlinie_zur_Förderung_der_Communitys"
NUTZUNGSBEDINGUNGEN = 'static/input/nutzungsbedingungen.html'
NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE = 'static/input/nutzungsbedingungen-mail.pdf'
NUTZUNGSBEDINGUNGEN_MAILINGLISTEN = 'static/input/nutzungsbedingungen-mailinglisten.pdf'
NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM = 'static/input/nutzungsbedingungen-literaturstipendium.pdf'
NUTZUNGSBEDINGUNGEN_OTRS = 'static/input/2025_Nutzungsvereinbarung_OTRS.docx.pdf'
NUTZUNGSBEDINGUNGEN_VISITENKARTEN = 'static/input/nutzungsbedingungen-visitenkarten.pdf'
LANGUAGE_CODE = 'de'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
ACCOUNTS ={ # "21103": '21103 Willkommen',
"DEF": 'DEFAULT VALUE, you hould not see this!',
"21111": '21111 Förderung (reaktiv)',
"21112": '21112 WikiCon',
# "21113": '21113 Wikimania/Unterstützung Ehrenamtliche',
"21115": '21115 Lokale Räume, Berlin',
"21116": '21116 Lokale Räume, Hamburg',
"21117": '21117 Lokale Räume, Hannover',
"21118": '21118 Lokale Räume, Köln',
"21119": '21119 Lokale Räume, München',
"21120": '21120 Lokale Räume, Fürth',
"21125": '21125 Lokale Räume, allgemein',
"21130": '21130 GLAM-Förderung',
"21131": '21131 Initiative Förderung',
# "21134": '21134 Größe',
# "21137": '21137 Beitragen',
# "21138": '21138 Vermittlung',
"21140": '21140 Wikipedia-Kampagne',
"21141": '21141 Wikipedia-Onboarding',
"21150": '21150 Fürsorge und Online-Kommunikationskultur',}
# teken from working oauth prototype as additional settings
WSGI_APPLICATION = 'oauth_demo.wsgi.application'
# OAuth Settings
OAUTH_URL_WHITELISTS = []
OAUTH_CLIENT_NAME = '<name-of-the-configured-wikimedia-app>'
OAUTH_CLIENT = {
'client_id': '<client-application-key-of-wikimedia-app>',
'client_secret': '<client-application-secret-of-wikimedia-app>',
'access_token_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/access_token',
'authorize_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/authorize',
'api_base_url': 'https://meta.wikimedia.org/w/rest.php/oauth2/resource',
'redirect_uri': 'http://localhost:8000/oauth/callback',
'client_kwargs': {
'scope': 'basic',
'token_placement': 'header'
},
'userinfo_endpoint': 'resource/profile',
}
OAUTH_COOKIE_SESSION_ID = 'sso_session_id'

View File

@ -1,41 +0,0 @@
# mail for IF-OTRS
IF_EMAIL = 'community@wikimedia.de'
#IF_EMAIL = 'test-luca-ext@wikimedia.de'
#SURVEY_EMAIL = 'christof.pins@wikimedia.de'
#SURVEY_EMAIL = 'luca.wulf@cannabinieri.de'
SURVEY_EMAIL = 'sandro.halank@wikimedia.de'
# prefix for urls
SURVEYPREFIX = 'https://wikimedia.sslsurvey.de/Foerderbarometer/?'
# some links
DATAPROTECTION = "https://www.wikimedia.de/datenschutz/#datenerfassung"
FOERDERRICHTLINIEN = "https://de.wikipedia.org/wiki/Wikipedia:Wikimedia_Deutschland/F%C3%B6rderrichtlinien"
NUTZUNGSBEDINGUNGEN = 'static/input/nutzungsbedingungen.html'
LANGUAGE_CODE = 'de'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
ACCOUNTS ={ # "21103": '21103 Willkommen',
"DEF": 'DEFAULT VALUE, you hould not see this!',
"21111": '21111 Förderung (reaktiv)',
"21112": '21112 WikiCon',
# "21113": '21113 Wikimania/Unterstützung Ehrenamtliche',
"21115": '21115 Lokale Räume, Berlin',
"21116": '21116 Lokale Räume, Hamburg',
"21117": '21117 Lokale Räume, Hannover',
"21118": '21118 Lokale Räume, Köln',
"21119": '21119 Lokale Räume, München',
"21120": '21120 Lokale Räume, Fürth',
"21125": '21125 Lokale Räume, allgemein',
"21130": '21130 GLAM-Förderung',
"21131": '21131 Initiative Förderung',
# "21134": '21134 Größe',
# "21137": '21137 Beitragen',
# "21138": '21138 Vermittlung',
"21140": '21140 Wikipedia-Kampagne',
"21141": '21141 Wikipedia-Onboarding',
"21150": '21150 Fürsorge und Online-Kommunikationskultur',}

24
input/static/css/base.css Normal file
View File

@ -0,0 +1,24 @@
ul > li {
list-style-type: none;
}
ul {
padding-left: 10px;
}
label.required::after {
content: ' *';
color: red;
}
.spacer-15 {
height: 15%;
}
.spacer-5 {
height: 5%;
}
.page-centered {
text-align: center;
}

View File

@ -0,0 +1,23 @@
.page-centered .button-login {
width: 40vw;
height: 6vh;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: 4vh;
margin: 0 auto;
background-color: #79AEC8;
color: #000000;
text-decoration: none;
padding: 10px 15px;
}
.page-centered .button-login:hover {
background-color: #659DB8;
}
.page-centered .button-login:focus-visible {
outline: 2px solid #000;
outline-offset: 2px;
}

View File

@ -1,10 +1,3 @@
/*
span.datetimeshortcuts > a:first-child {
visibility: hidden;
}
*/
span.datetimeshortcuts { span.datetimeshortcuts {
visibility: hidden; visibility: hidden;
} }
@ -12,3 +5,11 @@ span.datetimeshortcuts {
span.datetimeshortcuts > a:nth-child(2) { span.datetimeshortcuts > a:nth-child(2) {
visibility: visible; visibility: visible;
} }
span.datetimeshortcuts > a:first-child {
display: none;
}
.calendarbox .calendar-shortcuts {
display: none;
}

View File

@ -0,0 +1,21 @@
.star {
color: red;
}
.wm-table {
margin: 0 auto;
}
.wm-table.start {
td, th {
border: 0;
}
.applications {
text-align: left;
}
}
.col-request {
width: 40%;
}

View File

@ -0,0 +1,3 @@
.related-widget-wrapper div label {
width: auto;
}

View File

@ -0,0 +1,32 @@
(function ($) {
$(function () {
$('#id_categories, #id_wikimedia_projects').each(function () {
const otherCheckbox = $(this).find('input[value=0]');
const otherInputSelector = '#'.concat(this.id, '_other');
const otherInput = $(otherInputSelector);
const otherLabelSelector = 'label'.concat('[for="', this.id, '_other"]');
const otherLabel = $(otherLabelSelector);
const otherTableRow = otherInput.parents('tr');
const toggle = function () {
const checked = otherCheckbox.prop('checked');
otherInput.prop('disabled', !checked);
otherInput.prop('required', checked);
otherLabel.toggleClass('required', checked);
otherLabel.css('opacity', checked ? 1 : 0.3);
otherTableRow.css('visibility', checked ? 'visible' : 'collapse');
if (checked) {
otherInput.focus();
} else {
otherInput.val('');
}
};
toggle();
otherCheckbox.on('change', toggle);
});
});
})(django.jQuery);

View File

@ -0,0 +1,37 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<!-- Admin i18n -->
<script src="{% url 'jsi18n' %}"></script>
<!-- Admin Assets -->
<script src="{% static 'admin/js/core.js' %}"></script>
<script src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script>
<script src="{% static 'admin/js/jquery.init.js' %}"></script>
<link rel="stylesheet" href="{% static 'admin/css/base.css' %}">
<link rel="stylesheet" href="{% static 'admin/css/widgets.css' %}">
<!-- Project Styles -->
<link rel="stylesheet" type="text/css" href="{% static 'css/forms.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}">
{% block head_extra %}{% endblock %}
</head>
<body>
{% include "input/partials/_header.html" %}
<main class="wm-main">
{% block pre_content %}{% endblock %}
{% block content %}{% endblock %}
{% block post_content %}{% endblock %}
</main>
{% include "input/partials/_footer.html" %}
</body>
</html>

View File

@ -1,71 +0,0 @@
{% load static %}
<script type="text/javascript" src="/admin/jsi18n/"></script>
<script type="text/javascript" src="{% static 'admin/js/core.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/jquery.init.js' %}"></script>
{{ form.media }}
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/base.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/widgets.css' %}" />
{% load i18n %}
{% block content %}
<center>
<style>
ul > li {
list-style-type: none;
}
ul {
padding-left: 10;
}
label.required::after {
content: ' *';
color: red;
}
</style>
<img src="{% static 'input/logo.png' %}" />
<p>Schritt {{ wizard.steps.step1 }} von {{ wizard.steps.count }}</p>
<form action="" method="post">
{% csrf_token %}
<table>
{% if choice %}
Du hast {{choice}} ausgewählt.
{% endif %}
{{ wizard.management_form }}
{% if wizard.form.forms %}
{{ wizard.form.management_form }}
{% for form in wizard.form.forms %}
{{ form }}
{% endfor %}
{% else %}
{{ wizard.form }}
{% endif %}
</table>
<p>
<span style="color: red">*</span> Pflichtfeld
<p>
{% if wizard.steps.prev %}
<button formnovalidate="formnovalidate" name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}">Zurück</button>
{% endif %}
{% if wizard.steps.current == wizard.steps.last %}
<button type="submit" value="{% trans "Weiter" %}">Absenden</button>
{% else %}
<button type="submit" value="{% trans "Weiter" %}">Weiter</button>
{% endif %}
</form>
<p>
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c4/Figuren_klein.jpg"><p>
Eine Übersicht aller Förderangebote von Wikimedia Deutschland findest du im <a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Förderangebote">
Förderportal in der deutschsprachigen Wikipedia</a>.
<br>Für alle Fragen wende dich gern an das <a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Wikimedia_Deutschland">Team Communitys und Engagement</a>.
<p>
Für interessierte Hacker gibts auch den <a href="https://srcsrv.wikimedia.de/beba/foerderbarometer">Sourcecode</a> zum Formular und was damit passiert.
<p>
<a href="https://www.wikimedia.de/impressum/">Impressum</a>
</center>{% endblock %}

View File

@ -0,0 +1,44 @@
{% extends 'input/base.html' %}
{% load i18n %}
{% block head_extra %}
<title>Was möchtest du beantragen?</title>
{% endblock %}
{% block content %}
<form class="page-centered" method="post">
{% csrf_token %}
<table class="wm-table start">
<tbody class="applications">
<tr>
<th class="col-request">Was möchtest du beantragen?</th>
<td>
{% for title, services in applications %}
<strong>{{ title }}</strong>
<ul>
{% for service in services %}
<li>
<label>
<input type="radio" name="url" value="{% url 'extern' type=service.path %}" />
<span>{{ service.label|striptags }}</span>
</label>
<span>(<a href="{{ service.url }}" target="_blank">mehr erfahren</a>)</span>
</li>
{% endfor %}
</ul>
{% endfor %}
</td>
</tr>
</tbody>
<tbody>
<tr>
<td colspan="2">
<button type="submit">Beantragen</button>
</td>
</tr>
</tbody>
</table>
</form>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends "input/base.html" %}
{% load static %}
{% block head_extra %}
<title>{{ type_label|striptags }}</title>
{% endblock %}
{% block content %}
{{ form.media }}
<div class="page-centered">
<p>Du hast {{ type_label }} ausgewählt.</p>
</div>
<form method="post" class="wm-form" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
{% csrf_token %}
{% block pre_table %}{% endblock %}
<table class="wm-table">
{{ form.non_field_errors }}
{{ form.as_table }}
</table>
{% block post_table %}{% endblock %}
<p class="page-centered"><span class="star">*</span> Pflichtfeld</p>
<div class="page-centered">
<button type="button" onclick="history.back()">Zurück</button>
<button type="submit">Absenden</button>
</div>
</form>
{% endblock %}

View File

@ -1,58 +0,0 @@
<html>
<body>
Hallo Team Communitys und Engagement,
<br><br>
es gab einen neuen Antrag von {{data.realname}}.
<br><br>
Der Nutzer mit dem Username {{data.username}} ({{data.email}}) fragt ein_e {{data.typestring|striptags}} an.<br>
{% if data.choice in data.grant %}<br>
Vorraussichtliche Kosten: {{data.cost}}<br>
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br>
Domain: <a href="{{data.domain}}">{{data.domain}}</a><br>
Adressenbestandteil: {{data.address}} <br> {% endif %} {% if data.choice == 'BIB' %}
Bibliothek: {{data.library}}<br>
Dauer: {{data.duration}} <br> {% elif data.choice == 'ELIT' %}
Datenbank: {{data.library}}<br>
Dauer: {{data.duration}} <br> {% elif data.choice == 'SOFT' %}
Software: {{data.library}}<br>
Dauer: {{data.duration}} <br> {% elif data.choice == 'IFG'%}
Anfrage-URL: <a href="{{data.url}}">{{data.url}}</a> <br> {% elif data.choice == 'LIT'%}
Info zum Werk: {{data.info}}<br>
Bezugsquelle: {{data.source}} <br> {% elif data.choice == 'MAIL'%}
Adressenbestandteil frei gewählt: {{data.other}} <br> {% elif data.choice == 'VIS'%}
Wikimedia-Projekt: {{data.project}}<br>
Persönliche Daten: {{data.data}}<br>
Variante: {{data.variant}}<br>
Sendungsadrese: {{data.send_to}} <br> {% endif %}
<br><br>
Zum Eintrag in der Förderdatenbank:
{% if data.choice == 'BIB' %}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'ELIT'%}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'LIT'%}
<a href="{{data.urlprefix}}/admin/input/literature/{{data.pk}}/change">{{data.urlprefix}}/admin/input/literature/{{data.pk}}/change</a>
{% elif data.choice == 'MAIL'%}
<a href="{{data.urlprefix}}/admin/input/email/{{data.pk}}/change">{{data.urlprefix}}/admin/input/email/{{data.pk}}/change</a>
{% elif data.choice == 'IFG'%}
<a href="{{data.urlprefix}}/admin/input/ifg/{{data.pk}}/change">{{data.urlprefix}}/admin/input/ifg/{{data.pk}}/change</a>
{% elif data.choice == 'LIST'%}
<a href="{{data.urlprefix}}/admin/input/list/{{data.pk}}/change">{{data.urlprefix}}/admin/input/list/{{data.pk}}/change</a>
{% elif data.choice == 'TRAV'%}
<a href="{{data.urlprefix}}/admin/input/travel/{{data.pk}}/change">{{data.urlprefix}}/admin/input/travel/{{data.pk}}/change</a>
{% elif data.choice == 'SOFT'%}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'VIS'%}
<a href="{{data.urlprefix}}/admin/input/businesscard/{{data.pk}}/change">{{data.urlprefix}}/admin/input/businesscard/{{data.pk}}/change</a>
{% endif %}
<br><br>
Zum Genehmigen hier klicken: <a href="{{data.urlprefix}}{% url 'authorize' data.choice data.pk %}">{{data.urlprefix}}{% url 'authorize' data.choice data.pk %}</a>
<br><br>
Zu Ablehnen hier klicken: <a href="{{data.urlprefix}}{% url 'deny' data.choice data.pk %}">{{data.urlprefix}}{% url 'deny' data.choice data.pk %}</a>
<br><br>
Stets zu Diensten, Deine Förderdatenbank
</body>
</html>

View File

@ -1,52 +0,0 @@
Hallo Team Communitys und Engagement,
es gab einen neuen Antrag von {{data.realname}}.
Der Nutzer mit dem Username {{data.username}} ({{data.email}}) fragt ein_e {{data.typestring|striptags}} an.
{% if data.choice in data.grant %}
Vorraussichtliche Kosten: {{data.cost}}
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
Domain: {{data.domain}}
Adressenbestandteil: {{data.address}} {% endif %} {% if data.choice == 'BIB' %}
Bibliothek: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'ELIT' %}
Datenbank: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'SOFT' %}
Software: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'IFG'%}
Anfrage-URL: {{data.url}} {% elif data.choice == 'LIT'%}
Info zum Werk: {{data.info}}
Bezugsquelle: {{data.source}} {% elif data.choice == 'MAIL'%}
Adressenbestandteil frei gewählt: {{data.other}} {% elif data.choice == 'VIS'%}
Wikimedia-Projekt: {{data.project}}
Persönliche Daten: {{data.data}}
Variante: {{data.variant}}
Sendungsadrese: {{data.send_to}} {% endif %}
Zum Eintrag in der Förderdatenbank:
{% if data.choice == 'BIB' %}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'ELIT'%}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'LIT'%}
<a href="{{data.urlprefix}}/admin/input/literature/{{data.pk}}/change">{{data.urlprefix}}/admin/input/literature/{{data.pk}}/change</a>
{% elif data.choice == 'MAIL'%}
<a href="{{data.urlprefix}}/admin/input/email/{{data.pk}}/change">{{data.urlprefix}}/admin/input/email/{{data.pk}}/change</a>
{% elif data.choice == 'IFG'%}
<a href="{{data.urlprefix}}/admin/input/ifg/{{data.pk}}/change">{{data.urlprefix}}/admin/input/ifg/{{data.pk}}/change</a>
{% elif data.choice == 'LIST'%}
<a href="{{data.urlprefix}}/admin/input/list/{{data.pk}}/change">{{data.urlprefix}}/admin/input/list/{{data.pk}}/change</a>
{% elif data.choice == 'TRAV'%}
<a href="{{data.urlprefix}}/admin/input/travel/{{data.pk}}/change">{{data.urlprefix}}/admin/input/travel/{{data.pk}}/change</a>
{% elif data.choice == 'SOFT'%}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'VIS'%}
<a href="{{data.urlprefix}}/admin/input/businesscard/{{data.pk}}/change">{{data.urlprefix}}/admin/input/businesscard/{{data.pk}}/change</a>
{% endif %}
Zum Genehmigen hier klicken: {{data.urlprefix}}{% url 'authorize' data.choice data.pk %}
Zu Ablehnen hier klicken: {{data.urlprefix}}{% url 'deny' data.choice data.pk %}
Stets zu Diensten, Deine Förderdatenbank

View File

@ -1,34 +0,0 @@
<html>
<body>
Hallo {{data.realname}},
<br><br>
wir haben Deine Anfrage ({{data.typestring|striptags}}) erhalten.<br>
{% if data.choice in data.grant %}<br>
Vorraussichtliche Kosten: {{data.cost}}<br>
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br>
Domain: <a href="{{data.domain}}">{{data.domain}}</a><br>
Adressenbestandteil: {{data.address}} {% endif %} {% if data.choice == 'BIB' %}<br>
Bibliothek: {{data.library}}<br>
Dauer: {{data.duration}} {% elif data.choice == 'ELIT' %}<br>
Datenbank: {{data.library}}<br>
Dauer: {{data.duration}} {% elif data.choice == 'SOFT' %}<br>
Software: {{data.library}}<br>
Dauer: {{data.duration}} {% elif data.choice == 'IFG'%}<br>
Anfrage-URL: <a href="{{data.url}}">{{data.url}}</a> {% elif data.choice == 'LIT'%}<br>
Info zum Werk: {{data.info}}<br>
Bezugsquelle: {{data.source}} {% elif data.choice == 'MAIL'%}<br>
Adressenbestandteil frei gewählt: {{data.other}} {% elif data.choice == 'VIS'%}<br>
Wikimedia-Projekt: {{data.project}}<br>
Persönliche Daten: {{data.data}}<br>
Variante: {{data.variant}}<br>
Sendungsadrese: {{data.send_to}} {% endif %}<br>
<br><br>
Das Team Comunitys und Engagement wird sich um die Bearbeitung deiner Anfrage kümmern<br>
und sich in den nächsten Tagen bei dir melden. Solltest du Rückfragen haben,<br>
wende dich gern an <a href = "mailto: community@wikimedia.de">community@wikimedia.de</a>.<br>
<br><br>
Viele Grüße, dein freundliches aber komplett unmenschliches automatisches
Formularbeantwortungssystem.
</body>
</html>

View File

@ -1,29 +0,0 @@
Hallo {{data.realname}},
wir haben Deine Anfrage ({{data.typestring|striptags}}) erhalten.
{% if data.choice in data.grant %}
Vorraussichtliche Kosten: {{data.cost}}
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
Domain: {{data.domain}}
Adressenbestandteil: {{data.address}} {% endif %} {% if data.choice == 'BIB' %}
Bibliothek: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'ELIT' %}
Datenbank: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'SOFT' %}
Software: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'IFG'%}
Anfrage-URL: {{data.url}} {% elif data.choice == 'LIT'%}
Info zum Werk: {{data.info}}
Bezugsquelle: {{data.source}} {% elif data.choice == 'MAIL'%}
Adressenbestandteil frei gewählt: {{data.other}} {% elif data.choice == 'VIS'%}
Wikimedia-Projekt: {{data.project}}
Persönliche Daten: {{data.data}}
Variante: {{data.variant}}
Sendungsadrese: {{data.send_to}} {% endif %}
Das Team Comunitys und Engagement wird sich um die Bearbeitung deiner Anfrage kümmern
und sich in den nächsten Tagen bei dir melden. Solltest du Rückfragen haben,
wende dich gern an community@wikimedia.de.
Viele Grüße, dein freundliches aber komplett unmenschliches automatisches
Formularbeantwortungssystem.

View File

@ -1,69 +1,38 @@
{% load static %} {% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/base.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/widgets.css' %}" />
{% load i18n %} {% load i18n %}
{% csrf_token %} <link rel="stylesheet" type="text/css" href="{% static 'admin/css/base.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/widgets.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/button.css' %}">
<div class="page-centered">
<center> <div class="spacer-5"></div>
<style> <p role="heading" aria-level="1">
ul > li { Herzlich willkommen im Förderanfrageportal von Wikimedia Deutschland!
list-style-type: none; </p>
} <div class="spacer-5"></div>
ul { <p>
padding-left: 10; <img src="https://upload.wikimedia.org/wikipedia/commons/c/c4/Figuren_klein.jpg">
} </p>
label.required::after { <p>
content: ' *'; Um eine Unterstützungsleistung im Rahmen der Förderangebote anfragen zu können, verifiziere dich bitte mit
color: red; deinem Wikimedia-Konto.
} <br>Weitere Informationen und Hintergründe findest du unter
.div15 { <a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Förderangebote">
height: 15%;
}
.div5 {
height: 5%;
}
.button1 {
width: 40vw;
height: 6vh;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: 4vh;
}
</style>
<div class="div5"></div>
<p>
Herzlich willkommen im Förderanfrageportal von Wikimedia Deutschland!
</p>
<div class="div5"></div>
<!-- <a href="http://fdb-devel.wikimedia.de/extern"style="float:right;padding-right:10%;">OAUTH</a>
<a href="http://fdb-devel.wikimedia.de/extern" style="float:left;padding-left:10%;">OAUTH</a> -->
<p>
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c4/Figuren_klein.jpg"><p>
Um eine Unterstützungsleistung im Rahmen der Förderangebote anfragen zu können, verifiziere dich bitte mit deinem Wikimedia-Konto.
<br>Weitere Informationen und Hintergründe findest du unter
<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Förderangebote">
Förderportal</a> in der deutschsprachigen Wikipedia. Förderportal</a> in der deutschsprachigen Wikipedia.
<p> </p>
<div class="div5"></div> <div class="spacer-5"></div>
<div class="button button1"><a href="/extern"><div class="button1_text">Anmelden</div></a></div> <a href="/extern" class="button button-login">Anmelden</a>
<div class="div5"></div> <div class="spacer-5"></div>
<div class="div5"></div> <div class="spacer-5"></div>
<br>Für alle Fragen wende dich gern an das <a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Wikimedia_Deutschland">Team Communitys und Engagement</a>. <br>Für alle Fragen wende dich gern an das <a
<br>Für interessierte Hacker gibts auch den <a href="https://srcsrv.wikimedia.de/beba/foerderbarometer">Sourcecode</a> zum Formular und was damit passiert. href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Wikimedia_Deutschland">Team Communitys und
<p> Engagement</a>.
<br>Für interessierte Hacker gibts auch den
<a href="https://srcsrv.wikimedia.de/beba/foerderbarometer">Sourcecode</a> zum Formular und was damit passiert.
<p>
<a href="https://www.wikimedia.de/impressum/">Impressum</a> <a href="https://www.wikimedia.de/impressum/">Impressum</a>
<p> </p>
</center> </div>

View File

@ -0,0 +1,27 @@
{% extends 'input/base.html' %}
{% block head_extra %}
<title>Projektförderung ab 1.000,— EUR</title>
<style>
.wm-main {
max-width: 80vw;
margin: 0 auto;
text-align: center;
}
</style>
{% endblock %}
{% block content %}
<h1>Projektförderung mit einer Gesamtsumme ab 1.000,— EUR</h1>
<p>Vielen Dank für dein Interesse an einer Projektförderung!<br>
Für Projektförderungen mit einer Gesamtsumme ab 1.000,— EUR ist ein öffentlicher Projektplan
erforderlich. Weitere Informationen zu diesem Prozess findest du unter
<a href="https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/Projektplanung" target="_blank" rel="noopener">
Wikipedia:Förderung/Projektplanung</a>.<br>
Für Fragen steht dir das Team Community-Konferenzen &amp; Förderung gern unter
<a href="mailto:community@wikimedia.de">community@wikimedia.de</a> zur Verfügung.
</p>
<div class="page-centered">
<button type="button" onclick="history.back()">Zurück</button>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
<div class="wm-footer page-centered">
<p>
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c4/Figuren_klein.jpg" alt=""/>
</p>
<p>
Eine Übersicht aller Förderangebote von Wikimedia Deutschland findest du im
<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Förderangebote">Förderportal in der deutschsprachigen
Wikipedia</a>.
<br>
Für alle Fragen wende dich gern an das
<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Wikimedia_Deutschland">Team Communitys und
Engagement</a>.
</p>
<p>
Für interessierte Hacker gibts auch den
<a href="https://srcsrv.wikimedia.de/beba/foerderbarometer">Sourcecode</a> zum Formular und was damit passiert.
</p>
<p>
<a href="https://www.wikimedia.de/impressum/">Impressum</a>
</p>
</div>

View File

@ -0,0 +1,4 @@
{% load static %}
<div class="wm-header page-centered">
<img src="{% static 'input/logo.png' %}" alt="Wikimedia Deutschland"/>
</div>

View File

@ -8,7 +8,7 @@ Ende erreicht.<br><br>
Hier könnt ihr es in der Datenbank editieren: Hier könnt ihr es in der Datenbank editieren:
<br><br> <br><br>
<a href="{{URLPREFIX}}/admin/input/project/{{project.pk}}/change">{{URLPREFIX}}/admin/input/project/{{project.pk}}/change</a> <a href="{{URL_PREFIX}}/admin/input/project/{{project.pk}}/change">{{URL_PREFIX}}/admin/input/project/{{project.pk}}/change</a>
<br><br> <br><br>
mit freundlichen Grüßen, Eure Lieblingsdatenbank mit freundlichen Grüßen, Eure Lieblingsdatenbank

View File

@ -5,6 +5,6 @@ Ende erreicht.
Hier könnt ihr es in der Datenbank editieren: Hier könnt ihr es in der Datenbank editieren:
{{URLPREFIX}}/admin/input/project/{{project.pk}}/change {{URL_PREFIX}}/admin/input/project/{{project.pk}}/change
mit freundlichen Grüßen, Eure Lieblingsdatenbank mit freundlichen Grüßen, Eure Lieblingsdatenbank

View File

@ -8,7 +8,7 @@ Ende erreicht.
<br><br> <br><br>
Hier könnt ihr es in der Datenbank editieren: Hier könnt ihr es in der Datenbank editieren:
<br><br> <br><br>
<a href="{{URLPREFIX}}/admin/input/project/{{project.pk}}/change">{{URLPREFIX}}/admin/input/project/{{project.pk}}/change</a> <a href="{{URL_PREFIX}}/admin/input/project/{{project.pk}}/change">{{URL_PREFIX}}/admin/input/project/{{project.pk}}/change</a>
<br><br> <br><br>
Projektorganisator*in wurde über den Projektabschluss informiert. Projektorganisator*in wurde über den Projektabschluss informiert.

View File

@ -5,7 +5,7 @@ Ende erreicht.
Hier könnt ihr es in der Datenbank editieren: Hier könnt ihr es in der Datenbank editieren:
{{URLPREFIX}}/admin/input/project/{{project.pk}}/change {{URL_PREFIX}}/admin/input/project/{{project.pk}}/change
Projektorganisator*in wurde über den Projektabschluss informiert. Projektorganisator*in wurde über den Projektabschluss informiert.

View File

@ -0,0 +1,26 @@
<html lang="de">
<body>
<p>Hallo Team Community-Konferenzen &amp; Förderung,</p>
<p>es gibt eine neue Anfrage von {{ data.realname }}.</p>
<p>{{ data.username|default:data.realname }} ({{ data.email }}) fragt an: {{ type_label }}</p>
<p>
{% for label, value in form_data.items %}
{{ label }}: {{ value }} <br />
{% endfor %}
</p>
<p>Zum Eintrag in der Förderdatenbank: <a href="{{ urls.admin }}">{{ urls.admin }}</a></p>
{% if urls.authorize %}
<p>Zum Genehmigen hier klicken: <a href="{{ urls.authorize }}">{{ urls.authorize }}</a></p>
{% endif %}
{% if urls.deny %}
<p>Zum Ablehnen hier klicken: <a href="{{ urls.deny }}">{{ urls.deny }}</a></p>
{% endif %}
</body>
</html>

View File

@ -0,0 +1,12 @@
Hallo Team Community-Konferenzen & Förderung,
es gibt eine neue Anfrage von {{ data.realname }}.
{{ data.username|default:data.realname }} ({{ data.email }}) fragt an: {{ type_label }}
{% for label, value in form_data.items %}{{ label }}: {{ value }}
{% endfor %}
Zum Eintrag in der Förderdatenbank: {{ urls.admin }}
{% if urls.authorize %}Zum Genehmigen hier klicken: {{ urls.authorize }}{% endif %}
{% if urls.deny %}Zum Ablehnen hier klicken: {{ urls.deny }}{% endif %}

View File

@ -0,0 +1,32 @@
<html lang="de">
<body>
<p>Hallo {{ applicant_name }},</p>
<p>vielen Dank für deine Anfrage ({{ type_label }}), die bei uns eingegangen ist.</p>
Dies ist eine automatisch generierte E-Mail. Im Folgenden findest du deine Formulareingaben nochmals zu deiner Übersicht:<br>
{% for label, value in form_data.items %}
{{ label }}: {{ value }} <br />
{% endfor %}
<p>Das Team Community-Konferenzen &amp; Förderung wird sich um deine Anfrage kümmern und sich in den nächsten Tagen bei dir melden. Wenn du Fragen hast, wende dich gern jederzeit an <a href="mailto:community@wikimedia.de">community@wikimedia.de</a>.</p>
<p>
--<br>
Wikimedia Deutschland e. V. | Tempelhofer Ufer 2324 | 10963 Berlin<br>
Zentrale: +49 30 5771162-0<br>
<a href="https://wikimedia.de">wikimedia.de</a>
</p>
<p>
Unsere Vision ist eine Welt, in der alle Menschen am Wissen der Menschheit teilhaben, es nutzen und mehren können. Helfen Sie uns dabei!<br>
<a href="https://spenden.wikimedia.de">spenden.wikimedia.de</a>
</p>
<p>Wikimedia Deutschland Gesellschaft zur Förderung Freien Wissens e. V. Eingetragen im Vereinsregister des Amtsgerichts Charlottenburg, VR 23855 B. Als gemeinnützig anerkannt durch das Finanzamt für Körperschaften I Berlin, Steuernummer 27/029/42207. Geschäftsführende Vorständin: Franziska Heine.</p>
<p>Datenschutzerklärung:<br>
Soweit Sie uns personenbezogene Daten mitteilen, verarbeiten wir diese Daten gemäß unserer <a href="https://www.wikimedia.de/datenschutz/">Datenschutzerklärung </a>.</p>
</body>
</html>

View File

@ -0,0 +1,22 @@
Hallo {{ applicant_name }},
vielen Dank für deine Anfrage ({{type_label}}), die bei uns eingegangen ist.
{% for label, value in form_data.items %}{{ label }}: {{ value }}
{% endfor %}
Das Team Community-Konferenzen & Förderung wird sich um deine Anfrage kümmern und sich in den nächsten Tagen bei dir melden. Wenn du Fragen hast, wende dich gern jederzeit an community@wikimedia.de.
--
Wikimedia Deutschland e. V. | Tempelhofer Ufer 2324 | 10963 Berlin
Zentrale: +49 30 5771162-0
https://wikimedia.de
Unsere Vision ist eine Welt, in der alle Menschen am Wissen der Menschheit teilhaben, es nutzen und mehren können. Helfen Sie uns dabei!
https://spenden.wikimedia.de
Wikimedia Deutschland Gesellschaft zur Förderung Freien Wissens e. V. Eingetragen im Vereinsregister des Amtsgerichts Charlottenburg, VR 23855 B. Als gemeinnützig anerkannt durch das Finanzamt für Körperschaften I Berlin, Steuernummer 27/029/42207. Geschäftsführende Vorständin: Franziska Heine.
Datenschutzerklärung:
Soweit Sie uns personenbezogene Daten mitteilen, verarbeiten wir diese Daten gemäß unserer Datenschutzerklärung (https://www.wikimedia.de/datenschutz/).

View File

@ -34,7 +34,7 @@ Förderprogramme im Sinne der Communitys weiter zu verbessern. Wir freuen uns,
wenn du dir kurz die Zeit dafür nehmen würdest. Die Umfrage mit weiteren wenn du dir kurz die Zeit dafür nehmen würdest. Die Umfrage mit weiteren
Informationen findest du unter dem folgenden Link:<br> Informationen findest du unter dem folgenden Link:<br>
<a href="{{SURVEYPREFIX}}{% if type == 'PRO' %}O{% else %}I{% endif %}=1&{{pid}}=1">{{SURVEYPREFIX}}{% if type == 'PRO' %}O{% else %}I{% endif %}=1&{{pid}}=1</a><br><br> <a href="{{SURVEY_PREFIX}}{% if type == 'PRO' %}O{% else %}I{% endif %}=1&{{pid}}=1">{{SURVEY_PREFIX}}{% if type == 'PRO' %}O{% else %}I{% endif %}=1&{{pid}}=1</a><br><br>
Da dies eine automatisch erzeugte Nachricht ist, wende dich bei Rückfragen zur Umfrage bitte an <a href="mailto: community@wikimedia.de">community@wikimedia.de</a> Da dies eine automatisch erzeugte Nachricht ist, wende dich bei Rückfragen zur Umfrage bitte an <a href="mailto: community@wikimedia.de">community@wikimedia.de</a>

View File

@ -32,6 +32,6 @@ Förderprogramme im Sinne der Communitys weiter zu verbessern. Wir freuen uns,
wenn du dir kurz die Zeit dafür nehmen würdest. Die Umfrage mit weiteren wenn du dir kurz die Zeit dafür nehmen würdest. Die Umfrage mit weiteren
Informationen findest du unter dem folgenden Link: Informationen findest du unter dem folgenden Link:
{{SURVEYPREFIX}}{% if type == 'PRO' %}O{% else %}I{% endif %}=1&{{pid}}=1 {{SURVEY_PREFIX}}{% if type == 'PRO' %}O{% else %}I{% endif %}=1&{{pid}}=1
Da dies eine automatisch erzeugte Nachricht ist, wende dich bei Rückfragen zur Umfrage bitte an community@wikimedia.de Da dies eine automatisch erzeugte Nachricht ist, wende dich bei Rückfragen zur Umfrage bitte an community@wikimedia.de

View File

@ -1,155 +0,0 @@
from django.test import TestCase, Client
from django.conf import settings
from django.contrib.auth.models import User
from django.http import HttpResponse
from datetime import date
from .models import HonoraryCertificate, Project, Account, Literature
from .forms import LibraryForm
class TestWithoutLogin(TestCase):
def setUp(self):
#this setting supress an unwanted warning about missing root dir
settings.WHITENOISE_AUTOREFRESH = True
self.client = Client()
def test_set_granted(self):
'''test if the model function set_granted() works as intended'''
obj = HonoraryCertificate.objects.create(realname='hurzel',email='hurzel@web.de')
self.assertEqual(obj.granted,None)
HonoraryCertificate.set_granted(obj.pk, True)
obj2 = HonoraryCertificate.objects.get(pk=obj.pk)
self.assertEqual(obj2.granted,True)
def test_source_link(self):
'''test if link to source code is included in main page'''
response = self.client.get('', follow=True)
#print (response.content)
self.assertContains(response,'<a href="https://srcsrv.wikimedia.de/beba/foerderbarometer">Sourcecode</a>')
def test_access_denied(self):
'''test if /intern redirects to login page if not logged in'''
response = self.client.get('/intern', follow=True)
self.assertContains(response,'password')
def _postform(self, data, expected_form):
'''helper function to manage the Wizzard'''
response = self.client.post('/', data, follow=False)
self.assertEqual(200, self.response.status_code)
if not type(response) == HttpResponse:
if 'form' in response.context:
print('CONTENT')
print(response.content)
print('ITEMS')
print(response.items())
print('DATA')
print(data)
self.assertFalse(response.context['form'].errors)
else:
if expected_form:
print(response.context)
raise BaseException("NO FORM FOUND")
else:
self.assertContains(response,"Deine Anfrage wurde gesendet.")
self.assertEqual(
type(response.context['wizard']['form']),
expected_form
)
return response
def _notest_bib(self): # renamed because not working
'''full run through the forms to check Bibliotheksstipendium'''
self.response = self.client.get('/')
self.assertEqual(200, self.response.status_code)
print("\n\nEINS EINS\n\n")
response = self._postform({
'extern_view-current_step': '0',
'0-realname': 'vladimir reiherzehe',
'0-email': 'vlre@wikimedia.de',
'0-username': 'stoffel',
'0-choice': 'BIB',
'0-check': True
}, LibraryForm)
print("\n\nZWEI ZWEI\n\n")
response = self._postform({
'extern_view-current_step': '1',
'1-cost': 'teuroooo!',
'1-duration': 'looooong',
'1-library': 'of congress',
}, None)
class TestWithLogin(TestCase):
def setUp(self):
User.objects.create_superuser('testuser', 'nomail@nomail.com', 'testpasswd')
self.client = Client()
self.user = User.objects.create_user('vladimir', 'vladimir@reiherzehe.com', 'reiherzehe')
def test_access(self):
'''test if /intern gives an answer'''
self.assertEqual(self.client.login(username='testuser', password='testpasswd'), True)
response = self.client.get('/intern')
self.assertContains(response,'Übersicht aller Förderangebote')
def test_project_of_year(self):
''' test if the finance id is resettet ad start of year'''
acc = Account.objects.create()
acc.code='1234'
acc.description='blabla'
acc.save()
startdate = date(2022,1,1)
obj = Project.objects.create(account= acc, name='testproject', start=startdate)
self.assertEqual(obj.project_of_year,1)
obj2 = Project.objects.create(account= acc, name='testproject2', start=startdate)
self.assertEqual(obj2.project_of_year,2)
olddate = date(2021,12,31)
obj4 = Project.objects.create(account= acc, name='testproject2', start=olddate)
obj3 = Project.objects.create(account= acc, name='testproject2', start=startdate)
self.assertEqual(obj3.project_of_year,3)
def test_finance_id(self):
''' test if the finance counting is correct'''
acc = Account.objects.create(code='1234', description='blabla')
startdate = date(2022,1,1)
obj = Project.objects.create(account= acc, name='testproject', start=startdate)
self.assertEqual(obj.finance_id,"1234001")
obj2 = Project.objects.create(account= acc, name='testproject2', start=startdate)
self.assertEqual(obj2.finance_id,"1234002")
olddate = date(2021,12,31)
obj4 = Project.objects.create(account= acc, name='testproject2', start=olddate)
obj3 = Project.objects.create(account= acc, name='testproject2', start=startdate)
self.assertEqual(obj3.finance_id,"1234003")
# def test_pid(self):
# ''' test if the pid counting is correct '''
# acc = Account.objects.create(code='1234', description='blabla')
# startdate = date(2022,1,1)
# obj = Project.objects.create(account= acc, name='testproject', start=startdate)
# self.assertEqual(obj.pid,"1234001")
# self.assertEqual(obj.account.code,"1234")
#
# obj2 = Project.objects.create(account= acc, name='testproject2', start=startdate)
# self.assertEqual(obj2.pid,"1234002")
#
# olddate = date(2021,12,31)
# obj4 = Project.objects.create(account= acc, name='testproject2', start=olddate)
#
# obj3 = Project.objects.create(account= acc, name='testproject2', start=startdate)
# self.assertEqual(obj3.pid,"1234004")
def test_literature(self):
obj = Literature.objects.create(cost='100', notes='jolo')
self.assertEqual(obj.service_id,'Literature1')

3
input/tests/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .admin import AdminTestCase
from .models import ModelTestCase
from .views import AuthenticatedViewTestCase, AnonymousViewTestCase

111
input/tests/admin.py Normal file
View File

@ -0,0 +1,111 @@
import datetime
from django.forms import model_to_dict
from django.test import TestCase
from input.models import (
Project,
ProjectRequest,
ProjectDeclined,
Library,
ELiterature,
Email,
IFG,
Literature,
List,
Travel,
Software,
BusinessCard, ProjectCategory, WikimediaProject, Account,
)
from input.utils.admin import admin_url
from input.utils.testing import request, create_superuser, login
class AdminTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = user = create_superuser('admin', first_name='Max', last_name='Mustermann')
cls.data = data = {
'realname': f'{user.first_name} {user.last_name}',
'email': user.email,
}
cls.objs = [
ProjectCategory.objects.order_by('?').first(),
WikimediaProject.objects.order_by('?').first(),
Project.objects.create(**data, granted=True),
ProjectRequest.objects.create(**data, granted=None),
ProjectDeclined.objects.create(**data, granted=False),
Library.objects.create(**data, library='Test'),
ELiterature.objects.create(**data, library='Test'),
Software.objects.create(**data, library='Test'),
Email.objects.create(**data),
IFG.objects.create(**data),
Literature.objects.create(**data, selfbuy_give_data=False),
List.objects.create(**data),
Travel.objects.create(**data),
BusinessCard.objects.create(**data),
]
def setUp(self):
login(self)
def test_changelists(self):
for obj in self.objs:
model = type(obj)
url = admin_url(model, 'changelist')
with self.subTest(model=model):
request(self, url)
def test_change_views(self):
for obj in self.objs:
model = type(obj)
url = admin_url(model, 'change', obj.id)
with self.subTest(model=model):
request(self, url)
def test_display_values(self):
for obj in self.objs:
model = type(obj)
with self.subTest(model=model):
self.assertTrue(f'{obj}')
def test_grant_project_request(self):
account = Account.objects.create(code='test')
category = ProjectCategory.objects.first()
wikimedia = WikimediaProject.objects.first()
obj = ProjectRequest.objects.create(
**self.data,
name='Test',
description='Test',
otrs='https://example.com',
granted=None,
start=datetime.date(2025, 1, 1),
end=datetime.date(2026, 1, 1),
)
obj.categories.add(category)
obj.wikimedia_projects.add(wikimedia)
url = admin_url(ProjectRequest, 'change', obj.id)
expected_url = admin_url(ProjectRequest, 'changelist')
data = {
**model_to_dict(obj),
'granted': True,
'granted_date': obj.start,
'granted_from': self.user.username,
'account': account.code,
'categories': [category.id],
'wikimedia_projects': [wikimedia.id],
}
for key in list(data):
if data[key] is None:
data.pop(key)
request(self, url, expected_url=expected_url, data=data)

99
input/tests/models.py Executable file
View File

@ -0,0 +1,99 @@
from datetime import date
from django.test import TestCase
from input.models import HonoraryCertificate, Project, Account, Literature
class ModelTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.account = Account.objects.create(code='1234', description='blabla')
def assertLen(self, value, length, msg=None):
self.assertEqual(len(value), length, msg)
def assertStartsWith(self, value, start, msg=None):
self.assertTrue(f'{value}'.startswith(f'{start}'), msg)
def assertEndsWith(self, value, end, msg=None):
self.assertTrue(f'{value}'.endswith(f'{end}'), msg)
def test_set_granted(self):
""" test if the model function set_granted() works as intended """
obj = HonoraryCertificate.objects.create(realname='hurzel', email='hurzel@web.de')
self.assertEqual(obj.granted, None)
HonoraryCertificate.set_granted(obj.pk, True)
obj2 = HonoraryCertificate.objects.get(pk=obj.pk)
self.assertEqual(obj2.granted, True)
def test_project_of_year(self):
""" test if the finance id is resettet ad start of year """
acc = self.account
startdate = date(2022, 1, 1)
obj = Project.objects.create(account=acc, name='testproject', start=startdate)
self.assertEqual(obj.project_of_year, 1)
obj2 = Project.objects.create(account=acc, name='testproject2', start=startdate)
self.assertEqual(obj2.project_of_year, 2)
olddate = date(2021, 12, 31)
obj4 = Project.objects.create(account=acc, name='testproject2', start=olddate)
obj3 = Project.objects.create(account=acc, name='testproject2', start=startdate)
self.assertEqual(obj3.project_of_year, 3)
def test_finance_id(self):
""" test if the finance counting is correct """
acc = self.account
startdate = date(2022, 1, 1)
obj = Project.objects.create(account=acc, name='testproject', start=startdate)
self.assertEqual(obj.finance_id, "1234")
obj2 = Project.objects.create(account=acc, name='testproject2', start=startdate)
self.assertEqual(obj2.finance_id, "1234")
olddate = date(2021, 12, 31)
obj4 = Project.objects.create(account=acc, name='testproject2', start=olddate)
obj3 = Project.objects.create(account=acc, name='testproject2', start=startdate)
self.assertEqual(obj3.finance_id, "1234")
def test_financed_id_for_subaccounts(self):
account = Account.objects.create(code='21111', description='has subaccounts')
obj = Project.objects.create(account=account, name='test', start=date(2025, 1, 1))
self.assertEqual(obj.finance_id, f'{account.code}-001')
def test_finance_id_later(self):
obj = Project.objects.create(name='test', start=date(2025, 1, 1))
self.assertFalse(obj.finance_id)
obj.account = self.account
obj.save()
self.assertTrue(obj.finance_id)
def test_pid(self):
""" test if the pid counting is correct """
acc = self.account
startdate = date(2022, 1, 1)
obj = Project.objects.create(account=acc, name='testproject', start=startdate)
self.assertLen(obj.pid, len(acc.code) + 8)
self.assertStartsWith(obj.pid, acc.code)
self.assertEndsWith(obj.pid, obj.id)
obj2 = Project.objects.create(account=acc, name='testproject2', start=startdate)
self.assertEndsWith(obj2.pid, obj.id + 1)
olddate = date(2021, 12, 31)
Project.objects.create(account=acc, name='testproject2', start=olddate)
obj4 = Project.objects.create(account=acc, name='testproject2', start=startdate)
self.assertEndsWith(obj4.pid, obj.id + 3)
def test_literature(self):
obj = Literature.objects.create(cost='100', notes='jolo', selfbuy_give_data=False)
self.assertEqual(obj.service_id, f'Literature{obj.id}')

197
input/tests/views.py Normal file
View File

@ -0,0 +1,197 @@
import random
from django.forms import model_to_dict
from django.shortcuts import resolve_url
from django.test import TestCase
from foerderbarometer.constants import *
from input.models import Library, ProjectCategory, WikimediaProject
from input.utils.testing import create_superuser, login, request
from input.views import PROJECT_FUNDING, TYPES, ApplicationView
PATHS = {TYPES[path].code: path for path in TYPES}
PATHS[TYPE_PROJ] = PROJECT_FUNDING[0].path
CODES = list(PATHS)
class AnonymousViewTestCase(TestCase):
def test_index(self):
response = request(self, 'index')
self.assertContains(response, '<a href="https://srcsrv.wikimedia.de/beba/foerderbarometer">Sourcecode</a>')
def test_extern(self):
request(self, 'extern')
def test_extern_post(self):
code = random.choice(CODES)
url = self.helper_url(code)
request(self, 'extern', expected_url=url, data={'url': url})
def test_extern_invalid_url(self):
request(self, 'extern', data={'url': 'https://domain.not/allowed/to/be/redirected/'})
@classmethod
def get_step_data(cls, choice, **data):
return {
'realname': 'Test',
'email': 'test@example.com',
'choice': choice,
'check': True,
**data,
}
@staticmethod
def helper_url(code):
return resolve_url('extern', type=PATHS[code])
def helper_extern_base(self, choice, text, data):
url = self.helper_url(choice)
response = request(self, url)
self.assertContains(response, text)
data = self.get_step_data(choice, **data)
return request(self, url, data=data)
def helper_extern(self, choice, text, data):
response = self.helper_extern_base(choice, text, data)
self.assertContains(response, 'Deine Anfrage wurde gesendet.')
def test_extern_types(self):
types = [
(TYPE_BIB, 'Bibliotheksausweis'),
(TYPE_ELIT, 'Online-Ressource'),
(TYPE_MAIL, 'Mailadresse beantragen'),
(TYPE_IFG, 'gewonnenen Informationen'),
(TYPE_LIT, 'Literatur verwenden'),
(TYPE_LIST, 'Mailingliste beantragen'),
(TYPE_TRAV, 'Transportmittel'),
(TYPE_SOFT, 'Lizenz'),
(TYPE_VIS, 'DIN 5008'),
(TYPE_PROJ, 'Projektförderung'),
]
for code, text in types:
with self.subTest(type=code):
url = self.helper_url(code)
response = request(self, url)
self.assertContains(response, text)
def test_extern_travel(self):
self.helper_extern(TYPE_TRAV, 'Transportmittel', {
'project_name': 'Test',
'transport': 'BAHN',
'travelcost': 10,
'checkin': '2025-01-01',
'checkout': '2025-01-02',
'hotel': 'TRUE',
'notes': '',
})
def test_extern_lit(self):
self.helper_extern(TYPE_LIT, 'Literatur verwenden', {
'cost': 20,
'info': 'Test',
'source': 'Test',
'notes': '',
'selfbuy': 'FALSE',
'selfbuy_data': 'Test',
'selfbuy_give_data': True,
'check': True,
'terms_accepted': True,
})
def test_extern_lit_without_consent_fails(self):
response = self.helper_extern_base(TYPE_LIT, 'Literatur verwenden', {
'cost': 20,
'info': 'Test',
'source': 'Test',
'notes': '',
'selfbuy': 'TRUE',
'selfbuy_data': '',
'selfbuy_give_data': False,
'check': False,
})
self.assertContains(response, 'Dieses Feld ist zwingend erforderlich.')
def test_extern_bib(self):
self.helper_extern('BIB', 'Bibliotheksausweis', {
'cost': 20,
'library': 'Test',
'duration': 'Test',
'notes': '',
})
def test_extern_proj(self):
category = ProjectCategory.objects.order_by('?').first()
wikimedia_project = WikimediaProject.objects.order_by('?').first()
self.helper_extern(TYPE_PROJ, 'Projektförderung', {
'name': 'Test',
'description': 'Test',
'categories': [category.id, 0],
'categories_other': 'Test',
'wikimedia_projects': [wikimedia_project.id, 0],
'wikimedia_projects_other': 'Test',
'start': '2025-01-01',
'end': '2025-01-02',
'participants_estimated': 1,
'cost': 20,
})
def test_extern_invalid_code(self):
request(self, 'extern', args=['invalid'], status_code=404)
def test_unknown_name(self):
obj = Library(type=Library.TYPE)
data = model_to_dict(obj)
name = ApplicationView.get_recipient_name(obj, data)
self.assertEqual(name, 'Unbekannt')
class AuthenticatedViewTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = create_superuser('staff')
def setUp(self):
login(self)
def test_export(self):
request(self, 'export')
def helper_auth_deny(self, view, expected):
obj = Library.objects.create(library='Test')
request(self, view, args=[obj.type, obj.id])
obj.refresh_from_db(fields=['granted'])
self.assertEqual(obj.granted, expected)
def helper_auth_deny_error(self, view):
response = request(self, view, args=['TEST', 1])
self.assertContains(response, 'ERROR')
def test_authorize(self):
self.helper_auth_deny('authorize', True)
def test_authorize_error(self):
self.helper_auth_deny_error('authorize')
def test_deny(self):
self.helper_auth_deny('deny', False)
def test_deny_error(self):
self.helper_auth_deny_error('deny')

View File

@ -1,14 +1,28 @@
from django.urls import path from django.urls import path, include
from .views import ExternView, index, done, authorize, deny, InternView, export from django.views.i18n import JavaScriptCatalog
from django.contrib import admin
from .views import (
index,
done,
export,
authorize,
deny,
ApplicationView,
ApplicationStartView,
ProjectFundingInfoView,
)
urlpatterns = [ urlpatterns = [
path('', index, name='index'), path('', index, name='index'),
path('extern', ExternView.as_view(), name='extern'),
# path('intern', InternView.as_view(), name='intern'),
path('admin/', admin.site.urls),
path('saved', done, name='done'), path('saved', done, name='done'),
path('export', export, name='export'), path('export', export, name='export'),
path('authorize/<str:choice>/<int:pk>', authorize, name='authorize'), path('authorize/<str:choice>/<int:pk>', authorize, name='authorize'),
path('deny/<str:choice>/<int:pk>', deny, name='deny'), path('deny/<str:choice>/<int:pk>', deny, name='deny'),
path('extern/', include([
path('', ApplicationStartView.as_view(), name='extern'),
path('projektfoerderung-ab-1000/', ProjectFundingInfoView.as_view(), name='projektfoerderung-ab-1000'),
path('<slug:type>/', ApplicationView.as_view(), name='extern'),
])),
# JavaScript translations for date widgets, etc.
path('jsi18n/', JavaScriptCatalog.as_view(), name='jsi18n'),
] ]

0
input/utils/__init__.py Normal file
View File

18
input/utils/admin.py Normal file
View File

@ -0,0 +1,18 @@
from django.contrib import admin
from django.db.models import Model
from django.urls import reverse
def admin_url(model: type[Model], view: str, *args, site=None, **kwargs) -> str:
return reverse(admin_url_name(model, view, site=site), args=args, kwargs=kwargs)
def admin_url_name(model: type[Model], view: str, *, site=None) -> str:
namespace = (site or admin.site).name
view_name = admin_view_name(model, view)
return f'{namespace}:{view_name}'
def admin_view_name(model: type[Model], view: str) -> str:
return f'{model._meta.app_label}_{model._meta.model_name}_{view}'

View File

@ -0,0 +1,54 @@
YES_NO = {
'y': True,
'Y': True,
'yes': True,
'YES': True,
'n': False,
'N': False,
'no': False,
'NO': False,
}
ZERO_ONE = {
1: True,
'1': True,
0: False,
'0': False,
}
TRUE_FALSE = {
True: True,
'TRUE': True,
'True': True,
'true': True,
't': True,
False: False,
'FALSE': False,
'False': False,
'false': False,
'f': False,
None: False,
}
ON_OFF = {
'on': True,
'ON': True,
'off': False,
'OFF': False,
}
TRUTHY = {
**YES_NO,
**ZERO_ONE,
**TRUE_FALSE,
**ON_OFF,
}
def ask(question, default=False, truthy=None):
response = input(question).strip()
return (truthy or YES_NO).get(response, default)
confirm = ask

24
input/utils/list.py Normal file
View File

@ -0,0 +1,24 @@
from typing import Iterable
def reorder_value(values: Iterable, value, *, after=None, before=None):
"""
Reorders a value after or before another value in the given list.
Does not work properly for duplicate or None values.
Raises ValueError when any of the values is not contained in the list.
"""
assert (after is None) != (before is None), 'Either after or before is needed but not both.'
values = list(values)
values.remove(value)
if after is None:
index = values.index(before)
else:
index = values.index(after) + 1
values.insert(index, value)
return values

View File

@ -0,0 +1,34 @@
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import get_template
from input.models import Project
from .attachments import collect_and_attach
__all__ = [
'build_email',
'send_email',
'collect_and_attach',
]
def build_email(template_name: str, context: dict, subject: str, *recipients: str, **kwargs):
body = get_template(f'mails/{template_name}.txt').render(context)
html = get_template(f'mails/{template_name}.html').render(context)
kwargs.setdefault('from_email', settings.IF_EMAIL)
kwargs['subject'] = subject
kwargs['body'] = body
kwargs['to'] = recipients
email = EmailMultiAlternatives(**kwargs)
email.attach_alternative(html, 'text/html')
return email
def send_email(template_name: str, context: dict, subject: str, *recipients: str, fail_silently=False, **kwargs):
return build_email(template_name, context, subject, *recipients, **kwargs).send(fail_silently)

View File

@ -0,0 +1,98 @@
import os
import posixpath
import time
import mimetypes
from os import PathLike
from pathlib import Path
from urllib.parse import urlparse
from urllib.request import urlretrieve
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from foerderbarometer.constants import *
PathList = list[Path]
def ensure_dir(directory: PathLike) -> Path:
"""
Ensure that the given directory exists.
Creates it recursively if it doesn't.
"""
directory = Path(directory)
directory.mkdir(parents=True, exist_ok=True)
return directory
def is_fresh(path: Path, ttl_seconds: int) -> bool:
"""
Check if the cached file exists and is still fresh within TTL.
"""
try:
mtime = path.stat().st_mtime
except FileNotFoundError:
return False
else:
return time.time() - mtime < ttl_seconds
def get_attachment(url: str) -> Path:
filepath = urlparse(url).path
filename = posixpath.basename(filepath)
destination = ensure_dir(settings.MAIL_ATTACHMENT_CACHE_DIR) / filename
if is_fresh(destination, settings.MAIL_ATTACHMENT_TTL_SECONDS):
return destination
return download_attachment(url, destination)
def download_attachment(url: str, destination: Path) -> Path:
filepath = destination.with_suffix('.tmp')
try:
urlretrieve(url, filepath)
os.replace(filepath, destination)
finally:
filepath.unlink(missing_ok=True)
return destination
def collect_attachment_paths(recipient: str, type_code: str) -> PathList:
assert recipient in RECIPIENTS
assert type_code in TYPES
config = settings.MAIL_ATTACHMENT_URLS[recipient]
urls = [*config[TYPE_ALL], *config.get(type_code, [])]
return [get_attachment(url) for url in urls]
def get_mime_type(path: Path) -> str:
mime_type, encoding = mimetypes.guess_type(path)
return mime_type or 'application/octet-stream'
def attach_files(message: EmailMultiAlternatives, files: list[Path]):
"""
Attach files to the EmailMultiAlternatives message.
MIME type is guessed from path; falls back to application/octet-stream.
"""
for path in files:
mime_type = get_mime_type(path)
with open(path, 'rb') as fp:
message.attach(path.name, fp.read(), mime_type)
def collect_and_attach(email: EmailMultiAlternatives, recipient: str, type_code: str):
return attach_files(email, collect_attachment_paths(recipient, type_code))

View File

@ -0,0 +1,2 @@
def get_queryset(apps, schema_editor, *model):
return apps.get_model(*model).objects.using(schema_editor.connection.alias)

42
input/utils/settings.py Normal file
View File

@ -0,0 +1,42 @@
import os
from .confirmation import TRUTHY
def env(key, default=None, parser=None):
value = os.environ.get(key)
if value is None:
return default
if parser is None:
if default is None:
return value
else:
parser = type(default)
if parser is bool:
return truthy(value, default)
return parser(value)
def truthy(value, default=False):
return TRUTHY.get(value, default)
def password_validators(*validators):
return list(_parse_password_validators(validators))
def _parse_password_validators(validators):
for validator in validators:
if isinstance(validator, (tuple, list)):
validator, options = validator
else:
validator, options = validator, {}
if '.' not in validator:
validator = 'django.contrib.auth.password_validation.%s' % validator
yield dict(NAME=validator, OPTIONS=options)

152
input/utils/testing.py Normal file
View File

@ -0,0 +1,152 @@
from typing import Any, Iterable, Mapping, Union, Tuple, Protocol
from django.apps import apps
from django.conf import settings
from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseRedirectBase, StreamingHttpResponse
from django.shortcuts import resolve_url
from django.template import Context
from django.template.response import TemplateResponse
from django.test import Client, SimpleTestCase
from django.urls import reverse, ResolverMatch
from django.utils.http import urlencode
FormData = dict
JSONDict = dict
JSONList = list
RequestData = Union[FormData, JSONDict, JSONList]
QueryParams = Union[Mapping[str, Any], Iterable[Tuple[str, Any]]]
class TestClientResponse(Protocol):
client: Client
request: HttpRequest
templates: list
context: Context
resolver_match: ResolverMatch
def json(self) -> Union[JSONList, JSONDict]: ...
Response = Union[
HttpResponse,
HttpResponseRedirectBase,
StreamingHttpResponse,
TemplateResponse,
TestClientResponse,
]
class ObjectWithGetAbsoluteURLMethod(Protocol):
def get_absolute_url(self) -> str: ...
URL = Union[str, ObjectWithGetAbsoluteURLMethod]
URLArgs = Union[tuple, list]
URLKwargs = dict
def get_url(url: URL, args: URLArgs = None, kwargs: URLKwargs = None) -> str:
"""
Helper to reverse the given url name.
"""
if args or kwargs:
return reverse(url, args=args, kwargs=kwargs)
return resolve_url(url)
def get_handler(test_case: SimpleTestCase, method: str = None, data=None):
if data:
method = str.lower(method or 'POST')
else:
method = str.lower(method or 'GET')
return getattr(test_case.client, method)
def request(
test_case: SimpleTestCase,
url: URL,
status_code: int = None,
expected_url: URL = None,
args: URLArgs = None,
kwargs: URLKwargs = None,
headers: dict = None,
msg: str = None,
query_params: QueryParams = None,
method: str = None,
data: RequestData = None,
**options,
) -> Response:
"""
A helper to make a request with the test case's http client.
The given args and kwargs are used to reverse the url
but not the expected url. When expected url needs
args/kwargs pass an absolute url instead.
All additional kwargs are passed as post parameters.
When posting without parameters just pass post=True.
"""
data = data or options or None
handler = get_handler(test_case, method, data)
url = get_url(url, args, kwargs)
if query_params:
url = f'{url}?%s' % urlencode(query_params, doseq=True)
headers = headers or {}
status_code = status_code or 200
response = handler(url, data=data, **headers)
msg = msg or getattr(response, 'content', None)
if expected_url:
test_case.assertRedirects(
response=response,
expected_url=get_url(expected_url),
target_status_code=status_code,
)
else:
test_case.assertEqual(response.status_code, status_code, msg=msg)
return response
def login(test_case: SimpleTestCase, user=None, password: str = None) -> bool:
"""
Logs in the user trying to use the raw password or the given password.
Force logs in the user when no password is found.
"""
user = user or getattr(test_case, 'user')
password = password or getattr(user, 'raw_password', password)
if password is None:
return test_case.client.force_login(user=user) or True
return test_case.client.login(username=user.username, password=password)
def create_user(username: str, *, model=None, **kwargs):
model = model or apps.get_model(settings.AUTH_USER_MODEL)
password = kwargs.setdefault('password', 'P4sSW0rD')
kwargs.setdefault('email', f'{username}@test.case')
kwargs.setdefault(model.USERNAME_FIELD, username)
user = model.objects.create_user(**kwargs)
user.raw_password = password
return user
def create_superuser(username: str, **kwargs):
kwargs['is_superuser'] = True
kwargs['is_staff'] = True
return create_user(username, **kwargs)

View File

@ -1,301 +1,373 @@
from datetime import date import datetime
from dataclasses import dataclass
from smtplib import SMTPException from smtplib import SMTPException
from typing import Optional
from urllib.parse import urljoin
from django.shortcuts import render from django.forms import ChoiceField, Field
from django.forms import modelformset_factory from django.shortcuts import render, redirect
from django.http import HttpResponse from django.http import HttpResponse, Http404
from formtools.wizard.views import CookieWizardView from django.urls import reverse
from django.core.mail import send_mail, BadHeaderError, EmailMultiAlternatives from django.utils.choices import flatten_choices
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
from django.utils.text import get_text_list
from django.core.mail import BadHeaderError
from django.conf import settings from django.conf import settings
from django.template.loader import get_template
from django.template import Context
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import TemplateView
from django.utils.html import format_html from django.views.generic.edit import FormView
from django.utils.translation import gettext as _ from django.utils.html import strip_tags
from .forms import ProjectForm, ExternForm, LibraryForm, IFGForm, LiteratureForm,\ from input.utils.admin import admin_url_name
HonoraryCertificateForm, InternForm, TravelForm, EmailForm,\ from input.utils.mail import build_email, collect_and_attach
ListForm, BusinessCardForm, INTERN_CHOICES
from .models import Project, TYPE_CHOICES, Library, Literature, Travel, IFG, BusinessCard, Email, List
from .settings import IF_EMAIL
def auth_deny(choice,pk,auth): from .forms import (
if choice in ('BIB', 'ELIT', 'SOFT'): BaseApplicationForm,
Library.set_granted(pk,auth) ProjectForm,
elif choice == 'LIT': LibraryForm,
Literature.set_granted(pk,auth) ELiteratureForm,
elif choice == 'IFG': SoftwareForm,
IFG.set_granted(pk,auth) IFGForm,
elif choice == 'TRAV': LiteratureForm,
Travel.set_granted(pk,auth) TravelForm,
elif choice == 'VIS': EmailForm,
BusinessCard.set_granted(pk,auth) ListForm,
elif choice == 'MAIL': BusinessCardForm,
Email.set_granted(pk,auth) )
elif choice == 'LIST': from .models import (
List.set_granted(pk,auth) MODELS,
else: LIBRARY_TYPES,
TYPE_CHOICES,
TYPE_BIB,
TYPE_ELIT,
TYPE_IFG,
TYPE_LIT,
TYPE_LIST,
TYPE_MAIL,
TYPE_PROJ,
TYPE_SOFT,
TYPE_TRAV,
TYPE_VIS,
Project,
ProductCategoryFormField,
)
HELP_TEXTS = {
TYPE_IFG: {
'notes': (
'Bitte gib an, wie die gewonnenen Informationen den<br>'
'Wikimedia-Projekten zugute kommen sollen.'
)
},
TYPE_MAIL: {
'domain': (
'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>'
'möchtest du eine Mailadresse beantragen?'
)
},
TYPE_LIT: {
'notes': 'Bitte gib an, wofür du die Literatur verwenden möchtest.'
},
TYPE_LIST: {
'domain': (
'Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>'
'möchtest du eine Mailingliste beantragen?'
)
},
}
@dataclass
class ApplicationType:
code: str
path: str
form_class: type[BaseApplicationForm]
link: str
label: Optional[str] = None
help_texts: Optional[dict] = None
def __post_init__(self):
if self.label is None:
self.label = TYPE_CHOICES[self.code]
if self.help_texts is None: # pragma: no branch
self.help_texts = HELP_TEXTS.get(self.code)
@property
def url(self):
return f'https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/{self.link}'
PROJECT_FUNDING = [
ApplicationType(TYPE_PROJ, 'projektfoerderung', ProjectForm, 'Projektplanung',
'Projektförderung mit einer Gesamtsumme unter 1.000,— EUR'),
ApplicationType(TYPE_PROJ, 'projektfoerderung-ab-1000', ProjectForm, 'Projektplanung',
'Projektförderung mit einer Gesamtsumme ab 1.000,— EUR'),
]
SERVICES = [
ApplicationType(TYPE_BIB, 'bibliotheksstipendium', LibraryForm, 'Zugang_zu_Fachliteratur#Bibliotheksstipendium'),
ApplicationType(TYPE_ELIT, 'eliteraturstipendium', ELiteratureForm, 'Zugang_zu_Fachliteratur#eLiteraturstipendium'),
ApplicationType(TYPE_MAIL, 'email', EmailForm, 'E-Mail-Adressen_und_Visitenkarten#E-Mail-Adressen'),
ApplicationType(TYPE_IFG, 'ifg', IFGForm, 'Geb%C3%BChrenerstattungen_f%C3%BCr_Beh%C3%B6rdenanfragen'),
ApplicationType(TYPE_LIT, 'literaturstipendium', LiteratureForm, 'Zugang_zu_Fachliteratur#Literaturstipendium'),
ApplicationType(TYPE_LIST, 'mailingliste', ListForm, 'E-Mail-Adressen_und_Visitenkarten#Mailinglisten'),
ApplicationType(TYPE_TRAV, 'reisekosten', TravelForm, 'Reisekostenerstattungen'),
ApplicationType(TYPE_SOFT, 'softwarestipendium', SoftwareForm, 'Software-Stipendien'),
ApplicationType(TYPE_VIS, 'visitenkarten', BusinessCardForm, 'E-Mail-Adressen_und_Visitenkarten#Visitenkarten'),
]
APPLICATIONS = [
('Projektförderung', PROJECT_FUNDING),
('Serviceleistungen', SERVICES),
]
TYPES = {info.path: info for info in PROJECT_FUNDING + SERVICES}
def auth_deny(choice, pk, auth):
if choice not in MODELS:
return HttpResponse(f'ERROR! UNKNOWN CHOICE TYPE! {choice}') return HttpResponse(f'ERROR! UNKNOWN CHOICE TYPE! {choice}')
return False
MODELS[choice].set_granted(pk, auth)
@login_required @login_required
def export(request): def export(request):
'''export the project database to a csv''' '''export the project database to a csv'''
return HttpResponse('WE WANT CSV!') return HttpResponse('WE WANT CSV!')
@login_required @login_required
def authorize(request, choice, pk): def authorize(request, choice, pk):
'''If IF grant a support they click a link in a mail which leads here. '''If IF grant a support they click a link in a mail which leads here.
We write the granted field in the database here and set a timestamp.''' We write the granted field in the database here and set a timestamp.'''
ret = auth_deny(choice, pk, True) if ret := auth_deny(choice, pk, True):
if ret:
return ret return ret
else: else:
return HttpResponse(f"AUTHORIZED! choice: {choice}, pk: {pk}") return HttpResponse(f'AUTHORIZED! choice: {choice}, pk: {pk}')
@login_required @login_required
def deny(request, choice, pk): def deny(request, choice, pk):
'''If IF denies a support they click a link in a mail which leads here '''If IF denies a support they click a link in a mail which leads here
We write the granted field in the database here.''' We write the granted field in the database here.'''
ret = auth_deny(choice, pk, False) if ret := auth_deny(choice, pk, False):
if ret:
return ret return ret
else: else:
return HttpResponse(f"DENIED! choice: {choice}, pk: {pk}") return HttpResponse(f'DENIED! choice: {choice}, pk: {pk}')
def done(request): def done(request):
return HttpResponse("Deine Anfrage wurde gesendet. Du erhältst in Kürze eine E-Mail-Benachrichtigung mit deinen Angaben. Für alle Fragen kontaktiere bitte das Team Communitys und Engagement unter community@wikimedia.de.") return HttpResponse(
'Deine Anfrage wurde gesendet. Du erhältst in Kürze eine E-Mail-Benachrichtigung mit deinen Angaben. Für alle Fragen kontaktiere bitte das Team Communitys und Engagement unter community@wikimedia.de.')
def index(request): def index(request):
return render(request, 'input/index.html') return render(request, 'input/index.html')
class InternView(LoginRequiredMixin, CookieWizardView):
'''This View is for WMDE-employees only'''
template_name = 'input/extern.html' class ApplicationStartView(TemplateView):
form_list = [InternForm, ProjectForm] template_name = 'input/forms/extern.html'
extra_context = {'applications': APPLICATIONS}
def get_form(self, step=None, data=None, files=None): def post(self, request, *args, **kwargs):
'''this function determines which part of the multipart form is if url := request.POST.get('url'):
displayed next''' if url_has_allowed_host_and_scheme(url, None):
return redirect(url)
if step is None: return self.get(request, *args, **kwargs)
step = self.steps.current
print ("get_form() step " + step)
if step == '1':
prev_data = self.get_cleaned_data_for_step('0') class ProjectFundingInfoView(TemplateView):
choice = prev_data.get('choice') template_name = 'input/info_project_funding_gt_1000.html'
print(f'choice detection: {INTERN_CHOICES[choice]}')
if choice == 'HON':
form = HonoraryCertificateForm(data) class ApplicationView(FormView):
elif choice == 'PRO': """
form = ProjectForm(data) View for all application types.
elif choice == 'TRAV':
form = TravelForm(data) - Renders the generic form template.
else: - Handles saving the submitted form to the database.
raise RuntimeError(f'ERROR! UNKNOWN FORMTYPE {choice} in InternView') - Adds extra fields from the session or request type if needed.
self.choice = choice - Applies optional help_text overrides for certain fields.
else: - Sends confirmation mail to the applicant.
form = super().get_form(step, data, files) - Sends notification mail to the internal IF address.
form.fields['realname'].help_text = format_html("Vor- und Zuname (Realname), Wer hat das Projekt beantragt?<br>\ - Returns the "done" response after successful processing.
Wer ist Hauptansprechperson? Bei WMDE-MAs immer (WMDE),<br>\ """
bei externen Partnern (PART) hinzufügen.")
return form template_name = 'input/forms/form_generic.html'
@cached_property
def type_info(self) -> ApplicationType:
type_path = self.kwargs['type']
if type_info := TYPES.get(type_path):
return type_info
raise Http404(f'"{type_path}" existiert nicht.')
@property
def type_code(self):
return self.type_info.code
@property
def form_class(self):
return self.type_info.form_class
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) return super().get_context_data(**kwargs, type_label=self.type_info.label)
if hasattr(self, 'choice'):
context["choice"] = INTERN_CHOICES[self.choice]
return context
def done(self, form_list, **kwargs): def get_form(self, form_class=None):
print('InternView.done() reached') """Return the form instance and inject custom help_texts if defined for this type."""
# gather data from all forms form = super().get_form(form_class)
data = {}
for form in form_list:
data = {**data, **form.cleaned_data}
if data['choice'] == 'LIT': # Apply help_text overrides if defined for this type_code
if data['selfbuy'] == 'TRUE': if help_texts := self.type_info.help_texts:
data['selfbuy_give_data'] = 'False' for field, text in help_texts.items():
if field in form.fields:
form.fields[field].help_text = mark_safe(text)
# write data to database return form
form = form.save(commit=False)
# we have to copy the data from the first form here
# this is ugly code. how can we copy this without explicit writing?
# i found no way to access the ModelForm.Meta.exclude-tupel
form.realname = data['realname']
# form.username = data['username']
form.email = data['email']
form.granted = True
form.granted_date = date.today()
if data['choice'] == 'LIT': def form_valid(self, form):
form.selfbuy_give_data = data['selfbuy_give_data'] """
Process a valid form submission:
- Enrich form data (e.g., set type_code, handle special rules).
- Save the model instance and related data.
- Send confirmation and notification mails.
- Return the "done" response.
"""
form.save() data = self.prepare_data(form)
obj = self.save_obj(form, data)
if response := self.send_mail(obj, data):
return response
return done(self.request) return done(self.request)
# these where used as labels in the second form TYPE_CHOICES is used for the first form and the def prepare_data(self, form):
# text above the second form. only used for BIB, SOFT, ELIT in the moment # Collect cleaned data and mark the current type
LABEL_CHOICES = {'BIB': format_html('Bibliothek'), return {**form.cleaned_data, 'choice': self.type_code}
'ELIT': format_html('Datenbank/Online-Ressource'),
'MAIL': format_html('E-Mail-Adresse'),
'IFG': format_html('Kostenübernahme IFG-Anfrage'),
'LIT': format_html('Literaturstipendium'),
'LIST': format_html('Mailingliste'),
'TRAV': format_html('Reisekosten'),
'SOFT': format_html('Software'),
'VIS': format_html('Visitenkarten'),
}
HELP_CHOICES = {'BIB': format_html("In welchem Zeitraum möchtest du recherchieren oder<br>wie lange ist der Bibliotheksausweis gültig?"), def save_obj(self, form, data):
'ELIT': "Wie lange gilt der Zugang?", # Save model instance
'SOFT': "Wie lange gilt die Lizenz?",
}
class ExternView(CookieWizardView):
'''This View is for Volunteers'''
template_name = "input/extern.html"
form_list = [ExternForm, LibraryForm]
def get_form(self, step=None, data=None, files=None):
'''this function determines which part of the multipart form is
displayed next'''
if step is None:
step = self.steps.current
print ("get_form() step " + step)
if step == '1':
prev_data = self.get_cleaned_data_for_step('0')
choice = prev_data.get('choice')
print(f'choice detection in ExternView: {TYPE_CHOICES[choice]}')
if choice == 'IFG':
form = IFGForm(data)
form.fields['notes'].help_text = format_html("Bitte gib an, wie die gewonnenen Informationen den<br>Wikimedia-Projekten zugute kommen sollen.")
elif choice in ('BIB', 'SOFT', 'ELIT'):
form = LibraryForm(data)
form.fields['library'].label = LABEL_CHOICES[choice]
form.fields['library'].help_text = f"Für welche {LABEL_CHOICES[choice]} gilt das Stipendium?"
form.fields['duration'].help_text = HELP_CHOICES[choice]
elif choice == 'MAIL':
form = EmailForm(data)
form.fields['domain'].help_text = format_html("Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>möchtest du eine Mailadresse beantragen?")
elif choice == 'LIT':
form = LiteratureForm(data)
form.fields['notes'].help_text = "Bitte gib an, wofür du die Literatur verwenden möchtest."
elif choice == 'VIS':
form = BusinessCardForm(data)
elif choice == 'LIST':
form = ListForm(data)
form.fields['domain'].help_text = format_html("Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>möchtest du eine Mailingliste beantragen?")
elif choice == 'TRAV':
form = TravelForm(data)
else:
raise RuntimeError(f'ERROR! UNKNOWN FORMTYPE {choice} in ExternView')
self.choice = choice
else:
form = super().get_form(step, data, files)
return form
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if hasattr(self, 'choice'):
context["choice"] = TYPE_CHOICES[self.choice]
return context
def done(self, form_list, **kwargs):
print('ExternView.done() reached')
# gather data from all forms
data = {}
for form in form_list:
data = {**data, **form.cleaned_data}
data['username'] = self.request.session['user']['username']
if data['choice'] == 'LIT':
if data['selfbuy'] == 'TRUE':
data['selfbuy_give_data'] = 'False'
# write data to database
modell = form.save(commit=False) modell = form.save(commit=False)
# we have to copy the data from the first form here
# this is a bit ugly code. can we copy this without explicit writing?
if data['choice'] == 'LIT': # Username from session if present
if user := self.request.session.get('user'):
modell.username = user.get('username')
# Copy common fields if provided by the form
if 'realname' in data:
modell.realname = data['realname']
if 'email' in data:
modell.email = data['email']
# Set model.type for specific request types
if self.type_code in LIBRARY_TYPES:
modell.type = self.type_code
# Literature-specific extra field
if self.type_code == TYPE_LIT and 'selfbuy_give_data' in data:
modell.selfbuy_give_data = data['selfbuy_give_data'] modell.selfbuy_give_data = data['selfbuy_give_data']
modell.realname = data['realname'] modell.save()
modell.username = data['username']
modell.email = data['email']
# write type of form in some cases
if data['choice'] in ('BIB', 'ELIT', 'SOFT'):
modell.type = data['choice']
form.save() if hasattr(form, 'save_m2m'):
form.save_m2m()
# add some data to context for mail templates return modell
data['pk'] = modell.pk
data['urlprefix'] = settings.URLPREFIX def send_mail(self, obj, data):
data['grant'] = ('LIT', 'SOFT', 'ELIT', 'BIB', 'IFG') # Prepare minimal mail context and send mails
data['DOMAIN'] = ('MAIL', 'LIST')
data['typestring'] = TYPE_CHOICES[data['choice']] context = self.get_email_context(obj, data)
applicant_subject = 'Deine Förderanfrage bei Wikimedia Deutschland'
staff_subject = 'Anfrage {type_label} von {applicant_name}'.format(**context)
# we need to send the following mails here:
context = { 'data': data }
try: try:
# - mail with entered data to the Volunteer self.send_email('applicant', 'ifg_volunteer_mail', applicant_subject, data['email'], context)
self.send_email('staff', 'if_mail', staff_subject, settings.IF_EMAIL, context)
txt_mail_template1 = get_template('input/ifg_volunteer_mail.txt')
html_mail_template1 = get_template('input/ifg_volunteer_mail.html')
subject1, from_email1, to1 = 'Formular ausgefüllt', IF_EMAIL, data['email']
text_content1 = txt_mail_template1.render(context)
html_content1 = html_mail_template1.render(context)
msg1 = EmailMultiAlternatives(subject1, text_content1, from_email1, [to1])
msg1.attach_alternative(html_content1, "text/html")
msg1.send()
#print('ifg volunteer mail would have been sent')
#send_mail(
# 'Formular ausgefüllt',
# txt_mail_template1.render(context),
# IF_EMAIL,
# [data['email']],
# fail_silently=False)
## - mail to IF with link to accept/decline
txt_mail_template = get_template('input/if_mail.txt')
html_mail_template = get_template('input/if_mail.html')
subject, from_email, to = 'Formular ausgefüllt', IF_EMAIL, IF_EMAIL
text_content = txt_mail_template.render(context)
html_content = html_mail_template.render(context)
msg2 = EmailMultiAlternatives(subject, text_content, from_email, [to])
msg2.attach_alternative(html_content, "text/html")
msg2.send()
#print('if mail would have been sent')
#send_mail(
# 'Formular ausgefüllt',
# txt_mail_template.render(context),
# IF_EMAIL,
# [IF_EMAIL],
# fail_silently=False)
## raise SMTPException("testing pupose only")
except BadHeaderError: except BadHeaderError:
modell.delete() obj.delete()
return HttpResponse('Invalid header found. Data not saved!') return HttpResponse('Invalid header found. Data not saved!')
except SMTPException: except SMTPException:
modell.delete() obj.delete()
return HttpResponse('Error in sending mails (probably wrong adress?). Data not saved!') return HttpResponse('Error in sending mails (probably wrong address?). Data not saved!')
def get_email_context(self, obj, data):
return {
'data': data,
'urls': self.get_urls(obj),
'form_data': self.get_form_data(obj, data),
'applicant_name': self.get_recipient_name(obj, data),
'type_label': self.sanitize_label(self.type_info.label),
}
return done(self.request) def get_urls(self, obj, **urls):
urls['admin'] = self.get_absolute_url(admin_url_name(obj, 'change'), obj.id)
if isinstance(obj, Project):
urls['authorize'] = urls['deny'] = None
else:
urls['authorize'] = self.get_absolute_url('authorize', self.type_info.code, obj.id)
urls['deny'] = self.get_absolute_url('deny', self.type_info.code, obj.id)
return urls
@staticmethod
def get_absolute_url(view, *args):
return urljoin(settings.EMAIL_URL_PREFIX, reverse(view, args=args))
def get_form_data(self, obj, data):
return {
self.sanitize_label(field.label): self.format_value(field.field, field.initial)
for field in self.type_info.form_class(initial=data)
}
@staticmethod
def sanitize_label(label: str):
label = strip_tags(label)
words = str.split(label)
return ' '.join(words)
@staticmethod
def format_value(field: Field, value):
if isinstance(field, ProductCategoryFormField):
value = get_text_list(value, 'und')
elif isinstance(field, ChoiceField):
choices = flatten_choices(field.choices)
value = dict(choices).get(value, value)
elif isinstance(value, bool):
value = '' if value else ''
elif isinstance(value, datetime.date):
value = date_format(value)
elif value in field.empty_values:
value = ''
return value
def send_email(self, kind, template_name, subject, recipient, context, *, fail_silently=False):
email = build_email(template_name, context, subject, recipient)
collect_and_attach(email, kind, self.type_code)
return email.send(fail_silently)
@staticmethod
def get_recipient_name(obj, data):
for field in 'username', 'realname', 'email':
if name := getattr(obj, field, None) or data.get(field):
return name
return 'Unbekannt'

26
requirements.txt Executable file → Normal file
View File

@ -1,20 +1,6 @@
asgiref==3.2.10 Authlib==1.6.1
Authlib==1.2.1 Django==5.2.5
certifi==2023.7.22 gunicorn==23.0.0
cffi==1.16.0 mysqlclient==2.2.7
chardet==5.2.0 python-dotenv==1.1.1
charset-normalizer==3.3.0 whitenoise==6.9.0
cryptography==41.0.4
Django==3.1.2
django-formtools==2.4
gunicorn==20.0.4
idna==3.4
mysqlclient==2.1.1
pycparser==2.21
pytz==2023.3.post1
requests==2.31.0
six==1.16.0
sqlparse==0.4.3
typing_extensions==4.8.0
urllib3==2.0.6
whitenoise==6.2.0