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
This commit is contained in:
2025-11-04 13:58:40 +00:00
4 changed files with 221 additions and 191 deletions

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

@@ -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

@@ -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

@@ -64,36 +64,41 @@ 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()
has_langtext = vorgabe.vorgabelangtext_set.exists()
has_text = has_kurztext or has_langtext
has_checklistenfragen = vorgabe.checklistenfragen.exists()
if not has_kurztext and not has_langtext: # Only include Vorgaben that are incomplete in at least one way
no_text.append(vorgabe) 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,
}) })