Files
vgui-cicd/dokumente/templates/standards/standard_detail.html
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

423 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}{{ standard.nummer }} {{ standard.name }}{% endblock %}
{% block content %}
<div class="container-fluid">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Startseite</a></li>
<li class="breadcrumb-item"><a href="/dokumente">Standards</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ standard.nummer }}</li>
</ol>
</nav>
<h1>{{ standard.nummer }} {{ standard.name }}</h1>
{% if standard.history == True %}
<div class="alert alert-warning" role="alert">
{% if standard.is_future %}
<strong>Zukünftige Version vom {{ standard.check_date }}</strong>
{% else %}
<strong>Historische Version vom {{ standard.check_date }}</strong>
{% endif %}
</div>
{% endif %}
<!-- History Dates Dropdown -->
{% if standard.dates %}
<div class="mb-3">
<div class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" style="text-decoration: none;">
📅 Historische Versionen
</a>
<ul class="dropdown-menu" role="menu">
<li><a href="/dokumente/{{ standard.nummer }}/">Aktuelle Version</a></li>
<li class="divider"></li>
{% for date in standard.dates %}
<li><a href="/dokumente/{{ standard.nummer }}/history/{{ date|date:'Y-m-d' }}/">{{ date|date:'d.m.Y' }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<!-- Einleitung -->
{% if standard.einleitung_html %}
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h2>Einleitung</h2>
</div>
<div class="card-body">
{% for typ, html in standard.einleitung_html %}
<div class="mb-2">{{ html|safe }}</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
<!-- Geltungsbereich -->
{% if standard.geltungsbereich_html %}
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h2>Geltungsbereich</h2>
</div>
<div class="card-body">
{% for typ, html in standard.geltungsbereich_html %}
<div class="mb-2">{{ html|safe }}</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
<!-- Vorgaben -->
<div class="row">
<div class="col-md-12">
<h2>Vorgaben</h2>
</div>
</div>
{% for vorgabe in vorgaben %}
{% if standard.history == True or vorgabe.long_status == "active" %}
<!-- Vorgabe {{ vorgabe.Vorgabennummer }} -->
<div class="row">
<div class="col-md-12">
{% if not forloop.first %}
<hr style="border: 0; border-top: 1px solid #d3d3d3; margin: 2rem 0;">
{% endif %}
<a id="{{ vorgabe.Vorgabennummer }}"></a>
<div class="card mb-4">
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<h3>
{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}
{% if vorgabe.long_status != "active" and standard.history == True %}
<span class="badge badge-danger">{{ vorgabe.long_status }}</span>
{% endif %}
</h3>
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; white-space: nowrap;">
<span class="badge badge-info">{{ vorgabe.thema }}</span>
{% if vorgabe.relevanzset %}
<span class="badge badge-secondary">
Relevanz: {{ vorgabe.relevanzset|join:", " }}
</span>
{% endif %}
</div>
</div>
<div class="card-body">
<!-- Kurztext -->
{% if vorgabe.kurztext_html.0.1 %}
<div class="alert alert-info">
{% for typ, html in vorgabe.kurztext_html %}
{% if html %}
<div class="mb-2">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- Langtext -->
<div class="mb-3">
{% for typ, html in vorgabe.langtext_html %}
{% if html %}
<div class="mb-3">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div>
<!-- Checklistenfragen -->
<h4 class="h6">Checklistenfragen</h4>
{% if vorgabe.checklistenfragen.all %}
<ul class="list-group mb-3">
{% for frage in vorgabe.checklistenfragen.all %}
<li class="list-group-item">{{ frage.frage }}</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted"><em>Keine Checklistenfragen</em></p>
{% endif %}
<!-- Stichworte und Referenzen -->
<div class="mt-4 p-3" style="background-color: #f8f9fa; border-left: 3px solid #dee2e6; padding-left: 0.5en;">
<p class="mb-2">
<strong>Stichworte:</strong>
{% if vorgabe.stichworte.all %}
{% for s in vorgabe.stichworte.all %}
<a href="{% url 'stichwort_detail' stichwort=s %}" class="badge badge-secondary">{{ s }}</a>{% if not forloop.last %} {% endif %}
{% endfor %}
{% else %}
<span class="text-muted">Keine</span>
{% endif %}
</p>
<p class="mb-0">
<strong>Referenzen:</strong>
{% if vorgabe.referenzpfade %}
{% for ref in vorgabe.referenzpfade %}
{{ ref|safe }}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
<span class="text-muted">Keine</span>
{% endif %}
</p>
</div>
<!-- Comment Button -->
{% if user.is_authenticated %}
<div class="mt-3 text-right">
<button class="btn btn-sm btn-outline-primary comment-btn"
data-vorgabe-id="{{ vorgabe.id }}"
data-vorgabe-nummer="{{ vorgabe.Vorgabennummer }}">
<span class="emoji-icon">💬</span> Kommentare
{% if vorgabe.comment_count > 0 %}
<span class="comment-count">{{ vorgabe.comment_count }}</span>
{% endif %}
</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
<!-- Metadata -->
<h2>Metadaten</h2>
<div class="row mb-4">
<div class="col-md-12">
<dl class="row">
<dt class="col-sm-3">Autoren:</dt>
<dd class="col-sm-9">{{ standard.autoren.all|join:", " }}</dd>
<dt class="col-sm-3">Prüfende:</dt>
<dd class="col-sm-9">{{ standard.pruefende.all|join:", " }}</dd>
<dt class="col-sm-3">Gültigkeit:</dt>
<dd class="col-sm-9">{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</dd>
</dl>
<p>
<a href="{% url 'standard_json' standard.nummer %}"
class="btn btn-secondary icon icon--before icon--download"
download="{{ standard.nummer }}.json">
JSON herunterladen
</a>
</p>
</div>
</div>
</div>
<!-- Comment Modal -->
<div class="modal fade" id="commentModal" tabindex="-1" role="dialog" aria-labelledby="commentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="commentModalLabel">Kommentare für <span id="modalVorgabeNummer"></span></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="commentsContainer">
<!-- Comments will be loaded here -->
</div>
<!-- Add Comment Form -->
<div class="mt-4">
<h6>Neuen Kommentar hinzufügen:</h6>
<textarea id="newCommentText" class="form-control" rows="3" placeholder="Ihr Kommentar..."></textarea>
<button id="addCommentBtn" class="btn btn-primary btn-sm mt-2">Kommentar hinzufügen</button>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript for Comments -->
<script>
// Content Security Policy for comment system
document.addEventListener('DOMContentLoaded', function() {
// Prevent inline script execution in dynamically loaded content
const commentsContainer = document.getElementById('commentsContainer');
if (commentsContainer) {
// Use DOMPurify-like approach - only allow safe HTML
const allowedTags = ['br', 'small', 'div', 'span', 'button'];
const allowedAttributes = ['class', 'data-comment-id', 'aria-hidden'];
// Monitor for any script injection attempts
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) { // Element node
const tagName = node.tagName.toLowerCase();
if (tagName === 'script') {
console.warn('Script injection attempt blocked');
node.parentNode.removeChild(node);
}
}
});
});
});
observer.observe(commentsContainer, {
childList: true,
subtree: true
});
}
});
document.addEventListener('DOMContentLoaded', function() {
let currentVorgabeId = null;
let currentVorgabeNummer = null;
// Comment button click handler
document.querySelectorAll('.comment-btn').forEach(btn => {
btn.addEventListener('click', function() {
currentVorgabeId = this.dataset.vorgabeId;
currentVorgabeNummer = this.dataset.vorgabeNummer;
document.getElementById('modalVorgabeNummer').textContent = currentVorgabeNummer;
document.getElementById('newCommentText').value = '';
loadComments();
$('#commentModal').modal('show');
});
});
// Load comments function
function loadComments() {
fetch(`/dokumente/comments/${currentVorgabeId}/`)
.then(response => response.json())
.then(data => {
renderComments(data.comments);
})
.catch(error => {
console.error('Error loading comments:', error);
document.getElementById('commentsContainer').innerHTML =
'<div class="alert alert-danger">Fehler beim Laden der Kommentare</div>';
});
}
// Render comments function
function renderComments(comments) {
const container = document.getElementById('commentsContainer');
if (comments.length === 0) {
container.innerHTML = '<p class="text-muted">Noch keine Kommentare vorhanden.</p>';
return;
}
let html = '';
comments.forEach(comment => {
const canDelete = comment.is_own || {% if user.is_authenticated %}'{{ user.is_staff|yesno:"true,false" }}'{% else %}'false'{% endif %} === 'true';
html += `
<div class="comment-item border-bottom pb-2 mb-2">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<strong>${comment.user}</strong>
<small class="text-muted">(${comment.created_at})</small>
${comment.updated_at !== comment.created_at ? `<small class="text-muted">(bearbeitet: ${comment.updated_at})</small>` : ''}
<div class="mt-1">${comment.text}</div>
</div>
${canDelete ? `
<button class="btn btn-sm btn-outline-danger ml-2 delete-comment-btn" data-comment-id="${comment.id}">
<span aria-hidden="true">&times;</span>
</button>
` : ''}
</div>
</div>
`;
});
container.innerHTML = html;
// Add delete handlers
document.querySelectorAll('.delete-comment-btn').forEach(btn => {
btn.addEventListener('click', function() {
if (confirm('Möchten Sie diesen Kommentar wirklich löschen?')) {
deleteComment(this.dataset.commentId);
}
});
});
}
// Add comment function
document.getElementById('addCommentBtn').addEventListener('click', function() {
const text = document.getElementById('newCommentText').value.trim();
if (!text) {
alert('Bitte geben Sie einen Kommentar ein.');
return;
}
fetch(`/dokumente/comments/${currentVorgabeId}/add/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({ text: text })
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('newCommentText').value = '';
loadComments();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Error adding comment:', error);
alert('Fehler beim Hinzufügen des Kommentars');
});
});
// Delete comment function
function deleteComment(commentId) {
fetch(`/dokumente/comments/delete/${commentId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadComments();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Error deleting comment:', error);
alert('Fehler beim Löschen des Kommentars');
});
}
// CSRF token helper
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
});
</script>
{% endblock %}