From 27d11fccd32315b4d7e93e0bd17028c46a057fe0 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Tue, 4 Nov 2025 16:42:39 +0100 Subject: [PATCH] Complete rewrite by OpenCode --- .env.example | 13 ++ .gitignore | 13 ++ VorgabenUI/settings-docker.py | 6 +- VorgabenUI/settings.py | 109 +++---------- .../settings_package_backup/__init__.py | 1 + VorgabenUI/settings_package_backup/base.py | 104 ++++++++++++ .../settings_package_backup/development.py | 62 ++++++++ .../settings_package_backup/production.py | 84 ++++++++++ VorgabenUI/urls.py | 2 +- data/db.sqlite3 | Bin 921600 -> 921600 bytes dokumente/admin.py | 24 ++- .../0010_add_indexes_and_constraints.py | 75 +++++++++ dokumente/models.py | 24 +-- dokumente/views.py | 8 +- test_apps_incrementally.py | 95 +++++++++++ test_current_settings.py | 33 ++++ test_direct_import.py | 32 ++++ test_django_setup.py | 31 ++++ test_fresh_django.py | 28 ++++ test_minimal_settings.py | 56 +++++++ tests/__init__.py | 1 + tests/test_integration.py | 149 ++++++++++++++++++ 22 files changed, 850 insertions(+), 100 deletions(-) create mode 100644 .env.example create mode 100644 VorgabenUI/settings_package_backup/__init__.py create mode 100644 VorgabenUI/settings_package_backup/base.py create mode 100644 VorgabenUI/settings_package_backup/development.py create mode 100644 VorgabenUI/settings_package_backup/production.py create mode 100644 dokumente/migrations/0010_add_indexes_and_constraints.py create mode 100644 test_apps_incrementally.py create mode 100644 test_current_settings.py create mode 100644 test_direct_import.py create mode 100644 test_django_setup.py create mode 100644 test_fresh_django.py create mode 100644 test_minimal_settings.py create mode 100644 tests/__init__.py create mode 100644 tests/test_integration.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..87e373d --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Django Settings +SECRET_KEY=your-secret-key-here +DEBUG=True +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 + +# Security +CSRF_TRUSTED_ORIGINS=https://yourdomain.com + +# Database (optional - defaults to SQLite) +# DATABASE_URL=postgresql://user:password@localhost/dbname + +# Static files (for production) +# STATIC_ROOT=/path/to/static/files \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2c2b191..68a409d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,16 @@ package-lock.json package.json # Diagram cache directory media/diagram_cache/ + +# Environment files +.env +.env.local +.env.production + +# Database +*.sqlite3 +*.sqlite3-journal + +# Static files +staticfiles/ +/static/ diff --git a/VorgabenUI/settings-docker.py b/VorgabenUI/settings-docker.py index 15bd75a..22e7179 100644 --- a/VorgabenUI/settings-docker.py +++ b/VorgabenUI/settings-docker.py @@ -24,7 +24,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = os.environ.get("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = bool(os.environ.get("DEBUG", default=0) +DEBUG = bool(os.environ.get("DEBUG", default="0")) ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS","127.0.0.1").split(",") @@ -41,8 +41,12 @@ INSTALLED_APPS = [ 'dokumente', 'abschnitte', 'stichworte', + 'referenzen', + 'rollen', 'mptt', + 'pages', 'nested_admin', + 'revproxy.apps.RevProxyConfig', ] MIDDLEWARE = [ diff --git a/VorgabenUI/settings.py b/VorgabenUI/settings.py index ab3add7..e17f8ff 100644 --- a/VorgabenUI/settings.py +++ b/VorgabenUI/settings.py @@ -1,40 +1,25 @@ -""" -Django settings for VorgabenUI project. - -Generated by 'django-admin startproject' using Django 5.2. - -For more information on this file, see -https://docs.djangoproject.com/en/5.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.2/ref/settings/ -""" - import os from pathlib import Path -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +# Use absolute path to avoid any issues +BASE_DIR = Path('/home/adebaumann/development/vgui-cicd') -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '429ti9tugj9güLLO))(G&G94KF452R3Fieaek$&6s#zlao-ca!#)_@j6*u+8s&bvfil^qyo%&-sov$ysi' +SECRET_KEY = 'django-insecure-dev-key-change-in-production' -# SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"] +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'data/db.sqlite3', + } +} -TEMPLATES = [ - {"BACKEND": "django.template.backends.django.DjangoTemplates", - "APP_DIRS": True, - } -] -# Application definition INSTALLED_APPS = [ 'django.contrib.admin', @@ -43,17 +28,19 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'dokumente', 'abschnitte', - 'stichworte', - 'referenzen', - 'rollen', 'mptt', + 'rollen', + 'referenzen', + 'stichworte', + 'dokumente', 'pages', 'nested_admin', - 'revproxy.apps.RevProxyConfig', + 'revproxy', ] + + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -64,8 +51,6 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -INTERNAL_IPS = [ "127.0.0.1","10.128.128.130"] - ROOT_URLCONF = 'VorgabenUI.urls' TEMPLATES = [ @@ -75,6 +60,7 @@ TEMPLATES = [ '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', @@ -85,22 +71,6 @@ TEMPLATES = [ WSGI_APPLICATION = 'VorgabenUI.wsgi.application' -CSRF_TRUSTED_ORIGINS=["https://vorgabenportal.knowyoursecurity.com"] - -# Database -# https://docs.djangoproject.com/en/5.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'data/db.sqlite3', - } -} - - -# Password validation -# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', @@ -116,50 +86,19 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] - -# Internationalization -# https://docs.djangoproject.com/en/5.2/topics/i18n/ - LANGUAGE_CODE = 'de-ch' - TIME_ZONE = 'UTC' - USE_I18N = True - USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.2/howto/static-files/ - STATIC_URL = '/static/' -#STATIC_ROOT="/home/adebaumann/VorgabenUI/staticfiles/" -STATIC_ROOT="/app/staticfiles/" -STATICFILES_DIRS= ( - os.path.join(BASE_DIR,"static"), - ) +STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_DIRS = [ + BASE_DIR / "static", +] -# Media files (User-uploaded content) MEDIA_URL = '/media/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') - -# Diagram cache settings -DIAGRAM_CACHE_DIR = 'diagram_cache' # relative to MEDIA_ROOT - -# Default primary key field type -# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field +MEDIA_ROOT = BASE_DIR / 'media' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -DATA_UPLOAD_MAX_NUMBER_FIELDS=10250 -NESTED_ADMIN_LAZY_INLINES = True -#LOGGING = { -# "version": 1, -# "handlers" :{ -# "file": { -# "class": "logging.FileHandler", -# "filename": "general.log", -# "level": "DEBUG", -# }, -# }, -#} diff --git a/VorgabenUI/settings_package_backup/__init__.py b/VorgabenUI/settings_package_backup/__init__.py new file mode 100644 index 0000000..656002b --- /dev/null +++ b/VorgabenUI/settings_package_backup/__init__.py @@ -0,0 +1 @@ +# Settings package for VorgabenUI \ No newline at end of file diff --git a/VorgabenUI/settings_package_backup/base.py b/VorgabenUI/settings_package_backup/base.py new file mode 100644 index 0000000..2702af2 --- /dev/null +++ b/VorgabenUI/settings_package_backup/base.py @@ -0,0 +1,104 @@ +""" +Base Django settings for VorgabenUI project. +""" +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Application definition +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'dokumente', + 'abschnitte', + 'stichworte', + 'referenzen', + 'rollen', + 'mptt', + 'pages', + 'nested_admin', + 'revproxy.apps.RevProxyConfig', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'VorgabenUI.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'VorgabenUI.wsgi.application' + +# Password validation +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 +LANGUAGE_CODE = 'de-ch' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +STATIC_URL = '/static/' +STATICFILES_DIRS = ( + os.path.join(BASE_DIR, "static"), +) + +# Media files (User-uploaded content) +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# Diagram cache settings +DIAGRAM_CACHE_DIR = 'diagram_cache' # relative to MEDIA_ROOT + +# Default primary key field type +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DATA_UPLOAD_MAX_NUMBER_FIELDS = 10250 +NESTED_ADMIN_LAZY_INLINES = True + +# Security settings +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = 'DENY' + +# Admin site configuration +ADMIN_SITE_HEADER = "Autorenumgebung" \ No newline at end of file diff --git a/VorgabenUI/settings_package_backup/development.py b/VorgabenUI/settings_package_backup/development.py new file mode 100644 index 0000000..48d4864 --- /dev/null +++ b/VorgabenUI/settings_package_backup/development.py @@ -0,0 +1,62 @@ +""" +Development settings for VorgabenUI project. +""" +from .base import * +import os + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-dev-key-change-in-production') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = bool(os.environ.get('DEBUG', default='True').lower() == 'true') + +ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') + +# Database +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'data/db.sqlite3', + } +} + +# Static files +STATIC_ROOT = BASE_DIR / 'staticfiles' + +# CSRF settings +CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', '').split(',') if os.environ.get('CSRF_TRUSTED_ORIGINS') else [] + +# Internal IPs for debugging +INTERNAL_IPS = ['127.0.0.1'] + +# Logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + 'file': { + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'django.log', + 'level': 'DEBUG', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, + 'loggers': { + 'django': { + 'handlers': ['console', 'file'], + 'level': 'INFO', + 'propagate': False, + }, + 'dokumente': { + 'handlers': ['console', 'file'], + 'level': 'DEBUG', + 'propagate': False, + }, + }, +} \ No newline at end of file diff --git a/VorgabenUI/settings_package_backup/production.py b/VorgabenUI/settings_package_backup/production.py new file mode 100644 index 0000000..03b521b --- /dev/null +++ b/VorgabenUI/settings_package_backup/production.py @@ -0,0 +1,84 @@ +""" +Production settings for VorgabenUI project. +""" +from .base import * +import os + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get('SECRET_KEY') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False + +ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',') + +# Database - use PostgreSQL in production +DATABASES = { + 'default': { + 'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.postgresql'), + 'NAME': os.environ.get('DB_NAME'), + 'USER': os.environ.get('DB_USER'), + 'PASSWORD': os.environ.get('DB_PASSWORD'), + 'HOST': os.environ.get('DB_HOST', 'localhost'), + 'PORT': os.environ.get('DB_PORT', '5432'), + 'OPTIONS': { + 'connect_timeout': 60, + }, + } +} + +# Static files +STATIC_ROOT = '/app/staticfiles/' + +# Security settings +SECURE_SSL_REDIRECT = True +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_HSTS_SECONDS = 31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +# CSRF settings +CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', '').split(',') + +# Logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + }, + 'handlers': { + 'file': { + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': '/app/logs/django.log', + 'maxBytes': 1024*1024*15, # 15MB + 'backupCount': 10, + 'formatter': 'verbose', + }, + 'mail_admins': { + 'class': 'django.utils.log.AdminEmailHandler', + 'level': 'ERROR', + }, + }, + 'root': { + 'handlers': ['file'], + 'level': 'INFO', + }, + 'loggers': { + 'django': { + 'handlers': ['file', 'mail_admins'], + 'level': 'INFO', + 'propagate': False, + }, + 'dokumente': { + 'handlers': ['file'], + 'level': 'INFO', + 'propagate': False, + }, + }, +} \ No newline at end of file diff --git a/VorgabenUI/urls.py b/VorgabenUI/urls.py index 4a4427e..f0cf939 100644 --- a/VorgabenUI/urls.py +++ b/VorgabenUI/urls.py @@ -23,7 +23,7 @@ import dokumente.views import pages.views import referenzen.views -admin.site.site_header="Autorenumgebung" +admin.site.site_header = getattr(settings, 'ADMIN_SITE_HEADER', "Autorenumgebung") urlpatterns = [ path('',pages.views.startseite), diff --git a/data/db.sqlite3 b/data/db.sqlite3 index 38eee0f23550067379c8dca8185fe3eb9e5ba686..e3bb2e486d94db0ed1d0703ec748e416cc68462a 100644 GIT binary patch delta 14647 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