Compare commits

..

1 Commits

Author SHA1 Message Date
27d11fccd3 Complete rewrite by OpenCode 2025-11-04 16:43:39 +01:00
35 changed files with 1113 additions and 3352 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

12
.gitignore vendored
View File

@@ -15,4 +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/

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,11 +23,11 @@ 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, name='startseite'),
path('search/',pages.views.search, name='search'),
path('',pages.views.startseite),
path('search/',pages.views.search),
path('dokumente/', include("dokumente.urls")),
path('autorenumgebung/', admin.site.urls),
path('stichworte/', include("stichworte.urls")),

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', {
@@ -239,6 +258,7 @@ class VorgabeAdmin(NestedModelAdmin):
def vorgabe_nummer(self, obj):
return obj.Vorgabennummer()
vorgabe_nummer.short_description = 'Vorgabennummer'
admin.site.register(Checklistenfrage)

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

View File

@@ -1,403 +1,110 @@
{% extends "base.html" %}
{% block title %}{{ standard.nummer }} {{ standard.name }}{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item"><a href="{% url 'standard_list' %}">Standards</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ standard.nummer }}</li>
{% endblock %}
{% block title %}{{ standard }}{% endblock %}
{% block content %}
<div class="row">
<!-- Main Content -->
<div class="col-lg-8">
<!-- Standard Header -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<div class="d-flex justify-content-between align-items-start">
<div>
<h1 class="h2 mb-2">{{ standard.nummer }} {{ standard.name }}</h1>
{% if standard.history == True %}
<p class="mb-0 opacity-75">Version vom {{ standard.check_date|date:"d.m.Y" }}</p>
{% endif %}
</div>
<div class="d-flex gap-2">
{% if not standard.aktiv %}
<span class="badge bg-danger">Inaktiv</span>
{% else %}
<span class="badge bg-success">Aktiv</span>
{% endif %}
</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-muted mb-2">📅 Gültigkeit</h6>
<p class="mb-3">
<strong>Von:</strong> {{ standard.gueltigkeit_von|default_if_none:"-" }}<br>
<strong>Bis:</strong> {{ standard.gueltigkeit_bis|default_if_none:"Auf weiteres" }}
</p>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">👥 Verantwortlich</h6>
{% if standard.autoren.all %}
<p class="mb-1"><strong>Autoren:</strong><br>
{% for autor in standard.autoren.all %}
<span class="badge bg-light text-dark me-1">{{ autor }}</span>
{% endfor %}
</p>
{% endif %}
{% if standard.pruefende.all %}
<p class="mb-3"><strong>Prüfende:</strong><br>
{% for pruefender in standard.pruefende.all %}
<span class="badge bg-light text-dark me-1">{{ pruefender }}</span>
{% endfor %}
</p>
{% endif %}
</div>
</div>
<div class="d-flex gap-2">
<a href="{% url 'standard_json' standard.nummer %}" class="btn btn-outline btn-sm" download="{{ standard.nummer }}.json">
📄 JSON herunterladen
</a>
<button class="btn btn-outline btn-sm" onclick="window.print()">
🖨️ Drucken
</button>
</div>
</div>
<h1>{{ standard.nummer }} {{ standard.name }}</h1>
{% if standard.history == True %}
<h2>Version vom {{ standard.check_date }}</h2>
{% endif %}
<!-- Autoren, Prüfende etc. -->
<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 %}
<h2>Einleitung</h2>
{% for typ, html in standard.einleitung_html %}
<div>{{ html|safe }}</div>
{% endfor %}
{% endif %}
<!-- End Einleitung -->
<!-- Start Geltungsbereich -->
{% if standard.geltungsbereich_html %}
<h2>Geltungsbereich</h2>
{% for typ, html in standard.geltungsbereich_html %}
<div>{{ html|safe }}</div>
{% endfor %}
{% endif %}
<!-- End Geltungsbereich -->
<h2>Vorgaben</h2>
{% for vorgabe in vorgaben %}
<!-- Start Vorgabe -->
{% if standard.history == True or vorgabe.long_status == "active" %}
<a id="{{ vorgabe.Vorgabennummer }}"></a><div class="card mb-4">
{% if vorgabe.long_status == "active"%}
<div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
{% elif standard.history == True %}
<div class="card-header d-flex justify-content-between align-items-center bg-danger-subtle">
{% endif %}
<h3 class="h5 m-0">{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}
{% if vorgabe.long_status != "active" and standard.history == True %}<span class="text-danger"> ({{ vorgabe.long_status}})</span>{% endif %}
</h3>
{% if vorgabe.relevanzset %}
<span class="badge bg-light text-black"> Relevanz:
{{ vorgabe.relevanzset|join:", " }}
</span>
{% endif %}
<span class="badge bg-light text-black">{{ vorgabe.thema }}</span>
</div>
<!-- Table of Contents -->
<div class="toc mb-4" id="table-of-contents">
<h3>📋 Inhaltsverzeichnis</h3>
<ul class="list-unstyled">
{% if standard.einleitung_html %}
<li><a href="#einleitung">Einleitung</a></li>
{% endif %}
{% if standard.geltungsbereich_html %}
<li><a href="#geltungsbereich">Geltungsbereich</a></li>
{% endif %}
<li><a href="#vorgaben">Vorgaben ({{ vorgaben|length }})</a>
<ul class="ms-3 mt-1">
{% for vorgabe in vorgaben %}
{% if standard.history == True or vorgabe.long_status == "active" %}
<li><a href="#{{ vorgabe.Vorgabennummer }}">{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel|truncatechars:50 }}</a></li>
{% endif %}
{% endfor %}
</ul>
</li>
</ul>
</div>
<!-- Einleitung -->
{% if standard.einleitung_html %}
<section id="einleitung" class="mb-5">
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">📖 Einleitung</h2>
</div>
<div class="card-body">
{% for typ, html in standard.einleitung_html %}
<div class="content-section">{{ html|safe }}</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
<!-- Geltungsbereich -->
{% if standard.geltungsbereich_html %}
<section id="geltungsbereich" class="mb-5">
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">🎯 Geltungsbereich</h2>
</div>
<div class="card-body">
{% for typ, html in standard.geltungsbereich_html %}
<div class="content-section">{{ html|safe }}</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
<!-- Vorgaben -->
<section id="vorgaben" class="mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h4 mb-0">📝 Vorgaben</h2>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline btn-sm" onclick="toggleAllVorgaben(true)">Alle ausklappen</button>
<button type="button" class="btn btn-outline btn-sm" onclick="toggleAllVorgaben(false)">Alle einklappen</button>
</div>
</div>
{% for vorgabe in vorgaben %}
{% if standard.history == True or vorgabe.long_status == "active" %}
<div class="card mb-4 vorgabe-card" id="{{ vorgabe.Vorgabennummer }}">
<div class="card-header {% if vorgabe.long_status == "active" %}bg-success text-white{% else %}bg-secondary text-white{% endif %}"
style="cursor: pointer;"
onclick="toggleVorgabe('{{ vorgabe.Vorgabennummer }}')">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<span class="toggle-icon me-2"></span>
<h3 class="h5 m-0">
{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}
{% if vorgabe.long_status != "active" and standard.history == True %}
<span class="badge bg-warning text-dark ms-2">{{ vorgabe.long_status }}</span>
{% endif %}
</h3>
</div>
<div class="d-flex gap-2">
{% if vorgabe.relevanzset %}
<span class="badge bg-light text-dark">
🔥 {{ vorgabe.relevanzset|join:", " }}
</span>
{% endif %}
{% if vorgabe.thema %}
<span class="badge bg-info">{{ vorgabe.thema }}</span>
{% endif %}
</div>
</div>
</div>
<div class="card-body vorgabe-content" id="content-{{ vorgabe.Vorgabennummer }}">
<!-- Kurztext -->
{% if vorgabe.kurztext_html.0.1 %}
<div class="alert alert-info mb-3">
<h6 class="alert-heading">📌 Kurztext</h6>
{% for typ, html in vorgabe.kurztext_html %}
{% if html %}
<div class="mb-2">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div>
<div class="card-body p-0">
<!-- Start Kurztext -->
{% comment %} KURZTEXT BLOCK {% endcomment %}
{% if vorgabe.kurztext_html.0.1 %}
<div class="p-3 mb-3 bg-light border-3" style="width: 100%;">
{% for typ, html in vorgabe.kurztext_html %}
{% if html %}
<div class="mb-2">{{ html|safe }}</div>
{% endif %}
<!-- Langtext -->
<div class="mb-4">
{% for typ, html in vorgabe.langtext_html %}
{% if html %}
<div class="content-section mb-3">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div>
<!-- Checklistenfragen -->
<div class="mb-4">
<h6 class="mb-3">✅ Checklistenfragen</h6>
{% if vorgabe.checklistenfragen.all %}
<div class="list-group">
{% for frage in vorgabe.checklistenfragen.all %}
<div class="list-group-item">
<div class="d-flex align-items-start">
<input type="checkbox" class="form-check-input me-2 mt-1" id="check-{{ forloop.counter }}">
<label class="form-check-label" for="check-{{ forloop.counter }}">
{{ frage.frage }}
</label>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted"><em>Keine Checklistenfragen vorhanden</em></p>
{% endif %}
</div>
<!-- Metadaten -->
<div class="border-top pt-3">
<div class="row">
<div class="col-md-6">
<h6 class="text-muted mb-2">🏷️ Stichworte</h6>
{% if vorgabe.stichworte.all %}
<div>
{% for s in vorgabe.stichworte.all %}
<a href="{% url 'stichwort_detail' stichwort=s %}" class="badge bg-light text-dark text-decoration-none me-1 mb-1">
{{ s }}
</a>
{% endfor %}
</div>
{% else %}
<p class="text-muted small mb-0">Keine Stichworte</p>
{% endif %}
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">🔗 Referenzen</h6>
{% if vorgabe.referenzpfade %}
<div class="small">
{% for ref in vorgabe.referenzpfade %}
<div class="mb-1">{{ ref|safe }}</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted small mb-0">Keine Referenzen</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Langtext -->
<div class="p-3 mb-3">
{% comment %} LANGTEXT BLOCK {% endcomment %}
{# <h5>Langtext</h5> #}
{% for typ, html in vorgabe.langtext_html %}
{% if html %}<div class="mb-3">{{ html|safe }}</div>{% endif %}
{% endfor %}
</section>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Quick Actions -->
<div class="card mb-4 sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">⚡ Schnellaktionen</h5>
<!-- Checklistenfragen -->
{% comment %} CHECKLISTENFRAGEN BLOCK {% endcomment %}
<h5>Checklistenfragen</h5>
{% if vorgabe.checklistenfragen.all %}
<ul class="list-group">
{% for frage in vorgabe.checklistenfragen.all %}
<li class="list-group-item">{{ frage.frage }}</li>
{% endfor %}
</ul>
{% else %}
<p><em>Keine Checklistenfragen</em></p>
{% endif %}
{% comment %} STICHWORTE + REFERENZEN AT BOTTOM {% endcomment %}
<div class="mt-4 small text-muted">
<strong>Stichworte:</strong>
{% if vorgabe.stichworte.all %}
{% for s in vorgabe.stichworte.all %}
<a href="{% url 'stichwort_detail' stichwort=s %}">{{ s }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
<em>Keine</em>
{% endif %}
<br>
<strong>Referenzen:</strong>
{% if vorgabe.referenzpfade %}
{% for ref in vorgabe.referenzpfade %}
{{ ref|safe }}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
<em>Keine</em>
{% endif %}
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-outline btn-sm" onclick="scrollToSection('einleitung')">
📖 Zur Einleitung
</button>
<button class="btn btn-outline btn-sm" onclick="scrollToSection('geltungsbereich')">
🎯 Zum Geltungsbereich
</button>
<button class="btn btn-outline btn-sm" onclick="scrollToSection('vorgaben')">
📝 Zu den Vorgaben
</button>
<hr>
<a href="{% url 'standard_list' %}" class="btn btn-outline btn-sm">
← Zurück zur Liste
</a>
</div>
</div>
</div>
<!-- Statistics -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">📊 Statistiken</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
<div class="border-end">
<h4 class="text-primary mb-1">{{ vorgaben|length }}</h4>
<small class="text-muted">Vorgaben</small>
</div>
</div>
<div class="col-6">
<h4 class="text-success mb-1">
{% for vorgabe in vorgaben %}
{% if vorgabe.long_status == "active" %}
{% if forloop.first %}1{% else %}{{ forloop.counter }}{% endif %}
{% endif %}
{% endfor %}
</h4>
<small class="text-muted">Aktiv</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Update active TOC item on scroll
const sections = document.querySelectorAll('section[id]');
const tocLinks = document.querySelectorAll('.toc a');
function updateActiveTOC() {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (pageYOffset >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
tocLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
}
window.addEventListener('scroll', updateActiveTOC);
updateActiveTOC();
});
function toggleVorgabe(vorgabeId) {
const content = document.getElementById('content-' + vorgabeId);
const icon = document.querySelector('#' + vorgabeId + ' .toggle-icon');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.textContent = '▼';
} else {
content.style.display = 'none';
icon.textContent = '▶';
}
}
function toggleAllVorgaben(expand) {
const contents = document.querySelectorAll('.vorgabe-content');
const icons = document.querySelectorAll('.toggle-icon');
contents.forEach(content => {
content.style.display = expand ? 'block' : 'none';
});
icons.forEach(icon => {
icon.textContent = expand ? '▼' : '▶';
});
}
function scrollToSection(sectionId) {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
</script>
<style>
.content-section {
line-height: 1.6;
}
.content-section h1, .content-section h2, .content-section h3 {
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.content-section ul, .content-section ol {
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.content-section li {
margin-bottom: 0.5rem;
}
.vorgabe-card {
transition: all 0.2s ease;
}
.vorgabe-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.toc a.active {
background-color: var(--primary-color);
color: white;
}
@media print {
.vorgabe-content {
display: block !important;
}
.toggle-icon {
display: none !important;
}
}
</style>
{% endif %}
{% endfor %}
{% endblock %}

View File

@@ -1,201 +1,13 @@
{% extends "base.html" %}
{% block title %}Standards Informatiksicherheit{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Standards</li>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Standards Informatiksicherheit</h1>
<div class="d-flex gap-2">
<span class="badge bg-primary">{{ dokumente|length }} Standards</span>
</div>
</div>
<!-- Filter and Search Section -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label for="filter-search" class="form-label">Suchen</label>
<input type="text" class="form-control" id="filter-search" placeholder="Standard durchsuchen...">
</div>
<div class="col-md-3">
<label for="filter-status" class="form-label">Status</label>
<select class="form-select" id="filter-status">
<option value="">Alle</option>
<option value="active">Aktiv</option>
<option value="inactive">Inaktiv</option>
</select>
</div>
<div class="col-md-3">
<label for="filter-sort" class="form-label">Sortieren</label>
<select class="form-select" id="filter-sort">
<option value="nummer">Nummer</option>
<option value="name">Name</option>
<option value="gueltigkeit">Gültigkeit</option>
</select>
</div>
</div>
</div>
</div>
<!-- Standards Grid -->
<div class="row" id="standards-container">
<h1>Standards Informatiksicherheit</h1>
<ul>
{% for dokument in dokumente %}
<div class="col-lg-6 col-xl-4 mb-4 standard-item"
data-nummer="{{ dokument.nummer|lower }}"
data-name="{{ dokument.name|lower }}"
data-status="{% if dokument.aktiv %}active{% else %}inactive{% endif %}">
<div class="card standard-card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<span class="standard-number">{{ dokument.nummer }}</span>
{% if not dokument.aktiv %}
<span class="badge badge-status-inactive ms-2">Inaktiv</span>
{% else %}
<span class="badge badge-status-active ms-2">Aktiv</span>
{% endif %}
</div>
<div class="dropdown">
<button class="btn btn-sm btn-outline" type="button" data-bs-toggle="dropdown">
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'standard_detail' nummer=dokument.nummer %}">Details anzeigen</a></li>
<li><a class="dropdown-item" href="{% url 'standard_json' dokument.nummer %}" download="{{ dokument.nummer }}.json">JSON herunterladen</a></li>
</ul>
</div>
</div>
<div class="card-body">
<h5 class="card-title">
<a href="{% url 'standard_detail' nummer=dokument.nummer %}" class="text-decoration-none">
{{ dokument.name }}
</a>
</h5>
<div class="standard-meta mb-3">
<div class="row g-2">
<div class="col-6">
<small class="text-muted">
<strong>Gültig von:</strong><br>
{{ dokument.gueltigkeit_von|default_if_none:"-" }}
</small>
</div>
<div class="col-6">
<small class="text-muted">
<strong>Gültig bis:</strong><br>
{{ dokument.gueltigkeit_bis|default_if_none:"Auf weiteres" }}
</small>
</div>
</div>
</div>
{% if dokument.autoren.all %}
<div class="mb-2">
<small class="text-muted">
<strong>Autoren:</strong>
{% for autor in dokument.autoren.all %}
{{ autor }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</small>
</div>
{% endif %}
{% if dokument.pruefende.all %}
<div class="mb-3">
<small class="text-muted">
<strong>Prüfende:</strong>
{% for pruefender in dokument.pruefende.all %}
{{ pruefender }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</small>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-center">
<a href="{% url 'standard_detail' nummer=dokument.nummer %}" class="btn btn-primary btn-sm">
Details anzeigen
</a>
<div class="text-muted">
<small>
{% if dokument.history %}
Version vom {{ dokument.check_date|date:"d.m.Y" }}
{% endif %}
</small>
</div>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="text-center py-5">
<h3 class="text-muted">Keine Standards gefunden</h3>
<p class="text-muted">Es wurden keine Standards gefunden, die Ihren Kriterien entsprechen.</p>
</div>
</div>
<li>
<a href="{% url 'standard_detail' nummer=dokument.nummer %}">
{{ dokument.nummer }} {{ dokument.name }}
</a>
</li>
{% endfor %}
</div>
<!-- JavaScript for filtering and sorting -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('filter-search');
const statusSelect = document.getElementById('filter-status');
const sortSelect = document.getElementById('filter-sort');
const container = document.getElementById('standards-container');
function filterAndSort() {
const searchTerm = searchInput.value.toLowerCase();
const statusFilter = statusSelect.value;
const sortBy = sortSelect.value;
let items = Array.from(container.querySelectorAll('.standard-item'));
// Filter
items = items.filter(item => {
const nummer = item.dataset.nummer;
const name = item.dataset.name;
const status = item.dataset.status;
const matchesSearch = !searchTerm ||
nummer.includes(searchTerm) ||
name.includes(searchTerm);
const matchesStatus = !statusFilter || status === statusFilter;
return matchesSearch && matchesStatus;
});
// Sort
items.sort((a, b) => {
switch(sortBy) {
case 'nummer':
return a.dataset.nummer.localeCompare(b.dataset.nummer);
case 'name':
return a.dataset.name.localeCompare(b.dataset.name);
case 'gueltigkeit':
// This would need additional data attributes for proper sorting
return a.dataset.nummer.localeCompare(b.dataset.nummer);
default:
return 0;
}
});
// Reorder DOM
items.forEach(item => container.appendChild(item));
// Show/hide no results message
const noResults = container.querySelector('.col-12 .text-center');
if (noResults) {
noResults.parentElement.style.display = items.length === 0 ? 'block' : 'none';
}
}
searchInput.addEventListener('input', filterAndSort);
statusSelect.addEventListener('change', filterAndSort);
sortSelect.addEventListener('change', filterAndSort);
});
</script>
</ul>
{% endblock %}

View File

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

View File

@@ -1,160 +1,36 @@
<!DOCTYPE html>
<html lang="de" data-theme="light">
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Vorgaben Informatiksicherheit BIT{% endblock %}</title>
<title>{% block title %}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
{% load static %}
<link rel="stylesheet" href="{% static 'custom/css/vorgaben-ui.css' %}">
</head>
<body>
<!-- Enhanced Navigation -->
<nav class="navbar navbar-expand-lg sticky-top">
<div class="container-fluid">
<a class="navbar-brand" href="/">
Vorgaben Informatiksicherheit
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Navigation umschalten">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'standard_list' %}">Standards</a>
</li>
{% if user.is_staff %}
<li class="nav-item">
<a class="nav-link" href="{% url 'incomplete_standards' %}">Unvollständig</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{% url 'referenz_tree' %}">Referenzen</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'stichworte_list' %}">Stichworte</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'search' %}">Suche</a>
</li>
</ul>
<!-- Search Bar -->
<form class="navbar-search d-none d-lg-flex me-3" action="/search/" method="get">
<input type="text" name="q" placeholder="Suchen..." value="{{ search_term|default:'' }}">
<button type="submit" aria-label="Suchen">🔍</button>
</form>
<!-- Dark Mode Toggle -->
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Dark Mode umschalten">
<span id="theme-icon">🌙</span>
</button>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">Vorgaben</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<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>
</div>
</div>
</nav>
<!-- Breadcrumb -->
{% block breadcrumb %}
{% if request.resolver_match.url_name != 'startseite' and request.path != '/' %}
<nav aria-label="Breadcrumb">
<div class="container-fluid">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Startseite</a></li>
{% block breadcrumb_items %}{% endblock %}
</ol>
</div>
</nav>
{% endif %}
{% endblock %}
<!-- Main Content -->
<div class="container-fluid">
<div class="row">
<!-- Left Sidebar -->
{% if block.sidebar_left %}
<aside class="col-lg-2 d-none d-lg-block">
{% block sidebar_left %}{% endblock %}
</aside>
{% endif %}
<!-- Main Content Area -->
<main class="{% if block.sidebar_left or block.sidebar_right %}col-lg-8{% else %}col-lg-12{% endif %} py-4">
{% block content %}{% endblock %}
</main>
<!-- Right Sidebar -->
{% if block.sidebar_right %}
<aside class="col-lg-2 d-none d-lg-block">
{% block sidebar_right %}{% endblock %}
</aside>
{% endif %}
</div>
<div class="d-flex">
<div class="col-md-2">{% block sidebar_left %}{% endblock %}</div>
<div class="flex-fill">{% block content %}Main Content{% endblock %}</div>
<div class="col-md-2">{% block sidebar_right %}{% endblock %}</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="container-fluid">
<p class="mb-0">VorgabenUI v0.942 | © {{ "now"|date:"Y" }} Bundesamt für Informatik</p>
</div>
</footer>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Theme Toggle
function toggleTheme() {
const html = document.documentElement;
const themeIcon = document.getElementById('theme-icon');
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
themeIcon.textContent = newTheme === 'light' ? '🌙' : '☀️';
localStorage.setItem('theme', newTheme);
}
// Load saved theme
document.addEventListener('DOMContentLoaded', function() {
const savedTheme = localStorage.getItem('theme') || 'light';
const html = document.documentElement;
const themeIcon = document.getElementById('theme-icon');
html.setAttribute('data-theme', savedTheme);
themeIcon.textContent = savedTheme === 'light' ? '🌙' : '☀️';
});
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Active navigation highlighting
document.addEventListener('DOMContentLoaded', function() {
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll('.navbar-nav .nav-link');
navLinks.forEach(link => {
if (link.getAttribute('href') === currentPath) {
link.classList.add('active');
}
});
});
</script>
<div>VorgabenUI v0.942</div>
</body>
</html>

View File

@@ -1,160 +1,30 @@
{% extends "base.html" %}
{% load page_extras %}
{% block title %}Suchresultate für {{ suchbegriff }}{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item"><a href="/search/">Suche</a></li>
<li class="breadcrumb-item active" aria-current="page">Resultate</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-4">
<!-- Search Form -->
<div class="card search-form sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">🔍 Suche</h5>
</div>
<div class="card-body">
<form action="/search/" method="post" id="search-form">
{% csrf_token %}
<!-- Main Search Field -->
<div class="mb-3">
<label for="query" class="form-label">Suchbegriff</label>
<div class="input-group">
<input type="text"
class="form-control"
id="query"
name="q"
placeholder="Suchbegriff eingeben …"
value="{{ suchbegriff|default:'' }}"
required
maxlength="200">
<button class="btn btn-primary" type="submit">
Suchen
</button>
</div>
</div>
<h1 class="mb-4">Suchresultate für {{ suchbegriff }}</h1>
{% if resultat.geltungsbereich %}
<h2>Standards mit "{{suchbegriff}}" im Geltungsbereich</h2>
{% for standard,geltungsbereich in resultat.geltungsbereich.items %}
<h4>{{ standard }}</h4>
{% for typ, html in geltungsbereich %}
<div>{{ html|highlighttext:suchbegriff|safe }}</div>
{% endfor %}
{% endfor %}
{% endif %}
<!-- Search Options -->
<div class="mb-3">
<label class="form-label">Suchbereiche</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="search_in" value="standards" id="search-standards" checked>
<label class="form-check-label" for="search-standards">
Standards
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="search_in" value="geltungsbereich" id="search-geltungsbereich" checked>
<label class="form-check-label" for="search-geltungsbereich">
Geltungsbereich
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="search_in" value="vorgaben" id="search-vorgaben" checked>
<label class="form-check-label" for="search-vorgaben">
Vorgaben
</label>
</div>
</div>
<button class="btn btn-outline btn-sm w-100" onclick="history.back()">
← Zurück zur Suche
</button>
</form>
</div>
</div>
</div>
<div class="col-lg-8">
<!-- Search Results -->
<div class="search-results">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Suchresultate für "{{ suchbegriff }}"</h2>
<span class="badge bg-primary">
{% if resultat.geltungsbereich %}
{{ resultat.geltungsbereich|length }}
{% endif %}
{% if resultat.all %}
{% for standard, vorgaben in resultat.all.items %}
{{ vorgaben|length }}
{% endfor %}
{% endif %}
Ergebnisse
</span>
</div>
<!-- Geltungsbereich Results -->
{% if resultat.geltungsbereich %}
<div class="mb-5">
<h3 class="h4 mb-3">📋 Standards mit "{{ suchbegriff }}" im Geltungsbereich</h3>
{% for standard, geltungsbereich in resultat.geltungsbereich.items %}
<div class="result-item">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="mb-1">
<a href="/dokumente/{{ standard.nummer }}/" class="text-decoration-none">
{{ standard.nummer }} {{ standard.name }}
</a>
</h5>
<span class="badge bg-info">Geltungsbereich</span>
</div>
{% for typ, html in geltungsbereich %}
<div class="search-context">{{ html|highlighttext:suchbegriff|safe }}</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endif %}
<!-- Vorgaben Results -->
{% if resultat.all %}
<div class="mb-5">
<h3 class="h4 mb-3">📝 Vorgaben mit "{{ suchbegriff }}"</h3>
{% for standard, vorgaben in resultat.all.items %}
<div class="result-item">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="mb-1">
<a href="/dokumente/{{ standard.nummer }}/" class="text-decoration-none">
{{ standard }}
</a>
</h5>
<span class="badge bg-success">{{ vorgaben|length }} Vorgaben</span>
</div>
<div class="row">
{% if resultat.all %}
<h2>Vorgaben mit "{{ suchbegriff }}"</h2>
{% for standard, vorgaben in resultat.all.items %}
<h4>{{ standard }}</h4>
<ul>
{% for vorgabe in vorgaben %}
<div class="col-md-6 mb-2">
<div class="d-flex align-items-center">
<span class="badge bg-secondary me-2">{{ vorgabe.Vorgabennummer }}</span>
<a href="/dokumente/{{ vorgabe.dokument.nummer }}/#{{vorgabe.Vorgabennummer}}"
class="text-decoration-none">
{{ vorgabe.titel }}
</a>
</div>
</div>
{% endfor %}
</div>
</div>
<li><a href="{% url 'standard_detail' nummer=vorgabe.dokument.nummer %}#{{vorgabe.Vorgabennummer}}">{{vorgabe}}</a></li>
{% endfor %}
</div>
{% endif %}
</ul>
{% endfor %}
{% endif %}
<!-- No Results -->
{% if not resultat.geltungsbereich and not resultat.all %}
<div class="text-center py-5">
<div class="mb-3">
<span style="font-size: 3rem;">🔍</span>
</div>
<h3 class="text-muted">Keine Resultate für "{{ suchbegriff }}"</h3>
<p class="text-muted mb-4">
Es wurden keine Ergebnisse gefunden. Versuchen Sie es mit anderen Suchbegriffen.
</p>
<button class="btn btn-primary" onclick="history.back()">
← Zurück zur Suche
</button>
</div>
{% endif %}
</div>
</div>
</div>
{% if not resultat.all %}
<h2>Keine Resultate für "{{suchbegriff}}"</h2>
{% endif %}
{% endblock %}

View File

@@ -1,139 +1,28 @@
{% extends "base.html" %}
{% block title %}Suche{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Suche</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-4">
<!-- Search Form -->
<div class="card search-form sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">🔍 Suche</h5>
</div>
<div class="card-body">
<form action="/search/" method="post" id="search-form">
{% csrf_token %}
<!-- Main Search Field -->
<div class="mb-3">
<label for="query" class="form-label">Suchbegriff</label>
<div class="input-group">
<input type="text"
class="form-control"
id="query"
name="q"
placeholder="Suchbegriff eingeben …"
value="{{ search_term|default:'' }}"
required
maxlength="200">
<button class="btn btn-primary" type="submit">
Suchen
</button>
</div>
</div>
<h1 class="mb-4">Suche</h1>
<button class="btn btn-outline btn-sm w-100" onclick="history.back()">
← Zurück zur Suche
</button>
</form>
</div>
{% if error_message %}
<div class="alert alert-danger">
<strong>Fehler:</strong> {{ error_message }}
</div>
</div>
{% endif %}
<div class="col-lg-8">
<!-- Search Results -->
{% if search_term %}
<div class="search-results">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Suchresultate für "{{ search_term }}"</h2>
<span class="badge bg-primary">
{% if resultat.geltungsbereich %}
{{ resultat.geltungsbereich|length }}
{% endif %}
{% if resultat.all %}
{% for standard, vorgaben in resultat.all.items %}
{{ vorgaben|length }}
{% endfor %}
{% endif %}
Ergebnisse
</span>
<!-- Search form -->
<form action="." method="post">
{% csrf_token %}
<!-- Search field -->
<div class="mb-3">
<label for="query" class="form-label">Suchbegriff</label>
<input type="text"
class="form-control"
id="query"
name="q"
placeholder="Suchbegriff eingeben …"
value="{{ search_term|default:'' }}"
required
maxlength="200">
</div>
<!-- No Results -->
{% if not resultat.geltungsbereich and not resultat.all %}
<div class="text-center py-5">
<div class="mb-3">
<span style="font-size: 3rem;">🔍</span>
</div>
<h3 class="text-muted">Keine Resultate für "{{ search_term }}"</h3>
<p class="text-muted mb-4">
Es wurden keine Ergebnisse gefunden. Versuchen Sie es mit anderen Suchbegriffen.
</p>
<button class="btn btn-primary" onclick="history.back()">
← Zurück zur Suche
</button>
</div>
{% endif %}
</div>
{% else %}
<!-- Initial Search State -->
<div class="text-center py-5">
<div class="mb-4">
<span style="font-size: 4rem;">🔍</span>
</div>
<h2>Willkommen bei der Suche</h2>
<p class="text-muted mb-4">
Geben Sie einen Suchbegriff ein, um Standards, Vorgaben und Geltungsbereiche zu durchsuchen.
</p>
<!-- Quick Search Examples -->
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Beispielsuchen</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Passwort">
🔐 Passwort
</button>
</div>
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Netzwerk">
🌐 Netzwerk
</button>
</div>
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Verschlüsselung">
🔒 Verschlüsselung
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- JavaScript for quick search -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Quick search buttons
const quickSearchButtons = document.querySelectorAll('.quick-search');
quickSearchButtons.forEach(button => {
button.addEventListener('click', function() {
const term = this.dataset.term;
document.getElementById('query').value = term;
document.getElementById('search-form').submit();
});
});
});
</script>
{% endblock %}
<button type="submit" class="btn btn-primary">Suchen</button>
</form>
{% endblock %}

View File

@@ -1,139 +0,0 @@
{% extends "base.html" %}
{% block title %}Suche{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Suche</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-4">
<!-- Search Form -->
<div class="card search-form sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">🔍 Suche</h5>
</div>
<div class="card-body">
<form action="/search/" method="post" id="search-form">
{% csrf_token %}
<!-- Main Search Field -->
<div class="mb-3">
<label for="query" class="form-label">Suchbegriff</label>
<div class="input-group">
<input type="text"
class="form-control"
id="query"
name="q"
placeholder="Suchbegriff eingeben …"
value="{{ search_term|default:'' }}"
required
maxlength="200">
<button class="btn btn-primary" type="submit">
Suchen
</button>
</div>
</div>
<button class="btn btn-outline btn-sm w-100" onclick="history.back()">
← Zurück zur Suche
</button>
</form>
</div>
</div>
</div>
<div class="col-lg-8">
<!-- Search Results -->
{% if search_term %}
<div class="search-results">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Suchresultate für "{{ search_term }}"</h2>
<span class="badge bg-primary">
{% if resultat.geltungsbereich %}
{{ resultat.geltungsbereich|length }}
{% endif %}
{% if resultat.all %}
{% for standard, vorgaben in resultat.all.items %}
{{ vorgaben|length }}
{% endfor %}
{% endif %}
Ergebnisse
</span>
</div>
<!-- No Results -->
{% if not resultat.geltungsbereich and not resultat.all %}
<div class="text-center py-5">
<div class="mb-3">
<span style="font-size: 3rem;">🔍</span>
</div>
<h3 class="text-muted">Keine Resultate für "{{ search_term }}"</h3>
<p class="text-muted mb-4">
Es wurden keine Ergebnisse gefunden. Versuchen Sie es mit anderen Suchbegriffen.
</p>
<button class="btn btn-primary" onclick="history.back()">
← Zurück zur Suche
</button>
</div>
{% endif %}
</div>
{% else %}
<!-- Initial Search State -->
<div class="text-center py-5">
<div class="mb-4">
<span style="font-size: 4rem;">🔍</span>
</div>
<h2>Willkommen bei der Suche</h2>
<p class="text-muted mb-4">
Geben Sie einen Suchbegriff ein, um Standards, Vorgaben und Geltungsbereiche zu durchsuchen.
</p>
<!-- Quick Search Examples -->
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Beispielsuchen</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Passwort">
🔐 Passwort
</button>
</div>
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Netzwerk">
🌐 Netzwerk
</button>
</div>
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Verschlüsselung">
🔒 Verschlüsselung
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- JavaScript for quick search -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Quick search buttons
const quickSearchButtons = document.querySelectorAll('.quick-search');
quickSearchButtons.forEach(button => {
button.addEventListener('click', function() {
const term = this.dataset.term;
document.getElementById('query').value = term;
document.getElementById('search-form').submit();
});
});
});
</script>
{% endblock %}

View File

@@ -1,335 +1,10 @@
{% extends "base.html" %}
{% block title %}Startseite - Vorgaben Informatiksicherheit BIT{% endblock %}
{% block content %}
<!-- Hero Section -->
<div class="card bg-primary text-white mb-5">
<div class="card-body text-center py-5">
<h1 class="display-4 mb-3">🔒 Vorgaben Informatiksicherheit BIT</h1>
<p class="lead mb-4">
Zentraler Zugang zu allen Sicherheitsstandards, Vorgaben und Richtlinien des Bundesamtes für Informatik
</p>
<div class="d-flex justify-content-center gap-3">
<a href="/dokumente/" class="btn btn-light btn-lg">
📋 Standards durchsuchen
</a>
<a href="/search/" class="btn btn-outline-light btn-lg">
🔍 Volltextsuche
</a>
</div>
</div>
</div>
<!-- Statistics Dashboard -->
<div class="row mb-5">
<div class="col-lg-3 col-md-6 mb-4">
<div class="card h-100 text-center">
<div class="card-body">
<div class="mb-3">
<span style="font-size: 2.5rem;">📋</span>
</div>
<h3 class="h2 text-primary mb-2">{{ dokumente|length }}</h3>
<p class="text-muted mb-0">Standards</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<div class="card h-100 text-center">
<div class="card-body">
<div class="mb-3">
<span style="font-size: 2.5rem;">📝</span>
</div>
<h3 class="h2 text-success mb-2">
{% if total_vorgaben %}{{ total_vorgaben }}{% else %}--{% endif %}
</h3>
<p class="text-muted mb-0">Vorgaben</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<div class="card h-100 text-center">
<div class="card-body">
<div class="mb-3">
<span style="font-size: 2.5rem;">🏷️</span>
</div>
<h3 class="h2 text-info mb-2">
{% if total_stichworte %}{{ total_stichworte }}{% else %}--{% endif %}
</h3>
<p class="text-muted mb-0">Stichworte</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<div class="card h-100 text-center">
<div class="card-body">
<div class="mb-3">
<span style="font-size: 2.5rem;">🔗</span>
</div>
<h3 class="h2 text-warning mb-2">
{% if total_referenzen %}{{ total_referenzen }}{% else %}--{% endif %}
</h3>
<p class="text-muted mb-0">Referenzen</p>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row mb-5">
<div class="col-12">
<h2 class="h4 mb-4">⚡ Schnellzugriffe</h2>
</div>
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<div class="mb-3">
<span style="font-size: 2rem;">📋</span>
</div>
<h5 class="card-title">Standards</h5>
<p class="card-text text-muted">
Alle Sicherheitsstandards durchsuchen und filtern
</p>
<a href="/dokumente/" class="btn btn-primary">
Standards anzeigen
</a>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<div class="mb-3">
<span style="font-size: 2rem;">🔍</span>
</div>
<h5 class="card-title">Suche</h5>
<p class="card-text text-muted">
Volltextsuche in allen Standards und Vorgaben
</p>
<a href="/search/" class="btn btn-primary">
Suche starten
</a>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<div class="mb-3">
<span style="font-size: 2rem;">🏷️</span>
</div>
<h5 class="card-title">Stichworte</h5>
<p class="card-text text-muted">
Nach Stichworten browsen und entdecken
</p>
<a href="/stichworte/" class="btn btn-primary">
Stichworte anzeigen
</a>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<div class="mb-3">
<span style="font-size: 2rem;">🔗</span>
</div>
<h5 class="card-title">Referenzen</h5>
<p class="card-text text-muted">
Referenzbaum und Querverbindungen
</p>
<a href="/referenzen/" class="btn btn-primary">
Referenzen anzeigen
</a>
</div>
</div>
</div>
{% if user.is_staff %}
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<div class="mb-3">
<span style="font-size: 2rem;">⚠️</span>
</div>
<h5 class="card-title">Unvollständig</h5>
<p class="card-text text-muted">
Unvollständige Standards bearbeiten
</p>
<a href="/dokumente/unvollstaendig/" class="btn btn-warning">
Bearbeiten
</a>
</div>
</div>
</div>
{% endif %}
</div>
<!-- Recent Standards -->
<div class="row mb-5">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h3 class="h5 mb-0">📋 Aktuell erfasste Standards</h3>
<a href="/dokumente/" class="btn btn-outline btn-sm">
Alle anzeigen →
</a>
</div>
</div>
<div class="card-body">
{% if dokumente %}
<div class="row">
{% for standard in dokumente|slice:":6" %}
<div class="col-lg-4 col-md-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h6 class="card-title">
<a href="{% url 'standard_detail' nummer=standard.nummer %}" class="text-decoration-none">
{{ standard.nummer }} {{ standard.name|truncatechars:40 }}
</a>
</h6>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
{{ standard.gueltigkeit_von|default_if_none:"-" }}
</small>
{% if not standard.aktiv %}
<span class="badge bg-danger">Inaktiv</span>
{% else %}
<span class="badge bg-success">Aktiv</span>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<p class="text-muted mb-0">Keine Standards gefunden.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Quick Search -->
<div class="row mb-5">
<div class="col-12">
<div class="card bg-light">
<div class="card-body text-center py-4">
<h3 class="h5 mb-3">🔍 Schnellsuche</h3>
<form action="/search/" method="get" class="row justify-content-center">
<div class="col-md-6">
<div class="input-group">
<input type="text"
class="form-control"
name="q"
placeholder="Suchbegriff eingeben..."
maxlength="200">
<button class="btn btn-primary" type="submit">
Suchen
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Help Section -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="h5 mb-0">💡 Hinweise zur Nutzung</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<h6 class="text-primary">🔍 Suchen</h6>
<p class="small text-muted mb-0">
Nutzen Sie die Volltextsuche um gezielt nach Begriffen in allen Standards zu suchen.
</p>
</div>
<div class="col-md-4 mb-3">
<h6 class="text-primary">📋 Filtern</h6>
<p class="small text-muted mb-0">
Filtern Sie Standards nach Status, Gültigkeit oder anderen Kriterien.
</p>
</div>
<div class="col-md-4 mb-3">
<h6 class="text-primary">🏷️ Stichworte</h6>
<p class="small text-muted mb-0">
Entdecken Sie verwandte Inhalte durch die Stichwort-Navigation.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript for dynamic interactions -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add hover effects to cards
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.transition = 'all 0.2s ease';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
});
});
// Animate statistics on page load
const statNumbers = document.querySelectorAll('.h2');
statNumbers.forEach((stat, index) => {
setTimeout(() => {
stat.style.opacity = '0';
stat.style.transform = 'scale(0.8)';
stat.style.transition = 'all 0.5s ease';
setTimeout(() => {
stat.style.opacity = '1';
stat.style.transform = 'scale(1)';
}, 100);
}, index * 100);
});
});
</script>
<style>
.card {
transition: all 0.2s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.display-4 {
font-weight: 700;
}
.btn-lg {
padding: 0.75rem 2rem;
font-weight: 500;
}
@media (max-width: 768px) {
.display-4 {
font-size: 2rem;
}
.btn-lg {
padding: 0.5rem 1.5rem;
font-size: 1rem;
}
}
</style>
<h1>Vorgaben Informatiksicherheit BIT</h1>
<h2>Aktuell erfasste Standards</h2>
<ul>
{% for standard in dokumente %}
<li><a href="{% url 'standard_detail' nummer=standard.nummer %}">{{ standard }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -10,7 +10,7 @@ import datetime
def startseite(request):
standards=list(Dokument.objects.filter(aktiv=True))
return render(request, 'startseite.html', {"dokumente":standards})
return render(request, 'startseite.html', {"dokumente":standards,})
def validate_search_input(search_term):
"""

View File

@@ -1,190 +1,51 @@
{% extends "base.html" %}
{% load mptt_tags %}
{% block title %}Referenz: {{ referenz.Path }}{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item"><a href="/referenzen/">Referenzen</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ referenz.Path }}</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<!-- Referenz Header -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<div class="d-flex justify-content-between align-items-start">
<div>
<h1 class="h3 mb-2">🔗 {{ referenz.Path }}</h1>
{% if referenz.ParentID %}
<small class="opacity-75">
<a href="/referenzen/{{ referenz.ParentID }}/" class="text-white">
← Zurück zu übergeordneter Referenz
</a>
</small>
{% endif %}
</div>
{% if referenz.url %}
<a href="{{ referenz.url }}" class="btn btn-light btn-sm" target="_blank">
🔗 Externer Link
</a>
{% endif %}
</div>
</div>
{% if referenz.erklaerung %}
<div class="card-body">
<h5 class="card-title">📖 Beschreibung</h5>
<h1><a href="../{{ referenz.ParentID }}"></a>{{ referenz.Path }}</h1>
{% if referenz.erklaerung %}
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
<h3 class="h5 m-0">Beschreibung</h3>
{% if referenz.url %}
<span class="badge bg-light text-black">
<a href="{{ referenz.url }}">Link</a>
</span>{% endif %}
</div>
<div class="card-body p-2">
{% for typ, html in referenz.erklaerung %}
{% if html %}
<div class="content-section">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if html %}<div>{{ html|safe }}</div>{% endif %}{% endfor %}
</div>
</div>
{% endif %}
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
<h3 class="h5 m-0">Referenzierte Vorgaben</h3>
</div>
<!-- Referenzierte Vorgaben -->
<div class="card">
<div class="card-header">
<h3 class="h5 mb-0">📝 Referenzierte Vorgaben</h3>
</div>
<div class="card-body">
{% recursetree referenz.children %}
{% if not node == referenz %}
<div class="mb-3 p-3 border rounded">
<h6 class="mb-2">{{ node.Path }}</h6>
{% if node.referenziertvon %}
<div class="ms-3">
<small class="text-muted">Referenziert von:</small>
<ul class="list-unstyled mb-0">
{% for ref in node.referenziertvon %}
<li class="mb-1">
<a href="/dokumente/{{ ref.dokument.nummer }}/#{{ref.Vorgabennummer}}"
class="text-decoration-none">
<span class="badge bg-secondary me-2">{{ ref.Vorgabennummer }}</span>
{{ ref.titel }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
{% if node.referenziertvon %}
<div class="ms-3 mb-3">
<small class="text-muted">Referenziert von:</small>
<ul class="list-unstyled mb-0">
<div class="card-body p-2">
{% recursetree referenz.children %}
{% if not node == referenz %}
{#<a href="../{{node.id}}">#}
{{ node.Path }}
{#</a>#}
{% else %}
{{ node.Path }}
{% endif %}
<br>
{% if node.referenziertvon %}
<ul>
{% for ref in node.referenziertvon %}
<li class="mb-1">
<a href="/dokumente/{{ ref.dokument.nummer }}/#{{ref.Vorgabennummer}}"
class="text-decoration-none">
<span class="badge bg-secondary me-2">{{ ref.Vorgabennummer }}</span>
{{ ref.titel }}
</a>
</li>
<li><a href="{% url 'standard_detail' nummer=ref.dokument.nummer %}#{{ref.Vorgabennummer}}">{{ref}}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if not node.is_leaf_node %}
<div class="ms-3">
{{ children }}
</div>
{% endif %}
{% endrecursetree %}
</div>
</ul>
<br>
{% endif %}
{% if not node.is_leaf_node %}
{{ children }}
{% endif %}
{% endrecursetree %}
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Quick Actions -->
<div class="card mb-4 sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">⚡ Aktionen</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
{% if referenz.ParentID %}
<a href="/referenzen/{{ referenz.ParentID }}/" class="btn btn-outline btn-sm">
← Zurück
</a>
{% endif %}
<a href="/referenzen/" class="btn btn-outline btn-sm">
🌳 Zur Baumansicht
</a>
{% if referenz.url %}
<a href="{{ referenz.url }}" class="btn btn-outline btn-sm" target="_blank">
🔗 Externer Link
</a>
{% endif %}
</div>
</div>
</div>
<!-- Statistics -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">📊 Informationen</h5>
</div>
<div class="card-body">
<div class="mb-3">
<small class="text-muted">Referenz-ID</small>
<p class="mb-0 fw-bold">{{ referenz.id }}</p>
</div>
{% if referenz.ParentID %}
<div class="mb-3">
<small class="text-muted">Übergeordnete Referenz</small>
<p class="mb-0">
<a href="/referenzen/{{ referenz.ParentID }}/" class="text-decoration-none">
ID: {{ referenz.ParentID }}
</a>
</p>
</div>
{% endif %}
<div class="mb-3">
<small class="text-muted">Anzahl Unterelemente</small>
<p class="mb-0 fw-bold">{{ referenz.get_children.count }}</p>
</div>
</div>
</div>
<!-- Navigation Help -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">💡 Hinweise</h5>
</div>
<div class="card-body">
<ul class="small mb-0">
<li>Diese Seite zeigt Details zur ausgewählten Referenz</li>
<li>Verknüpfte Vorgaben sind direkt verlinkt</li>
<li>Nutzen Sie die Baumansicht für die Übersicht</li>
</ul>
</div>
</div>
</div>
</div>
<style>
.content-section {
line-height: 1.6;
margin-bottom: 1rem;
}
.content-section h1, .content-section h2, .content-section h3 {
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.border {
border-color: var(--border-color) !important;
}
.badge {
font-size: 0.75rem;
}
</style>
{% endblock %}

View File

@@ -1,466 +1,21 @@
{% extends "base.html" %}
{% block title %}Referenzen{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Referenzen</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<!-- Header -->
<div class="card mb-4">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0">🔗 Referenzbaum</h1>
<div class="d-flex gap-2">
<button class="btn btn-outline btn-sm" onclick="expandAll()">
Alle ausklappen
</button>
<button class="btn btn-outline btn-sm" onclick="collapseAll()">
Alle einklappen
</button>
</div>
</div>
</div>
<div class="card-body">
<p class="text-muted mb-0">
Hier finden Sie alle Referenzen und Querverbindungen zwischen den Standards und Vorgaben.
Klicken Sie auf die Pfeile um den Baum zu navigieren.
</p>
</div>
</div>
<h1>Referenzen</h1>
<!-- Search and Filter -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-8">
<label for="tree-search" class="form-label">🔍 Referenzen durchsuchen</label>
<input type="text"
class="form-control"
id="tree-search"
placeholder="Suchbegriff eingeben..."
onkeyup="filterTree()">
</div>
<div class="col-md-4">
<label for="tree-filter" class="form-label">🏷️ Filter</label>
<select class="form-select" id="tree-filter" onchange="filterTree()">
<option value="">Alle anzeigen</option>
<option value="has-children">Mit Unterelementen</option>
<option value="leaf-only">Nur Endpunkte</option>
</select>
</div>
</div>
</div>
</div>
<!-- Interactive Tree -->
<div class="card">
<div class="card-body">
{% load mptt_tags %}
<div id="tree-container">
<ul class="tree-root">
{% recursetree referenzen %}
<li class="tree-node" data-node-id="{{ node.id }}" data-node-text="{{ node.name_text|default:'' }} {{ node.name_nummer|default:'' }}">
<div class="tree-node-content" onclick="toggleNode(this)">
{% if not node.is_leaf_node %}
<span class="tree-toggle"></span>
{% else %}
<span class="tree-toggle-placeholder"></span>
{% endif %}
<a href="{{ node.id }}" class="tree-link">
{% if node.name_nummer %}
<span class="tree-number">{{ node.name_nummer }}</span>
{% endif %}
{% if node.name_text %}
<span class="tree-text">{{ node.name_text }}</span>
{% endif %}
</a>
<div class="tree-node-meta">
{% if not node.is_leaf_node %}
<span class="badge bg-info">{{ node.get_children.count }} Unterelemente</span>
{% else %}
<span class="badge bg-secondary">Endpunkt</span>
{% endif %}
</div>
</div>
{% if not node.is_leaf_node %}
<ul class="tree-children">
<div>
{% load mptt_tags %}
<ul class="tree">
{% recursetree referenzen %}
<li>
<a href="{{node.id}}">{{ node.name_nummer }}{% if node.name_text %} ({{node.name_text}}){% endif %}</a>
{% if not node.is_leaf_node %}
<ul class="children">
{{ children }}
</ul>
{% endif %}
</li>
{% endrecursetree %}
</ul>
</div>
{% if not referenzen %}
<div class="text-center py-5">
<span style="font-size: 3rem;">🔗</span>
<h4 class="text-muted mt-3">Keine Referenzen gefunden</h4>
<p class="text-muted">Es wurden keine Referenzen in der Datenbank gefunden.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Statistics -->
<div class="card mb-4 sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">📊 Statistiken</h5>
</div>
<div class="card-body">
<div class="row text-center mb-3">
<div class="col-6">
<h4 class="text-primary mb-1">{{ referenzen|length }}</h4>
<small class="text-muted">Gesamt</small>
</div>
<div class="col-6">
<h4 class="text-success mb-1">
{% for node in referenzen %}
{% if node.is_leaf_node %}
{% if forloop.first %}1{% else %}{{ forloop.counter }}{% endif %}
{% endif %}
{% endfor %}
</h4>
<small class="text-muted">Endpunkte</small>
</div>
</div>
<div class="border-top pt-3">
<h6 class="text-muted mb-2">Baumtiefe</h6>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-primary" style="width: 75%"></div>
</div>
<small class="text-muted">Maximale Tiefe: 4 Ebenen</small>
</div>
</div>
</div>
<!-- Quick Navigation -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">🧭 Navigation</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-outline btn-sm" onclick="scrollToTop()">
⬆️ Zum Anfang
</button>
<button class="btn btn-outline btn-sm" onclick="findNextMatch()">
⬇️ Nächster Treffer
</button>
<button class="btn btn-outline btn-sm" onclick="resetFilters()">
🔄 Filter zurücksetzen
</button>
</div>
</div>
</div>
<!-- Help -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">💡 Hinweise</h5>
</div>
<div class="card-body">
<ul class="small mb-0">
<li>Klicken Sie auf ▼/▶ um Knoten ein-/auszuklappen</li>
<li>Nutzen Sie die Suche um gezielt zu filtern</li>
<li>Referenznummern sind hervorgehoben</li>
<li>Endpunkte haben keine Unterelemente</li>
</ul>
</div>
</div>
</div>
</ul>
{% endif %}
</li>
{% endrecursetree %}
</ul>
</div>
<style>
/* Tree Styles */
.tree-root {
list-style: none;
padding-left: 0;
margin: 0;
}
.tree-node {
margin-bottom: 2px;
}
.tree-node-content {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.tree-node-content:hover {
background-color: var(--bg-secondary);
border-color: var(--primary-color);
}
.tree-toggle {
width: 20px;
text-align: center;
font-size: 12px;
color: var(--text-muted);
margin-right: 8px;
}
.tree-toggle-placeholder {
width: 20px;
margin-right: 8px;
}
.tree-link {
text-decoration: none;
color: var(--text-primary);
flex: 1;
display: flex;
align-items: center;
gap: 8px;
}
.tree-link:hover {
color: var(--primary-color);
}
.tree-number {
font-family: var(--font-mono);
font-weight: 600;
color: var(--primary-color);
background-color: var(--bg-secondary);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.875rem;
}
.tree-text {
font-weight: 500;
}
.tree-node-meta {
margin-left: auto;
display: flex;
gap: 4px;
}
.tree-children {
list-style: none;
padding-left: 28px;
margin: 2px 0 0 0;
border-left: 2px solid var(--border-color);
margin-left: 10px;
}
.tree-children .tree-node {
position: relative;
}
.tree-children .tree-node::before {
content: '';
position: absolute;
left: -30px;
top: 20px;
width: 20px;
height: 1px;
background-color: var(--border-color);
}
/* Collapsed state */
.tree-children.collapsed {
display: none;
}
.tree-node.collapsed .tree-toggle {
transform: rotate(-90deg);
}
/* Highlighted search results */
.tree-node.highlighted > .tree-node-content {
background-color: var(--warning-color);
color: white;
}
.tree-node.highlighted .tree-number {
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.tree-children {
padding-left: 20px;
}
.tree-node-meta {
display: none;
}
.tree-number {
font-size: 0.75rem;
}
}
</style>
<script>
let currentMatchIndex = -1;
let matches = [];
function toggleNode(element) {
const node = element.parentElement;
const children = node.querySelector(':scope > .tree-children');
const toggle = element.querySelector('.tree-toggle');
if (children) {
children.classList.toggle('collapsed');
node.classList.toggle('collapsed');
if (toggle) {
toggle.textContent = children.classList.contains('collapsed') ? '▶' : '▼';
}
}
}
function expandAll() {
const children = document.querySelectorAll('.tree-children');
const nodes = document.querySelectorAll('.tree-node');
const toggles = document.querySelectorAll('.tree-toggle');
children.forEach(child => child.classList.remove('collapsed'));
nodes.forEach(node => node.classList.remove('collapsed'));
toggles.forEach(toggle => {
if (toggle.textContent === '▶') {
toggle.textContent = '▼';
}
});
}
function collapseAll() {
const children = document.querySelectorAll('.tree-children');
const nodes = document.querySelectorAll('.tree-node');
const toggles = document.querySelectorAll('.tree-toggle');
children.forEach(child => child.classList.add('collapsed'));
nodes.forEach(node => node.classList.add('collapsed'));
toggles.forEach(toggle => {
if (toggle.textContent === '▼') {
toggle.textContent = '▶';
}
});
}
function filterTree() {
const searchTerm = document.getElementById('tree-search').value.toLowerCase();
const filterType = document.getElementById('tree-filter').value;
const nodes = document.querySelectorAll('.tree-node');
matches = [];
currentMatchIndex = -1;
nodes.forEach(node => {
const nodeText = node.dataset.nodeText.toLowerCase();
const hasChildren = node.querySelector(':scope > .tree-children') !== null;
const isLeaf = !hasChildren;
let showNode = true;
// Apply search filter
if (searchTerm && !nodeText.includes(searchTerm)) {
showNode = false;
}
// Apply type filter
if (filterType === 'has-children' && !hasChildren) {
showNode = false;
} else if (filterType === 'leaf-only' && !isLeaf) {
showNode = false;
}
// Show/hide node
node.style.display = showNode ? 'block' : 'none';
// Highlight search matches
if (searchTerm && nodeText.includes(searchTerm)) {
node.classList.add('highlighted');
matches.push(node);
} else {
node.classList.remove('highlighted');
}
// Auto-expand parent nodes of matches
if (searchTerm && nodeText.includes(searchTerm)) {
let parent = node.parentElement;
while (parent && parent.classList.contains('tree-children')) {
parent.classList.remove('collapsed');
parent.parentElement.classList.remove('collapsed');
const toggle = parent.parentElement.querySelector('.tree-toggle');
if (toggle) toggle.textContent = '▼';
parent = parent.parentElement.parentElement;
}
}
});
}
function findNextMatch() {
if (matches.length === 0) return;
currentMatchIndex = (currentMatchIndex + 1) % matches.length;
const match = matches[currentMatchIndex];
// Scroll to match
match.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Highlight temporarily
match.style.backgroundColor = 'var(--accent-color)';
setTimeout(() => {
match.style.backgroundColor = '';
}, 1000);
}
function scrollToTop() {
document.getElementById('tree-container').scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
function resetFilters() {
document.getElementById('tree-search').value = '';
document.getElementById('tree-filter').value = '';
filterTree();
expandAll();
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Add keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.ctrlKey || e.metaKey) {
switch(e.key) {
case 'f':
e.preventDefault();
document.getElementById('tree-search').focus();
break;
case 'e':
e.preventDefault();
expandAll();
break;
case 'w':
e.preventDefault();
collapseAll();
break;
}
}
});
});
</script>
{% endblock %}

View File

@@ -1,819 +0,0 @@
/* Vorgaben UI Custom Styles */
:root {
/* Professional color scheme for security standards */
--primary-color: #1e3a8a;
--primary-dark: #1e2f5a;
--primary-light: #3b82f6;
--secondary-color: #64748b;
--accent-color: #0ea5e9;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--info-color: #06b6d4;
/* Neutral colors */
--gray-50: #f8fafc;
--gray-100: #f1f5f9;
--gray-200: #e2e8f0;
--gray-300: #cbd5e1;
--gray-400: #94a3b8;
--gray-500: #64748b;
--gray-600: #475569;
--gray-700: #334155;
--gray-800: #1e293b;
--gray-900: #0f172a;
/* Typography */
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Border radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-normal: 250ms ease-in-out;
--transition-slow: 350ms ease-in-out;
}
/* Dark mode variables */
[data-theme="dark"] {
--bg-primary: var(--gray-900);
--bg-secondary: var(--gray-800);
--bg-tertiary: var(--gray-700);
--text-primary: var(--gray-100);
--text-secondary: var(--gray-300);
--text-muted: var(--gray-400);
--border-color: var(--gray-700);
--card-bg: var(--gray-800);
--navbar-bg: var(--gray-900);
}
/* Light mode variables */
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: var(--gray-50);
--bg-tertiary: var(--gray-100);
--text-primary: var(--gray-900);
--text-secondary: var(--gray-700);
--text-muted: var(--gray-500);
--border-color: var(--gray-200);
--card-bg: #ffffff;
--navbar-bg: #ffffff;
}
/* Base styles */
* {
box-sizing: border-box;
}
body {
font-family: var(--font-family);
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color var(--transition-normal), color var(--transition-normal);
line-height: 1.6;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.3;
margin-bottom: var(--spacing-md);
color: var(--text-primary);
}
h1 { font-size: 2.25rem; }
h2 { font-size: 1.875rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }
h5 { font-size: 1.125rem; }
h6 { font-size: 1rem; }
/* Navbar enhancements */
.navbar {
background-color: var(--navbar-bg) !important;
border-bottom: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
padding: var(--spacing-md) 0;
transition: all var(--transition-normal);
}
.navbar-brand {
font-weight: 700;
font-size: 1.5rem;
color: var(--primary-color) !important;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.navbar-brand::before {
content: "🔒";
font-size: 1.25rem;
}
.navbar-nav .nav-link {
color: var(--text-secondary) !important;
font-weight: 500;
padding: var(--spacing-sm) var(--spacing-md) !important;
border-radius: var(--radius-md);
transition: all var(--transition-fast);
margin: 0 var(--spacing-xs);
}
.navbar-nav .nav-link:hover,
.navbar-nav .nav-link.active {
color: var(--primary-color) !important;
background-color: var(--bg-secondary);
}
/* Search bar in navbar */
.navbar-search {
position: relative;
max-width: 400px;
flex: 1;
}
.navbar-search input {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: var(--radius-lg);
padding: var(--spacing-sm) var(--spacing-lg);
padding-right: 2.5rem;
width: 100%;
transition: all var(--transition-fast);
}
.navbar-search input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
}
.navbar-search button {
position: absolute;
right: var(--spacing-xs);
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
padding: var(--spacing-sm);
}
/* Dark mode toggle */
.theme-toggle {
background: none;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-sm);
cursor: pointer;
color: var(--text-secondary);
transition: all var(--transition-fast);
font-size: 1.25rem;
}
.theme-toggle:hover {
background-color: var(--bg-secondary);
color: var(--primary-color);
}
/* Breadcrumb */
.breadcrumb {
background-color: transparent;
padding: var(--spacing-md) 0;
margin-bottom: var(--spacing-lg);
}
.breadcrumb-item + .breadcrumb-item::before {
content: "";
color: var(--text-muted);
}
.breadcrumb-item a {
color: var(--text-secondary);
text-decoration: none;
transition: color var(--transition-fast);
}
.breadcrumb-item a:hover {
color: var(--primary-color);
}
/* Card enhancements */
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
transition: all var(--transition-normal);
overflow: hidden;
}
.card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.card-header {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
font-weight: 600;
padding: var(--spacing-lg);
}
.card-body {
padding: var(--spacing-lg);
}
/* Standard cards */
.standard-card {
border-left: 4px solid var(--primary-color);
margin-bottom: var(--spacing-lg);
}
.standard-card.inactive {
border-left-color: var(--gray-400);
opacity: 0.7;
}
.standard-card .card-title {
color: var(--text-primary);
font-size: 1.125rem;
font-weight: 600;
margin-bottom: var(--spacing-sm);
}
.standard-card .standard-number {
color: var(--primary-color);
font-weight: 700;
font-family: var(--font-mono);
}
.standard-card .standard-meta {
color: var(--text-muted);
font-size: 0.875rem;
margin-bottom: var(--spacing-md);
}
/* Badges */
.badge {
font-size: 0.75rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
}
.badge-relevance {
background-color: var(--info-color);
color: white;
}
.badge-status-active {
background-color: var(--success-color);
color: white;
}
.badge-status-inactive {
background-color: var(--gray-400);
color: white;
}
/* Buttons */
.btn {
font-weight: 500;
padding: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
border: none;
cursor: pointer;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-outline {
background-color: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.btn-outline:hover {
background-color: var(--bg-secondary);
border-color: var(--primary-color);
color: var(--primary-color);
}
/* Search enhancements */
.search-container {
max-width: 600px;
margin: 0 auto;
}
.search-form {
background-color: var(--card-bg);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
border: 1px solid var(--border-color);
}
.search-results {
margin-top: var(--spacing-xl);
}
.result-item {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-md);
transition: all var(--transition-fast);
}
.result-item:hover {
box-shadow: var(--shadow-sm);
border-color: var(--primary-color);
}
/* Table of contents */
.toc {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
position: sticky;
top: var(--spacing-lg);
}
.toc h3 {
margin-top: 0;
margin-bottom: var(--spacing-md);
font-size: 1.125rem;
color: var(--text-primary);
}
.toc ul {
list-style: none;
padding-left: 0;
margin: 0;
}
.toc li {
margin-bottom: var(--spacing-xs);
}
.toc a {
color: var(--text-secondary);
text-decoration: none;
display: block;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.toc a:hover,
.toc a.active {
background-color: var(--bg-tertiary);
color: var(--primary-color);
}
/* Footer */
.footer {
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-color);
padding: var(--spacing-xl) 0;
margin-top: var(--spacing-2xl);
color: var(--text-muted);
text-align: center;
}
/* Responsive design */
@media (max-width: 1200px) {
.container-fluid {
padding-left: var(--spacing-md);
padding-right: var(--spacing-md);
}
}
@media (max-width: 992px) {
.navbar-search {
max-width: 300px;
}
.standard-card {
margin-bottom: var(--spacing-md);
}
.toc {
position: static;
margin-bottom: var(--spacing-lg);
}
}
@media (max-width: 768px) {
/* Navigation */
.navbar {
padding: var(--spacing-sm) 0;
}
.navbar-brand {
font-size: 1.25rem;
}
.navbar-search {
max-width: 100%;
margin-top: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.navbar-nav .nav-link {
padding: var(--spacing-sm) var(--spacing-md) !important;
margin: 0;
}
/* Typography */
h1 { font-size: 1.875rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; }
h4 { font-size: 1.125rem; }
/* Cards */
.card {
margin-bottom: var(--spacing-md);
}
.standard-card {
border-left-width: 3px;
}
.standard-card .card-body {
padding: var(--spacing-md);
}
/* Buttons */
.btn {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.875rem;
}
.btn-lg {
padding: var(--spacing-md) var(--spacing-lg);
font-size: 1rem;
}
/* Forms */
.form-control, .form-select {
font-size: 0.875rem;
}
/* Search */
.search-form {
padding: var(--spacing-md);
}
.result-item {
padding: var(--spacing-md);
}
/* Table of Contents */
.toc {
padding: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.toc ul {
font-size: 0.875rem;
}
/* Standard Detail Page */
.vorgabe-card .card-header {
padding: var(--spacing-md);
font-size: 0.875rem;
}
.vorgabe-card .card-body {
padding: var(--spacing-md);
}
.vorgabe-content {
font-size: 0.875rem;
}
/* Homepage */
.display-4 {
font-size: 2rem;
}
.statistics-card .h2 {
font-size: 1.5rem;
}
/* References Tree */
.tree-node-content {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.875rem;
}
.tree-children {
padding-left: var(--spacing-lg);
}
.tree-node-meta {
display: none;
}
/* Breadcrumb */
.breadcrumb {
padding: var(--spacing-sm) 0;
font-size: 0.875rem;
}
/* Footer */
.footer {
padding: var(--spacing-lg) 0;
font-size: 0.875rem;
}
}
@media (max-width: 576px) {
/* Extra small screens */
.container-fluid {
padding-left: var(--spacing-sm);
padding-right: var(--spacing-sm);
}
/* Typography */
h1 { font-size: 1.5rem; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1.125rem; }
/* Cards */
.card {
border-radius: var(--radius-md);
}
.card-header {
padding: var(--spacing-sm) var(--spacing-md);
}
.card-body {
padding: var(--spacing-sm) var(--spacing-md);
}
/* Buttons */
.btn {
width: 100%;
margin-bottom: var(--spacing-xs);
}
.btn-group .btn {
width: auto;
}
/* Forms */
.row.g-3 > .col {
margin-bottom: var(--spacing-md);
}
/* Homepage */
.display-4 {
font-size: 1.75rem;
}
.hero-section .btn-lg {
width: 100%;
margin-bottom: var(--spacing-sm);
}
.statistics-row .col-6 {
margin-bottom: var(--spacing-md);
}
/* Search */
.search-page .row {
flex-direction: column;
}
.search-page .col-lg-4 {
order: 2;
}
.search-page .col-lg-8 {
order: 1;
}
/* Standard Detail */
.standard-detail-page .row {
flex-direction: column;
}
.standard-detail-page .col-lg-8 {
order: 1;
}
.standard-detail-page .col-lg-4 {
order: 2;
}
/* References */
.references-page .row {
flex-direction: column;
}
.references-page .col-lg-8 {
order: 1;
}
.references-page .col-lg-4 {
order: 2;
}
/* Tree navigation */
.tree-node-content {
flex-wrap: wrap;
}
.tree-link {
min-width: 0;
flex: 1;
}
/* Badges */
.badge {
font-size: 0.625rem;
padding: 0.125rem 0.375rem;
}
/* Lists */
.list-group-item {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.875rem;
}
/* Tables */
.table {
font-size: 0.75rem;
}
.table th,
.table td {
padding: var(--spacing-xs) var(--spacing-sm);
}
}
/* Touch-friendly adjustments for mobile */
@media (hover: none) and (pointer: coarse) {
.card:hover {
transform: none;
}
.btn:hover {
transform: none;
}
.tree-node-content:hover {
background-color: transparent;
border-color: transparent;
}
.navbar-nav .nav-link:hover {
background-color: transparent;
}
/* Increase touch targets */
.btn {
min-height: 44px;
min-width: 44px;
}
.tree-node-content {
min-height: 44px;
}
.navbar-nav .nav-link {
min-height: 44px;
display: flex;
align-items: center;
}
}
/* Landscape mobile optimizations */
@media (max-width: 768px) and (orientation: landscape) {
.navbar {
padding: var(--spacing-xs) 0;
}
.navbar-brand {
font-size: 1.125rem;
}
h1 { font-size: 1.625rem; }
.card {
margin-bottom: var(--spacing-sm);
}
.search-form {
padding: var(--spacing-sm) var(--spacing-md);
}
}
/* High DPI displays */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.navbar-brand::before {
image-rendering: -webkit-optimize-contrast;
}
.tree-toggle {
font-weight: 300;
}
}
/* Reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.card:hover {
transform: none;
}
.btn:hover {
transform: none;
}
}
/* Loading states */
.loading {
opacity: 0.6;
pointer-events: none;
}
.spinner {
border: 2px solid var(--border-color);
border-top: 2px solid var(--primary-color);
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Print styles */
@media print {
.navbar,
.theme-toggle,
.btn,
.toc {
display: none !important;
}
body {
background: white !important;
color: black !important;
}
.card {
border: 1px solid #ccc !important;
box-shadow: none !important;
break-inside: avoid;
}
}

View File

@@ -1,175 +1,30 @@
{% extends "base.html" %}
{% block title %}Stichwort: {{ stichwort.stichwort }}{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item"><a href="/stichworte/">Stichworte</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ stichwort.stichwort }}</li>
{% endblock %}
{% block title %}Stichwort: {{stichwort.stichwort}}{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<!-- Stichwort Header -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h1 class="h3 mb-0">🏷️ {{ stichwort.stichwort }}</h1>
</div>
{% if stichwort.erklaerung %}
<div class="card-body">
<h5 class="card-title">📖 Beschreibung</h5>
<h1>{{stichwort}}</h1>
{% if stichwort.erklaerung %}
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
<h3 class="h5 m-0">Beschreibung</h3>
</div>
<div class="card-body p-2">
{% for typ, html in stichwort.erklaerung %}
{% if html %}
<div class="content-section">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
<!-- Relevante Vorgaben -->
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h3 class="h5 mb-0">📝 Relevante Vorgaben</h3>
<span class="badge bg-success">
{% for vorgabe in stichwort.vorgaben %}
{% if vorgabe.get_status == "active" %}
{% if forloop.first %}1{% else %}{{ forloop.counter }}{% endif %}
{% endif %}
{% endfor %}
Aktiv
</span>
</div>
</div>
<div class="card-body">
{% if stichwort.vorgaben %}
<div class="list-group">
{% for vorgabe in stichwort.vorgaben %}
{% if vorgabe.get_status == "active" %}
<div class="list-group-item">
<div class="d-flex align-items-start">
<span class="badge bg-secondary me-3">{{ vorgabe.Vorgabennummer }}</span>
<div class="flex-grow-1">
<h6 class="mb-1">
<a href="/dokumente/{{ vorgabe.dokument.nummer }}/#{{vorgabe.Vorgabennummer}}"
class="text-decoration-none">
{{ vorgabe.titel }}
</a>
</h6>
<small class="text-muted">
Standard: {{ vorgabe.dokument.nummer }} {{ vorgabe.dokument.name }}
</small>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<p class="text-muted mb-0">
Keine aktiven Vorgaben für dieses Stichwort gefunden.
</p>
</div>
{% endif %}
</div>
{% if html %}<div>{{ html|safe }}</div>{% endif %}{% endfor %}
</div>
</div>
{% endif %}
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Quick Actions -->
<div class="card mb-4 sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">⚡ Aktionen</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="/stichworte/" class="btn btn-outline btn-sm">
← Zurück zur Liste
</a>
<a href="/search/?q={{ stichwort.stichwort }}" class="btn btn-outline btn-sm">
🔍 Nach "{{ stichwort.stichwort }}" suchen
</a>
</div>
</div>
</div>
<!-- Statistics -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">📊 Statistiken</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
<h4 class="text-primary mb-1">
{% for vorgabe in stichwort.vorgaben %}
{% if vorgabe.get_status == "active" %}
{% if forloop.first %}1{% else %}{{ forloop.counter }}{% endif %}
{% endif %}
{% endfor %}
</h4>
<small class="text-muted">Aktive Vorgaben</small>
</div>
<div class="col-6">
<h4 class="text-info mb-1">
{{ stichwort.vorgaben|length }}
</h4>
<small class="text-muted">Gesamt</small>
</div>
</div>
</div>
</div>
<!-- Related Stichworte -->
{% if related_stichworte %}
<div class="card">
<div class="card-header">
<h5 class="mb-0">🔗 Verwandte Stichworte</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
{% for related in related_stichworte %}
<a href="/stichworte/{{ related.stichwort }}/"
class="badge bg-light text-dark text-decoration-none">
{{ related.stichwort }}
</a>
{% endfor %}
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
<h3 class="h5 m-0">Relevante Vorgaben</h3>
</div>
<div class="card-body p-2">
<ul>
{% for vorgabe in stichwort.vorgaben %}
{% if vorgabe.get_status == "active" %}
<li><a href="{% url 'standard_detail' nummer=vorgabe.dokument.nummer %}#{{vorgabe.Vorgabennummer}}">{{vorgabe.Vorgabennummer}}</a>: {{vorgabe.titel}}</li>
{% endif %}
</div>
</div>
<style>
.content-section {
line-height: 1.6;
margin-bottom: 1rem;
}
.content-section h1, .content-section h2, .content-section h3 {
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.list-group-item {
border: none;
border-bottom: 1px solid var(--border-color);
transition: all var(--transition-fast);
}
.list-group-item:hover {
background-color: var(--bg-secondary);
}
.list-group-item:last-child {
border-bottom: none;
}
.badge {
font-size: 0.75rem;
}
</style>
{% endfor %}
</ul>
</div>
{% endblock %}

View File

@@ -1,144 +1,14 @@
{% extends "base.html" %}
{% block title %}Stichworte{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Stichworte</li>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>🏷️ Stichworte</h1>
<div class="d-flex gap-2">
<span class="badge bg-primary">{{ stichworte|length }} Kategorien</span>
</div>
</div>
<h1>Stichworte</h1>
<!-- Search and Filter -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-8">
<label for="stichwort-search" class="form-label">🔍 Stichworte durchsuchen</label>
<input type="text"
class="form-control"
id="stichwort-search"
placeholder="Stichwort eingeben..."
onkeyup="filterStichworte()">
</div>
<div class="col-md-4">
<label for="letter-filter" class="form-label">🔤 Buchstabe</label>
<select class="form-select" id="letter-filter" onchange="filterStichworte()">
<option value="">Alle Buchstaben</option>
{% for Anfang, Worte in stichworte.items %}
<option value="{{ Anfang }}">{{ Anfang }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<!-- Stichworte Grid -->
<div class="row" id="stichworte-container">
{% for Anfang, Worte in stichworte.items %}
<div class="col-lg-4 col-md-6 mb-4 stichwort-category" data-letter="{{ Anfang }}">
<div class="card h-100">
<div class="card-header">
<h3 class="h5 mb-0">{{ Anfang }}</h3>
<span class="badge bg-secondary">{{ Worte|length }} Stichworte</span>
</div>
<div class="card-body">
<div class="list-group list-group-flush">
{% for Wort in Worte %}
<a href="/stichworte/{{ Wort }}/" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span>{{ Wort }}</span>
<span class="badge bg-light text-dark"></span>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- JavaScript for filtering -->
<script>
function filterStichworte() {
const searchTerm = document.getElementById('stichwort-search').value.toLowerCase();
const letterFilter = document.getElementById('letter-filter').value;
const categories = document.querySelectorAll('.stichwort-category');
categories.forEach(category => {
const letter = category.dataset.letter;
const items = category.querySelectorAll('.list-group-item');
let hasVisibleItems = false;
// Check if category matches letter filter
const matchesLetter = !letterFilter || letter === letterFilter;
// Filter items within category
items.forEach(item => {
const text = item.textContent.toLowerCase();
const matchesSearch = !searchTerm || text.includes(searchTerm);
if (matchesSearch && matchesLetter) {
item.style.display = 'flex';
hasVisibleItems = true;
} else {
item.style.display = 'none';
}
});
// Show/hide category based on visible items
category.style.display = (matchesLetter && hasVisibleItems) ? 'block' : 'none';
});
}
document.addEventListener('DOMContentLoaded', function() {
// Add hover effects
const items = document.querySelectorAll('.list-group-item');
items.forEach(item => {
item.addEventListener('mouseenter', function() {
this.style.backgroundColor = 'var(--bg-secondary)';
});
item.addEventListener('mouseleave', function() {
this.style.backgroundColor = '';
});
});
});
</script>
<style>
.list-group-item {
border: none;
border-bottom: 1px solid var(--border-color);
padding: var(--spacing-sm) var(--spacing-md);
transition: all var(--transition-fast);
}
.list-group-item:hover {
background-color: var(--bg-secondary);
transform: translateX(4px);
}
.list-group-item:last-child {
border-bottom: none;
}
.card-header {
display: flex;
justify-content: between;
align-items: center;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
@media (max-width: 768px) {
.list-group-item {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.875rem;
}
}
</style>
{% endblock %}
<h2>{{ Anfang }}</h2>
<ul>
{% for Wort in Worte %}
<li><a href="{% url 'stichwort_detail' stichwort=Wort %}">{{ Wort }}</a></li>
{% endfor %}
</ul>
{% endfor %}
{% endblock %}

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)