Compare commits

..

43 Commits

Author SHA1 Message Date
Roman Fen d1ae90b6a1 moved inline styles to base.css and button.css and adjusted button styles 2025-10-23 11:27:38 +00:00
Roman d933f5a32b Add static info page for project funding ≥ 1,000 € 2025-10-23 11:27:38 +00:00
Roman 6698d6a6f3 Refactor CheckForm to use model field 'terms_accepted' instead of separate 'check' field 2025-10-23 11:27:38 +00:00
Roman 0b9fb801bd Add migration 0099: add terms_accepted field to BusinessCard, Email, List, and Literature 2025-10-23 11:27:38 +00:00
Roman 0efaa6910d Add custom save() in CheckForm to map 'check' field to model's 'terms_accepted' 2025-10-23 11:27:38 +00:00
Roman 32f8a8f50f add terms_accepted to admin list display for BusinessCard, Literature, Email and List 2025-10-23 11:27:38 +00:00
Roman f4698c3894 add terms_accepted field to Email, List, Literature and BusinessCard via mixin 2025-10-23 11:27:38 +00:00
Oliver Zander 0839439671 fixed literature test 2025-10-23 11:27:38 +00:00
Oliver Zander 295f41ff75 added tests for internal views 2025-10-23 11:27:38 +00:00
Oliver Zander c295f7182b skip finance id test 2025-10-23 11:27:38 +00:00
Oliver Zander 945550b8f6 readded test module file 2025-10-23 11:27:38 +00:00
Oliver Zander e44e0eb31c renamed model test file accordingly 2025-10-23 11:27:38 +00:00
Oliver Zander 52315d4378 added whitespace 2025-10-23 11:27:38 +00:00
Oliver Zander 17f763ba66 clean up tests 2025-10-23 11:27:38 +00:00
Oliver Zander 6306567ebd removed intern view 2025-10-23 11:27:38 +00:00
Oliver Zander 165ad050ad use a table form renderer 2025-10-23 11:27:38 +00:00
Andreas Gohr 5696072604 add docker compose setup for development 2025-10-23 11:27:38 +00:00
Oliver Zander 4c458c16d7 separate library, e-literature and software by using proxy models 2025-10-23 11:27:38 +00:00
Oliver Zander 60a5538033 set explicit library type choices 2025-10-23 11:27:38 +00:00
Oliver Zander d40c53bc80 added missing migration for real- & username fields 2025-10-23 11:27:38 +00:00
Oliver Zander e28d68516a prettify type choices 2025-10-23 11:27:38 +00:00
Oliver Zander 88e2c959d0 use mariadb for testing 2025-10-23 11:27:38 +00:00
Oliver Zander 61dcce3505 added gitlab ci 2025-10-23 11:27:38 +00:00
Oliver Zander 858e8519bd removed duplicate admin path 2025-10-23 11:27:38 +00:00
Oliver Zander fe50d9b465 replaced format_html with mark_safe where no formatting is applied 2025-10-23 11:27:38 +00:00
Oliver Zander edd0eb2205 updated deps, removed third party & unused deps 2025-10-23 11:27:38 +00:00
Oliver Zander 1495621ef0 simplified code 2025-10-23 11:27:38 +00:00
Oliver Zander d3f18b0b93 toggle intern view and tests by setting 2025-10-23 11:27:38 +00:00
Oliver Zander f0c8ca71bb added tests for sendmails command 2025-10-23 11:27:38 +00:00
Oliver Zander b5a0fbde98 added coveragerc 2025-10-23 11:27:38 +00:00
Oliver Zander eaddabbcdd added more tests for extern view 2025-10-23 11:27:38 +00:00
Oliver Zander 0afabe9325 fixed read only fields of travel admin 2025-10-23 11:27:38 +00:00
Oliver Zander d5e6a7c343 auto formatting: removed whitespace 2025-10-23 11:27:38 +00:00
Oliver Zander eb44094639 merged settings & use .env files to configure differences 2025-10-23 11:27:38 +00:00
Oliver Zander e5cea49673 added basic tests for extern view 2025-10-23 11:27:38 +00:00
Oliver Zander 233151968b conditionally set username so it can work local and in tests 2025-10-23 11:27:38 +00:00
Oliver Zander fe8985504e auto formatting: removed whitespace 2025-10-23 11:27:38 +00:00
Oliver Zander 80d1d17ff6 moved tests 2025-10-23 11:27:38 +00:00
Oliver Zander 02ee0097a8 fixed test 2025-10-23 11:27:38 +00:00
Oliver Zander 70c88440aa fixed and improved quarter calculation 2025-10-23 11:27:38 +00:00
Oliver Zander fa8d0ab898 ignore idea config files & mac stuff 2025-10-23 11:27:38 +00:00
Oliver Zander b9fe0c9a1f ignore linked settings file 2025-10-23 11:27:38 +00:00
Oliver Zander e0538bbc9a auto formatting: removed whitespace 2025-10-23 11:27:38 +00:00
50 changed files with 1465 additions and 1563 deletions

23
.coveragerc Normal file
View File

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

5
.env.develop.example Normal file
View File

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

13
.env.production.example Normal file
View File

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

10
.gitignore vendored
View File

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

28
.gitlab-ci.yml Normal file
View File

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

View File

@ -2,9 +2,13 @@
purpose: gather data from intern(WMDE) and extern(volunteers) forms to create a database ('förderdatenbank') and send emails with links for a questionary. purpose: gather data from intern(WMDE) and extern(volunteers) forms to create a database ('förderdatenbank') and send emails with links for a questionary.
## installation and development setup ## manual development setup
ln -sr foerderbarometer/settings_development.py foerderbarometer/settings.py cp .env.develop.example .env
By default a SQLite database is used for development.
To use a MariaDB change `DATABASE_ENGINE` in .env to `mysql` and amend `DATABASE_*` variables according to your setup.
For further information see the production setup below.
build the database with build the database with
@ -24,6 +28,25 @@ access via
http://localhost:8000/intern/ (login required) http://localhost:8000/intern/ (login required)
http://localhost:8000/admin/ (login reqiured) http://localhost:8000/admin/ (login reqiured)
## docker compose development setup
The project comes with a `docker-compose.yml` file to run the project in a containerized environment.
cp .env.develop.example .env
docker compose up
The setup will use a containerized MariaDB database.
Create your superuser account with
docker compose exec django python3 manage.py createsuperuser
You can access the application via
http://localhost:8000/
http://localhost:8000/intern/ (login required)
http://localhost:8000/admin/ (login required)
## additional admin functionality ## additional admin functionality
The admin page is the standard admin page delivered by django but with two additional functionalities: The admin page is the standard admin page delivered by django but with two additional functionalities:
@ -65,16 +88,26 @@ run some tests with
## production setup ## production setup
ln -sr foerderbarometer/settings_production.py foerderbarometer/settings.py cp .env.production.example .env
edit /secrets.json to contain something similar to edit .env and fill in the missing secrets
{ SECRET_KEY
"DATABASE_PASSWORD": "THIS IS TOP SECRET!", DATABASE_PASSWORD
"SECRET_KEY": "THIS IS ANOTHER SECRET!" EMAIL_HOST_USER
} EMAIL_HOST_PASSWORD
OAUTH_CLIENT_NAME
OAUTH_CLIENT_ID
OAUTH_CLIENT_SECRET
edit foerderbarometer/settings_production.py according to your database setup (tested with MariaDB 10.0.36) amend database variables to .env according to your database setup (tested with MariaDB 10.0.36), e.g.
DATABASE_NAME
DATABASE_USER
DATABASE_HOST
DATABASE_PORT
for a full set of all possible env vars have a look at foerderbarometer/settings.py
run the following commands: run the following commands:

33
docker-compose.yaml Normal file
View File

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

View File

@ -0,0 +1,168 @@
import os
from pathlib import Path
from dotenv import load_dotenv
from input.utils.settings import env, password_validators
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',
'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'
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', 'en-us')
USE_TZ = True
TIME_ZONE = env('TIME_ZONE', 'UTC')
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'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,20 @@ import csv
from django.contrib import admin from django.contrib import admin
from django.http import HttpResponse from django.http import HttpResponse
from .models import Account, Project, HonoraryCertificate, Library, IFG, Travel,\ from .models import (
Email, BusinessCard, List, Literature Account,
Project,
HonoraryCertificate,
Library,
ELiterature,
Software,
IFG,
Travel,
Email,
BusinessCard,
List,
Literature,
)
def export_as_csv(self, request, queryset): def export_as_csv(self, request, queryset):
@ -43,9 +55,9 @@ class ProjectAdmin(admin.ModelAdmin):
@admin.register(BusinessCard) @admin.register(BusinessCard)
class BusinessCardAdmin(admin.ModelAdmin): class BusinessCardAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project') search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
# action = ['export_as_csv'] # action = ['export_as_csv']
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
@ -57,7 +69,7 @@ class BusinessCardAdmin(admin.ModelAdmin):
class LiteratureAdmin(admin.ModelAdmin): class LiteratureAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
@ -78,7 +90,8 @@ class HonoraryCertificateAdmin(admin.ModelAdmin):
class Media: class Media:
js = ('dropdown/js/otrs_link.js',) js = ('dropdown/js/otrs_link.js',)
@admin.register(Library)
@admin.register(Library, ELiterature, Software)
class LibraryAdmin(admin.ModelAdmin): class LibraryAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
@ -86,6 +99,21 @@ class LibraryAdmin(admin.ModelAdmin):
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
exclude = ['type']
def get_queryset(self, request):
return super().get_queryset(request).filter(type=self.model.TYPE)
def formfield_for_dbfield(self, db_field, request, **kwargs):
if db_field.name == 'library':
kwargs['label'] = self.model.LIBRARY_LABEL
kwargs['help_text'] = self.model.LIBRARY_HELP_TEXT
elif db_field.name == 'duration':
kwargs['help_text'] = self.model.DURATION_HELP_TEXT
return super().formfield_for_dbfield(db_field, request, **kwargs)
@admin.register(IFG) @admin.register(IFG)
class IFGAdmin(admin.ModelAdmin): class IFGAdmin(admin.ModelAdmin):
@ -103,9 +131,8 @@ class TravelAdmin(admin.ModelAdmin):
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project_end', 'project', 'project_end_quartal') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project_end', 'project', 'project_end_quartal')
list_display_links = ('realname', 'project') list_display_links = ('realname', 'project')
date_hierarchy = 'project_end' date_hierarchy = 'project_end'
readonly_fields = ('project_end_quartal', 'project_end')
autocomplete_fields = ['project'] autocomplete_fields = ['project']
readonly_fields = ['service_id'] readonly_fields = ['service_id', 'project_end', 'project_end_quartal']
class Media: class Media:
js = ('dropdown/js/otrs_link.js',) js = ('dropdown/js/otrs_link.js',)
@ -115,7 +142,7 @@ class TravelAdmin(admin.ModelAdmin):
class EmailAdmin(admin.ModelAdmin): class EmailAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
radio_fields = {'adult': admin.VERTICAL} radio_fields = {'adult': admin.VERTICAL}
@ -128,7 +155,7 @@ class EmailAdmin(admin.ModelAdmin):
class ListAdmin(admin.ModelAdmin): class ListAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']

View File

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

View File

@ -1,15 +1,30 @@
from django.db import models from django.conf import settings
from django.forms import ModelForm, DateField, ChoiceField, RadioSelect, BooleanField from django.forms import ModelForm, ChoiceField, RadioSelect, BooleanField
from django.contrib.admin.widgets import AdminDateWidget from django.contrib.admin.widgets import AdminDateWidget
from django.forms.renderers import DjangoTemplates
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .models import Project, Volunteer, ConcreteVolunteer, Extern, ConcreteExtern, IFG, Library, TYPE_CHOICES,\ from .models import (
HonoraryCertificate, Travel, Email, Literature, List,\ TYPE_CHOICES,
BusinessCard Project,
from .settings import DATAPROTECTION, FOERDERRICHTLINIEN, NUTZUNGSBEDINGUNGEN ConcreteVolunteer,
ConcreteExtern,
IFG,
Library,
ELiterature,
Software,
HonoraryCertificate,
Travel,
Email,
Literature,
List,
BusinessCard,
)
from . import settings
class TableFormRenderer(DjangoTemplates):
form_template_name = 'django/forms/table.html'
class FdbForm(ModelForm): class FdbForm(ModelForm):
@ -39,7 +54,7 @@ class ExternForm(FdbForm):
check = BooleanField(required=True, 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", 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)) settings.DATAPROTECTION, settings.FOERDERRICHTLINIEN))
class Meta: class Meta:
model = ConcreteExtern model = ConcreteExtern
@ -59,14 +74,14 @@ class InternForm(FdbForm):
exclude = ('granted', 'granted_date', 'survey_mail_send', 'survey_mail_date', 'mail_state') exclude = ('granted', 'granted_date', 'survey_mail_send', 'survey_mail_date', 'mail_state')
HOTEL_CHOICES = {'TRUE': format_html('Hotelzimmer benötigt'), HOTEL_CHOICES = {'TRUE': mark_safe('Hotelzimmer benötigt'),
'FALSE': format_html('Kein Hotelzimmer benötigt') 'FALSE': mark_safe('Kein Hotelzimmer benötigt')
} }
class TravelForm(FdbForm): class TravelForm(FdbForm):
# TODO: add some javascript to show/hide other-field # TODO: add some javascript to show/hide other-field
# this is the code, to change required to false if needed # this is the code, to change required to false if needed
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -91,6 +106,7 @@ class TravelForm(FdbForm):
'all': ('css/dateFieldNoNowShortcutInTravels.css',) 'all': ('css/dateFieldNoNowShortcutInTravels.css',)
} }
class LibraryForm(FdbForm): class LibraryForm(FdbForm):
class Meta: class Meta:
@ -98,15 +114,35 @@ class LibraryForm(FdbForm):
fields = ['cost', 'library', 'duration', 'notes', 'survey_mail_send'] fields = ['cost', 'library', 'duration', 'notes', 'survey_mail_send']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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 ELiteratureForm(LibraryForm):
class Meta(LibraryForm.Meta):
model = ELiterature
class SoftwareForm(LibraryForm):
class Meta(LibraryForm.Meta):
model = Software
class HonoraryCertificateForm(FdbForm): class HonoraryCertificateForm(FdbForm):
class Meta: class Meta:
model = HonoraryCertificate model = HonoraryCertificate
fields = ['request_url', 'project'] fields = ['request_url', 'project']
exclude = ['intern_notes'] exclude = ['intern_notes']
class Media: class Media:
js = ('dropdown/js/otrs_link.js',) js = ('dropdown/js/otrs_link.js',)
class IFGForm(FdbForm): class IFGForm(FdbForm):
class Meta: class Meta:
@ -116,26 +152,20 @@ class IFGForm(FdbForm):
class CheckForm(FdbForm): class CheckForm(FdbForm):
termstoaccept = NUTZUNGSBEDINGUNGEN termstoaccept = settings.NUTZUNGSBEDINGUNGEN
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['check'] = BooleanField(
required=True, # Check if the model field 'terms_accepted' is present
label=format_html( if 'terms_accepted' in self.fields:
# Make the field required (HTML5 validation)
self.fields['terms_accepted'].required = True
# Set custom label with link to terms
self.fields['terms_accepted'].label = format_html(
"Ich stimme den <a href='{}'>Nutzungsbedingungen</a> zu", "Ich stimme den <a href='{}'>Nutzungsbedingungen</a> zu",
self.termstoaccept self.termstoaccept
) )
)
"""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): class LiteratureForm(CheckForm):
@ -146,20 +176,20 @@ class LiteratureForm(CheckForm):
self.fields['selfbuy_give_data'].required = True self.fields['selfbuy_give_data'].required = True
class Meta: class Meta:
model = Literature model = Literature
fields = ['cost', 'info', 'source', 'notes', 'selfbuy', 'selfbuy_data', 'selfbuy_give_data'] fields = ['cost', 'info', 'source', 'notes', 'selfbuy', 'selfbuy_data', 'selfbuy_give_data', 'terms_accepted']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
class Media: class Media:
js = ('dropdown/js/literature.js',) js = ('dropdown/js/literature.js',)
ADULT_CHOICES = {'TRUE': format_html('Ich bin volljährig.'), ADULT_CHOICES = {'TRUE': mark_safe('Ich bin volljährig.'),
'FALSE': format_html('Ich bin noch nicht volljährig.') 'FALSE': mark_safe('Ich bin noch nicht volljährig.')
} }
class EmailForm(CheckForm): class EmailForm(CheckForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE termstoaccept = settings.NUTZUNGSBEDINGUNGEN_EMAIL_SERVICE
# this is the code, to change required to false if needed # this is the code, to change required to false if needed
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -173,7 +203,7 @@ class EmailForm(CheckForm):
# TODO: add some javascript to show/hide other-field # TODO: add some javascript to show/hide other-field
class Meta: class Meta:
model = Email model = Email
fields = ['domain', 'address', 'other', 'adult'] fields = ['domain', 'address', 'other', 'adult', 'terms_accepted']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
class Media: class Media:
js = ('dropdown/js/mail.js',) js = ('dropdown/js/mail.js',)
@ -190,8 +220,8 @@ class BusinessCardForm(CheckForm):
class Meta: class Meta:
model = BusinessCard model = BusinessCard
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
fields = ['project', 'data', 'variant', 'url_of_pic', 'send_data_to_print', 'sent_to'] fields = ['project', 'data', 'variant', 'url_of_pic', 'send_data_to_print', 'sent_to', 'terms_accepted']
class Media: class Media:
js = ('dropdown/js/businessCard.js',) js = ('dropdown/js/businessCard.js',)
@ -200,6 +230,5 @@ class ListForm(CheckForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN
class Meta: class Meta:
model = List model = List
fields = ['domain', 'address'] fields = ['domain', 'address', 'terms_accepted']
exclude = ['intern_notes', 'survey_mail_send','mail_state'] exclude = ['intern_notes', 'survey_mail_send','mail_state']

View File

View File

View File

@ -1,18 +1,16 @@
from datetime import date, timedelta from datetime import date, timedelta
import sys
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand
from django.template.loader import get_template from django.template.loader import get_template
from django.core.mail import send_mail, BadHeaderError, EmailMessage from django.core.mail import BadHeaderError
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.conf import settings from django.conf import settings
from input.models import Project, Library, HonoraryCertificate, Travel, Email,\ from input.models import Project, Library, HonoraryCertificate, Travel, Email,\
BusinessCard, List, IFG, Literature BusinessCard, List, IFG, Literature
from input.settings import IF_EMAIL, SURVEYPREFIX, SURVEY_EMAIL
class Command(BaseCommand): class Command(BaseCommand):
''' mails will be send here: ''' mails will be sent here:
- two weeks after confirmation of support for volunteer (/extern) send link - two weeks after confirmation of support for volunteer (/extern) send link
with surveylink with surveylink
@ -34,14 +32,14 @@ class Command(BaseCommand):
'type': type, 'type': type,
'name': name, 'name': name,
'pid': pid, 'pid': pid,
'SURVEYPREFIX': SURVEYPREFIX, } 'SURVEY_PREFIX': settings.SURVEY_PREFIX, }
txt_mail_template = get_template('input/survey_mail.txt') txt_mail_template = get_template('input/survey_mail.txt')
html_mail_template = get_template('input/survey_mail.html') html_mail_template = get_template('input/survey_mail.html')
try: try:
subject, from_email, to = 'Dein Feedback zur Förderung durch Wikimedia Deutschland', IF_EMAIL, email subject, from_email, to = 'Dein Feedback zur Förderung durch Wikimedia Deutschland', settings.IF_EMAIL, email
text_content = txt_mail_template.render(context) text_content = txt_mail_template.render(context)
html_content = html_mail_template.render(context) html_content = html_mail_template.render(context)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to], bcc=[SURVEY_EMAIL]) msg = EmailMultiAlternatives(subject, text_content, from_email, [to], bcc=[settings.SURVEY_EMAIL])
msg.attach_alternative(html_content, "text/html") msg.attach_alternative(html_content, "text/html")
msg.send() msg.send()
#print('survey mail would have been send') #print('survey mail would have been send')
@ -53,7 +51,7 @@ class Command(BaseCommand):
# bcc=[SURVEY_EMAIL]) # bcc=[SURVEY_EMAIL])
#survey_mail.send(fail_silently=False) #survey_mail.send(fail_silently=False)
except BadHeaderError: except BadHeaderError:
return HttpResponse('Invalid header found.') return HttpResponse('Invalid header found.') # FIXME HttpResponse???
print(f'send surveylinkemail to {email}...') print(f'send surveylinkemail to {email}...')
@ -66,21 +64,21 @@ class Command(BaseCommand):
def end_of_projects_reached(self): def end_of_projects_reached(self):
''' end of project reached ''' ''' end of project reached '''
# get all projects which ended # get all projects which ended
old = Project.objects.filter(end__lt = date.today())\ old = Project.objects.filter(end__lt = date.today())\
.exclude(end_mail_send = True)\ .exclude(end_mail_send = True)\
.filter(mail_state = 'NONE') .filter(mail_state = 'NONE')
txt_mail_template = get_template('input/if_end_of_project.txt') txt_mail_template = get_template('input/if_end_of_project.txt')
html_mail_template = get_template('input/if_end_of_project.html') html_mail_template = get_template('input/if_end_of_project.html')
for project in old: for project in old:
context = {'project': project} context = {'project': project}
context['URLPREFIX'] = settings.URLPREFIX context['URL_PREFIX'] = settings.EMAIL_URL_PREFIX
try: try:
subject, from_email, to = 'Projektende erreicht', IF_EMAIL, IF_EMAIL subject, from_email, to = 'Projektende erreicht', settings.IF_EMAIL, settings.IF_EMAIL
text_content = txt_mail_template.render(context) text_content = txt_mail_template.render(context)
html_content = html_mail_template.render(context) html_content = html_mail_template.render(context)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
@ -117,22 +115,22 @@ class Command(BaseCommand):
txt_informMail_template = get_template('input/if_end_of_project_orginformed.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') html_informMail_template = get_template('input/if_end_of_project_orginformed.html')
# send the mail to project.email, which would be the mail of the volunteer filling out the form # send the mail to project.email, which would be the mail of the volunteer filling out the form
for project in approved_end: for project in approved_end:
context = {'project': project} context = {'project': project}
context['URLPREFIX'] = settings.URLPREFIX context['URL_PREFIX'] = settings.EMAIL_URL_PREFIX
try: try:
subject, from_email, to = 'Projektende erreicht', IF_EMAIL, project.email subject, from_email, to = 'Projektende erreicht', settings.IF_EMAIL, project.email
text_content = txt_mail_template.render(context) text_content = txt_mail_template.render(context)
html_content = html_mail_template.render(context) html_content = html_mail_template.render(context)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
msg.attach_alternative(html_content, "text/html") msg.attach_alternative(html_content, "text/html")
msg.send() msg.send()
#print('if and of project approved mail would have been sent') #print('if and of project approved mail would have been sent')
inform_subject, inform_from_email, inform_to = 'Projektorganisator*in wurde informiert', IF_EMAIL, IF_EMAIL inform_subject, inform_from_email, inform_to = 'Projektorganisator*in wurde informiert', settings.IF_EMAIL, settings.IF_EMAIL
inform_text_content = txt_informMail_template.render(context) inform_text_content = txt_informMail_template.render(context)
inform_html_content = html_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 = EmailMultiAlternatives(inform_subject, inform_text_content, inform_from_email, [inform_to])
@ -171,16 +169,16 @@ class Command(BaseCommand):
html_mail_template = get_template('input/if_not_of_project_approved.html') 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_mail_template = get_template('input/if_not_of_project_approved.txt')
txt_informMail_template = get_template('input/if_end_of_project_orginformed.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') html_informMail_template = get_template('input/if_end_of_project_orginformed.html')
# send the mail to project.email, which would be the mail of the volunteer that filled out the form # send the mail to project.email, which would be the mail of the volunteer that filled out the form
for project in approved_notHappened: for project in approved_notHappened:
context = {'project': project} context = {'project': project}
context['URLPREFIX'] = settings.URLPREFIX context['URL_PREFIX'] = settings.EMAIL_URL_PREFIX
try: try:
subject, from_email, to = 'Projektende erreicht', IF_EMAIL, project.email subject, from_email, to = 'Projektende erreicht', settings.IF_EMAIL, project.email
text_content = txt_mail_template.render(context) text_content = txt_mail_template.render(context)
html_content = html_mail_template.render(context) html_content = html_mail_template.render(context)
msg = EmailMultiAlternatives(subject, text_content, from_email, [to]) msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
@ -194,8 +192,8 @@ class Command(BaseCommand):
# IF_EMAIL, # IF_EMAIL,
# [project.email], # [project.email],
# fail_silently=False) # fail_silently=False)
inform_subject, inform_from_email, inform_to = 'Projektorganisator*in wurde informiert', IF_EMAIL, IF_EMAIL inform_subject, inform_from_email, inform_to = 'Projektorganisator*in wurde informiert', settings.IF_EMAIL, settings.IF_EMAIL
inform_text_content = txt_informMail_template.render(context) inform_text_content = txt_informMail_template.render(context)
inform_html_content = html_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 = EmailMultiAlternatives(inform_subject, inform_text_content, inform_from_email, [inform_to])
@ -297,16 +295,15 @@ class Command(BaseCommand):
'''send survey link 2 weeks after mailadresss, mailinglist or businesscards are granted''' '''send survey link 2 weeks after mailadresss, mailinglist or businesscards are granted'''
lastdate = date.today() - timedelta(days=14) lastdate = date.today() - timedelta(days=14)
typefield = ('MAIL','VIS','LIST') models = Email, BusinessCard, List
count = 0 types = 'MAIL', 'VIS', 'LIST'
for c in ('Email', 'BusinessCard', 'List'):
# get class via string for model, typ in zip(models, types):
supported = getattr(sys.modules[__name__], c).objects.filter(granted=True)\ supported = model.objects.filter(granted=True)\
.filter(granted_date__lt = lastdate)\ .filter(granted_date__lt = lastdate)\
.exclude(survey_mail_send=True)\ .exclude(survey_mail_send=True)\
.exclude(mail_state = 'END') .exclude(mail_state = 'END')
self.surveymails_to_object(supported, type=typefield[count]) self.surveymails_to_object(supported, type=typ)
count += 1
def handle(self, *args, **options): def handle(self, *args, **options):

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,20 +2,28 @@ from datetime import date
from django.db import models from django.db import models
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .settings import ACCOUNTS
EMAIL_STATES = {'NONE': 'noch keine Mail versendet', EMAIL_STATES = {'NONE': 'noch keine Mail versendet',
'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet', 'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet',
'CLOSE': 'die Projektabschlussmail wurde versendet', 'CLOSE': 'die Projektabschlussmail wurde versendet',
'END': 'alle automatischen Mails, auch surveyMail, wurden versendet'} 'END': 'alle automatischen Mails, auch surveyMail, wurden versendet'}
class TermsConsentMixin(models.Model):
"""Abstract mixin to add a terms_accepted field for documenting user consent."""
terms_accepted = models.BooleanField(default=False, verbose_name="Nutzungsbedingungen zugestimmt")
class Meta:
abstract = True
class Volunteer(models.Model): class Volunteer(models.Model):
realname = models.CharField(max_length=200, null=True, verbose_name="Realname", realname = models.CharField(max_length=200, null=True, verbose_name="Realname",
help_text="Bitte gib deinen Vornamen und deinen Nachnamen ein.", default='') help_text="Bitte gib deinen Vornamen und deinen Nachnamen ein.", default='')
email = models.EmailField(max_length=200, null=True, verbose_name='E-Mail-Adresse', email = models.EmailField(max_length=200, null=True, verbose_name='E-Mail-Adresse',
help_text=format_html('Bitte gib deine E-Mail-Adresse ein, damit dich<br>Wikimedia Deutschland bei Rückfragen oder für<br>die Zusage kontaktieren kann.')) help_text=mark_safe('Bitte gib deine E-Mail-Adresse ein, damit dich<br>Wikimedia Deutschland bei Rückfragen oder für<br>die Zusage kontaktieren kann.'))
# the following Fields are not supposed to be edited by users # the following Fields are not supposed to be edited by users
@ -40,9 +48,9 @@ class Volunteer(models.Model):
class Extern(Volunteer): class Extern(Volunteer):
''' abstract basis class for all data entered by extern volunteers ''' ''' abstract basis class for all data entered by extern volunteers '''
username = models.CharField(max_length=200, null=True, verbose_name='Benutzer_innenname', username = models.CharField(max_length=200, null=True, verbose_name='Benutzer_innenname',
help_text=format_html("Wikimedia Benutzer_innenname")) help_text=mark_safe("Wikimedia Benutzer_innenname"))
# the following Fields are not supposed to be edited by users # the following Fields are not supposed to be edited by users
service_id = models.CharField(max_length=15, null=True, blank=True) service_id = models.CharField(max_length=15, null=True, blank=True)
@ -91,7 +99,7 @@ class Project(Volunteer):
granted_from = models.CharField(max_length=100,null=True,verbose_name='Bewilligt von') 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') 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") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
# the following Fields are not supposed to be edited by users # the following Fields are not supposed to be edited by users
pid = models.CharField(max_length=15, null=True, blank=True) pid = models.CharField(max_length=15, null=True, blank=True)
@ -102,7 +110,7 @@ class Project(Volunteer):
def save(self,*args,**kwargs): def save(self,*args,**kwargs):
generate_finance_id=False generate_finance_id=False
'''we generate the autogenerated fields here''' '''we generate the autogenerated fields here'''
@ -123,7 +131,7 @@ class Project(Volunteer):
else: else:
self.finance_id = str(self.account.code) self.finance_id = str(self.account.code)
@ -141,7 +149,7 @@ class Project(Volunteer):
self.project_of_year = int(projects.project_of_year) + 1 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) # self.pid = str(self.start.year) + '-' + str(self.account.code) + str(self.project_of_year).zfill(3)
if str(self.account.code) == '21111': if str(self.account.code) == '21111':
self.finance_id = str(self.account.code) + '-' + str(self.project_of_year).zfill(3) self.finance_id = str(self.account.code) + '-' + str(self.project_of_year).zfill(3)
else: else:
@ -154,16 +162,8 @@ class Project(Volunteer):
# self.pid = str(self.account.code) + str(self.pk).zfill(3) # self.pid = str(self.account.code) + str(self.pk).zfill(3)
print (("Hallo Leute! Ich save jetzt mal MIT PID DANN!!!",self.pid)) print (("Hallo Leute! Ich save jetzt mal MIT PID DANN!!!",self.pid))
if self.end:
# generation of field quartals self.end_quartal = f'Q{self.end.month // 4 + 1}'
if self.end.month in [1, 2, 3]:
self.end_quartal = 'Q1'
if self.end.month in [4, 5, 6]:
self.end_quartal = 'Q2'
if self.end.month in [7, 8, 9]:
self.end_quartal = 'Q3'
if self.end.month in [10, 11, 12]:
self.end_quartal = 'Q4'
super().save() super().save()
@ -198,8 +198,8 @@ TRANSPORT_CHOICES = {'BAHN': 'Bahn',
PAYEDBY_CHOICES = {'WMDE': 'WMDE', PAYEDBY_CHOICES = {'WMDE': 'WMDE',
'REQU': 'Antragstellender Mensch'} 'REQU': 'Antragstellender Mensch'}
HOTEL_CHOICES = {'TRUE': format_html('Hotelzimmer benötigt'), HOTEL_CHOICES = {'TRUE': mark_safe('Hotelzimmer benötigt'),
'FALSE': format_html('Kein Hotelzimmer benötigt') 'FALSE': mark_safe('Kein Hotelzimmer benötigt')
} }
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -229,7 +229,7 @@ from django.dispatch import receiver
@receiver(pre_save, sender=Travel, dispatch_uid="get_project_end") @receiver(pre_save, sender=Travel, dispatch_uid="get_project_end")
def getProjectEnd(sender, instance, **kwargs): def getProjectEnd(sender, instance, **kwargs):
#instance.project_end = instance.project.end #instance.project_end = instance.project.end
if instance.project: if instance.project:
instance.project_end = instance.project.end instance.project_end = instance.project.end
instance.project_end_quartal = instance.project.end_quartal instance.project_end_quartal = instance.project.end_quartal
@ -254,47 +254,95 @@ class Grant(Extern):
abstract = True abstract = True
TYPE_CHOICES = {'BIB': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#Bibliotheksstipendium" target="_blank" rel="noopener">Bibliotheksstipendium</a>'), def type_link(path, label):
'ELIT': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#eLiteraturstipendium" target="_blank" rel="noopener">eLiteraturstipendium</a>'), return format_html(
'MAIL': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/E-Mail-Adressen_und_Visitenkarten#E-Mail-Adressen" target="_blank" rel="noopener">E-Mail-Adresse</a>'), format_string='<a href="{href}" target="_blank" rel="noopener">{label}</a>',
'IFG': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Gebührenerstattungen_für_Behördenanfragen" target="_blank" rel="noopener">Kostenübernahme IFG-Anfrage</a>'), href=f'https://de.wikipedia.org/wiki/Wikipedia:Förderung/{path}',
'LIT': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Zugang_zu_Fachliteratur#Literaturstipendium" target="_blank" rel="noopener">Literaturstipendium</a>'), label=label,
'LIST': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/E-Mail-Adressen_und_Visitenkarten#Mailinglisten" target="_blank" rel="noopener">Mailingliste</a>'), )
'TRAV': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/Reisekostenerstattungen" target="_blank" rel="noopener">Reisekosten</a>'),
'SOFT': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/Software-Stipendien" target="_blank" rel="noopener">Softwarestipendium</a>'),
'VIS': format_html('<a href="https://de.wikipedia.org/wiki/Wikipedia:Förderung/E-Mail-Adressen_und_Visitenkarten#Visitenkarten" target="_blank" rel="noopener">Visitenkarten</a>'), TYPE_BIB = 'BIB'
} TYPE_ELIT = 'ELIT'
TYPE_MAIL = 'MAIL'
TYPE_IFG = 'IFG'
TYPE_LIT = 'LIT'
TYPE_LIST = 'LIST'
TYPE_TRAV = 'TRAV'
TYPE_SOFT = 'SOFT'
TYPE_VIS = 'VIS'
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'),
}
LIBRARY_TYPES = TYPE_BIB, TYPE_ELIT, TYPE_SOFT
LIBRARY_TYPE_CHOICES = [(choice, TYPE_CHOICES[choice]) for choice in LIBRARY_TYPES]
# same model is used for Library, ELitStip and Software! # same model is used for Library, ELitStip and Software!
class Library(Grant): class Library(Grant):
TYPE = TYPE_BIB
LIBRARY_LABEL = 'Bibliothek'
LIBRARY_HELP_TEXT = 'Für welche Bibliothek gilt das Stipendium?'
DURATION_HELP_TEXT = mark_safe('In welchem Zeitraum möchtest du recherchieren oder<br>wie lange ist der Bibliotheksausweis gültig?')
type = models.CharField( type = models.CharField(max_length=4, choices=LIBRARY_TYPE_CHOICES, default=TYPE_BIB)
max_length=4,
choices=TYPE_CHOICES.items(), #attention: actually only BIB, ELIT, SOFT should be used here
default='BIB',
)
library = models.CharField(max_length=200) library = models.CharField(max_length=200)
duration = models.CharField(max_length=100, verbose_name="Dauer") duration = models.CharField(max_length=100, verbose_name="Dauer")
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
def __str__(self): def __str__(self):
return self.library return self.library
def save(self, **kwargs):
self.type = self.TYPE
SELFBUY_CHOICES = {'TRUE': format_html('Ich möchte das Werk selbst kaufen und per Kostenerstattung bei Wikimedia Deutschland abrechnen.'), return super().save(**kwargs)
'FALSE': format_html('Ich möchte, dass Wikimedia Deutschland das Werk für mich kauft'),
class 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
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
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(Grant): class Literature(TermsConsentMixin, Grant):
info = models.CharField(max_length=500, verbose_name='Informationen zum Werk', info = models.CharField(max_length=500, verbose_name='Informationen zum Werk',
help_text=format_html("Bitte gib alle Informationen zum benötigten Werk an,<br>\ help_text=mark_safe("Bitte gib alle Informationen zum benötigten Werk an,<br>\
die eine eindeutige Identifizierung ermöglichen (Autor, Titel, Verlag, ISBN, ...)")) die eine eindeutige Identifizierung ermöglichen (Autor, Titel, Verlag, ISBN, ...)"))
source = models.CharField(max_length=200, verbose_name='Bezugsquelle', source = models.CharField(max_length=200, verbose_name='Bezugsquelle',
help_text="Bitte gib an, wo du das Werk kaufen möchtest.") help_text="Bitte gib an, wo du das Werk kaufen möchtest.")
selfbuy = models.CharField( max_length=10, verbose_name='Selbstkauf?', choices=SELFBUY_CHOICES.items(), default='TRUE') selfbuy = models.CharField( max_length=10, verbose_name='Selbstkauf?', choices=SELFBUY_CHOICES.items(), default='TRUE')
selfbuy_give_data = models.BooleanField(verbose_name=format_html('Datenweitergabe erlauben'), help_text=format_html('Ich stimme der Weitergabe meiner Daten (Name, Postadresse) an den von mir angegebenen Anbieter/Dienstleister zu.')) selfbuy_give_data = models.BooleanField(verbose_name=mark_safe('Datenweitergabe erlauben'), help_text=mark_safe('Ich stimme der Weitergabe meiner Daten (Name, Postadresse) an den von mir angegebenen Anbieter/Dienstleister zu.'))
selfbuy_data = models.TextField(max_length=1000, verbose_name='Persönliche Daten sowie Adresse', default='',\ selfbuy_data = models.TextField(max_length=1000, verbose_name='Persönliche Daten sowie Adresse', default='',\
help_text=format_html("Bitte gib hier alle persönlichen Daten an, die wir benötigen, um das Werk<br>\ help_text=mark_safe("Bitte gib hier alle persönlichen Daten an, die wir benötigen, um das Werk<br>\
für dich zu kaufen und es dir anschließend zu schicken (z.B. Vorname Nachname, Anschrift, <br>\ für dich zu kaufen und es dir anschließend zu schicken (z.B. Vorname Nachname, Anschrift, <br>\
Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.")) Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche."))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
@ -303,7 +351,7 @@ class IFG(Grant):
url = models.URLField(max_length=2000, verbose_name="URL", url = models.URLField(max_length=2000, verbose_name="URL",
help_text="Bitte gib den Link zu deiner Anfrage bei Frag den Staat an.") help_text="Bitte gib den Link zu deiner Anfrage bei Frag den Staat an.")
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
def __str__(self): def __str__(self):
return "IFG-Anfrage von " + self.realname return "IFG-Anfrage von " + self.realname
@ -325,24 +373,24 @@ MAIL_CHOICES = {'REALNAME': 'Vorname.Nachname',
'USERNAME': 'Username', 'USERNAME': 'Username',
'OTHER': 'Sonstiges:'} 'OTHER': 'Sonstiges:'}
ADULT_CHOICES = {'TRUE': format_html('Ich bin volljährig.'), ADULT_CHOICES = {'TRUE': mark_safe('Ich bin volljährig.'),
'FALSE': format_html('Ich bin noch nicht volljährig.') 'FALSE': mark_safe('Ich bin noch nicht volljährig.')
} }
class Email(Domain): class Email(TermsConsentMixin, Domain):
address = models.CharField(max_length=50, address = models.CharField(max_length=50,
choices=MAIL_CHOICES.items(), choices=MAIL_CHOICES.items(),
default='USERNAME', verbose_name='Adressbestandteil', default='USERNAME', verbose_name='Adressbestandteil',
help_text=format_html("Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.")) help_text=mark_safe("Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll."))
other = models.CharField(max_length=50,blank=True,null=True, verbose_name="Sonstiges") other = models.CharField(max_length=50,blank=True,null=True, verbose_name="Sonstiges")
adult = models.CharField( max_length=10, verbose_name='Volljährigkeit', choices=ADULT_CHOICES.items(), default='FALSE') adult = models.CharField( max_length=10, verbose_name='Volljährigkeit', choices=ADULT_CHOICES.items(), default='FALSE')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
class List(Domain): class List(TermsConsentMixin, Domain):
address = models.CharField(max_length=50, default='NO_ADDRESS', address = models.CharField(max_length=50, default='NO_ADDRESS',
verbose_name="Adressbestandteil für Projektmailingliste", verbose_name="Adressbestandteil für Projektmailingliste",
help_text=format_html("Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.")) help_text=mark_safe("Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll."))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
PROJECT_CHOICE = {'PEDIA': 'Wikipedia', PROJECT_CHOICE = {'PEDIA': 'Wikipedia',
@ -358,13 +406,13 @@ PROJECT_CHOICE = {'PEDIA': 'Wikipedia',
BC_VARIANT = {'PIC': 'mit Bild', BC_VARIANT = {'PIC': 'mit Bild',
'NOPIC': 'ohne Bild'} 'NOPIC': 'ohne Bild'}
class BusinessCard(Extern): class BusinessCard(TermsConsentMixin, Extern):
project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(), project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(),
default='PEDIA', verbose_name='Wikimedia-Projekt', default='PEDIA', verbose_name='Wikimedia-Projekt',
help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?') help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?')
data = models.TextField(max_length=1000, verbose_name='Persönliche Daten für die Visitenkarten', default='', data = models.TextField(max_length=1000, verbose_name='Persönliche Daten für die Visitenkarten', default='',
help_text=format_html("Bitte gib hier alle persönlichen Daten an, und zwar genau so,<br>\ help_text=mark_safe("Bitte gib hier alle persönlichen Daten an, und zwar genau so,<br>\
wie sie (auch in der entsprechenden Reihenfolge) auf den Visitenkarten stehen sollen<br>\ wie sie (auch in der entsprechenden Reihenfolge) auf den Visitenkarten stehen sollen<br>\
(z.B. Vorname Nachname, Benutzer:/Benutzerin:, Benutzer-/-innenname, Anschrift,<br>\ (z.B. Vorname Nachname, Benutzer:/Benutzerin:, Benutzer-/-innenname, Anschrift,<br>\
Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.<br>\ Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.<br>\
@ -373,12 +421,25 @@ class BusinessCard(Extern):
Adresse, die du ebenfalls beantragen kannst, sofern du nicht bereits eine besitzt.")) Adresse, die du ebenfalls beantragen kannst, sofern du nicht bereits eine besitzt."))
variant = models.CharField(max_length=5, choices=BC_VARIANT.items(), variant = models.CharField(max_length=5, choices=BC_VARIANT.items(),
default='NOPIC', verbose_name='Variante', default='NOPIC', verbose_name='Variante',
help_text=format_html('so sehen die Varianten aus: <a href="https://upload.wikimedia.org/wikipedia/commons/c/cd/Muster_Visitenkarten_WMDE_2018.jpg">\ help_text=mark_safe('so sehen die Varianten aus: <a href="https://upload.wikimedia.org/wikipedia/commons/c/cd/Muster_Visitenkarten_WMDE_2018.jpg">\
mit Bild</a> <a href="https://upload.wikimedia.org/wikipedia/commons/d/d3/Muster_Visitenkarte_WMDE.png">ohne Bild</a>' )) mit Bild</a> <a href="https://upload.wikimedia.org/wikipedia/commons/d/d3/Muster_Visitenkarte_WMDE.png">ohne Bild</a>' ))
url_of_pic = models.CharField(max_length=200, verbose_name='Url des Bildes', default='', help_text="Bitte gib die Wikimedia-Commons-URL des Bildes an.") url_of_pic = models.CharField(max_length=200, verbose_name='Url des Bildes', default='', help_text="Bitte gib die Wikimedia-Commons-URL des Bildes an.")
sent_to = models.TextField(max_length=1000, verbose_name='Versandadresse', sent_to = models.TextField(max_length=1000, verbose_name='Versandadresse',
default='', help_text="Bitte gib den Namen und die vollständige Adresse ein, an welche die Visitenkarten geschickt werden sollen.") default='', help_text="Bitte gib den Namen und die vollständige Adresse ein, an welche die Visitenkarten geschickt werden sollen.")
send_data_to_print = models.BooleanField(default=False, verbose_name=format_html('Datenweitergabe erlauben'), help_text=format_html('Hiermit erlaube ich die Weitergabe meiner Daten (Name, Postadresse) an den von Wikimedia<br> Deutschland ausgewählten Dienstleister (z. B. <a href="wir-machen-druck.de">wir-machen-druck.de</a>) zum Zwecke des direkten <br> Versands der Druckerzeugnisse an mich.')) send_data_to_print = models.BooleanField(default=False, verbose_name=mark_safe('Datenweitergabe erlauben'), help_text=mark_safe('Hiermit erlaube ich die Weitergabe meiner Daten (Name, Postadresse) an den von Wikimedia<br> Deutschland ausgewählten Dienstleister (z. B. <a href="wir-machen-druck.de">wir-machen-druck.de</a>) zum Zwecke des direkten <br> Versands der Druckerzeugnisse an mich.'))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen")
MODELS = {
TYPE_BIB: Library,
TYPE_ELIT: ELiterature,
TYPE_MAIL: Email,
TYPE_IFG: IFG,
TYPE_LIT: Literature,
TYPE_LIST: List,
TYPE_TRAV: Travel,
TYPE_SOFT: Software,
TYPE_VIS: BusinessCard,
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,29 +28,29 @@ Sendungsadrese: {{data.send_to}} <br> {% endif %}
Zum Eintrag in der Förderdatenbank: Zum Eintrag in der Förderdatenbank:
{% if data.choice == 'BIB' %} {% if data.choice == 'BIB' %}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'ELIT'%} {% elif data.choice == 'ELIT'%}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'LIT'%} {% elif data.choice == 'LIT'%}
<a href="{{data.urlprefix}}/admin/input/literature/{{data.pk}}/change">{{data.urlprefix}}/admin/input/literature/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/literature/{{data.pk}}/change">{{data.url_prefix}}/admin/input/literature/{{data.pk}}/change</a>
{% elif data.choice == 'MAIL'%} {% elif data.choice == 'MAIL'%}
<a href="{{data.urlprefix}}/admin/input/email/{{data.pk}}/change">{{data.urlprefix}}/admin/input/email/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/email/{{data.pk}}/change">{{data.url_prefix}}/admin/input/email/{{data.pk}}/change</a>
{% elif data.choice == 'IFG'%} {% elif data.choice == 'IFG'%}
<a href="{{data.urlprefix}}/admin/input/ifg/{{data.pk}}/change">{{data.urlprefix}}/admin/input/ifg/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/ifg/{{data.pk}}/change">{{data.url_prefix}}/admin/input/ifg/{{data.pk}}/change</a>
{% elif data.choice == 'LIST'%} {% elif data.choice == 'LIST'%}
<a href="{{data.urlprefix}}/admin/input/list/{{data.pk}}/change">{{data.urlprefix}}/admin/input/list/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/list/{{data.pk}}/change">{{data.url_prefix}}/admin/input/list/{{data.pk}}/change</a>
{% elif data.choice == 'TRAV'%} {% elif data.choice == 'TRAV'%}
<a href="{{data.urlprefix}}/admin/input/travel/{{data.pk}}/change">{{data.urlprefix}}/admin/input/travel/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/travel/{{data.pk}}/change">{{data.url_prefix}}/admin/input/travel/{{data.pk}}/change</a>
{% elif data.choice == 'SOFT'%} {% elif data.choice == 'SOFT'%}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'VIS'%} {% elif data.choice == 'VIS'%}
<a href="{{data.urlprefix}}/admin/input/businesscard/{{data.pk}}/change">{{data.urlprefix}}/admin/input/businesscard/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/businesscard/{{data.pk}}/change">{{data.url_prefix}}/admin/input/businesscard/{{data.pk}}/change</a>
{% endif %} {% endif %}
<br><br> <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> Zum Genehmigen hier klicken: <a href="{{data.url_prefix}}{% url 'authorize' data.choice data.pk %}">{{data.url_prefix}}{% url 'authorize' data.choice data.pk %}</a>
<br><br> <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> Zu Ablehnen hier klicken: <a href="{{data.url_prefix}}{% url 'deny' data.choice data.pk %}">{{data.url_prefix}}{% url 'deny' data.choice data.pk %}</a>
<br><br> <br><br>
Stets zu Diensten, Deine Förderdatenbank Stets zu Diensten, Deine Förderdatenbank

View File

@ -23,30 +23,30 @@ Persönliche Daten: {{data.data}}
Variante: {{data.variant}} Variante: {{data.variant}}
Sendungsadrese: {{data.send_to}} {% endif %} Sendungsadrese: {{data.send_to}} {% endif %}
Zum Eintrag in der Förderdatenbank: Zum Eintrag in der Förderdatenbank:
{% if data.choice == 'BIB' %} {% if data.choice == 'BIB' %}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'ELIT'%} {% elif data.choice == 'ELIT'%}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'LIT'%} {% elif data.choice == 'LIT'%}
<a href="{{data.urlprefix}}/admin/input/literature/{{data.pk}}/change">{{data.urlprefix}}/admin/input/literature/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/literature/{{data.pk}}/change">{{data.url_prefix}}/admin/input/literature/{{data.pk}}/change</a>
{% elif data.choice == 'MAIL'%} {% elif data.choice == 'MAIL'%}
<a href="{{data.urlprefix}}/admin/input/email/{{data.pk}}/change">{{data.urlprefix}}/admin/input/email/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/email/{{data.pk}}/change">{{data.url_prefix}}/admin/input/email/{{data.pk}}/change</a>
{% elif data.choice == 'IFG'%} {% elif data.choice == 'IFG'%}
<a href="{{data.urlprefix}}/admin/input/ifg/{{data.pk}}/change">{{data.urlprefix}}/admin/input/ifg/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/ifg/{{data.pk}}/change">{{data.url_prefix}}/admin/input/ifg/{{data.pk}}/change</a>
{% elif data.choice == 'LIST'%} {% elif data.choice == 'LIST'%}
<a href="{{data.urlprefix}}/admin/input/list/{{data.pk}}/change">{{data.urlprefix}}/admin/input/list/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/list/{{data.pk}}/change">{{data.url_prefix}}/admin/input/list/{{data.pk}}/change</a>
{% elif data.choice == 'TRAV'%} {% elif data.choice == 'TRAV'%}
<a href="{{data.urlprefix}}/admin/input/travel/{{data.pk}}/change">{{data.urlprefix}}/admin/input/travel/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/travel/{{data.pk}}/change">{{data.url_prefix}}/admin/input/travel/{{data.pk}}/change</a>
{% elif data.choice == 'SOFT'%} {% elif data.choice == 'SOFT'%}
<a href="{{data.urlprefix}}/admin/input/library/{{data.pk}}/change">{{data.urlprefix}}/admin/input/library/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'VIS'%} {% elif data.choice == 'VIS'%}
<a href="{{data.urlprefix}}/admin/input/businesscard/{{data.pk}}/change">{{data.urlprefix}}/admin/input/businesscard/{{data.pk}}/change</a> <a href="{{data.url_prefix}}/admin/input/businesscard/{{data.pk}}/change">{{data.url_prefix}}/admin/input/businesscard/{{data.pk}}/change</a>
{% endif %} {% endif %}
Zum Genehmigen hier klicken: {{data.urlprefix}}{% url 'authorize' data.choice data.pk %} Zum Genehmigen hier klicken: {{data.url_prefix}}{% url 'authorize' data.choice data.pk %}
Zu Ablehnen hier klicken: {{data.urlprefix}}{% url 'deny' data.choice data.pk %} Zu Ablehnen hier klicken: {{data.url_prefix}}{% url 'deny' data.choice data.pk %}
Stets zu Diensten, Deine Förderdatenbank Stets zu Diensten, Deine Förderdatenbank

View File

@ -1,69 +1,38 @@
{% load static %} {% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/base.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/widgets.css' %}" />
{% load i18n %} {% load i18n %}
{% csrf_token %} <link rel="stylesheet" type="text/css" href="{% static 'admin/css/base.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/widgets.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}"/>
<center> <link rel="stylesheet" type="text/css" href="{% static 'css/button.css' %}">
<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="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>
</center>
<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">
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>
<a href="https://www.wikimedia.de/impressum/">Impressum</a>
</p>
</div>

View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Projektförderung ab 1.000,— EUR</title>
</head>
<body>
<h1>Projektförderung mit einer Gesamtsumme ab 1.000,— EUR</h1>
<p>Für Projektförderungen ab 1.000,— EUR ist ein öffentlicher Projektplan erforderlich.</p>
<p><em>Dummy-Content wird bei Freigabe ersetzt.</em></p>
</body>
</html>

View File

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

View File

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

View File

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

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

@ -0,0 +1,2 @@
from .models import ModelTestCase
from .views import AuthenticatedViewTestCase, AnonymousViewTestCase

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

@ -0,0 +1,74 @@
from datetime import date
from unittest import skip
from django.test import TestCase
from input.models import HonoraryCertificate, Project, Account, Literature
class ModelTestCase(TestCase):
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 = 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)
@skip('Finance ID generation has been changed and this test has not been adapted accordingly.')
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', selfbuy_give_data=False)
self.assertEqual(obj.service_id, f'Literature{obj.id}')

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

@ -0,0 +1,133 @@
from django.test import TestCase
from input.models import Library
from input.utils.testing import create_superuser, login, request
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')
@staticmethod
def get_step_data(step, data):
return {'extern_view-current_step': step, **data}
@classmethod
def get_first_step_data(cls, choice):
return cls.get_step_data(0, {
'0-realname': 'Test',
'0-email': 'test@example.com',
'0-choice': choice,
'0-check': True,
})
def helper_extern(self, choice, text, data):
first_step_data = self.get_first_step_data(choice)
response = request(self, 'extern', data=first_step_data)
self.assertContains(response, text)
second_step_data = self.get_step_data(1, data)
response = request(self, 'extern', data=second_step_data)
self.assertContains(response, 'Deine Anfrage wurde gesendet.')
def test_extern_first_steps(self):
types = [
('BIB', 'Bibliotheksausweis'),
('ELIT', 'Online-Ressource'),
('MAIL', 'Mailadresse beantragen'),
('IFG', 'gewonnenen Informationen'),
('LIT', 'Literatur verwenden'),
('LIST', 'Mailingliste beantragen'),
('TRAV', 'Transportmittel'),
('SOFT', 'Lizenz'),
('VIS', 'DIN 5008'),
]
for choice, text in types:
with self.subTest(type=choice):
self.client.session.clear()
data = self.get_first_step_data(choice)
response = request(self, 'extern', data=data)
self.assertContains(response, text)
def test_extern_travel(self):
self.helper_extern('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('LIT', 'Literatur verwenden', {
'cost': 20,
'info': 'Test',
'source': 'Test',
'notes': '',
'selfbuy': 'TRUE',
'selfbuy_data': 'NONE',
'selfbuy_give_data': True,
'check': True,
})
def test_extern_bib(self):
self.helper_extern('BIB', 'Bibliotheksausweis', {
'cost': 20,
'library': 'Test',
'duration': 'Test',
'notes': '',
})
class AuthenticatedViewTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = create_superuser('staff')
def setUp(self):
login(self)
def test_export(self):
request(self, 'export')
def helper_auth_deny(self, view, expected):
obj = Library.objects.create(library='Test')
request(self, view, args=[obj.type, obj.id])
obj.refresh_from_db(fields=['granted'])
self.assertEqual(obj.granted, expected)
def helper_auth_deny_error(self, view):
response = request(self, view, args=['TEST', 1])
self.assertContains(response, 'ERROR')
def test_authorize(self):
self.helper_auth_deny('authorize', True)
def test_authorize_error(self):
self.helper_auth_deny_error('authorize')
def test_deny(self):
self.helper_auth_deny('deny', False)
def test_deny_error(self):
self.helper_auth_deny_error('deny')

View File

@ -1,14 +1,15 @@
from django.urls import path from django.urls import path
from .views import ExternView, index, done, authorize, deny, InternView, export from django.views.generic import TemplateView
from django.contrib import admin from .views import ExternView, index, done, authorize, deny, export
urlpatterns = [ urlpatterns = [
path('', index, name='index'), path('', index, name='index'),
path('extern', ExternView.as_view(), name='extern'), path('extern', ExternView.as_view(), name='extern'),
# path('intern', InternView.as_view(), name='intern'),
path('admin/', admin.site.urls),
path('saved', done, name='done'), path('saved', done, name='done'),
path('export', export, name='export'), path('export', export, name='export'),
path('authorize/<str:choice>/<int:pk>', authorize, name='authorize'), path('authorize/<str:choice>/<int:pk>', authorize, name='authorize'),
path('deny/<str:choice>/<int:pk>', deny, name='deny'), path('deny/<str:choice>/<int:pk>', deny, name='deny'),
path('extern/info/projektfoerderung-ab-1000/',
TemplateView.as_view(template_name='input/info_project_funding_gt_1000.html'),
name="info-foerderprojekt-ab-1000"),
] ]

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

View File

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

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

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

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

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

View File

@ -1,67 +1,65 @@
from datetime import date
from smtplib import SMTPException from smtplib import SMTPException
from django.shortcuts import render from django.shortcuts import render
from django.forms import modelformset_factory
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.safestring import mark_safe
from formtools.wizard.views import CookieWizardView from formtools.wizard.views import CookieWizardView
from django.core.mail import send_mail, BadHeaderError, EmailMultiAlternatives from django.core.mail import BadHeaderError, EmailMultiAlternatives
from django.conf import settings
from django.template.loader import get_template from django.template.loader import get_template
from django.template import Context from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils.html import format_html
from django.utils.translation import gettext as _
from .forms import ProjectForm, ExternForm, LibraryForm, IFGForm, LiteratureForm,\ from .forms import (
HonoraryCertificateForm, InternForm, TravelForm, EmailForm,\ ExternForm,
ListForm, BusinessCardForm, INTERN_CHOICES LibraryForm,
from .models import Project, TYPE_CHOICES, Library, Literature, Travel, IFG, BusinessCard, Email, List ELiteratureForm,
from .settings import IF_EMAIL SoftwareForm,
IFGForm,
LiteratureForm,
TravelForm,
EmailForm,
ListForm,
BusinessCardForm,
)
from .models import TYPE_CHOICES, MODELS, TYPE_BIB, TYPE_ELIT, TYPE_SOFT
def auth_deny(choice,pk,auth): LIBRARY_FORMS = {
if choice in ('BIB', 'ELIT', 'SOFT'): TYPE_BIB: LibraryForm,
Library.set_granted(pk,auth) TYPE_ELIT: ELiteratureForm,
elif choice == 'LIT': TYPE_SOFT: SoftwareForm,
Literature.set_granted(pk,auth) }
elif choice == 'IFG':
IFG.set_granted(pk,auth)
elif choice == 'TRAV': def auth_deny(choice, pk, auth):
Travel.set_granted(pk,auth) if choice not in MODELS:
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}') return HttpResponse(f'ERROR! UNKNOWN CHOICE TYPE! {choice}')
return False
MODELS[choice].set_granted(pk, auth)
@login_required @login_required
def export(request): def export(request):
'''export the project database to a csv''' '''export the project database to a csv'''
return HttpResponse('WE WANT CSV!') return HttpResponse('WE WANT CSV!')
@login_required @login_required
def authorize(request, choice, pk): def authorize(request, choice, pk):
'''If IF grant a support they click a link in a mail which leads here. '''If IF grant a support they click a link in a mail which leads here.
We write the granted field in the database here and set a timestamp.''' We write the granted field in the database here and set a timestamp.'''
ret = auth_deny(choice, pk, True) if ret := auth_deny(choice, pk, True):
if ret:
return ret return ret
else: else:
return HttpResponse(f"AUTHORIZED! choice: {choice}, pk: {pk}") return HttpResponse(f"AUTHORIZED! choice: {choice}, pk: {pk}")
@login_required @login_required
def deny(request, choice, pk): def deny(request, choice, pk):
'''If IF denies a support they click a link in a mail which leads here '''If IF denies a support they click a link in a mail which leads here
We write the granted field in the database here.''' We write the granted field in the database here.'''
ret = auth_deny(choice, pk, False) if ret := auth_deny(choice, pk, False):
if ret:
return ret return ret
else: else:
return HttpResponse(f"DENIED! choice: {choice}, pk: {pk}") return HttpResponse(f"DENIED! choice: {choice}, pk: {pk}")
@ -70,95 +68,10 @@ def deny(request, choice, pk):
def done(request): def done(request):
return HttpResponse("Deine Anfrage wurde gesendet. Du erhältst in Kürze eine E-Mail-Benachrichtigung mit deinen Angaben. Für alle Fragen kontaktiere bitte das Team Communitys und Engagement unter community@wikimedia.de.") return HttpResponse("Deine Anfrage wurde gesendet. Du erhältst in Kürze eine E-Mail-Benachrichtigung mit deinen Angaben. Für alle Fragen kontaktiere bitte das Team Communitys und Engagement unter community@wikimedia.de.")
def index(request): def index(request):
return render(request, 'input/index.html') return render(request, 'input/index.html')
class InternView(LoginRequiredMixin, CookieWizardView):
'''This View is for WMDE-employees only'''
template_name = 'input/extern.html'
form_list = [InternForm, ProjectForm]
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: {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 get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if hasattr(self, 'choice'):
context["choice"] = INTERN_CHOICES[self.choice]
return context
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 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)
# 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'),
}
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): class ExternView(CookieWizardView):
'''This View is for Volunteers''' '''This View is for Volunteers'''
@ -180,15 +93,12 @@ class ExternView(CookieWizardView):
print(f'choice detection in ExternView: {TYPE_CHOICES[choice]}') print(f'choice detection in ExternView: {TYPE_CHOICES[choice]}')
if choice == 'IFG': if choice == 'IFG':
form = IFGForm(data) form = IFGForm(data)
form.fields['notes'].help_text = format_html("Bitte gib an, wie die gewonnenen Informationen den<br>Wikimedia-Projekten zugute kommen sollen.") form.fields['notes'].help_text = mark_safe("Bitte gib an, wie die gewonnenen Informationen den<br>Wikimedia-Projekten zugute kommen sollen.")
elif choice in ('BIB', 'SOFT', 'ELIT'): elif choice in LIBRARY_FORMS:
form = LibraryForm(data) form = LIBRARY_FORMS[choice](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': elif choice == 'MAIL':
form = EmailForm(data) 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?") form.fields['domain'].help_text = mark_safe("Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>möchtest du eine Mailadresse beantragen?")
elif choice == 'LIT': elif choice == 'LIT':
form = LiteratureForm(data) form = LiteratureForm(data)
form.fields['notes'].help_text = "Bitte gib an, wofür du die Literatur verwenden möchtest." form.fields['notes'].help_text = "Bitte gib an, wofür du die Literatur verwenden möchtest."
@ -196,10 +106,10 @@ class ExternView(CookieWizardView):
form = BusinessCardForm(data) form = BusinessCardForm(data)
elif choice == 'LIST': elif choice == 'LIST':
form = ListForm(data) 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?") form.fields['domain'].help_text = mark_safe("Mit welcher Domain, bzw. für welches Wikimedia-Projekt,<br>möchtest du eine Mailingliste beantragen?")
elif choice == 'TRAV': elif choice == 'TRAV':
form = TravelForm(data) form = TravelForm(data)
else: else: # pragma: no cover
raise RuntimeError(f'ERROR! UNKNOWN FORMTYPE {choice} in ExternView') raise RuntimeError(f'ERROR! UNKNOWN FORMTYPE {choice} in ExternView')
self.choice = choice self.choice = choice
else: else:
@ -219,8 +129,6 @@ class ExternView(CookieWizardView):
for form in form_list: for form in form_list:
data = {**data, **form.cleaned_data} data = {**data, **form.cleaned_data}
data['username'] = self.request.session['user']['username']
if data['choice'] == 'LIT': if data['choice'] == 'LIT':
if data['selfbuy'] == 'TRUE': if data['selfbuy'] == 'TRUE':
data['selfbuy_give_data'] = 'False' data['selfbuy_give_data'] = 'False'
@ -229,12 +137,14 @@ class ExternView(CookieWizardView):
modell = form.save(commit=False) modell = form.save(commit=False)
# we have to copy the data from the first form here # we have to copy the data from the first form here
# this is a bit ugly code. can we copy this without explicit writing? # this is a bit ugly code. can we copy this without explicit writing?
if data['choice'] == 'LIT': if data['choice'] == 'LIT':
modell.selfbuy_give_data = data['selfbuy_give_data'] modell.selfbuy_give_data = data['selfbuy_give_data']
if user := self.request.session.get('user'):
modell.username = user['username']
modell.realname = data['realname'] modell.realname = data['realname']
modell.username = data['username']
modell.email = data['email'] modell.email = data['email']
# write type of form in some cases # write type of form in some cases
if data['choice'] in ('BIB', 'ELIT', 'SOFT'): if data['choice'] in ('BIB', 'ELIT', 'SOFT'):
@ -244,7 +154,7 @@ class ExternView(CookieWizardView):
# add some data to context for mail templates # add some data to context for mail templates
data['pk'] = modell.pk data['pk'] = modell.pk
data['urlprefix'] = settings.URLPREFIX data['url_prefix'] = settings.EMAIL_URL_PREFIX
data['grant'] = ('LIT', 'SOFT', 'ELIT', 'BIB', 'IFG') data['grant'] = ('LIT', 'SOFT', 'ELIT', 'BIB', 'IFG')
data['DOMAIN'] = ('MAIL', 'LIST') data['DOMAIN'] = ('MAIL', 'LIST')
data['typestring'] = TYPE_CHOICES[data['choice']] data['typestring'] = TYPE_CHOICES[data['choice']]
@ -253,11 +163,11 @@ class ExternView(CookieWizardView):
context = { 'data': data } context = { 'data': data }
try: try:
# - mail with entered data to the Volunteer # - mail with entered data to the Volunteer
txt_mail_template1 = get_template('input/ifg_volunteer_mail.txt') txt_mail_template1 = get_template('input/ifg_volunteer_mail.txt')
html_mail_template1 = get_template('input/ifg_volunteer_mail.html') html_mail_template1 = get_template('input/ifg_volunteer_mail.html')
subject1, from_email1, to1 = 'Formular ausgefüllt', IF_EMAIL, data['email'] subject1, from_email1, to1 = 'Formular ausgefüllt', settings.IF_EMAIL, data['email']
text_content1 = txt_mail_template1.render(context) text_content1 = txt_mail_template1.render(context)
html_content1 = html_mail_template1.render(context) html_content1 = html_mail_template1.render(context)
msg1 = EmailMultiAlternatives(subject1, text_content1, from_email1, [to1]) msg1 = EmailMultiAlternatives(subject1, text_content1, from_email1, [to1])
@ -271,11 +181,11 @@ class ExternView(CookieWizardView):
# [data['email']], # [data['email']],
# fail_silently=False) # fail_silently=False)
## - mail to IF with link to accept/decline ## - mail to IF with link to accept/decline
txt_mail_template = get_template('input/if_mail.txt') txt_mail_template = get_template('input/if_mail.txt')
html_mail_template = get_template('input/if_mail.html') html_mail_template = get_template('input/if_mail.html')
subject, from_email, to = 'Formular ausgefüllt', IF_EMAIL, IF_EMAIL subject, from_email, to = 'Formular ausgefüllt', settings.IF_EMAIL, settings.IF_EMAIL
text_content = txt_mail_template.render(context) text_content = txt_mail_template.render(context)
html_content = html_mail_template.render(context) html_content = html_mail_template.render(context)
msg2 = EmailMultiAlternatives(subject, text_content, from_email, [to]) msg2 = EmailMultiAlternatives(subject, text_content, from_email, [to])
@ -289,7 +199,7 @@ class ExternView(CookieWizardView):
# [IF_EMAIL], # [IF_EMAIL],
# fail_silently=False) # fail_silently=False)
## raise SMTPException("testing pupose only") ## raise SMTPException("testing pupose only")
except BadHeaderError: except BadHeaderError:
modell.delete() modell.delete()
return HttpResponse('Invalid header found. Data not saved!') return HttpResponse('Invalid header found. Data not saved!')

27
requirements.txt Executable file → Normal file
View File

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