Compare commits

..

7 Commits

Author SHA1 Message Date
32917113f2 Design suggestion from AI. Not very useable 2025-11-05 10:16:56 +01:00
0cd09d0878 .env ignored 2025-11-04 17:01:49 +01:00
994ba5d797 Merge pull request 'feature/json' (#6) from feature/json into development
Reviewed-on: #6
2025-11-04 15:58:34 +00:00
af636fe6ea JSON functionality extended to website. Tests pending. 2025-11-04 16:07:29 +01:00
3ccb32e8e1 feat: add comprehensive JSON export command for dokumente
- Add Django management command 'export_json' for exporting all dokumente data
- Implement structured JSON format with proper section types from database
- Include all document fields: gueltigkeit, signatur_cso, anhaenge, changelog
- Support Kurztext, Geltungsbereich, Einleitung with Langtext-style structure
- Use actual abschnitttyp values instead of hardcoded 'text'
- Handle Referenz model fields correctly (name_nummer, name_text)
- Support --output parameter for file export or stdout by default
2025-11-04 15:56:54 +01:00
af4e1c61aa Added early JSON file for reference 2025-11-04 14:45:00 +00:00
8153aa56ce Merge pull request 'feat: enhance incomplete Vorgaben page with table layout and admin integration' (#4) from feature/list-of-incomplete-vorgaben into development
Reviewed-on: #4
2025-11-04 13:58:40 +00:00
19 changed files with 5168 additions and 269 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ package-lock.json
package.json package.json
# Diagram cache directory # Diagram cache directory
media/diagram_cache/ media/diagram_cache/
.env

1599
R0066.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -26,8 +26,8 @@ import referenzen.views
admin.site.site_header="Autorenumgebung" admin.site.site_header="Autorenumgebung"
urlpatterns = [ urlpatterns = [
path('',pages.views.startseite), path('',pages.views.startseite, name='startseite'),
path('search/',pages.views.search), path('search/',pages.views.search, name='search'),
path('dokumente/', include("dokumente.urls")), path('dokumente/', include("dokumente.urls")),
path('autorenumgebung/', admin.site.urls), path('autorenumgebung/', admin.site.urls),
path('stichworte/', include("stichworte.urls")), path('stichworte/', include("stichworte.urls")),

View File

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

View File

@@ -1,60 +1,170 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ standard }}{% endblock %} {% 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 content %} {% block content %}
<h1>{{ standard.nummer }} {{ standard.name }}</h1> <div class="row">
{% if standard.history == True %} <!-- Main Content -->
<h2>Version vom {{ standard.check_date }}</h2> <div class="col-lg-8">
{% endif %} <!-- Standard Header -->
<!-- Autoren, Prüfende etc. --> <div class="card mb-4">
<p><strong>Autoren:</strong> {{ standard.autoren.all|join:", " }}</p> <div class="card-header bg-primary text-white">
<p><strong>Prüfende:</strong> {{ standard.pruefende.all|join:", " }}</p> <div class="d-flex justify-content-between align-items-start">
<p><strong>Gültigkeit:</strong> {{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</p> <div>
<h1 class="h2 mb-2">{{ standard.nummer }} {{ standard.name }}</h1>
<!-- Start Einleitung --> {% if standard.history == True %}
{% if standard.einleitung_html %} <p class="mb-0 opacity-75">Version vom {{ standard.check_date|date:"d.m.Y" }}</p>
<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 %} {% endif %}
<h3 class="h5 m-0">{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }} </div>
{% if vorgabe.long_status != "active" and standard.history == True %}<span class="text-danger"> ({{ vorgabe.long_status}})</span>{% endif %} <div class="d-flex gap-2">
</h3> {% if not standard.aktiv %}
{% if vorgabe.relevanzset %} <span class="badge bg-danger">Inaktiv</span>
<span class="badge bg-light text-black"> Relevanz: {% else %}
{{ vorgabe.relevanzset|join:", " }} <span class="badge bg-success">Aktiv</span>
</span>
{% endif %} {% endif %}
</div>
<span class="badge bg-light text-black">{{ vorgabe.thema }}</span> </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>
<div class="card-body p-0"> <div class="d-flex gap-2">
<!-- Start Kurztext --> <a href="{% url 'standard_json' standard.nummer %}" class="btn btn-outline btn-sm" download="{{ standard.nummer }}.json">
{% comment %} KURZTEXT BLOCK {% endcomment %} 📄 JSON herunterladen
</a>
<button class="btn btn-outline btn-sm" onclick="window.print()">
🖨️ Drucken
</button>
</div>
</div>
</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 %} {% if vorgabe.kurztext_html.0.1 %}
<div class="p-3 mb-3 bg-light border-3" style="width: 100%;"> <div class="alert alert-info mb-3">
<h6 class="alert-heading">📌 Kurztext</h6>
{% for typ, html in vorgabe.kurztext_html %} {% for typ, html in vorgabe.kurztext_html %}
{% if html %} {% if html %}
<div class="mb-2">{{ html|safe }}</div> <div class="mb-2">{{ html|safe }}</div>
@@ -62,48 +172,232 @@
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
<!-- Langtext --> <!-- Langtext -->
<div class="p-3 mb-3"> <div class="mb-4">
{% comment %} LANGTEXT BLOCK {% endcomment %}
{# <h5>Langtext</h5> #}
{% for typ, html in vorgabe.langtext_html %} {% for typ, html in vorgabe.langtext_html %}
{% if html %}<div class="mb-3">{{ html|safe }}</div>{% endif %} {% if html %}
<div class="content-section mb-3">{{ html|safe }}</div>
{% endif %}
{% endfor %} {% endfor %}
<!-- Checklistenfragen --> </div>
{% comment %} CHECKLISTENFRAGEN BLOCK {% endcomment %}
<h5>Checklistenfragen</h5> <!-- Checklistenfragen -->
<div class="mb-4">
<h6 class="mb-3">✅ Checklistenfragen</h6>
{% if vorgabe.checklistenfragen.all %} {% if vorgabe.checklistenfragen.all %}
<ul class="list-group"> <div class="list-group">
{% for frage in vorgabe.checklistenfragen.all %} {% for frage in vorgabe.checklistenfragen.all %}
<li class="list-group-item">{{ frage.frage }}</li> <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 %} {% endfor %}
</ul> </div>
{% else %} {% else %}
<p><em>Keine Checklistenfragen</em></p> <p class="text-muted"><em>Keine Checklistenfragen vorhanden</em></p>
{% endif %} {% endif %}
{% comment %} STICHWORTE + REFERENZEN AT BOTTOM {% endcomment %} </div>
<div class="mt-4 small text-muted">
<strong>Stichworte:</strong> <!-- 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 %} {% if vorgabe.stichworte.all %}
<div>
{% for s in vorgabe.stichworte.all %} {% for s in vorgabe.stichworte.all %}
<a href="{% url 'stichwort_detail' stichwort=s %}">{{ s }}</a>{% if not forloop.last %}, {% endif %} <a href="{% url 'stichwort_detail' stichwort=s %}" class="badge bg-light text-dark text-decoration-none me-1 mb-1">
{{ s }}
</a>
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<em>Keine</em> <p class="text-muted small mb-0">Keine Stichworte</p>
{% endif %} {% endif %}
<br> </div>
<strong>Referenzen:</strong> <div class="col-md-6">
<h6 class="text-muted mb-2">🔗 Referenzen</h6>
{% if vorgabe.referenzpfade %} {% if vorgabe.referenzpfade %}
<div class="small">
{% for ref in vorgabe.referenzpfade %} {% for ref in vorgabe.referenzpfade %}
{{ ref|safe }}{% if not forloop.last %}, {% endif %} <div class="mb-1">{{ ref|safe }}</div>
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<em>Keine</em> <p class="text-muted small mb-0">Keine Referenzen</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endif %} {% endif %}
{% endfor %} {% 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>
</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>
{% endblock %} {% endblock %}

View File

@@ -1,13 +1,201 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block title %}Standards Informatiksicherheit{% endblock %}
<h1>Standards Informatiksicherheit</h1> {% block breadcrumb_items %}
<ul> <li class="breadcrumb-item active" aria-current="page">Standards</li>
{% for dokument in dokumente %} {% endblock %}
<li>
<a href="{% url 'standard_detail' nummer=dokument.nummer %}"> {% block content %}
{{ dokument.nummer }} {{ dokument.name }} <div class="d-flex justify-content-between align-items-center mb-4">
</a> <h1>Standards Informatiksicherheit</h1>
</li> <div class="d-flex gap-2">
{% endfor %} <span class="badge bg-primary">{{ dokumente|length }} Standards</span>
</ul> </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">
{% 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>
{% 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>
{% endblock %} {% endblock %}

View File

@@ -7,6 +7,7 @@ urlpatterns = [
path('<str:nummer>/', views.standard_detail, name='standard_detail'), path('<str:nummer>/', views.standard_detail, name='standard_detail'),
path('<str:nummer>/history/<str:check_date>/', views.standard_detail), path('<str:nummer>/history/<str:check_date>/', views.standard_detail),
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'), path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste') path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
path('<str:nummer>/json/', views.standard_json, name='standard_json')
] ]

View File

@@ -1,5 +1,8 @@
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import JsonResponse
from django.core.serializers.json import DjangoJSONEncoder
import json
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage
from abschnitte.utils import render_textabschnitte from abschnitte.utils import render_textabschnitte
@@ -102,3 +105,135 @@ def incomplete_vorgaben(request):
return render(request, 'standards/incomplete_vorgaben.html', { return render(request, 'standards/incomplete_vorgaben.html', {
'vorgaben_data': vorgaben_data, 'vorgaben_data': vorgaben_data,
}) })
def standard_json(request, nummer):
"""
Export a single Dokument as JSON
"""
# Get the document with all related data
dokument = get_object_or_404(
Dokument.objects.prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
'einleitung_set', 'changelog__autoren'
),
nummer=nummer
)
# Build document structure (reusing logic from export_json command)
doc_data = {
"Typ": dokument.dokumententyp.name if dokument.dokumententyp else "",
"Nummer": dokument.nummer,
"Name": dokument.name,
"Autoren": [autor.name for autor in dokument.autoren.all()],
"Pruefende": [pruefender.name for pruefender in dokument.pruefende.all()],
"Gueltigkeit": {
"Von": dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else "",
"Bis": dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
},
"SignaturCSO": dokument.signatur_cso,
"Geltungsbereich": {},
"Einleitung": {},
"Ziel": "",
"Grundlagen": "",
"Changelog": [],
"Anhänge": dokument.anhaenge,
"Verantwortlich": "Information Security Management BIT",
"Klassifizierung": None,
"Glossar": {},
"Vorgaben": []
}
# Process Geltungsbereich sections
geltungsbereich_sections = []
for gb in dokument.geltungsbereich_set.all().order_by('order'):
geltungsbereich_sections.append({
"typ": gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text",
"inhalt": gb.inhalt
})
if geltungsbereich_sections:
doc_data["Geltungsbereich"] = {
"Abschnitt": geltungsbereich_sections
}
# Process Einleitung sections
einleitung_sections = []
for ei in dokument.einleitung_set.all().order_by('order'):
einleitung_sections.append({
"typ": ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text",
"inhalt": ei.inhalt
})
if einleitung_sections:
doc_data["Einleitung"] = {
"Abschnitt": einleitung_sections
}
# Process Changelog entries
changelog_entries = []
for cl in dokument.changelog.all().order_by('-datum'):
changelog_entries.append({
"Datum": cl.datum.strftime("%Y-%m-%d"),
"Autoren": [autor.name for autor in cl.autoren.all()],
"Aenderung": cl.aenderung
})
doc_data["Changelog"] = changelog_entries
# Process Vorgaben for this document
vorgaben = dokument.vorgaben.all().order_by('order')
for vorgabe in vorgaben:
# Get Kurztext and Langtext sections
kurztext_sections = []
for kt in vorgabe.vorgabekurztext_set.all().order_by('order'):
kurztext_sections.append({
"typ": kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text",
"inhalt": kt.inhalt
})
langtext_sections = []
for lt in vorgabe.vorgabelangtext_set.all().order_by('order'):
langtext_sections.append({
"typ": lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text",
"inhalt": lt.inhalt
})
# Build text structures following Langtext pattern
kurztext = {
"Abschnitt": kurztext_sections if kurztext_sections else []
} if kurztext_sections else {}
langtext = {
"Abschnitt": langtext_sections if langtext_sections else []
} if langtext_sections else {}
# Get references and keywords
referenzen = [f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer for ref in vorgabe.referenzen.all()]
stichworte = [stw.stichwort for stw in vorgabe.stichworte.all()]
# Get checklist questions
checklistenfragen = [cf.frage for cf in vorgabe.checklistenfragen.all()]
vorgabe_data = {
"Nummer": str(vorgabe.nummer),
"Titel": vorgabe.titel,
"Thema": vorgabe.thema.name if vorgabe.thema else "",
"Kurztext": kurztext,
"Langtext": langtext,
"Referenz": referenzen,
"Gueltigkeit": {
"Von": vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else "",
"Bis": vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
},
"Checklistenfragen": checklistenfragen,
"Stichworte": stichworte
}
doc_data["Vorgaben"].append(vorgabe_data)
# Return JSON response
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)

View File

@@ -1,36 +1,160 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="de" data-theme="light">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>{% block title %}{% endblock %}</title> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Vorgaben Informatiksicherheit BIT{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <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 %} {% load static %}
<link rel="stylesheet" href="{% static 'custom/css/vorgaben-ui.css' %}">
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg navbar-light bg-light"> <!-- Enhanced Navigation -->
<a class="navbar-brand" href="#">Vorgaben</a> <nav class="navbar navbar-expand-lg sticky-top">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation"> <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> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav"> <div class="collapse navbar-collapse" id="navbarNav">
<a class="nav-item nav-link active" href="/dokumente">Standards</a> <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 %} {% if user.is_staff %}
<a class="nav-item nav-link" href="/dokumente/unvollstaendig/">Unvollständig</a> <li class="nav-item">
<a class="nav-link" href="{% url 'incomplete_standards' %}">Unvollständig</a>
</li>
{% endif %} {% endif %}
<a class="nav-item nav-link" href="/referenzen">Referenzen</a> <li class="nav-item">
<a class="nav-item nav-link" href="/stichworte">Stichworte</a> <a class="nav-link" href="{% url 'referenz_tree' %}">Referenzen</a>
<a class="nav-item nav-link" href="/search">Suche</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>
</div> </div>
</div> </div>
</nav> </nav>
<div class="d-flex">
<div class="col-md-2">{% block sidebar_left %}{% endblock %}</div> <!-- Breadcrumb -->
<div class="flex-fill">{% block content %}Main Content{% endblock %}</div> {% block breadcrumb %}
<div class="col-md-2">{% block sidebar_right %}{% endblock %}</div> {% 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> </div>
<div>VorgabenUI v0.942</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>
</body> </body>
</html> </html>

View File

@@ -1,30 +1,160 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load page_extras %} {% load page_extras %}
{% block content %} {% block title %}Suchresultate für {{ suchbegriff }}{% endblock %}
<h1 class="mb-4">Suchresultate für {{ suchbegriff }}</h1> {% block breadcrumb_items %}
{% if resultat.geltungsbereich %} <li class="breadcrumb-item"><a href="/search/">Suche</a></li>
<h2>Standards mit "{{suchbegriff}}" im Geltungsbereich</h2> <li class="breadcrumb-item active" aria-current="page">Resultate</li>
{% for standard,geltungsbereich in resultat.geltungsbereich.items %} {% endblock %}
<h4>{{ standard }}</h4>
{% for typ, html in geltungsbereich %} {% block content %}
<div>{{ html|highlighttext:suchbegriff|safe }}</div> <div class="row">
{% endfor %} <div class="col-lg-4">
{% endfor %} <!-- Search Form -->
{% endif %} <div class="card search-form sticky-top" style="top: 1rem;">
<div class="card-header">
{% if resultat.all %} <h5 class="mb-0">🔍 Suche</h5>
<h2>Vorgaben mit "{{ suchbegriff }}"</h2> </div>
{% for standard, vorgaben in resultat.all.items %} <div class="card-body">
<h4>{{ standard }}</h4> <form action="/search/" method="post" id="search-form">
<ul> {% csrf_token %}
{% for vorgabe in vorgaben %}
<li><a href="{% url 'standard_detail' nummer=vorgabe.dokument.nummer %}#{{vorgabe.Vorgabennummer}}">{{vorgabe}}</a></li> <!-- Main Search Field -->
{% endfor %} <div class="mb-3">
</ul> <label for="query" class="form-label">Suchbegriff</label>
{% endfor %} <div class="input-group">
{% endif %} <input type="text"
class="form-control"
{% if not resultat.all %} id="query"
<h2>Keine Resultate für "{{suchbegriff}}"</h2> name="q"
{% endif %} placeholder="Suchbegriff eingeben …"
value="{{ suchbegriff|default:'' }}"
required
maxlength="200">
<button class="btn btn-primary" type="submit">
Suchen
</button>
</div>
</div>
<!-- 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">
{% 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>
{% endfor %}
</div>
{% 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>
{% endblock %} {% endblock %}

View File

@@ -1,19 +1,25 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Suche{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Suche</li>
{% endblock %}
{% block content %} {% block content %}
<h1 class="mb-4">Suche</h1> <div class="row">
<div class="col-lg-4">
{% if error_message %} <!-- Search Form -->
<div class="alert alert-danger"> <div class="card search-form sticky-top" style="top: 1rem;">
<strong>Fehler:</strong> {{ error_message }} <div class="card-header">
<h5 class="mb-0">🔍 Suche</h5>
</div> </div>
{% endif %} <div class="card-body">
<form action="/search/" method="post" id="search-form">
<!-- Search form -->
<form action="." method="post">
{% csrf_token %} {% csrf_token %}
<!-- Search field -->
<!-- Main Search Field -->
<div class="mb-3"> <div class="mb-3">
<label for="query" class="form-label">Suchbegriff</label> <label for="query" class="form-label">Suchbegriff</label>
<div class="input-group">
<input type="text" <input type="text"
class="form-control" class="form-control"
id="query" id="query"
@@ -22,7 +28,112 @@
value="{{ search_term|default:'' }}" value="{{ search_term|default:'' }}"
required required
maxlength="200"> maxlength="200">
<button class="btn btn-primary" type="submit">
Suchen
</button>
</div> </div>
<button type="submit" class="btn btn-primary">Suchen</button> </div>
<button class="btn btn-outline btn-sm w-100" onclick="history.back()">
← Zurück zur Suche
</button>
</form> </form>
{% endblock %} </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

@@ -0,0 +1,139 @@
{% 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,10 +1,335 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Startseite - Vorgaben Informatiksicherheit BIT{% endblock %}
{% block content %} {% block content %}
<h1>Vorgaben Informatiksicherheit BIT</h1> <!-- Hero Section -->
<h2>Aktuell erfasste Standards</h2> <div class="card bg-primary text-white mb-5">
<ul> <div class="card-body text-center py-5">
{% for standard in dokumente %} <h1 class="display-4 mb-3">🔒 Vorgaben Informatiksicherheit BIT</h1>
<li><a href="{% url 'standard_detail' nummer=standard.nummer %}">{{ standard }}</a></li> <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 %} {% endfor %}
</ul> </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>
{% endblock %} {% endblock %}

View File

@@ -10,7 +10,7 @@ import datetime
def startseite(request): def startseite(request):
standards=list(Dokument.objects.filter(aktiv=True)) 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): def validate_search_input(search_term):
""" """

View File

@@ -1,51 +1,190 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load mptt_tags %} {% 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 %} {% block content %}
<h1><a href="../{{ referenz.ParentID }}"></a>{{ referenz.Path }}</h1> <div class="row">
{% if referenz.erklaerung %} <div class="col-lg-8">
<!-- Referenz Header -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light"> <div class="card-header bg-primary text-white">
<h3 class="h5 m-0">Beschreibung</h3> <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 %} {% if referenz.url %}
<span class="badge bg-light text-black"> <a href="{{ referenz.url }}" class="btn btn-light btn-sm" target="_blank">
<a href="{{ referenz.url }}">Link</a> 🔗 Externer Link
</span>{% endif %} </a>
{% endif %}
</div>
</div> </div>
<div class="card-body p-2"> {% if referenz.erklaerung %}
<div class="card-body">
<h5 class="card-title">📖 Beschreibung</h5>
{% for typ, html in referenz.erklaerung %} {% for typ, html in referenz.erklaerung %}
{% if html %}<div>{{ html|safe }}</div>{% endif %}{% endfor %} {% if html %}
<div class="content-section">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div> </div>
</div> {% endif %}
{% 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> </div>
<div class="card-body p-2"> <!-- 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 %} {% recursetree referenz.children %}
{% if not node == referenz %} {% if not node == referenz %}
{#<a href="../{{node.id}}">#} <div class="mb-3 p-3 border rounded">
{{ node.Path }} <h6 class="mb-2">{{ node.Path }}</h6>
{#</a>#}
{% else %}
{{ node.Path }}
{% endif %}
<br>
{% if node.referenziertvon %} {% if node.referenziertvon %}
<ul> <div class="ms-3">
<small class="text-muted">Referenziert von:</small>
<ul class="list-unstyled mb-0">
{% for ref in node.referenziertvon %} {% for ref in node.referenziertvon %}
<li><a href="{% url 'standard_detail' nummer=ref.dokument.nummer %}#{{ref.Vorgabennummer}}">{{ref}}</a></li> <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 %} {% endfor %}
</ul> </ul>
<br> </div>
{% endif %} {% 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">
{% 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 %}
{% if not node.is_leaf_node %} {% if not node.is_leaf_node %}
<div class="ms-3">
{{ children }} {{ children }}
</div>
{% endif %} {% endif %}
{% endrecursetree %} {% endrecursetree %}
</div> </div>
</div> </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 %} {% endblock %}

View File

@@ -1,21 +1,466 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Referenzen{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Referenzen</li>
{% endblock %}
{% block content %} {% block content %}
<h1>Referenzen</h1> <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>
<div> <!-- Search and Filter -->
{% load mptt_tags %} <div class="card mb-4">
<ul class="tree"> <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 %} {% recursetree referenzen %}
<li> <li class="tree-node" data-node-id="{{ node.id }}" data-node-text="{{ node.name_text|default:'' }} {{ node.name_nummer|default:'' }}">
<a href="{{node.id}}">{{ node.name_nummer }}{% if node.name_text %} ({{node.name_text}}){% endif %}</a> <div class="tree-node-content" onclick="toggleNode(this)">
{% if not node.is_leaf_node %} {% if not node.is_leaf_node %}
<ul class="children"> <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">
{{ children }} {{ children }}
</ul> </ul>
{% endif %} {% endif %}
</li> </li>
{% endrecursetree %} {% endrecursetree %}
</ul> </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>
</div> </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 %} {% endblock %}

View File

@@ -0,0 +1,819 @@
/* 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,30 +1,175 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Stichwort: {{stichwort.stichwort}}{% endblock %} {% block title %}Stichwort: {{ stichwort.stichwort }}{% endblock %}
{% block content %} {% block breadcrumb_items %}
<h1>{{stichwort}}</h1> <li class="breadcrumb-item"><a href="/stichworte/">Stichworte</a></li>
{% if stichwort.erklaerung %} <li class="breadcrumb-item active" aria-current="page">{{ stichwort.stichwort }}</li>
<div class="card mb-4"> {% endblock %}
<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>{{ html|safe }}</div>{% endif %}{% endfor %}
</div>
</div>
{% endif %}
<div class="card mb-4"> {% block content %}
<div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light"> <div class="row">
<h3 class="h5 m-0">Relevante Vorgaben</h3> <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> </div>
<div class="card-body p-2">
<ul> {% if stichwort.erklaerung %}
{% for vorgabe in stichwort.vorgaben %} <div class="card-body">
{% if vorgabe.get_status == "active" %} <h5 class="card-title">📖 Beschreibung</h5>
<li><a href="{% url 'standard_detail' nummer=vorgabe.dokument.nummer %}#{{vorgabe.Vorgabennummer}}">{{vorgabe.Vorgabennummer}}</a>: {{vorgabe.titel}}</li> {% for typ, html in stichwort.erklaerung %}
{% if html %}
<div class="content-section">{{ html|safe }}</div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul>
</div> </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>
</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">
<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>
{% 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>
{% endblock %} {% endblock %}

View File

@@ -1,14 +1,144 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Stichworte{% endblock %} {% block title %}Stichworte{% endblock %}
{% block content %} {% block breadcrumb_items %}
<h1>Stichworte</h1> <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>
<!-- 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 %} {% for Anfang, Worte in stichworte.items %}
<h2>{{ Anfang }}</h2> <option value="{{ Anfang }}">{{ Anfang }}</option>
<ul> {% 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 %} {% for Wort in Worte %}
<li><a href="{% url 'stichwort_detail' stichwort=Wort %}">{{ Wort }}</a></li> <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 %} {% endfor %}
</ul> </div>
</div>
</div>
</div>
{% endfor %} {% endfor %}
{% endblock %} </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 %}