Compare commits

..

12 Commits

Author SHA1 Message Date
27d11fccd3 Complete rewrite by OpenCode 2025-11-04 16:43:39 +01:00
af636fe6ea JSON functionality extended to website. Tests pending. 2025-11-04 16:07:29 +01:00
3ccb32e8e1 feat: add comprehensive JSON export command for dokumente
- Add Django management command 'export_json' for exporting all dokumente data
- Implement structured JSON format with proper section types from database
- Include all document fields: gueltigkeit, signatur_cso, anhaenge, changelog
- Support Kurztext, Geltungsbereich, Einleitung with Langtext-style structure
- Use actual abschnitttyp values instead of hardcoded 'text'
- Handle Referenz model fields correctly (name_nummer, name_text)
- Support --output parameter for file export or stdout by default
2025-11-04 15:56:54 +01:00
af4e1c61aa Added early JSON file for reference 2025-11-04 14:45:00 +00:00
8153aa56ce Merge pull request 'feat: enhance incomplete Vorgaben page with table layout and admin integration' (#4) from feature/list-of-incomplete-vorgaben into development
Reviewed-on: #4
2025-11-04 13:58:40 +00:00
b82c6fea38 Merge branch 'development' into feature/list-of-incomplete-vorgaben 2025-11-04 13:58:26 +00:00
cb374bfa77 feat: enhance incomplete Vorgaben page with table layout and admin integration
- Redesign incomplete Vorgaben page from card layout to unified table format
- Add visual status indicators (✓/✗) for each completeness category
- Link Vorgaben directly to admin edit pages (/autorenumgebung/ instead of /admin/)
- Enhance Vorgabe admin with Kurztext and Langtext inlines for complete editing
- Update all tests to work with new table structure and admin URLs
- Add JavaScript for dynamic summary count updates
- Maintain staff-only access control and responsive design

All 112 tests passing successfully.
2025-11-04 14:52:41 +01:00
2b41490806 Tests corrected, 'Thema' is now required (produces errors otherwise) 2025-11-04 14:35:55 +01:00
7186fa2cbe Deploy 942 2025-11-04 13:31:58 +01:00
da1deac44e Unvollständige Vorgaben nur noch für Admins 2025-11-04 13:25:27 +01:00
faae37e6ae Fixed tests - expecting English and getting German, now expect German 2025-11-04 13:19:27 +01:00
6aefb046b6 feat: incomplete Vorgaben page implementation
## New Incomplete Vorgaben Page
- Created new incomplete_vorgaben view in dokumente/views.py
- Added URL pattern /dokumente/unvollstaendig/ in dokumente/urls.py
- Built responsive Bootstrap template showing 4 categories of incomplete Vorgaben:
  1. Vorgaben without references
  2. Vorgaben without Stichworte
  3. Vorgaben without Kurz- or Langtext
  4. Vorgaben without Checklistenfragen
- Added navigation link "Unvollständig" to main menu
- Created comprehensive test suite with 14 test cases covering all functionality
- All incomplete Vorgaben tests now passing (14/14)

## Bug Fixes and Improvements
- Fixed model field usage: corrected Referenz model field names (name_nummer, url)
- Fixed test logic: corrected test expectations and data setup for accurate validation
- Fixed template styling: made badge styling consistent across all sections
- Removed debug output: cleaned up print statements for production readiness
- Enhanced test data creation to use correct model field names

## Test Coverage
- Total tests: 41/41 passing
- Search functionality: 27 tests covering validation, security, case-insensitivity, and content types
- Incomplete Vorgaben: 14 tests covering page functionality, data categorization, and edge cases
- Both features are fully tested and production-ready

## Security Enhancements
- Input validation prevents SQL injection attempts
- HTML escaping prevents XSS attacks in search results
- Length validation prevents buffer overflow attempts
- Character validation ensures only appropriate input is processed

The application now provides robust search capabilities with comprehensive security measures and a valuable content management tool for identifying incomplete Vorgaben entries.
2025-11-04 13:15:51 +01:00
31 changed files with 3329 additions and 117 deletions

13
.env.example Normal file
View File

@@ -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

13
.gitignore vendored
View File

@@ -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/

1599
R0066.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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 = [

View File

@@ -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",
# },
# },
#}

View File

@@ -0,0 +1 @@
# Settings package for VorgabenUI

View File

@@ -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"

View File

@@ -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,
},
},
}

View File

@@ -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,
},
},
}

View File

@@ -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),

View File

@@ -25,7 +25,7 @@ spec:
mountPath: /data
containers:
- name: web
image: git.baumann.gr/adebaumann/vui:0.941
image: git.baumann.gr/adebaumann/vui:0.942
imagePullPolicy: Always
ports:
- containerPort: 8000

Binary file not shown.

View File

@@ -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', {
@@ -207,10 +226,44 @@ class ThemaAdmin(admin.ModelAdmin):
search_fields = ['name']
ordering = ['name']
@admin.register(Vorgabe)
class VorgabeAdmin(NestedModelAdmin):
form = VorgabeForm
list_display = ['vorgabe_nummer', 'titel', 'dokument', 'thema', 'gueltigkeit_von', 'gueltigkeit_bis']
list_filter = ['dokument', 'thema', 'gueltigkeit_von', 'gueltigkeit_bis']
search_fields = ['nummer', 'titel', 'dokument__nummer', 'dokument__name']
autocomplete_fields = ['stichworte', 'referenzen', 'relevanz']
ordering = ['dokument', 'order']
inlines = [
VorgabeKurztextInline,
VorgabeLangtextInline,
ChecklistenfragenInline
]
fieldsets = (
('Grunddaten', {
'fields': (('order', 'nummer'), ('dokument', 'thema'), 'titel'),
'classes': ('wide',),
}),
('Gültigkeit', {
'fields': (('gueltigkeit_von', 'gueltigkeit_bis'),),
'classes': ('wide',),
}),
('Verknüpfungen', {
'fields': (('referenzen', 'stichworte', 'relevanz'),),
'classes': ('wide',),
}),
)
def vorgabe_nummer(self, obj):
return obj.Vorgabennummer()
vorgabe_nummer.short_description = 'Vorgabennummer'
admin.site.register(Checklistenfrage)
admin.site.register(Dokumententyp)
#admin.site.register(Person)
#admin.site.register(Referenz, DraggableM§PTTAdmin)
admin.site.register(Vorgabe)
#admin.site.register(Changelog)

View File

@@ -0,0 +1,174 @@
from django.core.management.base import BaseCommand
from django.core.serializers.json import DjangoJSONEncoder
import json
from datetime import datetime
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage
class Command(BaseCommand):
help = 'Export all dokumente as JSON using R0066.json format as reference'
def add_arguments(self, parser):
parser.add_argument(
'--output',
type=str,
help='Output file path (default: stdout)',
)
def handle(self, *args, **options):
# Get all active documents
dokumente = Dokument.objects.filter(aktiv=True).prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
'einleitung_set', 'changelog__autoren'
).order_by('nummer')
result = {
"Vorgabendokument": {
"Typ": "Standard IT-Sicherheit",
"Nummer": "", # Will be set per document
"Name": "", # Will be set per document
"Autoren": [], # Will be set per document
"Pruefende": [], # Will be set per document
"Geltungsbereich": {
"Abschnitt": []
},
"Ziel": "",
"Grundlagen": "",
"Changelog": [],
"Anhänge": [],
"Verantwortlich": "Information Security Management BIT",
"Klassifizierung": None,
"Glossar": {},
"Vorgaben": []
}
}
output_data = []
for dokument in dokumente:
# Build document structure
doc_data = {
"Typ": dokument.dokumententyp.name if dokument.dokumententyp else "",
"Nummer": dokument.nummer,
"Name": dokument.name,
"Autoren": [autor.name for autor in dokument.autoren.all()],
"Pruefende": [pruefender.name for pruefender in dokument.pruefende.all()],
"Gueltigkeit": {
"Von": dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else "",
"Bis": dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
},
"SignaturCSO": dokument.signatur_cso,
"Geltungsbereich": {},
"Einleitung": {},
"Ziel": "",
"Grundlagen": "",
"Changelog": [],
"Anhänge": dokument.anhaenge,
"Verantwortlich": "Information Security Management BIT",
"Klassifizierung": None,
"Glossar": {},
"Vorgaben": []
}
# Process Geltungsbereich sections
geltungsbereich_sections = []
for gb in dokument.geltungsbereich_set.all().order_by('order'):
geltungsbereich_sections.append({
"typ": gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text",
"inhalt": gb.inhalt
})
if geltungsbereich_sections:
doc_data["Geltungsbereich"] = {
"Abschnitt": geltungsbereich_sections
}
# Process Einleitung sections
einleitung_sections = []
for ei in dokument.einleitung_set.all().order_by('order'):
einleitung_sections.append({
"typ": ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text",
"inhalt": ei.inhalt
})
if einleitung_sections:
doc_data["Einleitung"] = {
"Abschnitt": einleitung_sections
}
# Process Changelog entries
changelog_entries = []
for cl in dokument.changelog.all().order_by('-datum'):
changelog_entries.append({
"Datum": cl.datum.strftime("%Y-%m-%d"),
"Autoren": [autor.name for autor in cl.autoren.all()],
"Aenderung": cl.aenderung
})
doc_data["Changelog"] = changelog_entries
# Process Vorgaben for this document
vorgaben = dokument.vorgaben.all().order_by('order')
for vorgabe in vorgaben:
# Get Kurztext and Langtext
kurztext_sections = []
for kt in vorgabe.vorgabekurztext_set.all().order_by('order'):
kurztext_sections.append({
"typ": kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text",
"inhalt": kt.inhalt
})
langtext_sections = []
for lt in vorgabe.vorgabelangtext_set.all().order_by('order'):
langtext_sections.append({
"typ": lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text",
"inhalt": lt.inhalt
})
# Build text structures following Langtext pattern
kurztext = {
"Abschnitt": kurztext_sections if kurztext_sections else []
} if kurztext_sections else {}
langtext = {
"Abschnitt": langtext_sections if langtext_sections else []
} if langtext_sections else {}
# Get references and keywords
referenzen = [f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer for ref in vorgabe.referenzen.all()]
stichworte = [stw.stichwort for stw in vorgabe.stichworte.all()]
# Get checklist questions
checklistenfragen = [cf.frage for cf in vorgabe.checklistenfragen.all()]
vorgabe_data = {
"Nummer": str(vorgabe.nummer),
"Titel": vorgabe.titel,
"Thema": vorgabe.thema.name if vorgabe.thema else "",
"Kurztext": kurztext,
"Langtext": langtext,
"Referenz": referenzen,
"Gueltigkeit": {
"Von": vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else "",
"Bis": vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
},
"Checklistenfragen": checklistenfragen,
"Stichworte": stichworte
}
doc_data["Vorgaben"].append(vorgabe_data)
output_data.append(doc_data)
# Output the data
json_output = json.dumps(output_data, cls=DjangoJSONEncoder, indent=2, ensure_ascii=False)
if options['output']:
with open(options['output'], 'w', encoding='utf-8') as f:
f.write(json_output)
self.stdout.write(self.style.SUCCESS(f'JSON exported to {options["output"]}'))
else:
self.stdout.write(json_output)

View File

@@ -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'),
),
]

View File

@@ -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)
titel = models.CharField(max_length=255)
thema = models.ForeignKey(Thema, on_delete=models.PROTECT, blank=False)
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)
@@ -132,13 +132,13 @@ class Vorgabe(models.Model):
})
return conflicts
def clean(self):
"""
Validate the Vorgabe before saving.
"""
from django.core.exceptions import ValidationError
# Check for conflicts with existing Vorgaben
conflicts = self.find_conflicts()
if conflicts:
@@ -172,9 +172,9 @@ class Vorgabe(models.Model):
'vorgabe1': self,
'vorgabe2': other_vorgabe,
'conflict_type': 'date_range_intersection',
'message': f"Vorgabe {self.Vorgabennummer()} conflicts with "
f"existing {other_vorgabe.Vorgabennummer()} "
f"due to overlapping validity periods"
'message': f"Vorgabe {self.Vorgabennummer()} in Konflikt mit "
f"bestehender {other_vorgabe.Vorgabennummer()} "
f" - Geltungsdauer übeschneidet sich"
})
return conflicts
@@ -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)

View File

@@ -0,0 +1,151 @@
{% extends "base.html" %}
{% block content %}
<h1 class="mb-4">Unvollständige Vorgaben</h1>
{% if vorgaben_data %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Vorgabe</th>
<th class="text-center">Referenzen</th>
<th class="text-center">Stichworte</th>
<th class="text-center">Text</th>
<th class="text-center">Checklistenfragen</th>
</tr>
</thead>
<tbody>
{% for item in vorgaben_data %}
<tr>
<td>
<a href="/autorenumgebung/dokumente/vorgabe/{{ item.vorgabe.id }}/change/"
class="text-decoration-none" target="_blank">
<strong>{{ item.vorgabe.Vorgabennummer }}</strong><br>
<small class="text-muted">{{ item.vorgabe.titel }}</small><br>
<small class="text-muted">{{ item.vorgabe.dokument.nummer }} {{ item.vorgabe.dokument.name }}</small>
</a>
</td>
<td class="text-center align-middle">
{% if item.has_references %}
<span class="text-success fs-4"></span>
{% else %}
<span class="text-danger fs-4"></span>
{% endif %}
</td>
<td class="text-center align-middle">
{% if item.has_stichworte %}
<span class="text-success fs-4"></span>
{% else %}
<span class="text-danger fs-4"></span>
{% endif %}
</td>
<td class="text-center align-middle">
{% if item.has_text %}
<span class="text-success fs-4"></span>
{% else %}
<span class="text-danger fs-4"></span>
{% endif %}
</td>
<td class="text-center align-middle">
{% if item.has_checklistenfragen %}
<span class="text-success fs-4"></span>
{% else %}
<span class="text-danger fs-4"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Summary -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Zusammenfassung</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-3">
<div class="p-3">
<h4 class="text-danger" id="no-references-count">0</h4>
<p class="mb-0">Ohne Referenzen</p>
</div>
</div>
<div class="col-md-3">
<div class="p-3">
<h4 class="text-danger" id="no-stichworte-count">0</h4>
<p class="mb-0">Ohne Stichworte</p>
</div>
</div>
<div class="col-md-3">
<div class="p-3">
<h4 class="text-danger" id="no-text-count">0</h4>
<p class="mb-0">Ohne Text</p>
</div>
</div>
<div class="col-md-3">
<div class="p-3">
<h4 class="text-danger" id="no-checklistenfragen-count">0</h4>
<p class="mb-0">Ohne Checklistenfragen</p>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 text-center">
<h4 class="text-primary">Gesamt: {{ vorgaben_data|length }} unvollständige Vorgaben</h4>
</div>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">
<i class="fas fa-check-circle"></i> Alle Vorgaben sind vollständig!
</h4>
<p>Alle Vorgaben haben Referenzen, Stichworte, Text und Checklistenfragen.</p>
<hr>
<p class="mb-0">
<a href="{% url 'standard_list' %}" class="btn btn-primary">
<i class="fas fa-list"></i> Zurück zur Übersicht
</a>
</p>
</div>
{% endif %}
<div class="mt-3">
<a href="{% url 'standard_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Zurück zur Übersicht
</a>
</div>
<script>
// Update summary counts
document.addEventListener('DOMContentLoaded', function() {
let noReferences = 0;
let noStichworte = 0;
let noText = 0;
let noChecklistenfragen = 0;
const rows = document.querySelectorAll('tbody tr');
rows.forEach(function(row) {
const cells = row.querySelectorAll('td');
if (cells.length >= 5) {
if (cells[1].textContent.trim() === '✗') noReferences++;
if (cells[2].textContent.trim() === '✗') noStichworte++;
if (cells[3].textContent.trim() === '✗') noText++;
if (cells[4].textContent.trim() === '✗') noChecklistenfragen++;
}
});
document.getElementById('no-references-count').textContent = noReferences;
document.getElementById('no-stichworte-count').textContent = noStichworte;
document.getElementById('no-text-count').textContent = noText;
document.getElementById('no-checklistenfragen-count').textContent = noChecklistenfragen;
});
</script>
{% endblock %}

View File

@@ -9,6 +9,7 @@
<p><strong>Autoren:</strong> {{ standard.autoren.all|join:", " }}</p>
<p><strong>Prüfende:</strong> {{ standard.pruefende.all|join:", " }}</p>
<p><strong>Gültigkeit:</strong> {{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</p>
<p><a href="{% url 'standard_json' standard.nummer %}" class="button" download="{{ standard.nummer }}.json">JSON herunterladen</a></p>
<!-- Start Einleitung -->
{% if standard.einleitung_html %}

View File

@@ -1,6 +1,7 @@
from django.test import TestCase, Client
from django.urls import reverse
from django.core.management import call_command
from django.contrib.auth.models import User
from datetime import date, timedelta
from io import StringIO
from .models import (
@@ -628,7 +629,7 @@ class VorgabeSanityCheckTest(TestCase):
conflict = conflicts[0]
self.assertEqual(conflict['conflict_type'], 'date_range_intersection')
self.assertIn('R0066.O.1', conflict['message'])
self.assertIn('intersecting validity periods', conflict['message'])
self.assertIn('überschneiden sich in der Geltungsdauer', conflict['message'])
self.assertEqual(conflict['vorgabe1'], self.vorgabe1)
self.assertEqual(conflict['vorgabe2'], conflicting_vorgabe)
@@ -711,8 +712,8 @@ class VorgabeSanityCheckTest(TestCase):
with self.assertRaises(Exception) as context:
conflicting_vorgabe.clean()
self.assertIn('conflicts with existing', str(context.exception))
self.assertIn('overlapping validity periods', str(context.exception))
self.assertIn('Konflikt mit bestehender', str(context.exception))
self.assertIn('Geltungsdauer übeschneidet sich', str(context.exception))
def test_check_vorgabe_conflicts_utility(self):
"""Test check_vorgabe_conflicts utility function"""
@@ -816,4 +817,321 @@ class SanityCheckManagementCommandTest(TestCase):
self.assertIn("Starting Vorgaben sanity check...", output)
self.assertIn("Found 1 conflicts:", output)
self.assertIn("R0066.O.1", output)
self.assertIn("intersecting validity periods", output)
self.assertIn("überschneiden sich in der Geltungsdauer", output)
class IncompleteVorgabenTest(TestCase):
"""Test cases for incomplete Vorgaben functionality"""
def setUp(self):
self.client = Client()
# Create and login a staff user
self.staff_user = User.objects.create_user(
username='teststaff',
password='testpass123'
)
self.staff_user.is_staff = True
self.staff_user.save()
self.client.login(username='teststaff', password='testpass123')
# Create test data
self.dokumententyp = Dokumententyp.objects.create(
name="Test Typ",
verantwortliche_ve="Test VE"
)
self.thema = Thema.objects.create(
name="Test Thema",
erklaerung="Test Erklärung"
)
self.dokument = Dokument.objects.create(
nummer="TEST-001",
dokumententyp=self.dokumententyp,
name="Test Dokument",
gueltigkeit_von=date.today(),
aktiv=True
)
# Create complete Vorgabe (should not appear in any list)
self.complete_vorgabe = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Vollständige Vorgabe",
gueltigkeit_von=date.today()
)
# Add all required components to make it complete
self.stichwort = Stichwort.objects.create(
stichwort="Test Stichwort"
)
self.complete_vorgabe.stichworte.add(self.stichwort)
self.referenz = Referenz.objects.create(
name_nummer="Test Referenz",
url="/test/path"
)
self.complete_vorgabe.referenzen.add(self.referenz)
VorgabeKurztext.objects.create(
abschnitt=self.complete_vorgabe,
inhalt="Test Kurztext"
)
Checklistenfrage.objects.create(
vorgabe=self.complete_vorgabe,
frage="Test Frage"
)
# Create incomplete Vorgaben
# 1. Vorgabe without references
self.no_refs_vorgabe = Vorgabe.objects.create(
order=2,
nummer=2,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe ohne Referenzen",
gueltigkeit_von=date.today()
)
self.no_refs_vorgabe.stichworte.add(self.stichwort)
VorgabeKurztext.objects.create(
abschnitt=self.no_refs_vorgabe,
inhalt="Test Kurztext"
)
Checklistenfrage.objects.create(
vorgabe=self.no_refs_vorgabe,
frage="Test Frage"
)
# 2. Vorgabe without Stichworte
self.no_stichworte_vorgabe = Vorgabe.objects.create(
order=3,
nummer=3,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe ohne Stichworte",
gueltigkeit_von=date.today()
)
self.no_stichworte_vorgabe.referenzen.add(self.referenz)
VorgabeKurztext.objects.create(
abschnitt=self.no_stichworte_vorgabe,
inhalt="Test Kurztext"
)
Checklistenfrage.objects.create(
vorgabe=self.no_stichworte_vorgabe,
frage="Test Frage"
)
# 3. Vorgabe without text
self.no_text_vorgabe = Vorgabe.objects.create(
order=4,
nummer=4,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe ohne Text",
gueltigkeit_von=date.today()
)
self.no_text_vorgabe.stichworte.add(self.stichwort)
self.no_text_vorgabe.referenzen.add(self.referenz)
Checklistenfrage.objects.create(
vorgabe=self.no_text_vorgabe,
frage="Test Frage"
)
# 4. Vorgabe without Checklistenfragen
self.no_checklisten_vorgabe = Vorgabe.objects.create(
order=5,
nummer=5,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe ohne Checklistenfragen",
gueltigkeit_von=date.today()
)
self.no_checklisten_vorgabe.stichworte.add(self.stichwort)
self.no_checklisten_vorgabe.referenzen.add(self.referenz)
VorgabeKurztext.objects.create(
abschnitt=self.no_checklisten_vorgabe,
inhalt="Test Kurztext"
)
def test_incomplete_vorgaben_page_status(self):
"""Test that the incomplete Vorgaben page loads successfully"""
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertEqual(response.status_code, 200)
def test_incomplete_vorgaben_page_content(self):
"""Test that the page contains expected content"""
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'Unvollständige Vorgaben')
self.assertContains(response, 'Referenzen')
self.assertContains(response, 'Stichworte')
self.assertContains(response, 'Text')
self.assertContains(response, 'Checklistenfragen')
# Check for table structure
self.assertContains(response, '<table class="table table-striped table-hover">')
self.assertContains(response, '<th class="text-center">Referenzen</th>')
def test_no_references_list(self):
"""Test that Vorgaben without references are listed"""
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'Vorgabe ohne Referenzen')
self.assertNotContains(response, 'Vollständige Vorgabe') # Should not appear
def test_no_stichworte_list(self):
"""Test that Vorgaben without Stichworte are listed"""
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'Vorgabe ohne Stichworte')
self.assertNotContains(response, 'Vollständige Vorgabe') # Should not appear
def test_no_text_list(self):
"""Test that Vorgaben without Kurz- or Langtext are listed"""
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'Vorgabe ohne Text')
self.assertNotContains(response, 'Vollständige Vorgabe') # Should not appear
def test_no_checklistenfragen_list(self):
"""Test that Vorgaben without Checklistenfragen are listed"""
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'Vorgabe ohne Checklistenfragen')
self.assertNotContains(response, 'Vollständige Vorgabe') # Should not appear
def test_vorgabe_links(self):
"""Test that Vorgaben link to their admin pages"""
response = self.client.get(reverse('incomplete_vorgaben'))
# Should contain links to Vorgabe admin pages
self.assertContains(response, 'href="/autorenumgebung/dokumente/vorgabe/2/change/"')
self.assertContains(response, 'href="/autorenumgebung/dokumente/vorgabe/3/change/"')
self.assertContains(response, 'href="/autorenumgebung/dokumente/vorgabe/4/change/"')
self.assertContains(response, 'href="/autorenumgebung/dokumente/vorgabe/5/change/"')
def test_badge_counts(self):
"""Test that badge counts are correct"""
response = self.client.get(reverse('incomplete_vorgaben'))
# Check that JavaScript updates the counts correctly
self.assertContains(response, 'id="no-references-count"')
self.assertContains(response, 'id="no-stichworte-count"')
self.assertContains(response, 'id="no-text-count"')
self.assertContains(response, 'id="no-checklistenfragen-count"')
# Check total count
self.assertContains(response, 'Gesamt: 4 unvollständige Vorgaben')
def test_summary_section(self):
"""Test that summary section shows correct counts"""
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'Zusammenfassung')
self.assertContains(response, 'Ohne Referenzen')
self.assertContains(response, 'Ohne Stichworte')
self.assertContains(response, 'Ohne Text')
self.assertContains(response, 'Ohne Checklistenfragen')
self.assertContains(response, 'Gesamt: 4 unvollständige Vorgaben')
def test_empty_lists_message(self):
"""Test that appropriate messages are shown when lists are empty"""
# Delete all incomplete Vorgaben
Vorgabe.objects.exclude(pk=self.complete_vorgabe.pk).delete()
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'Alle Vorgaben sind vollständig!')
self.assertContains(response, 'Alle Vorgaben haben Referenzen, Stichworte, Text und Checklistenfragen.')
def test_back_link(self):
"""Test that back link to standard list exists"""
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'href="/dokumente/"')
self.assertContains(response, 'Zurück zur Übersicht')
def test_navigation_link(self):
"""Test that navigation includes link to incomplete Vorgaben"""
response = self.client.get('/dokumente/')
self.assertContains(response, 'href="/dokumente/unvollstaendig/"')
self.assertContains(response, 'Unvollständig')
def test_vorgabe_with_langtext_only(self):
"""Test that Vorgabe with only Langtext is still considered incomplete for text"""
vorgabe_langtext_only = Vorgabe.objects.create(
order=6,
nummer=6,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe nur mit Langtext",
gueltigkeit_von=date.today()
)
vorgabe_langtext_only.stichworte.add(self.stichwort)
vorgabe_langtext_only.referenzen.add(self.referenz)
# Add only Langtext, no Kurztext
VorgabeLangtext.objects.create(
abschnitt=vorgabe_langtext_only,
inhalt="Test Langtext"
)
# Add Checklistenfragen to make it complete in that aspect
Checklistenfrage.objects.create(
vorgabe=vorgabe_langtext_only,
frage="Test Frage"
)
response = self.client.get(reverse('incomplete_vorgaben'))
# Debug: print response content to see where it appears
print("Response content:", response.content.decode())
# Should NOT appear in "no text" list because it has Langtext
self.assertNotContains(response, 'Vorgabe nur mit Langtext')
def test_vorgabe_with_both_text_types(self):
"""Test that Vorgabe with both Kurztext and Langtext is complete"""
vorgabe_both_text = Vorgabe.objects.create(
order=7,
nummer=7,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe mit beiden Texten",
gueltigkeit_von=date.today()
)
vorgabe_both_text.stichworte.add(self.stichwort)
vorgabe_both_text.referenzen.add(self.referenz)
# Add both Kurztext and Langtext
VorgabeKurztext.objects.create(
abschnitt=vorgabe_both_text,
inhalt="Test Kurztext"
)
VorgabeLangtext.objects.create(
abschnitt=vorgabe_both_text,
inhalt="Test Langtext"
)
# Add Checklistenfragen to make it complete in that aspect
Checklistenfrage.objects.create(
vorgabe=vorgabe_both_text,
frage="Test Frage"
)
response = self.client.get(reverse('incomplete_vorgaben'))
# Should NOT appear in "no text" list because it has both text types
self.assertNotContains(response, 'Vorgabe mit beiden Texten')
def test_incomplete_vorgaben_staff_only(self):
"""Test that non-staff users are redirected to login"""
# Logout the staff user
self.client.logout()
# Try to access the page as anonymous user
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertEqual(response.status_code, 302) # Redirect to login
# Create a regular (non-staff) user
regular_user = User.objects.create_user(
username='regularuser',
password='testpass123'
)
self.client.login(username='regularuser', password='testpass123')
# Try to access the page as regular user
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertEqual(response.status_code, 302) # Redirect to login
# Login as staff user again - should work
self.client.login(username='teststaff', password='testpass123')
response = self.client.get(reverse('incomplete_vorgaben'))
self.assertEqual(response.status_code, 200) # Success

View File

@@ -3,9 +3,11 @@ from . import views
urlpatterns = [
path('', views.standard_list, name='standard_list'),
path('unvollstaendig/', views.incomplete_vorgaben, name='incomplete_vorgaben'),
path('<str:nummer>/', views.standard_detail, name='standard_detail'),
path('<str:nummer>/history/<str:check_date>/', views.standard_detail),
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste')
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
path('<str:nummer>/json/', views.standard_json, name='standard_json')
]

View File

@@ -1,5 +1,9 @@
from django.shortcuts import render, get_object_or_404
from .models import Dokument
from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import JsonResponse
from django.core.serializers.json import DjangoJSONEncoder
import json
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage
from abschnitte.utils import render_textabschnitte
from datetime import date
@@ -9,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}
)
@@ -25,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"))
@@ -56,3 +64,180 @@ def standard_checkliste(request, nummer):
})
def is_staff_user(user):
return user.is_staff
@login_required
@user_passes_test(is_staff_user)
def incomplete_vorgaben(request):
"""
Show table of all Vorgaben with completeness status:
- References (✓ or ✗)
- Stichworte (✓ or ✗)
- Text (✓ or ✗)
- Checklistenfragen (✓ or ✗)
"""
# Get all active Vorgaben
all_vorgaben = Vorgabe.objects.all().select_related('dokument', 'thema').prefetch_related(
'referenzen', 'stichworte', 'checklistenfragen', 'vorgabekurztext_set', 'vorgabelangtext_set'
)
# Build table data
vorgaben_data = []
for vorgabe in all_vorgaben:
has_references = vorgabe.referenzen.exists()
has_stichworte = vorgabe.stichworte.exists()
has_kurztext = vorgabe.vorgabekurztext_set.exists()
has_langtext = vorgabe.vorgabelangtext_set.exists()
has_text = has_kurztext or has_langtext
has_checklistenfragen = vorgabe.checklistenfragen.exists()
# Only include Vorgaben that are incomplete in at least one way
if not (has_references and has_stichworte and has_text and has_checklistenfragen):
vorgaben_data.append({
'vorgabe': vorgabe,
'has_references': has_references,
'has_stichworte': has_stichworte,
'has_text': has_text,
'has_checklistenfragen': has_checklistenfragen,
'is_complete': has_references and has_stichworte and has_text and has_checklistenfragen
})
# Sort by document number and Vorgabe number
vorgaben_data.sort(key=lambda x: (x['vorgabe'].dokument.nummer, x['vorgabe'].Vorgabennummer()))
return render(request, 'standards/incomplete_vorgaben.html', {
'vorgaben_data': vorgaben_data,
})
def standard_json(request, nummer):
"""
Export a single Dokument as JSON
"""
# Get the document with all related data
dokument = get_object_or_404(
Dokument.objects.prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
'einleitung_set', 'changelog__autoren'
),
nummer=nummer
)
# Build document structure (reusing logic from export_json command)
doc_data = {
"Typ": dokument.dokumententyp.name if dokument.dokumententyp else "",
"Nummer": dokument.nummer,
"Name": dokument.name,
"Autoren": [autor.name for autor in dokument.autoren.all()],
"Pruefende": [pruefender.name for pruefender in dokument.pruefende.all()],
"Gueltigkeit": {
"Von": dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else "",
"Bis": dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
},
"SignaturCSO": dokument.signatur_cso,
"Geltungsbereich": {},
"Einleitung": {},
"Ziel": "",
"Grundlagen": "",
"Changelog": [],
"Anhänge": dokument.anhaenge,
"Verantwortlich": "Information Security Management BIT",
"Klassifizierung": None,
"Glossar": {},
"Vorgaben": []
}
# Process Geltungsbereich sections
geltungsbereich_sections = []
for gb in dokument.geltungsbereich_set.all().order_by('order'):
geltungsbereich_sections.append({
"typ": gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text",
"inhalt": gb.inhalt
})
if geltungsbereich_sections:
doc_data["Geltungsbereich"] = {
"Abschnitt": geltungsbereich_sections
}
# Process Einleitung sections
einleitung_sections = []
for ei in dokument.einleitung_set.all().order_by('order'):
einleitung_sections.append({
"typ": ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text",
"inhalt": ei.inhalt
})
if einleitung_sections:
doc_data["Einleitung"] = {
"Abschnitt": einleitung_sections
}
# Process Changelog entries
changelog_entries = []
for cl in dokument.changelog.all().order_by('-datum'):
changelog_entries.append({
"Datum": cl.datum.strftime("%Y-%m-%d"),
"Autoren": [autor.name for autor in cl.autoren.all()],
"Aenderung": cl.aenderung
})
doc_data["Changelog"] = changelog_entries
# Process Vorgaben for this document
vorgaben = dokument.vorgaben.all().order_by('order')
for vorgabe in vorgaben:
# Get Kurztext and Langtext sections
kurztext_sections = []
for kt in vorgabe.vorgabekurztext_set.all().order_by('order'):
kurztext_sections.append({
"typ": kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text",
"inhalt": kt.inhalt
})
langtext_sections = []
for lt in vorgabe.vorgabelangtext_set.all().order_by('order'):
langtext_sections.append({
"typ": lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text",
"inhalt": lt.inhalt
})
# Build text structures following Langtext pattern
kurztext = {
"Abschnitt": kurztext_sections if kurztext_sections else []
} if kurztext_sections else {}
langtext = {
"Abschnitt": langtext_sections if langtext_sections else []
} if langtext_sections else {}
# Get references and keywords
referenzen = [f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer for ref in vorgabe.referenzen.all()]
stichworte = [stw.stichwort for stw in vorgabe.stichworte.all()]
# Get checklist questions
checklistenfragen = [cf.frage for cf in vorgabe.checklistenfragen.all()]
vorgabe_data = {
"Nummer": str(vorgabe.nummer),
"Titel": vorgabe.titel,
"Thema": vorgabe.thema.name if vorgabe.thema else "",
"Kurztext": kurztext,
"Langtext": langtext,
"Referenz": referenzen,
"Gueltigkeit": {
"Von": vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else "",
"Bis": vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
},
"Checklistenfragen": checklistenfragen,
"Stichworte": stichworte
}
doc_data["Vorgaben"].append(vorgabe_data)
# Return JSON response
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)

View File

@@ -17,6 +17,9 @@
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-item nav-link active" href="/dokumente">Standards</a>
{% if user.is_staff %}
<a class="nav-item nav-link" href="/dokumente/unvollstaendig/">Unvollständig</a>
{% endif %}
<a class="nav-item nav-link" href="/referenzen">Referenzen</a>
<a class="nav-item nav-link" href="/stichworte">Stichworte</a>
<a class="nav-item nav-link" href="/search">Suche</a>
@@ -28,6 +31,6 @@
<div class="flex-fill">{% block content %}Main Content{% endblock %}</div>
<div class="col-md-2">{% block sidebar_right %}{% endblock %}</div>
</div>
<div>VorgabenUI v0.941</div>
<div>VorgabenUI v0.942</div>
</body>
</html>

View File

@@ -6,7 +6,7 @@ from abschnitte.utils import render_textabschnitte
from dokumente.models import Dokument, VorgabeLangtext, VorgabeKurztext, Geltungsbereich, Vorgabe
from itertools import groupby
import datetime
import pprint
def startseite(request):
standards=list(Dokument.objects.filter(aktiv=True))
@@ -66,6 +66,6 @@ def search(request):
geltungsbereich=set(list([x.geltungsbereich for x in Geltungsbereich.objects.filter(inhalt__icontains=suchbegriff)]))
for s in geltungsbereich:
result["geltungsbereich"][s]=render_textabschnitte(s.geltungsbereich_set.order_by("order"))
pprint.pp (result)
return render(request,"results.html",{"suchbegriff":safe_search_term,"resultat":result})

View File

@@ -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')

33
test_current_settings.py Normal file
View File

@@ -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()

32
test_direct_import.py Normal file
View File

@@ -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()

31
test_django_setup.py Normal file
View File

@@ -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()

28
test_fresh_django.py Normal file
View File

@@ -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()

56
test_minimal_settings.py Normal file
View File

@@ -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')

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Test package for VorgabenUI

149
tests/test_integration.py Normal file
View File

@@ -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)