Files
vgui-cicd/dokumente/views.py
Adrian A. Baumann 0d7e63d3a2
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 16s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
Changed "Historische Version..." to "Zukünftige Version..." when appropriate
2025-12-08 15:34:14 +01:00

425 lines
16 KiB
Python

from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import JsonResponse
from django.core.serializers.json import DjangoJSONEncoder
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from django.utils.html import escape, mark_safe
from django.utils.safestring import SafeString
import json
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
from abschnitte.utils import render_textabschnitte
from datetime import date
import parsedatetime
calendar=parsedatetime.Calendar()
def standard_list(request):
dokumente = Dokument.objects.all()
return render(request, 'standards/standard_list.html',
{'dokumente': dokumente}
)
def standard_detail(request, nummer,check_date=""):
standard = get_object_or_404(Dokument, nummer=nummer)
if check_date:
check_date = calendar.parseDT(check_date)[0].date()
standard.history = True
standard.is_future = check_date > date.today()
else:
check_date = date.today()
standard.history = False
standard.is_future = False
standard.check_date=check_date
vorgaben = list(standard.vorgaben.order_by("thema","nummer").select_related("thema","dokument")) # convert queryset to list so we can attach attributes
standard.geltungsbereich_html = render_textabschnitte(standard.geltungsbereich_set.order_by("order").select_related("abschnitttyp"))
standard.einleitung_html=render_textabschnitte(standard.einleitung_set.order_by("order"))
for vorgabe in vorgaben:
# Prepare Kurztext HTML
vorgabe.kurztext_html = render_textabschnitte(vorgabe.vorgabekurztext_set.order_by("order").select_related("abschnitttyp","abschnitt"))
vorgabe.langtext_html = render_textabschnitte(vorgabe.vorgabelangtext_set.order_by("order").select_related("abschnitttyp","abschnitt"))
vorgabe.long_status=vorgabe.get_status(check_date,verbose=True)
vorgabe.relevanzset=list(vorgabe.relevanz.all())
referenz_items = []
for r in vorgabe.referenzen.all():
referenz_items.append(r.Path())
vorgabe.referenzpfade = referenz_items
# Add comment count
if request.user.is_authenticated:
if request.user.is_staff:
vorgabe.comment_count = vorgabe.comments.count()
else:
vorgabe.comment_count = vorgabe.comments.filter(user=request.user).count()
else:
vorgabe.comment_count = 0
return render(request, 'standards/standard_detail.html', {
'standard': standard,
'vorgaben': vorgaben,
})
def standard_checkliste(request, nummer):
standard = get_object_or_404(Dokument, nummer=nummer)
vorgaben = list(standard.vorgaben.all())
return render(request, 'standards/standard_checkliste.html', {
'standard': standard,
'vorgaben': vorgaben,
})
def is_staff_user(user):
return user.is_staff
@login_required
@user_passes_test(is_staff_user)
def incomplete_vorgaben(request):
"""
Show table of all Vorgaben with completeness status:
- References (✓ or ✗)
- Stichworte (✓ or ✗)
- Text (✓ or ✗)
- Checklistenfragen (✓ or ✗)
"""
# Get all active Vorgaben
all_vorgaben = Vorgabe.objects.all().select_related('dokument', 'thema').prefetch_related(
'referenzen', 'stichworte', 'checklistenfragen', 'vorgabekurztext_set', 'vorgabelangtext_set'
)
# Build table data
vorgaben_data = []
for vorgabe in all_vorgaben:
has_references = vorgabe.referenzen.exists()
has_stichworte = vorgabe.stichworte.exists()
has_kurztext = vorgabe.vorgabekurztext_set.exists()
has_langtext = vorgabe.vorgabelangtext_set.exists()
has_text = has_kurztext or has_langtext
has_checklistenfragen = vorgabe.checklistenfragen.exists()
# Only include Vorgaben that are incomplete in at least one way
if not (has_references and has_stichworte and has_text and has_checklistenfragen):
vorgaben_data.append({
'vorgabe': vorgabe,
'has_references': has_references,
'has_stichworte': has_stichworte,
'has_text': has_text,
'has_checklistenfragen': has_checklistenfragen,
'is_complete': has_references and has_stichworte and has_text and has_checklistenfragen
})
# Sort by document number and Vorgabe number
vorgaben_data.sort(key=lambda x: (x['vorgabe'].dokument.nummer, x['vorgabe'].Vorgabennummer()))
return render(request, 'standards/incomplete_vorgaben.html', {
'vorgaben_data': vorgaben_data,
})
def standard_json(request, nummer):
"""
Export a single Dokument as JSON
"""
# Get the document with all related data
dokument = get_object_or_404(
Dokument.objects.prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
'einleitung_set', 'changelog__autoren'
),
nummer=nummer
)
# Build document structure (reusing logic from export_json command)
doc_data = {
"Typ": dokument.dokumententyp.name if dokument.dokumententyp else "",
"Nummer": dokument.nummer,
"Name": dokument.name,
"Autoren": [autor.name for autor in dokument.autoren.all()],
"Pruefende": [pruefender.name for pruefender in dokument.pruefende.all()],
"Gueltigkeit": {
"Von": dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else "",
"Bis": dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
},
"SignaturCSO": dokument.signatur_cso,
"Geltungsbereich": {},
"Einleitung": {},
"Ziel": "",
"Grundlagen": "",
"Changelog": [],
"Anhänge": dokument.anhaenge,
"Verantwortlich": "Information Security Management BIT",
"Klassifizierung": None,
"Glossar": {},
"Vorgaben": []
}
# Process Geltungsbereich sections
geltungsbereich_sections = []
for gb in dokument.geltungsbereich_set.all().order_by('order'):
geltungsbereich_sections.append({
"typ": gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text",
"inhalt": gb.inhalt
})
if geltungsbereich_sections:
doc_data["Geltungsbereich"] = {
"Abschnitt": geltungsbereich_sections
}
# Process Einleitung sections
einleitung_sections = []
for ei in dokument.einleitung_set.all().order_by('order'):
einleitung_sections.append({
"typ": ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text",
"inhalt": ei.inhalt
})
if einleitung_sections:
doc_data["Einleitung"] = {
"Abschnitt": einleitung_sections
}
# Process Changelog entries
changelog_entries = []
for cl in dokument.changelog.all().order_by('-datum'):
changelog_entries.append({
"Datum": cl.datum.strftime("%Y-%m-%d"),
"Autoren": [autor.name for autor in cl.autoren.all()],
"Aenderung": cl.aenderung
})
doc_data["Changelog"] = changelog_entries
# Process Vorgaben for this document
vorgaben = dokument.vorgaben.all().order_by('order')
for vorgabe in vorgaben:
# Get Kurztext and Langtext sections
kurztext_sections = []
for kt in vorgabe.vorgabekurztext_set.all().order_by('order'):
kurztext_sections.append({
"typ": kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text",
"inhalt": kt.inhalt
})
langtext_sections = []
for lt in vorgabe.vorgabelangtext_set.all().order_by('order'):
langtext_sections.append({
"typ": lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text",
"inhalt": lt.inhalt
})
# Build text structures following Langtext pattern
kurztext = {
"Abschnitt": kurztext_sections if kurztext_sections else []
} if kurztext_sections else {}
langtext = {
"Abschnitt": langtext_sections if langtext_sections else []
} if langtext_sections else {}
# Get references and keywords
referenzen = [f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer for ref in vorgabe.referenzen.all()]
stichworte = [stw.stichwort for stw in vorgabe.stichworte.all()]
# Get checklist questions
checklistenfragen = [cf.frage for cf in vorgabe.checklistenfragen.all()]
vorgabe_data = {
"Nummer": str(vorgabe.nummer),
"Titel": vorgabe.titel,
"Thema": vorgabe.thema.name if vorgabe.thema else "",
"Kurztext": kurztext,
"Langtext": langtext,
"Referenz": referenzen,
"Gueltigkeit": {
"Von": vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else "",
"Bis": vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
},
"Checklistenfragen": checklistenfragen,
"Stichworte": stichworte
}
doc_data["Vorgaben"].append(vorgabe_data)
# Return JSON response
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
@login_required
def get_vorgabe_comments(request, vorgabe_id):
"""Get comments for a specific Vorgabe"""
vorgabe = get_object_or_404(Vorgabe, id=vorgabe_id)
if request.user.is_staff:
# Staff can see all comments
comments = vorgabe.comments.all().select_related('user').order_by('created_at')
else:
# Regular users can only see their own comments
comments = vorgabe.comments.filter(user=request.user).select_related('user').order_by('created_at')
comments_data = []
for comment in comments:
# Escape HTML but preserve line breaks
escaped_text = escape(comment.text).replace('\n', '<br>')
comments_data.append({
'id': comment.id,
'text': escaped_text,
'user': escape(comment.user.first_name+" "+comment.user.last_name),
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
'is_own': comment.user == request.user
})
response = JsonResponse({'comments': comments_data})
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
@require_POST
@login_required
def add_vorgabe_comment(request, vorgabe_id):
"""Add a new comment to a Vorgabe"""
vorgabe = get_object_or_404(Vorgabe, id=vorgabe_id)
try:
data = json.loads(request.body)
text = data.get('text', '').strip()
# Validate input
if not text:
return JsonResponse({'error': 'Kommentar darf nicht leer sein'}, status=400)
if len(text) > 2000: # Reasonable length limit
return JsonResponse({'error': 'Kommentar ist zu lang (max 2000 Zeichen)'}, status=400)
# Additional XSS prevention - check for dangerous patterns
dangerous_patterns = ['<script', 'javascript:', 'onload=', 'onerror=', 'onclick=', 'onmouseover=']
text_lower = text.lower()
for pattern in dangerous_patterns:
if pattern in text_lower:
return JsonResponse({'error': 'Kommentar enthält ungültige Zeichen'}, status=400)
comment = VorgabeComment.objects.create(
vorgabe=vorgabe,
user=request.user,
text=text
)
# Escape HTML but preserve line breaks
escaped_text = escape(comment.text).replace('\n', '<br>')
response = JsonResponse({
'success': True,
'comment': {
'id': comment.id,
'text': escaped_text,
'user': escape(comment.user.username),
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
'is_own': True
}
})
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
except json.JSONDecodeError:
response = JsonResponse({'error': 'Ungültige Daten'}, status=400)
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
except Exception as e:
response = JsonResponse({'error': 'Serverfehler'}, status=500)
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
@require_POST
@login_required
def delete_vorgabe_comment(request, comment_id):
"""Delete a comment (only own comments or staff can delete)"""
comment = get_object_or_404(VorgabeComment, id=comment_id)
# Check if user can delete this comment
if comment.user != request.user and not request.user.is_staff:
response = JsonResponse({'error': 'Keine Berechtigung zum Löschen dieses Kommentars'}, status=403)
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
try:
comment.delete()
response = JsonResponse({'success': True})
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
except Exception as e:
response = JsonResponse({'error': 'Serverfehler'}, status=500)
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
@login_required
def user_comments(request):
"""
Display all comments made by the logged-in user, grouped by document.
"""
# Get all comments by the current user
user_comments = VorgabeComment.objects.filter(
user=request.user
).select_related('vorgabe', 'vorgabe__dokument').order_by(
'vorgabe__dokument__nummer', '-created_at'
)
# Group comments by document
comments_by_document = {}
for comment in user_comments:
dokument = comment.vorgabe.dokument
if dokument not in comments_by_document:
comments_by_document[dokument] = []
comments_by_document[dokument].append(comment)
return render(request, 'standards/user_comments.html', {
'comments_by_document': comments_by_document,
'total_comments': user_comments.count(),
})
@login_required
@user_passes_test(is_staff_user)
def all_comments(request):
"""
Display all comments from all users, grouped by document.
Staff only.
"""
# Get all comments
all_comments_qs = VorgabeComment.objects.select_related(
'vorgabe', 'vorgabe__dokument', 'user'
).order_by(
'vorgabe__dokument__nummer', '-created_at'
)
# Group comments by document
comments_by_document = {}
for comment in all_comments_qs:
dokument = comment.vorgabe.dokument
if dokument not in comments_by_document:
comments_by_document[dokument] = []
comments_by_document[dokument].append(comment)
return render(request, 'standards/all_comments.html', {
'comments_by_document': comments_by_document,
'total_comments': all_comments_qs.count(),
})