Compare commits

..

No commits in common. "feature-cc" and "master" have entirely different histories.

93 changed files with 2216 additions and 3563 deletions

View File

@ -1,23 +0,0 @@
[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

View File

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

View File

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

View File

@ -1,28 +0,0 @@
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,13 +2,9 @@
purpose: gather data from intern(WMDE) and extern(volunteers) forms to create a database ('förderdatenbank') and send emails with links for a questionary.
## manual development setup
## installation and development setup
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.
ln -sr foerderbarometer/settings_development.py foerderbarometer/settings.py
build the database with
@ -25,26 +21,9 @@ run the development server with
access via
http://localhost:8000/
http://localhost:8000/intern/ (login required)
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
The admin page is the standard admin page delivered by django but with two additional functionalities:
@ -54,24 +33,29 @@ entries to a csv file
- There is a new button in the bottom of every Project to "save as new"
## mail attachments
## versions used in development
For all mails, attachments can be defined as URLs. These URLs are fetched and chached when sending the mail and attached to the mail.
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:
* `TYPE_BIB` Bibliotheksstipendium
* `TYPE_ELIT` eLiteraturstipendium
* `TYPE_SOFT` Softwarestipendium
* `TYPE_MAIL` E-Mail-Adresse
* `TYPE_IFG` Kostenübernahme IFG-Anfrage
* `TYPE_LIT` Literaturstipendium
* `TYPE_LIST` Mailingliste
* `TYPE_TRAV` Reisekosten
* `TYPE_VIS` Visitenkarten
* `TYPE_PROJ` Projektförderung
For further details see `foerderbarometer/settings.py`
asgiref==3.2.10
Django==3.1.2
django-formtools==2.4
gunicorn==20.0.4
mysqlclient==2.1.1
sqlparse==0.4.3
whitenoise==6.2.0
asgiref==3.2.10
Authlib==1.2.1
certifi==2023.7.22
cffi==1.16.0
chardet==5.2.0
charset-normalizer==3.3.0
cryptography==41.0.4
idna==3.4
pycparser==2.21
pytz==2023.3.post1
requests==2.31.0
six==1.16.0
typing_extensions==4.8.0
urllib3==2.0.6
## testing
@ -81,26 +65,16 @@ run some tests with
## production setup
cp .env.production.example .env
ln -sr foerderbarometer/settings_production.py foerderbarometer/settings.py
edit .env and fill in the missing secrets
edit /secrets.json to contain something similar to
SECRET_KEY
DATABASE_PASSWORD
EMAIL_HOST_USER
EMAIL_HOST_PASSWORD
OAUTH_CLIENT_NAME
OAUTH_CLIENT_ID
OAUTH_CLIENT_SECRET
{
"DATABASE_PASSWORD": "THIS IS TOP SECRET!",
"SECRET_KEY": "THIS IS ANOTHER SECRET!"
}
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
edit foerderbarometer/settings_production.py according to your database setup (tested with MariaDB 10.0.36)
run the following commands:

View File

@ -1,33 +0,0 @@
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

@ -1,33 +0,0 @@
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

@ -1,195 +0,0 @@
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

@ -0,0 +1,203 @@
"""
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'

151
foerderbarometer/settings.py_old Executable file
View File

@ -0,0 +1,151 @@
"""
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

@ -0,0 +1,158 @@
"""
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

@ -0,0 +1,175 @@
"""
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

@ -0,0 +1,171 @@
"""
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,49 +1,14 @@
import csv
from django.contrib import admin
from django.db import models
from django.http import HttpResponse
from input.utils.list import reorder_value
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
from .models import Account, Project, HonoraryCertificate, Library, IFG, Travel,\
Email, BusinessCard, List, Literature
def export_as_csv(self, request, queryset):
meta = self.model._meta
field_names = [field.name for field in meta.fields]
@ -57,160 +22,46 @@ def export_as_csv(self, request, queryset):
return response
export_as_csv.short_description = "Ausgewähltes zu CSV exportieren"
admin.site.add_action(export_as_csv)
@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):
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
save_as = True
form = ProjectAdminForm
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')
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'
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:
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)
class BusinessCardAdmin(WMDEAdmin):
class BusinessCardAdmin(admin.ModelAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project')
list_display_links = ('realname', 'service_id')
# action = ['export_as_csv']
date_hierarchy = 'granted_date'
readonly_fields = ['service_id']
class Media:
js = ('dropdown/js/base.js', 'dropdown/js/otrs_link.js')
js = ('dropdown/js/base.js',)
@admin.register(Literature)
class LiteratureAdmin(WMDEAdmin):
class LiteratureAdmin(admin.ModelAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display = ('realname', 'service_id', 'granted', 'granted_date')
list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date'
readonly_fields = ['service_id']
class Media:
js = ('dropdown/js/otrs_link.js',)
@admin.register(Account)
class AccountAdmin(admin.ModelAdmin):
@ -221,43 +72,23 @@ class AccountAdmin(admin.ModelAdmin):
class HonoraryCertificateAdmin(admin.ModelAdmin):
save_as = True
search_fields = ['realname', 'granted', 'project__name', 'project__pid']
list_display = ('realname', 'granted', 'project')
list_display = ('realname', 'granted','project')
date_hierarchy = 'granted_date'
autocomplete_fields = ['project']
class Media:
js = ('dropdown/js/otrs_link.js',)
@admin.register(Library, ELiterature, Software)
class LibraryAdmin(WMDEAdmin):
@admin.register(Library)
class LibraryAdmin(admin.ModelAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date')
list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date'
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)
class IFGAdmin(WMDEAdmin):
class IFGAdmin(admin.ModelAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date')
@ -265,44 +96,51 @@ class IFGAdmin(WMDEAdmin):
date_hierarchy = 'granted_date'
readonly_fields = ['service_id']
class Media:
js = ('dropdown/js/otrs_link.js',)
@admin.register(Travel)
class TravelAdmin(WMDEAdmin):
class TravelAdmin(admin.ModelAdmin):
save_as = True
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')
date_hierarchy = 'project_end'
readonly_fields = ('project_end_quartal', 'project_end')
autocomplete_fields = ['project']
readonly_fields = ['service_id', 'project_end', 'project_end_quartal']
readonly_fields = ['service_id']
class Media:
js = ('dropdown/js/otrs_link.js',)
@admin.register(Email)
class EmailAdmin(WMDEAdmin):
class EmailAdmin(admin.ModelAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display = ('realname', 'service_id', 'granted', 'granted_date')
list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date'
radio_fields = {'adult': admin.VERTICAL}
readonly_fields = ['service_id']
class Media:
js = ('dropdown/js/base.js', 'dropdown/js/otrs_link.js')
js = ('dropdown/js/base.js',)
@admin.register(List)
class ListAdmin(WMDEAdmin):
class ListAdmin(admin.ModelAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display = ('realname', 'service_id', 'granted', 'granted_date')
list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date'
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,
# ])

16
input/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
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,198 +1,89 @@
from django import forms
from django.conf import settings
from django.db import models
from django.forms import ModelForm, DateField, ChoiceField, RadioSelect, BooleanField
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.safestring import mark_safe
from django.utils.translation import gettext_lazy as trans
from .models import (
Project,
ProjectCategory,
WikimediaProject,
IFG,
Library,
ELiterature,
Software,
Travel,
Email,
Literature,
List,
BusinessCard,
)
from .models import Project, Volunteer, ConcreteVolunteer, Extern, ConcreteExtern, IFG, Library, TYPE_CHOICES,\
HonoraryCertificate, Travel, Email, Literature, List,\
BusinessCard
from .settings import DATAPROTECTION, FOERDERRICHTLINIEN, NUTZUNGSBEDINGUNGEN
from . import settings
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'
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):
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
# start = DateField(widget=AdminDateWidget())
class Meta:
model = Project
fields = [
'realname',
'email',
'name',
'description',
'categories',
'categories_other',
'wikimedia_projects',
'wikimedia_projects_other',
'start',
'end',
'participants_estimated',
'page',
'group',
'location',
'cost',
'insurance',
'notes',
]
labels = {
'cost': 'Kosten in Euro',
'insurance': 'Haftpflicht- und Unfallversicherung gewünscht',
'participants_estimated': 'Voraussichtliche Zahl der Teilnehmenden',
}
widgets = {
'start': AdminDateWidget,
'end': AdminDateWidget,
}
exclude = ('pid', 'project_of_year', 'finance_id','granted', 'granted_date', 'realname', 'email',\
'end_mail_send', 'status', 'persons', 'survey_mail_date', 'mail_state')
widgets = {'start': AdminDateWidget(),
'end': AdminDateWidget(),}
class Media:
css = {
'all': ('css/dateFieldNoNowShortcutInTravels.css',)
js = ('dropdown/js/otrs_link.js',)
class ExternForm(FdbForm):
choice = ChoiceField(choices=TYPE_CHOICES.items(), widget=RadioSelect,
label='Was möchtest Du beantragen?')
check = 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",
DATAPROTECTION, FOERDERRICHTLINIEN))
class Meta:
model = ConcreteExtern
exclude = ('username', 'granted', 'granted_date', 'survey_mail_send', 'service_id', 'survey_mail_date', 'mail_state')
INTERN_CHOICES = {'PRO': 'Projektsteckbrief',
'HON': 'Ehrenamtsbescheinigung, Akkreditierung oder Redaktionsbestätigung',
'TRAV': 'Reisekostenerstattung'}
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')
}
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):
class TravelForm(FdbForm):
# 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
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['project_name'].required = True
self.fields['transport'].required = True
self.fields['travelcost'].required = True
self.fields['travelcost'].initial = None
self.fields['checkin'].required = True
self.fields['checkout'].required = True
self.fields['hotel'].required = True
class Meta:
model = Travel
fields = [
'realname',
'email',
'project_name',
'transport',
'travelcost',
'checkin',
'checkout',
'hotel',
'notes',
]
labels = {
'checkin': 'Datum der Anreise',
'checkout': 'Datum der Abreise',
}
widgets = {
'checkin': AdminDateWidget,
'checkout': AdminDateWidget,
}
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' )
widgets = {'checkin': AdminDateWidget(),
'checkout': AdminDateWidget(),}
fields = ['project_name', 'transport', 'travelcost', 'checkin', 'checkout', 'hotel', 'notes']
hotel = ChoiceField(label='Hotelzimmer benötigt:', choices=HOTEL_CHOICES.items(), widget=RadioSelect())
class Media:
js = ('dropdown/js/otrs_link.js',)
@ -200,115 +91,74 @@ class TravelForm(BaseApplicationForm):
'all': ('css/dateFieldNoNowShortcutInTravels.css',)
}
class LibraryForm(BaseApplicationForm):
class LibraryForm(FdbForm):
class Meta:
model = Library
fields = [
'realname',
'email',
'cost',
'library',
'duration',
'notes',
]
labels = {
'cost': 'Kosten in Euro',
}
fields = ['cost', 'library', 'duration', 'notes', 'survey_mail_send']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class HonoraryCertificateForm(FdbForm):
self.fields['library'].label = self._meta.model.LIBRARY_LABEL
self.fields['library'].help_text = self._meta.model.LIBRARY_HELP_TEXT
self.fields['duration'].help_text = self._meta.model.DURATION_HELP_TEXT
class Meta:
model = HonoraryCertificate
fields = ['request_url', 'project']
exclude = ['intern_notes']
class Media:
js = ('dropdown/js/otrs_link.js',)
class ELiteratureForm(LibraryForm):
class Meta(LibraryForm.Meta):
model = ELiterature
class SoftwareForm(LibraryForm):
class Meta(LibraryForm.Meta):
model = Software
class IFGForm(BaseApplicationForm):
class IFGForm(FdbForm):
class Meta:
model = IFG
fields = [
'realname',
'email',
'cost',
'url',
'notes',
]
fields = ['cost', 'url', 'notes']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
class TermsForm(BaseApplicationForm):
terms_accepted_label = 'Ich stimme den <a href="{}">Nutzungsbedingungen</a> zu.'
terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN
class CheckForm(FdbForm):
termstoaccept = NUTZUNGSBEDINGUNGEN
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['terms_accepted'].required = True
self.fields['terms_accepted'].label = format_html(self.terms_accepted_label, self.terms_accepted_url)
self.fields['check'] = BooleanField(
required=True,
label=format_html(
"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:
model = Literature
fields = [
'realname',
'email',
'cost',
'info',
'source',
'notes',
'selfbuy',
'selfbuy_data',
'selfbuy_give_data',
'terms_accepted',
]
fields = ['cost', 'info', 'source', 'notes', 'selfbuy', 'selfbuy_data', 'selfbuy_give_data']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
class Media:
js = ('dropdown/js/literature.js',)
def clean(self):
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
ADULT_CHOICES = {'TRUE': format_html('Ich bin volljährig.'),
'FALSE': format_html('Ich bin noch nicht volljährig.')
}
ADULT_CHOICES = {
'TRUE': mark_safe('Ich bin volljährig.'),
'FALSE': mark_safe('Ich bin noch nicht volljährig.'),
}
class EmailForm(CheckForm):
class EmailForm(TermsForm):
terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE
# this is the code, to change required to false if needed
def __init__(self, *args, **kwargs):
@ -316,28 +166,22 @@ class EmailForm(TermsForm):
self.fields['adult'].required = True
self.fields['other'].required = True
adult = RadioField(label='Volljährigkeit', choices=ADULT_CHOICES)
adult = ChoiceField(label='Volljährigkeit', choices=ADULT_CHOICES.items(), widget=RadioSelect())
# TODO: add some javascript to show/hide other-field
class Meta:
model = Email
fields = [
'realname',
'email',
'domain',
'address',
'other',
'adult',
'terms_accepted',
]
fields = ['domain', 'address', 'other', 'adult']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
class Media:
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
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -346,36 +190,16 @@ class BusinessCardForm(TermsForm):
class Meta:
model = BusinessCard
fields = [
'realname',
'email',
'project',
'data',
'variant',
'url_of_pic',
'send_data_to_print',
'sent_to',
'terms_accepted',
]
exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
fields = ['project', 'data', 'variant', 'url_of_pic', 'send_data_to_print', 'sent_to']
class Media:
js = ('dropdown/js/businessCard.js',)
class ListForm(TermsForm):
terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN
class ListForm(CheckForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN
class Meta:
model = List
fields = [
'realname',
'email',
'domain',
'address',
'terms_accepted',
]
fields = ['domain', 'address']
exclude = ['intern_notes', 'survey_mail_send','mail_state']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['address'].initial = ''

View File

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

View File

@ -1,57 +0,0 @@
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,13 +1,11 @@
from authlib.integrations.base_client import OAuthError
from authlib.integrations.django_client import OAuth
from authlib.oauth2.rfc6749 import OAuth2Token
from django.shortcuts import redirect
from django.utils.deprecation import MiddlewareMixin
from django.conf import settings
from input.models import Extern
from foerderbarometer import settings
from input import views
from input import models
class OAuthMiddleware(MiddlewareMixin):
@ -16,7 +14,7 @@ class OAuthMiddleware(MiddlewareMixin):
self.oauth = OAuth()
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 == '/':
return self.get_response(request)
if settings.OAUTH_URL_WHITELISTS is not None:
@ -39,12 +37,12 @@ class OAuthMiddleware(MiddlewareMixin):
self.clear_session(request)
request.session['token'] = sso_client.authorize_access_token(request)
# print('blub', request.session['token'])
Extern.username = self.get_current_user(sso_client, request)['username']
models.Extern.username = self.get_current_user(sso_client, request)['username']
if self.get_current_user(sso_client, request) is not None:
redirect_uri = request.session.pop('redirect_uri', None)
if redirect_uri is not None:
return redirect(redirect_uri)
return redirect('extern')
return redirect(views.ExternView)
if request.session.get('token', None) is not None:
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)),
('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)),
('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)),
],
options={

View File

@ -45,7 +45,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='email',
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(
model_name='list',

View File

@ -13,7 +13,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='email',
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(
model_name='email',

View File

@ -18,12 +18,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='ifg',
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(
model_name='library',
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(
model_name='library',
@ -33,7 +33,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='literature',
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(
model_name='project',

View File

@ -13,7 +13,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='travel',
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(
model_name='library',

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='travel',
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(
model_name='travel',
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(
model_name='travel',
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

@ -1,113 +0,0 @@
# 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

@ -1,40 +0,0 @@
# 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

@ -1,33 +0,0 @@
# 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

@ -1,77 +0,0 @@
# 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

@ -1,34 +0,0 @@
# 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

@ -1,43 +0,0 @@
# 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

@ -1,43 +0,0 @@
# 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

@ -1,28 +0,0 @@
# 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

@ -1,57 +0,0 @@
# 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

@ -1,18 +0,0 @@
# 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

@ -1,23 +0,0 @@
# 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,55 +1,21 @@
from contextlib import suppress
from datetime import date
from django import forms
from django.contrib.contenttypes.models import ContentType
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.safestring import mark_safe
from foerderbarometer.constants import *
from .settings import ACCOUNTS
EMAIL_STATES = {
'NONE': 'noch keine Mail versendet',
EMAIL_STATES = {'NONE': 'noch keine Mail versendet',
'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet',
'CLOSE': 'die Projektabschlussmail wurde 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
'END': 'alle automatischen Mails, auch surveyMail, wurden versendet'}
class Volunteer(models.Model):
realname = models.CharField(max_length=200, null=True, verbose_name='Realname',
help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', default='')
realname = models.CharField(max_length=200, null=True, verbose_name="Realname",
help_text="Bitte gib deinen Vornamen und deinen Nachnamen ein.", default='')
email = models.EmailField(max_length=200, null=True, verbose_name='E-Mail-Adresse',
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.'))
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.'))
# the following Fields are not supposed to be edited by users
@ -59,6 +25,7 @@ class Volunteer(models.Model):
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')
@classmethod
def set_granted(cl, key, b):
obj = cl.objects.get(pk=key)
@ -70,16 +37,17 @@ class Volunteer(models.Model):
abstract = True
class Extern(Volunteer):
''' abstract basis class for all data entered by extern volunteers '''
username = models.CharField(max_length=200, null=True, verbose_name='Benutzer_innenname',
help_text=mark_safe('Wikimedia Benutzer_innenname'))
help_text=format_html("Wikimedia Benutzer_innenname"))
# the following Fields are not supposed to be edited by users
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
# but maybe there is a better solution?
super().save()
@ -89,470 +57,263 @@ class Extern(Volunteer):
class Meta:
abstract = True
class ConcreteExtern(Extern):
''' needed because we can't initiate abstract base classes in the view'''
pass
class Account(models.Model):
code = models.CharField('Kostenstelle', max_length=5, default='DEF', null=False, primary_key=True)
code = models.CharField('Kostenstelle', max_length=5, default="DEF",
null=False, primary_key = True)
description = models.CharField('Beschreibung', max_length=60, default='NO DESCRIPTION')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Kostenstelle'
verbose_name_plural = 'Kostenstellen'
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
def __str__(self):
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)
return f"{self.code} {self.description}"
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')
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)
end = models.DateField('Erwartetes Projektende', null=True)
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')
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')
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')
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")
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")
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_real = models.IntegerField(blank=True, null=True, verbose_name='Teilnehmende ausgezählt')
insurance = models.BooleanField(default=False, verbose_name='Haftpflichtversicherung')
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')
cost = models.IntegerField(blank=True, null=True, verbose_name='Kosten')
account = models.ForeignKey('Account', on_delete=models.CASCADE, blank=True, null=True, to_field='code', db_constraint=False)
granted_date = models.DateField(blank=True, null=True, verbose_name='Bewilligt am')
granted_from = models.CharField(max_length=100, blank=True, null=True, verbose_name='Bewilligt von')
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')
account = models.ForeignKey('Account', on_delete=models.CASCADE, null=True, to_field='code', db_constraint = False)
granted_from = models.CharField(max_length=100,null=True,verbose_name='Bewilligt von')
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
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')
finance_id = models.CharField(max_length=15, null= True, blank=True)
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")
class Meta:
verbose_name = 'Projekt'
verbose_name_plural = 'Projekte'
def __str__(self):
return f'{self.pid or self.id} {self.name}'
def save(self,*args,**kwargs):
def save(self, *, using=None, **kwargs):
kwargs['using'] = using
generate_finance_id=False
if self.end:
self.end_quartal = f'Q{self.end.month // 4 + 1}'
'''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:
self.end_quartal = ''
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 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 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 = 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.'),
],
})
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))
class ProjectRequest(Project):
# 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'
class Meta:
proxy = True
verbose_name = 'Projekt (beantragt)'
verbose_name_plural = 'Projekte (beantragt)'
super().save()
class ProjectDeclined(Project):
class Meta:
proxy = True
verbose_name = 'Projekt (abgelehnt)'
verbose_name_plural = 'Projekte (abgelehnt)'
def __str__(self):
return f"{self.pid} {self.name}"
class Intern(Volunteer):
'''abstract base class for data entry from /intern (except Project)'''
request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
abstract = True
class ConcreteVolunteer(Volunteer):
''' needed because we can't initiate abstract base classes in the view'''
pass
class HonoraryCertificate(Intern):
''' this class is also used for accreditations '''
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):
return f'Bescheinigung für {self.realname}'
return "Certificate for " + self.realname
TRANSPORT_CHOICES = {
'BAHN': 'Bahn',
TRANSPORT_CHOICES = {'BAHN': 'Bahn',
'NONE': 'Keine Fahrtkosten',
'OTHER': 'Sonstiges (mit Begründung)',
}
'OTHER': 'Sonstiges (mit Begründung)'}
PAYEDBY_CHOICES = {
'WMDE': 'WMDE',
'REQU': 'Antragstellender Mensch',
}
PAYEDBY_CHOICES = {'WMDE': 'WMDE',
'REQU': 'Antragstellender Mensch'}
HOTEL_CHOICES = {
'TRUE': mark_safe('Hotelzimmer benötigt'),
'FALSE': mark_safe('Kein Hotelzimmer benötigt'),
}
HOTEL_CHOICES = {'TRUE': format_html('Hotelzimmer benötigt'),
'FALSE': format_html('Kein Hotelzimmer benötigt')
}
from django.contrib.contenttypes.models import ContentType
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 = 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')
transport = models.CharField(max_length=5, choices=TRANSPORT_CHOICES.items(), default='BAHN', verbose_name='Transportmittel')
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:')
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')
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_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')
request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
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
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")
class Meta:
verbose_name = 'Reisekosten'
verbose_name_plural = 'Reisekosten'
from django.db.models.signals import pre_save
from django.dispatch import receiver
def __str__(self):
return f'Reisekosten für {self.realname}'
@receiver(pre_save, sender=Travel, dispatch_uid="get_project_end")
def getProjectEnd(sender, instance, **kwargs):
#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:
instance.project_end = instance.project.end
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
class Grant(RequestUrlMixin, Extern):
#abstract base class for Library and IFG
class Grant(Extern):
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',
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:
abstract = True
def type_link(path, label):
return format_html(
format_string='<a href="{href}" target="_blank" rel="noopener">{label}</a>',
href=f'https://de.wikipedia.org/wiki/Wikipedia:Förderung/{path}',
label=label,
)
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]
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>'),
'ELIT': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#eLiteraturstipendium" target="_blank" rel="noopener">eLiteraturstipendium</a>'),
'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>'),
'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>'),
'LIT': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#Literaturstipendium" target="_blank" rel="noopener">Literaturstipendium</a>'),
'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>'),
}
# same model is used for Library, ELitStip and Software!
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(max_length=4, choices=LIBRARY_TYPE_CHOICES, default=TYPE_BIB)
type = models.CharField(
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)
duration = models.CharField(max_length=100, verbose_name='Dauer')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Bibliotheksstipendium'
verbose_name_plural = 'Bibliotheksstipendien'
duration = models.CharField(max_length=100, verbose_name="Dauer")
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
def __str__(self):
return self.library
def save(self, **kwargs):
self.type = self.TYPE
return super().save(**kwargs)
SELFBUY_CHOICES = {'TRUE': format_html('Ich möchte das Werk selbst kaufen und per Kostenerstattung bei Wikimedia Deutschland abrechnen.'),
'FALSE': format_html('Ich möchte, dass Wikimedia Deutschland das Werk für mich kauft'),
}
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):
class Literature(Grant):
info = models.CharField(max_length=500, verbose_name='Informationen zum Werk',
help_text=mark_safe('Bitte gib alle Informationen zum benötigten Werk an,<br>\
die eine eindeutige Identifizierung ermöglichen (Autor, Titel, Verlag, ISBN, ...)'))
help_text=format_html("Bitte gib alle Informationen zum benötigten Werk an,<br>\
die eine eindeutige Identifizierung ermöglichen (Autor, Titel, Verlag, ISBN, ...)"))
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_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_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_data = models.TextField(max_length=1000, verbose_name='Persönliche Daten sowie Adresse', default='',\
help_text=mark_safe('Bitte gib hier alle persönlichen Daten an, die wir benötigen, um das Werk<br>\
help_text=format_html("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>\
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')
class Meta:
verbose_name = 'Literaturstipendium'
verbose_name_plural = 'Literaturstipendien'
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")
class IFG(Grant):
url = models.URLField(max_length=2000, verbose_name='URL',
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')
class Meta:
verbose_name = 'IFG-Anfrage'
verbose_name_plural = 'IFG-Anfragen'
url = models.URLField(max_length=2000, verbose_name="URL",
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")
def __str__(self):
return f'IFG-Anfrage von {self.realname}'
return "IFG-Anfrage von " + self.realname
DOMAIN_CHOICES = {
'PEDIA': '@wikipedia.de',
DOMAIN_CHOICES = {'PEDIA': '@wikipedia.de',
'BOOKS': '@wikibooks.de',
'QUOTE': '@wikiquote.de',
'SOURCE': '@wikisource.de',
'VERSITY': '@wikiversity.de',
}
'VERSITY': '@wikiversity.de',}
class Domain(RequestUrlMixin, Extern):
class Domain(Extern):
domain = models.CharField(max_length=10,
choices=DOMAIN_CHOICES.items(),
default='PEDIA')
@ -560,47 +321,31 @@ class Domain(RequestUrlMixin, Extern):
class Meta:
abstract = True
MAIL_CHOICES = {
'REALNAME': 'Vorname.Nachname',
MAIL_CHOICES = {'REALNAME': 'Vorname.Nachname',
'USERNAME': 'Username',
'OTHER': 'Sonstiges',
}
'OTHER': 'Sonstiges:'}
ADULT_CHOICES = {
'TRUE': mark_safe('Ich bin volljährig.'),
'FALSE': mark_safe('Ich bin noch nicht volljährig.'),
}
ADULT_CHOICES = {'TRUE': format_html('Ich bin volljährig.'),
'FALSE': format_html('Ich bin noch nicht volljährig.')
}
class Email(TermsConsentMixin, Domain):
class Email(Domain):
address = models.CharField(max_length=50,
choices=MAIL_CHOICES.items(),
default='USERNAME', verbose_name='Adressbestandteil',
help_text=mark_safe('Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.'))
help_text=format_html("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')
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')
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')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
class Meta:
verbose_name = 'E-Mail-Adresse'
verbose_name_plural = 'E-Mail-Adressen'
class List(TermsConsentMixin, Domain):
class List(Domain):
address = models.CharField(max_length=50, default='NO_ADDRESS',
verbose_name='Adressbestandteil für Projektmailingliste',
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')
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."))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
class Meta:
verbose_name = 'Mailingliste'
verbose_name_plural = 'Mailinglisten'
PROJECT_CHOICE = {
'PEDIA': 'Wikipedia',
PROJECT_CHOICE = {'PEDIA': 'Wikipedia',
'SOURCE': 'Wikisource',
'BOOKS': 'Wikibooks',
'QUOTE': 'Wikiquote',
@ -608,53 +353,32 @@ PROJECT_CHOICE = {
'VOYAGE': 'Wikivoyage',
'DATA': 'Wikidata',
'NEWS': 'Wikinews',
'COMMONS': 'Wikimedia Commons',
}
'COMMONS': 'Wikimedia Commons'}
BC_VARIANT = {
'PIC': 'mit Bild',
'NOPIC': 'ohne Bild',
}
BC_VARIANT = {'PIC': 'mit Bild',
'NOPIC': 'ohne Bild'}
class BusinessCard(RequestUrlMixin, TermsConsentMixin, Extern):
class BusinessCard(Extern):
project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(),
default='PEDIA', verbose_name='Wikimedia-Projekt',
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='',
help_text=mark_safe('Bitte gib hier alle persönlichen Daten an, und zwar genau so,<br>\
help_text=format_html("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>\
(z.B. Vorname Nachname, Benutzer:/Benutzerin:, Benutzer-/-innenname, Anschrift,<br>\
Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.<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>\
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(),
default='NOPIC', verbose_name='Variante',
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>'))
help_text=format_html('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>' ))
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',
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=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')
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,
}
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.'))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")

78
input/settings.py Executable file
View File

@ -0,0 +1,78 @@
# 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'

41
input/settings.py.old Executable file
View File

@ -0,0 +1,41 @@
# 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',}

View File

@ -1,24 +0,0 @@
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

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

View File

@ -1,21 +0,0 @@
.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

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

View File

@ -1,32 +0,0 @@
(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

@ -1,37 +0,0 @@
{% 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

@ -0,0 +1,71 @@
{% 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

@ -1,44 +0,0 @@
{% 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

@ -1,34 +0,0 @@
{% 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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,58 @@
<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

@ -0,0 +1,52 @@
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

@ -0,0 +1,34 @@
<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

@ -0,0 +1,29 @@
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,38 +1,69 @@
{% 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 %}
<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' %}">
{% csrf_token %}
<div class="page-centered">
<div class="spacer-5"></div>
<p role="heading" aria-level="1">
Herzlich willkommen im Förderanfrageportal von Wikimedia Deutschland!
</p>
<div class="spacer-5"></div>
<p>
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c4/Figuren_klein.jpg">
</p>
<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">
<center>
<style>
ul > li {
list-style-type: none;
}
ul {
padding-left: 10;
}
label.required::after {
content: ' *';
color: red;
}
.div15 {
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.
</p>
<div class="spacer-5"></div>
<a href="/extern" class="button button-login">Anmelden</a>
<div class="spacer-5"></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 interessierte Hacker gibts auch den
<a href="https://srcsrv.wikimedia.de/beba/foerderbarometer">Sourcecode</a> zum Formular und was damit passiert.
<p>
<p>
<div class="div5"></div>
<div class="button button1"><a href="/extern"><div class="button1_text">Anmelden</div></a></div>
<div class="div5"></div>
<div class="div5"></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 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>
</p>
</div>
<p>
</center>

View File

@ -1,27 +0,0 @@
{% 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

@ -1,21 +0,0 @@
<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

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

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
Informationen findest du unter dem folgenden Link:<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>
<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>
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
Informationen findest du unter dem folgenden Link:
{{SURVEY_PREFIX}}{% if type == 'PRO' %}O{% else %}I{% endif %}=1&{{pid}}=1
{{SURVEYPREFIX}}{% 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

View File

@ -1,26 +0,0 @@
<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

@ -1,12 +0,0 @@
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

@ -1,32 +0,0 @@
<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

@ -1,22 +0,0 @@
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/).

155
input/tests.py Executable file
View File

@ -0,0 +1,155 @@
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')

View File

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

View File

@ -1,111 +0,0 @@
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)

View File

@ -1,99 +0,0 @@
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}')

View File

@ -1,197 +0,0 @@
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,28 +1,14 @@
from django.urls import path, include
from django.views.i18n import JavaScriptCatalog
from .views import (
index,
done,
export,
authorize,
deny,
ApplicationView,
ApplicationStartView,
ProjectFundingInfoView,
)
from django.urls import path
from .views import ExternView, index, done, authorize, deny, InternView, export
from django.contrib import admin
urlpatterns = [
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('export', export, name='export'),
path('authorize/<str:choice>/<int:pk>', authorize, name='authorize'),
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'),
]

View File

@ -1,18 +0,0 @@
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

@ -1,54 +0,0 @@
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

View File

@ -1,24 +0,0 @@
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

@ -1,34 +0,0 @@
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

@ -1,98 +0,0 @@
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

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

View File

@ -1,42 +0,0 @@
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)

View File

@ -1,152 +0,0 @@
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,373 +1,301 @@
import datetime
from dataclasses import dataclass
from datetime import date
from smtplib import SMTPException
from typing import Optional
from urllib.parse import urljoin
from django.forms import ChoiceField, Field
from django.shortcuts import render, redirect
from django.http import HttpResponse, Http404
from django.urls import reverse
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.shortcuts import render
from django.forms import modelformset_factory
from django.http import HttpResponse
from formtools.wizard.views import CookieWizardView
from django.core.mail import send_mail, BadHeaderError, EmailMultiAlternatives
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.views.generic import TemplateView
from django.views.generic.edit import FormView
from django.utils.html import strip_tags
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.html import format_html
from django.utils.translation import gettext as _
from input.utils.admin import admin_url_name
from input.utils.mail import build_email, collect_and_attach
from .forms import ProjectForm, ExternForm, LibraryForm, IFGForm, LiteratureForm,\
HonoraryCertificateForm, InternForm, TravelForm, EmailForm,\
ListForm, BusinessCardForm, INTERN_CHOICES
from .models import Project, TYPE_CHOICES, Library, Literature, Travel, IFG, BusinessCard, Email, List
from .settings import IF_EMAIL
from .forms import (
BaseApplicationForm,
ProjectForm,
LibraryForm,
ELiteratureForm,
SoftwareForm,
IFGForm,
LiteratureForm,
TravelForm,
EmailForm,
ListForm,
BusinessCardForm,
)
from .models import (
MODELS,
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:
def auth_deny(choice,pk,auth):
if choice in ('BIB', 'ELIT', 'SOFT'):
Library.set_granted(pk,auth)
elif choice == 'LIT':
Literature.set_granted(pk,auth)
elif choice == 'IFG':
IFG.set_granted(pk,auth)
elif choice == 'TRAV':
Travel.set_granted(pk,auth)
elif choice == 'VIS':
BusinessCard.set_granted(pk,auth)
elif choice == 'MAIL':
Email.set_granted(pk,auth)
elif choice == 'LIST':
List.set_granted(pk,auth)
else:
return HttpResponse(f'ERROR! UNKNOWN CHOICE TYPE! {choice}')
MODELS[choice].set_granted(pk, auth)
return False
@login_required
def export(request):
'''export the project database to a csv'''
return HttpResponse('WE WANT CSV!')
@login_required
def authorize(request, choice, pk):
'''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.'''
if ret := auth_deny(choice, pk, True):
ret = auth_deny(choice, pk, True)
if ret:
return ret
else:
return HttpResponse(f'AUTHORIZED! choice: {choice}, pk: {pk}')
return HttpResponse(f"AUTHORIZED! choice: {choice}, pk: {pk}")
@login_required
def deny(request, choice, pk):
'''If IF denies a support they click a link in a mail which leads here
We write the granted field in the database here.'''
if ret := auth_deny(choice, pk, False):
ret = auth_deny(choice, pk, False)
if ret:
return ret
else:
return HttpResponse(f'DENIED! choice: {choice}, pk: {pk}')
return HttpResponse(f"DENIED! choice: {choice}, pk: {pk}")
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):
return render(request, 'input/index.html')
class InternView(LoginRequiredMixin, CookieWizardView):
'''This View is for WMDE-employees only'''
class ApplicationStartView(TemplateView):
template_name = 'input/forms/extern.html'
extra_context = {'applications': APPLICATIONS}
template_name = 'input/extern.html'
form_list = [InternForm, ProjectForm]
def post(self, request, *args, **kwargs):
if url := request.POST.get('url'):
if url_has_allowed_host_and_scheme(url, None):
return redirect(url)
def get_form(self, step=None, data=None, files=None):
'''this function determines which part of the multipart form is
displayed next'''
return self.get(request, *args, **kwargs)
class ProjectFundingInfoView(TemplateView):
template_name = 'input/info_project_funding_gt_1000.html'
class ApplicationView(FormView):
"""
View for all application types.
- Renders the generic form template.
- Handles saving the submitted form to the database.
- Adds extra fields from the session or request type if needed.
- Applies optional help_text overrides for certain fields.
- Sends confirmation mail to the applicant.
- Sends notification mail to the internal IF address.
- Returns the "done" response after successful processing.
"""
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):
return super().get_context_data(**kwargs, type_label=self.type_info.label)
def get_form(self, form_class=None):
"""Return the form instance and inject custom help_texts if defined for this type."""
form = super().get_form(form_class)
# Apply help_text overrides if defined for this type_code
if help_texts := self.type_info.help_texts:
for field, text in help_texts.items():
if field in form.fields:
form.fields[field].help_text = mark_safe(text)
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: {INTERN_CHOICES[choice]}')
if choice == 'HON':
form = HonoraryCertificateForm(data)
elif choice == 'PRO':
form = ProjectForm(data)
elif choice == 'TRAV':
form = TravelForm(data)
else:
raise RuntimeError(f'ERROR! UNKNOWN FORMTYPE {choice} in InternView')
self.choice = choice
else:
form = super().get_form(step, data, files)
form.fields['realname'].help_text = format_html("Vor- und Zuname (Realname), Wer hat das Projekt beantragt?<br>\
Wer ist Hauptansprechperson? Bei WMDE-MAs immer (WMDE),<br>\
bei externen Partnern (PART) hinzufügen.")
return form
def form_valid(self, form):
"""
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.
"""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if hasattr(self, 'choice'):
context["choice"] = INTERN_CHOICES[self.choice]
return context
data = self.prepare_data(form)
obj = self.save_obj(form, data)
def done(self, form_list, **kwargs):
print('InternView.done() reached')
# gather data from all forms
data = {}
for form in form_list:
data = {**data, **form.cleaned_data}
if response := self.send_mail(obj, data):
return response
if data['choice'] == 'LIT':
if data['selfbuy'] == 'TRUE':
data['selfbuy_give_data'] = 'False'
# write data to database
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':
form.selfbuy_give_data = data['selfbuy_give_data']
form.save()
return done(self.request)
def prepare_data(self, form):
# Collect cleaned data and mark the current type
return {**form.cleaned_data, 'choice': self.type_code}
# these where used as labels in the second form TYPE_CHOICES is used for the first form and the
# text above the second form. only used for BIB, SOFT, ELIT in the moment
LABEL_CHOICES = {'BIB': format_html('Bibliothek'),
'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'),
}
def save_obj(self, form, data):
# Save model instance
HELP_CHOICES = {'BIB': format_html("In welchem Zeitraum möchtest du recherchieren oder<br>wie lange ist der Bibliotheksausweis gültig?"),
'ELIT': "Wie lange gilt der Zugang?",
'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)
# we have to copy the data from the first form here
# this is a bit ugly code. can we copy this without explicit writing?
# 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:
if data['choice'] == 'LIT':
modell.selfbuy_give_data = data['selfbuy_give_data']
modell.save()
modell.realname = data['realname']
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']
if hasattr(form, 'save_m2m'):
form.save_m2m()
form.save()
return modell
def send_mail(self, obj, data):
# Prepare minimal mail context and send mails
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)
# add some data to context for mail templates
data['pk'] = modell.pk
data['urlprefix'] = settings.URLPREFIX
data['grant'] = ('LIT', 'SOFT', 'ELIT', 'BIB', 'IFG')
data['DOMAIN'] = ('MAIL', 'LIST')
data['typestring'] = TYPE_CHOICES[data['choice']]
# we need to send the following mails here:
context = { 'data': data }
try:
self.send_email('applicant', 'ifg_volunteer_mail', applicant_subject, data['email'], context)
self.send_email('staff', 'if_mail', staff_subject, settings.IF_EMAIL, context)
# - mail with entered data to the Volunteer
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:
obj.delete()
modell.delete()
return HttpResponse('Invalid header found. Data not saved!')
except SMTPException:
obj.delete()
return HttpResponse('Error in sending mails (probably wrong address?). Data not saved!')
modell.delete()
return HttpResponse('Error in sending mails (probably wrong adress?). 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),
}
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'
return done(self.request)

26
requirements.txt Normal file → Executable file
View File

@ -1,6 +1,20 @@
Authlib==1.6.1
Django==5.2.5
gunicorn==23.0.0
mysqlclient==2.2.7
python-dotenv==1.1.1
whitenoise==6.9.0
asgiref==3.2.10
Authlib==1.2.1
certifi==2023.7.22
cffi==1.16.0
chardet==5.2.0
charset-normalizer==3.3.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