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
423 lines
14 KiB
HTML
423 lines
14 KiB
HTML
{% 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">×</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">×</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 %}
|