Compare commits

...

13 Commits

Author SHA1 Message Date
28f87509d6 Removed diagram proxy - no longer needed because of cacheing function 2025-11-05 14:46:03 +01:00
f7e6795c00 Deploy 944 2025-11-05 12:22:13 +01:00
e94f61a697 Deploy 943 2025-11-05 11:16:28 +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
b82c6fea38 Merge branch 'development' into feature/list-of-incomplete-vorgaben 2025-11-04 13:58:26 +00:00
cb374bfa77 feat: enhance incomplete Vorgaben page with table layout and admin integration
- Redesign incomplete Vorgaben page from card layout to unified table format
- Add visual status indicators (✓/✗) for each completeness category
- Link Vorgaben directly to admin edit pages (/autorenumgebung/ instead of /admin/)
- Enhance Vorgabe admin with Kurztext and Langtext inlines for complete editing
- Update all tests to work with new table structure and admin URLs
- Add JavaScript for dynamic summary count updates
- Maintain staff-only access control and responsive design

All 112 tests passing successfully.
2025-11-04 14:52:41 +01:00
2b41490806 Tests corrected, 'Thema' is now required (produces errors otherwise) 2025-11-04 14:35:55 +01:00
7186fa2cbe Deploy 942 2025-11-04 13:31:58 +01:00
17 changed files with 2143 additions and 210 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

@@ -51,7 +51,6 @@ INSTALLED_APPS = [
'mptt', 'mptt',
'pages', 'pages',
'nested_admin', 'nested_admin',
'revproxy.apps.RevProxyConfig',
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@@ -18,7 +18,6 @@ from django.contrib import admin
from django.urls import include, path, re_path from django.urls import include, path, re_path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from diagramm_proxy.views import DiagrammProxyView
import dokumente.views import dokumente.views
import pages.views import pages.views
import referenzen.views import referenzen.views
@@ -33,7 +32,6 @@ urlpatterns = [
path('stichworte/', include("stichworte.urls")), path('stichworte/', include("stichworte.urls")),
path('referenzen/', referenzen.views.tree, name="referenz_tree"), path('referenzen/', referenzen.views.tree, name="referenz_tree"),
path('referenzen/<str:refid>/', referenzen.views.detail, name="referenz_detail"), path('referenzen/<str:refid>/', referenzen.views.detail, name="referenz_detail"),
re_path(r'^diagramm/(?P<path>.*)$', DiagrammProxyView.as_view()),
] ]
# Serve static files # Serve static files

View File

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

Binary file not shown.

View File

@@ -1 +0,0 @@
# Diagram proxy module

View File

@@ -1,4 +0,0 @@
from revproxy.views import ProxyView
class DiagrammProxyView(ProxyView):
upstream = "http://svckroki:8000/"

View File

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

View File

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

View File

@@ -60,7 +60,7 @@ class Vorgabe(models.Model):
order = models.IntegerField() order = models.IntegerField()
nummer = models.IntegerField() nummer = models.IntegerField()
dokument = models.ForeignKey(Dokument, on_delete=models.CASCADE, related_name='vorgaben') dokument = models.ForeignKey(Dokument, on_delete=models.CASCADE, related_name='vorgaben')
thema = models.ForeignKey(Thema, on_delete=models.PROTECT) thema = models.ForeignKey(Thema, on_delete=models.PROTECT, blank=False)
titel = models.CharField(max_length=255) titel = models.CharField(max_length=255)
referenzen = models.ManyToManyField(Referenz, blank=True) referenzen = models.ManyToManyField(Referenz, blank=True)
gueltigkeit_von = models.DateField() gueltigkeit_von = models.DateField()
@@ -132,13 +132,13 @@ class Vorgabe(models.Model):
}) })
return conflicts return conflicts
def clean(self): def clean(self):
""" """
Validate the Vorgabe before saving. Validate the Vorgabe before saving.
""" """
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
# Check for conflicts with existing Vorgaben # Check for conflicts with existing Vorgaben
conflicts = self.find_conflicts() conflicts = self.find_conflicts()
if conflicts: if conflicts:
@@ -172,9 +172,9 @@ class Vorgabe(models.Model):
'vorgabe1': self, 'vorgabe1': self,
'vorgabe2': other_vorgabe, 'vorgabe2': other_vorgabe,
'conflict_type': 'date_range_intersection', 'conflict_type': 'date_range_intersection',
'message': f"Vorgabe {self.Vorgabennummer()} conflicts with " 'message': f"Vorgabe {self.Vorgabennummer()} in Konflikt mit "
f"existing {other_vorgabe.Vorgabennummer()} " f"bestehender {other_vorgabe.Vorgabennummer()} "
f"due to overlapping validity periods" f" - Geltungsdauer übeschneidet sich"
}) })
return conflicts return conflicts

View File

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

View File

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

View File

@@ -712,8 +712,8 @@ class VorgabeSanityCheckTest(TestCase):
with self.assertRaises(Exception) as context: with self.assertRaises(Exception) as context:
conflicting_vorgabe.clean() conflicting_vorgabe.clean()
self.assertIn('conflicts with existing', str(context.exception)) self.assertIn('Konflikt mit bestehender', str(context.exception))
self.assertIn('overlapping validity periods', str(context.exception)) self.assertIn('Geltungsdauer übeschneidet sich', str(context.exception))
def test_check_vorgabe_conflicts_utility(self): def test_check_vorgabe_conflicts_utility(self):
"""Test check_vorgabe_conflicts utility function""" """Test check_vorgabe_conflicts utility function"""
@@ -966,10 +966,13 @@ class IncompleteVorgabenTest(TestCase):
"""Test that the page contains expected content""" """Test that the page contains expected content"""
response = self.client.get(reverse('incomplete_vorgaben')) response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'Unvollständige Vorgaben') self.assertContains(response, 'Unvollständige Vorgaben')
self.assertContains(response, 'Vorgaben ohne Referenzen') self.assertContains(response, 'Referenzen')
self.assertContains(response, 'Vorgaben ohne Stichworte') self.assertContains(response, 'Stichworte')
self.assertContains(response, 'Vorgaben ohne Kurz- oder Langtext') self.assertContains(response, 'Text')
self.assertContains(response, 'Vorgaben ohne Checklistenfragen') self.assertContains(response, 'Checklistenfragen')
# Check for table structure
self.assertContains(response, '<table class="table table-striped table-hover">')
self.assertContains(response, '<th class="text-center">Referenzen</th>')
def test_no_references_list(self): def test_no_references_list(self):
"""Test that Vorgaben without references are listed""" """Test that Vorgaben without references are listed"""
@@ -996,27 +999,34 @@ class IncompleteVorgabenTest(TestCase):
self.assertNotContains(response, 'Vollständige Vorgabe') # Should not appear self.assertNotContains(response, 'Vollständige Vorgabe') # Should not appear
def test_vorgabe_links(self): def test_vorgabe_links(self):
"""Test that Vorgaben link to their detail pages""" """Test that Vorgaben link to their admin pages"""
response = self.client.get(reverse('incomplete_vorgaben')) response = self.client.get(reverse('incomplete_vorgaben'))
# Should contain links to Vorgabe detail pages # Should contain links to Vorgabe admin pages
self.assertContains(response, f'href="/dokumente/{self.dokument.nummer}/#TEST-001.T.2"') self.assertContains(response, 'href="/autorenumgebung/dokumente/vorgabe/2/change/"')
self.assertContains(response, f'href="/dokumente/{self.dokument.nummer}/#TEST-001.T.3"') self.assertContains(response, 'href="/autorenumgebung/dokumente/vorgabe/3/change/"')
self.assertContains(response, f'href="/dokumente/{self.dokument.nummer}/#TEST-001.T.4"') self.assertContains(response, 'href="/autorenumgebung/dokumente/vorgabe/4/change/"')
self.assertContains(response, f'href="/dokumente/{self.dokument.nummer}/#TEST-001.T.5"') self.assertContains(response, 'href="/autorenumgebung/dokumente/vorgabe/5/change/"')
def test_badge_counts(self): def test_badge_counts(self):
"""Test that badge counts are correct""" """Test that badge counts are correct"""
response = self.client.get(reverse('incomplete_vorgaben')) response = self.client.get(reverse('incomplete_vorgaben'))
# Each category should have exactly 1 Vorgabe # Check that JavaScript updates the counts correctly
self.assertContains(response, '<span class="badge bg-secondary float-end">1</span>', count=4) self.assertContains(response, 'id="no-references-count"')
self.assertContains(response, 'id="no-stichworte-count"')
self.assertContains(response, 'id="no-text-count"')
self.assertContains(response, 'id="no-checklistenfragen-count"')
# Check total count
self.assertContains(response, 'Gesamt: 4 unvollständige Vorgaben')
def test_summary_section(self): def test_summary_section(self):
"""Test that summary section shows correct counts""" """Test that summary section shows correct counts"""
response = self.client.get(reverse('incomplete_vorgaben')) response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'Zusammenfassung') self.assertContains(response, 'Zusammenfassung')
self.assertContains(response, '<h4 class="text-warning">1</h4>', count=2) # No refs, no stichworte self.assertContains(response, 'Ohne Referenzen')
self.assertContains(response, '<h4 class="text-danger">1</h4>') # No text self.assertContains(response, 'Ohne Stichworte')
self.assertContains(response, '<h4 class="text-info">1</h4>') # No checklistenfragen self.assertContains(response, 'Ohne Text')
self.assertContains(response, 'Ohne Checklistenfragen')
self.assertContains(response, 'Gesamt: 4 unvollständige Vorgaben')
def test_empty_lists_message(self): def test_empty_lists_message(self):
"""Test that appropriate messages are shown when lists are empty""" """Test that appropriate messages are shown when lists are empty"""
@@ -1024,10 +1034,8 @@ class IncompleteVorgabenTest(TestCase):
Vorgabe.objects.exclude(pk=self.complete_vorgabe.pk).delete() Vorgabe.objects.exclude(pk=self.complete_vorgabe.pk).delete()
response = self.client.get(reverse('incomplete_vorgaben')) response = self.client.get(reverse('incomplete_vorgaben'))
self.assertContains(response, 'Alle Vorgaben haben Referenzen.') self.assertContains(response, 'Alle Vorgaben sind vollständig!')
self.assertContains(response, 'Alle Vorgaben haben Stichworte.') self.assertContains(response, 'Alle Vorgaben haben Referenzen, Stichworte, Text und Checklistenfragen.')
self.assertContains(response, 'Alle Vorgaben haben Kurz- oder Langtext.')
self.assertContains(response, 'Alle Vorgaben haben Checklistenfragen.')
def test_back_link(self): def test_back_link(self):
"""Test that back link to standard list exists""" """Test that back link to standard list exists"""

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
@@ -64,36 +67,173 @@ def is_staff_user(user):
@user_passes_test(is_staff_user) @user_passes_test(is_staff_user)
def incomplete_vorgaben(request): def incomplete_vorgaben(request):
""" """
Show lists of incomplete Vorgaben: Show table of all Vorgaben with completeness status:
1. Ones with no references - References (✓ or ✗)
2. Ones with no Stichworte - Stichworte (✓ or ✗)
3. Ones without Kurz- or Langtext - Text (✓ or ✗)
4. Ones without Checklistenfragen - Checklistenfragen (✓ or ✗)
""" """
# Get all active Vorgaben # Get all active Vorgaben
all_vorgaben = Vorgabe.objects.all().select_related('dokument', 'thema') all_vorgaben = Vorgabe.objects.all().select_related('dokument', 'thema').prefetch_related(
'referenzen', 'stichworte', 'checklistenfragen', 'vorgabekurztext_set', 'vorgabelangtext_set'
)
# 1. Vorgaben with no references # Build table data
no_references = [v for v in all_vorgaben if not v.referenzen.exists()] vorgaben_data = []
# 2. Vorgaben with no Stichworte
no_stichworte = [v for v in all_vorgaben if not v.stichworte.exists()]
# 3. Vorgaben without Kurz- or Langtext
no_text = []
for vorgabe in all_vorgaben: for vorgabe in all_vorgaben:
has_kurztext = VorgabeKurztext.objects.filter(abschnitt=vorgabe).exists() has_references = vorgabe.referenzen.exists()
has_langtext = VorgabeLangtext.objects.filter(abschnitt=vorgabe).exists() has_stichworte = vorgabe.stichworte.exists()
has_kurztext = vorgabe.vorgabekurztext_set.exists()
if not has_kurztext and not has_langtext: has_langtext = vorgabe.vorgabelangtext_set.exists()
no_text.append(vorgabe) has_text = has_kurztext or has_langtext
has_checklistenfragen = vorgabe.checklistenfragen.exists()
# Only include Vorgaben that are incomplete in at least one way
if not (has_references and has_stichworte and has_text and has_checklistenfragen):
vorgaben_data.append({
'vorgabe': vorgabe,
'has_references': has_references,
'has_stichworte': has_stichworte,
'has_text': has_text,
'has_checklistenfragen': has_checklistenfragen,
'is_complete': has_references and has_stichworte and has_text and has_checklistenfragen
})
# 4. Vorgaben without Checklistenfragen # Sort by document number and Vorgabe number
no_checklistenfragen = [v for v in all_vorgaben if not v.checklistenfragen.exists()] vorgaben_data.sort(key=lambda x: (x['vorgabe'].dokument.nummer, x['vorgabe'].Vorgabennummer()))
return render(request, 'standards/incomplete_vorgaben.html', { return render(request, 'standards/incomplete_vorgaben.html', {
'no_references': no_references, 'vorgaben_data': vorgaben_data,
'no_stichworte': no_stichworte,
'no_text': no_text,
'no_checklistenfragen': no_checklistenfragen,
}) })
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

@@ -31,6 +31,6 @@
<div class="flex-fill">{% block content %}Main Content{% endblock %}</div> <div class="flex-fill">{% block content %}Main Content{% endblock %}</div>
<div class="col-md-2">{% block sidebar_right %}{% endblock %}</div> <div class="col-md-2">{% block sidebar_right %}{% endblock %}</div>
</div> </div>
<div>VorgabenUI v0.941</div> <div>VorgabenUI v0.945</div>
</body> </body>
</html> </html>