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 38eee0f..e3bb2e4 100644 Binary files a/data/db.sqlite3 and b/data/db.sqlite3 differ diff --git a/dokumente/admin.py b/dokumente/admin.py index 82331ae..6173c0f 100644 --- a/dokumente/admin.py +++ b/dokumente/admin.py @@ -1,6 +1,12 @@ from django.contrib import admin #from nested_inline.admin import NestedStackedInline, NestedModelAdmin -from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline +try: + from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline +except ImportError: + # Fallback to regular admin if nested_admin is not available + NestedStackedInline = admin.StackedInline + NestedModelAdmin = admin.ModelAdmin + NestedTabularInline = admin.TabularInline from django import forms from mptt.forms import TreeNodeMultipleChoiceField from mptt.admin import DraggableMPTTAdmin @@ -180,7 +186,18 @@ class DokumentAdmin(SortableAdminBase, NestedModelAdmin): @admin.register(VorgabenTable) class VorgabenTableAdmin(admin.ModelAdmin): - list_display = ['order', 'nummer', 'dokument', 'thema', 'titel', 'gueltigkeit_von', 'gueltigkeit_bis'] + list_display = ['order', 'nummer', 'dokument', 'thema', 'titel', 'gueltigkeit_von', 'gueltigkeit_bis', 'get_status_display'] + + def get_status_display(self, obj): + """ + Display status with emoji indicators for better visibility. + """ + if obj.dokument.aktiv: + return "✅ Aktiv" + else: + return "❌ Inaktiv" + + get_status_display.short_description = 'Status' list_display_links = ['dokument'] list_editable = ['order', 'nummer', 'thema', 'titel', 'gueltigkeit_von', 'gueltigkeit_bis'] list_filter = ['dokument', 'thema', 'gueltigkeit_von', 'gueltigkeit_bis'] @@ -188,6 +205,8 @@ class VorgabenTableAdmin(admin.ModelAdmin): autocomplete_fields = ['dokument', 'thema', 'stichworte', 'referenzen', 'relevanz'] ordering = ['order'] list_per_page = 100 + date_hierarchy = 'gueltigkeit_von' + date_hierarchy = 'gueltigkeit_von' fieldsets = ( ('Grunddaten', { @@ -239,6 +258,7 @@ class VorgabeAdmin(NestedModelAdmin): def vorgabe_nummer(self, obj): return obj.Vorgabennummer() + vorgabe_nummer.short_description = 'Vorgabennummer' admin.site.register(Checklistenfrage) diff --git a/dokumente/migrations/0010_add_indexes_and_constraints.py b/dokumente/migrations/0010_add_indexes_and_constraints.py new file mode 100644 index 0000000..2035634 --- /dev/null +++ b/dokumente/migrations/0010_add_indexes_and_constraints.py @@ -0,0 +1,75 @@ +# Generated by Django 4.2.25 on 2025-11-04 15:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dokumente', '0009_alter_vorgabe_options_vorgabe_order'), + ] + + operations = [ + migrations.CreateModel( + name='VorgabenTable', + fields=[ + ], + options={ + 'verbose_name': 'Vorgabe (Tabellenansicht)', + 'verbose_name_plural': 'Vorgaben (Tabellenansicht)', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('dokumente.vorgabe',), + ), + migrations.AlterField( + model_name='dokument', + name='aktiv', + field=models.BooleanField(), + ), + migrations.AlterField( + model_name='dokument', + name='gueltigkeit_bis', + field=models.DateField(blank=True, db_index=True, null=True), + ), + migrations.AlterField( + model_name='dokument', + name='gueltigkeit_von', + field=models.DateField(blank=True, db_index=True, null=True), + ), + migrations.AlterField( + model_name='dokument', + name='name', + field=models.CharField(db_index=True, max_length=255), + ), + migrations.AlterField( + model_name='vorgabe', + name='gueltigkeit_bis', + field=models.DateField(blank=True, db_index=True, null=True), + ), + migrations.AlterField( + model_name='vorgabe', + name='gueltigkeit_von', + field=models.DateField(db_index=True), + ), + migrations.AlterField( + model_name='vorgabe', + name='nummer', + field=models.IntegerField(db_index=True), + ), + migrations.AlterField( + model_name='vorgabe', + name='order', + field=models.IntegerField(db_index=True), + ), + migrations.AlterField( + model_name='vorgabe', + name='titel', + field=models.CharField(db_index=True, max_length=255), + ), + migrations.AddConstraint( + model_name='vorgabe', + constraint=models.UniqueConstraint(fields=('dokument', 'thema', 'nummer', 'gueltigkeit_von'), name='unique_vorgabe_active_period'), + ), + ] diff --git a/dokumente/models.py b/dokumente/models.py index 13a95e7..0f13349 100644 --- a/dokumente/models.py +++ b/dokumente/models.py @@ -40,14 +40,14 @@ class Thema(models.Model): class Dokument(models.Model): nummer = models.CharField(max_length=50, primary_key=True) dokumententyp = models.ForeignKey(Dokumententyp, on_delete=models.PROTECT) - name = models.CharField(max_length=255) + name = models.CharField(max_length=255, db_index=True) autoren = models.ManyToManyField(Person, related_name='verfasste_dokumente') pruefende = models.ManyToManyField(Person, related_name='gepruefte_dokumente') - gueltigkeit_von = models.DateField(null=True, blank=True) - gueltigkeit_bis = models.DateField(null=True, blank=True) + gueltigkeit_von = models.DateField(null=True, blank=True, db_index=True) + gueltigkeit_bis = models.DateField(null=True, blank=True, db_index=True) signatur_cso = models.CharField(max_length=255, blank=True) anhaenge = models.TextField(blank=True) - aktiv = models.BooleanField(blank=True) + aktiv = models.BooleanField() def __str__(self): return f"{self.nummer} – {self.name}" @@ -57,14 +57,14 @@ class Dokument(models.Model): verbose_name="Dokument" class Vorgabe(models.Model): - order = models.IntegerField() - nummer = models.IntegerField() + order = models.IntegerField(db_index=True) + nummer = models.IntegerField(db_index=True) dokument = models.ForeignKey(Dokument, on_delete=models.CASCADE, related_name='vorgaben') thema = models.ForeignKey(Thema, on_delete=models.PROTECT, blank=False) - titel = models.CharField(max_length=255) + titel = models.CharField(max_length=255, db_index=True) referenzen = models.ManyToManyField(Referenz, blank=True) - gueltigkeit_von = models.DateField() - gueltigkeit_bis = models.DateField(blank=True,null=True) + gueltigkeit_von = models.DateField(db_index=True) + gueltigkeit_bis = models.DateField(blank=True,null=True, db_index=True) stichworte = models.ManyToManyField(Stichwort, blank=True) relevanz = models.ManyToManyField(Rolle,blank=True) @@ -206,6 +206,12 @@ class Vorgabe(models.Model): class Meta: verbose_name_plural="Vorgaben" ordering = ['order'] + constraints = [ + models.UniqueConstraint( + fields=['dokument', 'thema', 'nummer', 'gueltigkeit_von'], + name='unique_vorgabe_active_period' + ), + ] class VorgabeLangtext(Textabschnitt): abschnitt=models.ForeignKey(Vorgabe,on_delete=models.CASCADE) diff --git a/dokumente/views.py b/dokumente/views.py index 0c6df0a..06516b7 100644 --- a/dokumente/views.py +++ b/dokumente/views.py @@ -13,7 +13,7 @@ calendar=parsedatetime.Calendar() def standard_list(request): - dokumente = Dokument.objects.all() + dokumente = Dokument.objects.select_related('dokumententyp').prefetch_related('vorgaben__thema').all() return render(request, 'standards/standard_list.html', {'dokumente': dokumente} ) @@ -29,7 +29,11 @@ def standard_detail(request, nummer,check_date=""): check_date = date.today() standard.history = False standard.check_date=check_date - vorgaben = list(standard.vorgaben.order_by("thema","nummer").select_related("thema","dokument")) # convert queryset to list so we can attach attributes + vorgaben = list(standard.vorgaben.order_by("thema","nummer") + .select_related("thema","dokument") + .prefetch_related("relevanz", "referenzen", + "vorgabekurztext_set__abschnitttyp", + "vorgabelangtext_set__abschnitttyp")) # convert queryset to list so we can attach attributes standard.geltungsbereich_html = render_textabschnitte(standard.geltungsbereich_set.order_by("order").select_related("abschnitttyp")) standard.einleitung_html=render_textabschnitte(standard.einleitung_set.order_by("order")) diff --git a/test_apps_incrementally.py b/test_apps_incrementally.py new file mode 100644 index 0000000..c5eb4eb --- /dev/null +++ b/test_apps_incrementally.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +"""Test Django settings with apps added incrementally""" +import os +import sys +from pathlib import Path + +# Base settings template +base_settings = """ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = 'test-key' +DEBUG = True +ALLOWED_HOSTS = [] + +INSTALLED_APPS = [ + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + {apps} +] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'test.db.sqlite3', + } +} +""" + +# Apps to test +apps_to_test = [ + "'dokumente'", + "'abschnitte'", + "'stichworte'", + "'referenzen'", + "'rollen'", + "'mptt'", + "'pages'", + "'nested_admin'", + "'revproxy'", +] + +# Test apps incrementally +current_apps = "" +for i, app in enumerate(apps_to_test): + print(f"\n=== Testing with apps {i+1}/{len(apps_to_test)}: {app} ===") + + # Add the next app + if current_apps: + current_apps += ",\n " + app + else: + current_apps = app + + # Write settings file + settings_content = base_settings.format(apps=current_apps) + with open('test_settings.py', 'w') as f: + f.write(settings_content) + + # Test the settings + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_settings') + + try: + # Clear Django's internal cache + if 'django.conf' in sys.modules: + del sys.modules['django.conf'] + if 'django.conf.settings' in sys.modules: + del sys.modules['django.conf.settings'] + if 'test_settings' in sys.modules: + del sys.modules['test_settings'] + + import django + from django.conf import settings + + # Reset Django + if hasattr(settings, '_wrapped'): + settings._wrapped = None + + django.setup() + print(f"✓ SUCCESS: Apps up to {app} work fine") + + except Exception as e: + print(f"✗ FAILED: Error with {app}: {e}") + break + +# Clean up +import os +if os.path.exists('test_settings.py'): + os.remove('test_settings.py') +if os.path.exists('test.db.sqlite3'): + os.remove('test.db.sqlite3') \ No newline at end of file diff --git a/test_current_settings.py b/test_current_settings.py new file mode 100644 index 0000000..825fd6b --- /dev/null +++ b/test_current_settings.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +"""Test current settings file directly""" +import os +import sys +from pathlib import Path + +# Set up environment like Django would +BASE_DIR = Path('.').resolve() + +# Read and execute the settings file with proper context +settings_globals = { + '__name__': 'VorgabenUI.settings', + '__file__': 'VorgabenUI/settings.py', + '__package__': 'VorgabenUI', + 'os': os, + 'Path': Path, + 'BASE_DIR': BASE_DIR, +} + +with open('VorgabenUI/settings.py', 'r') as f: + settings_code = f.read() + +print('=== Executing current settings file ===') +try: + exec(settings_code, settings_globals) + print('Settings executed successfully') + print('DATABASES:', settings_globals.get('DATABASES', 'NOT FOUND')) + print('SECRET_KEY:', settings_globals.get('SECRET_KEY', 'NOT FOUND')) + print('DEBUG:', settings_globals.get('DEBUG', 'NOT FOUND')) +except Exception as e: + print('Error executing settings:', e) + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/test_direct_import.py b/test_direct_import.py new file mode 100644 index 0000000..dc85df2 --- /dev/null +++ b/test_direct_import.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +import os +import sys + +# Change to project directory +os.chdir('/home/adebaumann/development/vgui-cicd') +sys.path.insert(0, '.') + +print("Testing direct import of settings module...") + +try: + import VorgabenUI.settings as settings_module + print("✓ Import successful") + + # Check if attributes exist + print("Has DATABASES:", hasattr(settings_module, 'DATABASES')) + print("Has BASE_DIR:", hasattr(settings_module, 'BASE_DIR')) + + if hasattr(settings_module, 'DATABASES'): + print("DATABASES value:", settings_module.DATABASES) + else: + print("DATABASES not found") + + if hasattr(settings_module, 'BASE_DIR'): + print("BASE_DIR value:", settings_module.BASE_DIR) + else: + print("BASE_DIR not found") + +except Exception as e: + print("✗ Import failed:", e) + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/test_django_setup.py b/test_django_setup.py new file mode 100644 index 0000000..79524ac --- /dev/null +++ b/test_django_setup.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +"""Test Django settings loading""" +import os +import sys +import django +from django.conf import settings + +# Set the settings module +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'VorgabenUI.settings') + +print("=== Before Django setup ===") +print(f"Settings module: {os.environ.get('DJANGO_SETTINGS_MODULE')}") + +try: + print("Calling django.setup()...") + django.setup() + print("Django setup completed successfully") + + print("=== After Django setup ===") + print(f"Settings configured: {settings.configured}") + print(f"Available attributes: {[attr for attr in dir(settings) if not attr.startswith('_')]}") + + if hasattr(settings, 'DATABASES'): + print(f"DATABASES: {settings.DATABASES}") + else: + print("DATABASES not found in settings") + +except Exception as e: + print(f"Error during Django setup: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/test_fresh_django.py b/test_fresh_django.py new file mode 100644 index 0000000..e4d0df7 --- /dev/null +++ b/test_fresh_django.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +import os +import sys +import subprocess + +# Change to project directory +os.chdir('/home/adebaumann/development/vgui-cicd') + +# Clear Python cache +subprocess.run(['find', '.', '-name', '*.pyc', '-delete'], capture_output=True) +subprocess.run(['find', '.', '-name', '__pycache__', '-type', 'd', '-exec', 'rm', '-rf', '{}', '+'], capture_output=True) + +# Set environment variable +os.environ['DJANGO_SETTINGS_MODULE'] = 'VorgabenUI.settings' + +# Test Django import +import django +from django.conf import settings + +try: + django.setup() + print('✓ Django setup successful') + print('DATABASES:', settings.DATABASES) + print('BASE_DIR:', getattr(settings, 'BASE_DIR', 'NOT SET')) +except Exception as e: + print('✗ Django setup failed:', e) + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/test_minimal_settings.py b/test_minimal_settings.py new file mode 100644 index 0000000..06baa03 --- /dev/null +++ b/test_minimal_settings.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +"""Test minimal Django settings""" +import os +import sys +from pathlib import Path + +# Create a minimal settings file +minimal_settings = """ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = 'test-key' +DEBUG = True +ALLOWED_HOSTS = [] + +INSTALLED_APPS = [ + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'test.db.sqlite3', + } +} +""" + +# Write minimal settings to a file +with open('minimal_settings.py', 'w') as f: + f.write(minimal_settings) + +# Test with minimal settings +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'minimal_settings') + +import django +from django.conf import settings + +print("=== Testing with minimal settings ===") +try: + django.setup() + print("Django setup completed successfully") + print(f"DATABASES: {settings.DATABASES}") +except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + +# Clean up +import os +os.remove('minimal_settings.py') \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..4688656 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Test package for VorgabenUI \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..c094808 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,149 @@ +""" +Integration tests for VorgabenUI application. +""" +from django.test import TestCase, Client +from django.urls import reverse +from django.contrib.auth.models import User +from datetime import date, timedelta +from dokumente.models import Dokument, Dokumententyp, Vorgabe, Thema + + +class DokumentIntegrationTestCase(TestCase): + """Test document functionality end-to-end.""" + + def setUp(self): + self.client = Client() + self.dokumententyp = Dokumententyp.objects.create( + name="Test Typ", + verantwortliche_ve="Test VE" + ) + self.dokument = Dokument.objects.create( + nummer="TEST-001", + name="Test Dokument", + dokumententyp=self.dokumententyp, + gueltigkeit_von=date.today(), + aktiv=True + ) + self.thema = Thema.objects.create( + name="Test Thema", + erklaerung="Test Erklärung" + ) + self.vorgabe = Vorgabe.objects.create( + order=1, + nummer=1, + dokument=self.dokument, + thema=self.thema, + titel="Test Vorgabe", + gueltigkeit_von=date.today() + ) + + def test_standard_list_view(self): + """Test that standard list view loads and displays documents.""" + response = self.client.get(reverse('standard_list')) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "TEST-001") + self.assertContains(response, "Test Dokument") + + def test_standard_detail_view(self): + """Test that standard detail view loads and displays document details.""" + response = self.client.get( + reverse('standard_detail', kwargs={'nummer': 'TEST-001'}) + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Test Dokument") + self.assertContains(response, "Test Vorgabe") + + def test_standard_json_view(self): + """Test that JSON endpoint returns valid data.""" + response = self.client.get( + reverse('standard_json', kwargs={'nummer': 'TEST-001'}) + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json') + + def test_vorgabe_status_calculation(self): + """Test vorgabe status calculation for different dates.""" + # Test active vorgabe + self.assertEqual(self.vorgabe.get_status(), 'active') + + # Test future vorgabe + future_date = date.today() + timedelta(days=30) + future_vorgabe = Vorgabe.objects.create( + order=2, + nummer=2, + dokument=self.dokument, + thema=self.thema, + titel="Future Vorgabe", + gueltigkeit_von=future_date + ) + self.assertEqual(future_vorgabe.get_status(), 'future') + + # Test expired vorgabe + past_date = date.today() - timedelta(days=30) + expired_vorgabe = Vorgabe.objects.create( + order=3, + nummer=3, + dokument=self.dokument, + thema=self.thema, + titel="Expired Vorgabe", + gueltigkeit_von=past_date, + gueltigkeit_bis=date.today() - timedelta(days=1) + ) + self.assertEqual(expired_vorgabe.get_status(), 'expired') + + +class SearchIntegrationTestCase(TestCase): + """Test search functionality.""" + + def setUp(self): + self.client = Client() + # Create test data for search testing + self.dokumententyp = Dokumententyp.objects.create( + name="Test Typ", + verantwortliche_ve="Test VE" + ) + self.dokument = Dokument.objects.create( + nummer="SEARCH-001", + name="Searchable Document", + dokumententyp=self.dokumententyp, + gueltigkeit_von=date.today(), + aktiv=True + ) + + def test_search_view_loads(self): + """Test that search view loads.""" + response = self.client.get(reverse('search')) + self.assertEqual(response.status_code, 200) + + def test_search_functionality(self): + """Test search functionality with query parameters.""" + response = self.client.get(reverse('search'), {'q': 'Searchable'}) + self.assertEqual(response.status_code, 200) + + +class AdminIntegrationTestCase(TestCase): + """Test admin interface functionality.""" + + def setUp(self): + self.user = User.objects.create_superuser( + username='admin', + email='admin@example.com', + password='testpass123' + ) + self.client = Client() + self.client.login(username='admin', password='testpass123') + + def test_admin_accessible(self): + """Test that admin interface is accessible.""" + response = self.client.get('/autorenumgebung/') + self.assertEqual(response.status_code, 200) + + def test_dokument_admin_list(self): + """Test document admin list view.""" + response = self.client.get('/autorenumgebung/dokumente/dokument/') + self.assertEqual(response.status_code, 200) + + def test_vorgabe_admin_list(self): + """Test vorgabe admin list view.""" + response = self.client.get('/autorenumgebung/dokumente/vorgabentable/') + self.assertEqual(response.status_code, 200) \ No newline at end of file