>Renamed app "standards" to "dokumente" - finally working as expected.

This commit is contained in:
Adrian A. Baumann
2025-10-22 15:08:42 +02:00
parent 4de2ad38c5
commit b391ab0ef6
27 changed files with 32 additions and 32 deletions

0
dokumente/__init__.py Normal file
View File

127
dokumente/admin.py Normal file
View File

@@ -0,0 +1,127 @@
from django.contrib import admin
#from nested_inline.admin import NestedStackedInline, NestedModelAdmin
from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline
from django import forms
from mptt.forms import TreeNodeMultipleChoiceField
from mptt.admin import DraggableMPTTAdmin
# Register your models here.
from .models import *
from stichworte.models import Stichwort, Stichworterklaerung
from referenzen.models import Referenz
#class ChecklistenForm(forms.ModelForm):
# class Meta:
# model=Checklistenfrage
# fields="__all__"
# widgets = {
# 'frage': forms.Textarea(attrs={'rows': 1, 'cols': 100}),
# }
class ChecklistenfragenInline(NestedTabularInline):
model=Checklistenfrage
extra=0
fk_name="vorgabe"
# form=ChecklistenForm
classes = ['collapse']
class VorgabeKurztextInline(NestedTabularInline):
model=VorgabeKurztext
extra=0
sortable_field_name = "order"
show_change_link=True
classes = ['collapse']
#inline=inhalt
class VorgabeLangtextInline(NestedStackedInline):
model=VorgabeLangtext
extra=0
sortable_field_name = "order"
show_change_link=True
classes = ['collapse']
#inline=inhalt
class GeltungsbereichInline(NestedTabularInline):
model=Geltungsbereich
extra=0
sortable_field_name = "order"
show_change_link=True
classes = ['collapse']
classes = ['collapse']
#inline=inhalt
class EinleitungInline(NestedTabularInline):
model = Einleitung
extra = 0
sortable_field_name = "order"
show_change_link = True
classes = ['collapse']
class VorgabeForm(forms.ModelForm):
# referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False)
class Meta:
model = Vorgabe
fields = '__all__'
class VorgabeInline(NestedTabularInline): # or StackedInline for more vertical layout
model = Vorgabe
form = VorgabeForm
extra = 0
#show_change_link = True
inlines = [VorgabeKurztextInline,VorgabeLangtextInline,ChecklistenfragenInline]
autocomplete_fields = ['stichworte','referenzen','relevanz']
#search_fields=['nummer','name']ModelAdmin.
list_filter=['stichworte']
#classes=["collapse"]
class StichworterklaerungInline(NestedStackedInline):
model=Stichworterklaerung
extra=0
sortable_field_name = "order"
ordering=("order",)
show_change_link = True
@admin.register(Stichwort)
class StichwortAdmin(NestedModelAdmin):
search_fields = ('stichwort',)
ordering=('stichwort',)
inlines=[StichworterklaerungInline]
@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
class Media:
js = ['admin/js/jquery.init.js', 'custom/js/inline_toggle.js']
css = {'all': ['custom/css/admin_extras.css']}
list_display=['name']
@admin.register(Dokument)
class DokumentAdmin(NestedModelAdmin):
actions_on_top=True
inlines = [EinleitungInline,GeltungsbereichInline,VorgabeInline]
#filter_horizontal=['autoren','pruefende']
list_display=['nummer','name','dokumententyp']
search_fields=['nummer','name']
class Media:
# js = ('admin/js/vorgabe_collapse.js',)
css = {
'all': ('admin/css/vorgabe_border.css',
# 'admin/css/vorgabe_collapse.css',
)
}
#admin.site.register(Stichwort)
admin.site.register(Checklistenfrage)
admin.site.register(Dokumententyp)
#admin.site.register(Person)
admin.site.register(Thema)
#admin.site.register(Referenz, DraggableM§PTTAdmin)
admin.site.register(Vorgabe)
#admin.site.register(Changelog)

6
dokumente/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class standardsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'dokumente'

View File

@@ -0,0 +1,352 @@
# Document/management/commands/import_standard.py
import re
from pathlib import Path
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from dokumente.models import (
Dokument,
Dokumententyp,
Thema,
Vorgabe,
VorgabeKurztext,
VorgabeLangtext,
Geltungsbereich,
Einleitung, # <-- make sure this model exists as a Textabschnitt subclass
Checklistenfrage,
)
from abschnitte.models import AbschnittTyp
from stichworte.models import Stichwort
class Command(BaseCommand):
help = (
"Import a policy document from a structured text file.\n"
"Supports Einleitung, Geltungsbereich, Vorgaben (Kurztext/Langtext with AbschnittTyp), "
"Stichworte (comma-separated), Checklistenfragen, dry-run, verbose, and purge."
)
def add_arguments(self, parser):
parser.add_argument("file_path", type=str, help="Path to the plaintext file")
parser.add_argument("--nummer", required=True, help="Document number (e.g., STD-001)")
parser.add_argument("--name", required=True, help='Document name (e.g., "IT-Sicherheit Container")')
parser.add_argument("--dokumententyp", required=True, help='Dokumententyp name (e.g., "IT-Sicherheit")')
parser.add_argument("--gueltigkeit_von", default=None, help="Start date (YYYY-MM-DD)")
parser.add_argument("--gueltigkeit_bis", default=None, help="End date (YYYY-MM-DD)")
parser.add_argument("--dry-run", action="store_true", help="Perform a dry run without saving to DB")
parser.add_argument("--verbose", action="store_true", help="Verbose output for dry run")
parser.add_argument("--purge", action="store_true", help="Delete existing Einleitung/Geltungsbereich/Vorgaben first")
# normalize header: "liste-ungeordnet" -> "liste ungeordnet"
@staticmethod
def _norm_header(h: str) -> str:
return h.lower().replace("-", " ").strip()
def handle(self, *args, **options):
dry_run = options["dry_run"]
verbose = options["verbose"]
purge = options["purge"]
file_path = Path(options["file_path"])
if not file_path.exists():
raise CommandError(f"File {file_path} does not exist")
nummer = options["nummer"]
name = options["name"]
dokumententyp_name = options["dokumententyp"]
try:
dokumententyp = Dokumententyp.objects.get(name=dokumententyp_name)
except Dokumententyp.DoesNotExist:
raise CommandError(f"Dokumententyp '{dokumententyp_name}' does not exist")
if dry_run:
self.stdout.write(self.style.WARNING("Dry run: no database changes will be made."))
# get or create Document (we want a real instance even in purge to count existing rows)
standard, created = Dokument.objects.get_or_create(
nummer=nummer,
defaults={
"dokumententyp": dokumententyp,
"name": name,
"gueltigkeit_von": options["gueltigkeit_von"],
"gueltigkeit_bis": options["gueltigkeit_bis"],
},
)
if created:
self.stdout.write(self.style.SUCCESS(f"Created Document {nummer} {name}"))
else:
self.stdout.write(self.style.WARNING(f"Document {nummer} already exists; content may be updated."))
# purge (Einleitung + Geltungsbereich + Vorgaben cascade)
if purge:
qs_vorgaben = standard.vorgaben.all()
qs_check = Checklistenfrage.objects.filter(vorgabe__in=qs_vorgaben)
qs_kurz = VorgabeKurztext.objects.filter(abschnitt__in=qs_vorgaben)
qs_lang = VorgabeLangtext.objects.filter(abschnitt__in=qs_vorgaben)
qs_gb = Geltungsbereich.objects.filter(geltungsbereich=standard)
qs_einl = Einleitung.objects.filter(einleitung=standard)
c_vorgaben = qs_vorgaben.count()
c_check = qs_check.count()
c_kurz = qs_kurz.count()
c_lang = qs_lang.count()
c_gb = qs_gb.count()
c_einl = qs_einl.count()
if dry_run:
self.stdout.write(self.style.WARNING(
f"[DRY RUN] Would purge: {c_einl} Einleitung-Abschnitte, "
f"{c_gb} Geltungsbereich-Abschnitte, {c_vorgaben} Vorgaben "
f"({c_kurz} Kurztext, {c_lang} Langtext, {c_check} Checklistenfragen)."
))
else:
deleted_einl = qs_einl.delete()[0]
deleted_gb = qs_gb.delete()[0]
deleted_vorgaben = qs_vorgaben.delete()[0]
self.stdout.write(self.style.SUCCESS(
f"Purged {deleted_einl} Einleitung, {deleted_gb} Geltungsbereich, "
f"{deleted_vorgaben} Vorgaben (incl. Kurz/Lang/Checklistenfragen)."
))
# read and split file
content = file_path.read_text(encoding="utf-8")
blocks = re.split(r"^>>>", content, flags=re.MULTILINE)
blocks = [b.strip() for b in blocks if b.strip()]
# state
abschnittstyp_names = {"text", "liste geordnet", "liste ungeordnet"}
current_context = "geltungsbereich" # default before first Vorgabe
einleitung_sections = [] # list of {inhalt, typ: AbschnittTyp}
geltungsbereich_sections = [] # list of {inhalt, typ: AbschnittTyp}
current_vorgabe = None
vorgaben_data = [] # each: {thema, titel, nummer, kurztext:[{inhalt,typ}], langtext:[{inhalt,typ}], stichworte:set(), checkliste:[str]}
for block in blocks:
lines = block.splitlines()
header = lines[0].strip()
text = "\n".join(lines[1:]).strip()
header_norm = self._norm_header(header)
# resolve AbschnittTyp if this is a section block
abschnitt_typ = None
if header_norm in abschnittstyp_names:
try:
abschnitt_typ = AbschnittTyp.objects.get(abschnitttyp=header_norm)
except AbschnittTyp.DoesNotExist:
self.stdout.write(self.style.WARNING(
f"AbschnittTyp '{header_norm}' not found; defaulting to 'text'."
))
abschnitt_typ = AbschnittTyp.objects.get(abschnitttyp="text")
# contexts
if header_norm == "einleitung":
current_context = "einleitung"
continue
if header_norm == "geltungsbereich":
current_context = "geltungsbereich"
continue
if header_norm.startswith("vorgabe"):
# save previous
if current_vorgabe:
vorgaben_data.append(current_vorgabe)
parts = header.split(" ", 1)
thema_name = parts[1].strip() if len(parts) > 1 else ""
current_vorgabe = {
"thema": thema_name,
"titel": "",
"nummer": None,
"kurztext": [],
"langtext": [],
"stichworte": set(),
"checkliste": [],
}
current_context = "vorgabe_none"
continue
if header_norm.startswith("titel") and current_vorgabe:
# inline title or next text block
inline = header[len("Titel"):].strip() if header.startswith("Titel") else ""
title_value = inline or text
current_vorgabe["titel"] = title_value
continue
if header_norm.startswith("nummer") and current_vorgabe:
m = re.search(r"\d+", header)
if m:
current_vorgabe["nummer"] = int(m.group())
current_context = "vorgabe_none"
continue
if header_norm == "kurztext":
current_context = "vorgabe_kurztext"
continue
if header_norm == "langtext":
current_context = "vorgabe_langtext"
continue
if header_norm.startswith("stichworte") and current_vorgabe:
inline = header[len("Stichworte"):].strip() if header.startswith("Stichworte") else ""
kw_str = inline or text
if kw_str:
for k in kw_str.split(","):
k = k.strip()
if k:
current_vorgabe["stichworte"].add(k)
else:
current_context = "vorgabe_stichworte"
continue
if header_norm == "checkliste" and current_vorgabe:
if text:
current_vorgabe["checkliste"].extend([q.strip() for q in text.splitlines() if q.strip()])
else:
current_context = "vorgabe_checkliste"
continue
# Abschnitt content blocks
if header_norm in abschnittstyp_names:
section = {"inhalt": text, "typ": abschnitt_typ}
if current_context == "einleitung":
einleitung_sections.append(section)
if dry_run and verbose:
self.stdout.write(self.style.SUCCESS(
f"[DRY RUN] Einleitung Abschnitt ({section['typ']}): {text[:50]}..."
))
elif current_context == "geltungsbereich":
geltungsbereich_sections.append(section)
if dry_run and verbose:
self.stdout.write(self.style.SUCCESS(
f"[DRY RUN] Geltungsbereich Abschnitt ({section['typ']}): {text[:50]}..."
))
elif current_context == "vorgabe_kurztext" and current_vorgabe:
current_vorgabe["kurztext"].append(section)
if dry_run and verbose:
self.stdout.write(self.style.SUCCESS(
f"[DRY RUN] Vorgabe {current_vorgabe.get('nummer')} Kurztext ({section['typ']}): {text[:50]}..."
))
elif current_context == "vorgabe_langtext" and current_vorgabe:
current_vorgabe["langtext"].append(section)
if dry_run and verbose:
self.stdout.write(self.style.SUCCESS(
f"[DRY RUN] Vorgabe {current_vorgabe.get('nummer')} Langtext ({section['typ']}): {text[:50]}..."
))
elif current_context == "vorgabe_stichworte" and current_vorgabe:
for k in text.split(","):
k = k.strip()
if k:
current_vorgabe["stichworte"].add(k)
current_context = "vorgabe_none"
elif current_context == "vorgabe_checkliste" and current_vorgabe:
current_vorgabe["checkliste"].extend([q.strip() for q in text.splitlines() if q.strip()])
current_context = "vorgabe_none"
# append last vorgabe
if current_vorgabe:
vorgaben_data.append(current_vorgabe)
# === SAVE: Einleitung ===
for sektion in einleitung_sections:
if dry_run:
self.stdout.write(self.style.SUCCESS(
f"[DRY RUN] Would create Einleitung Abschnitt ({sektion['typ']}): {sektion['inhalt'][:50]}..."
))
else:
Einleitung.objects.create(
einleitung=standard,
abschnitttyp=sektion["typ"],
inhalt=sektion["inhalt"],
)
# === SAVE: Geltungsbereich ===
for sektion in geltungsbereich_sections:
if dry_run:
self.stdout.write(self.style.SUCCESS(
f"[DRY RUN] Would create Geltungsbereich Abschnitt ({sektion['typ']}): {sektion['inhalt'][:50]}..."
))
else:
Geltungsbereich.objects.create(
geltungsbereich=standard,
abschnitttyp=sektion["typ"],
inhalt=sektion["inhalt"],
)
# === SAVE: Vorgaben and children ===
for v in vorgaben_data:
try:
thema = Thema.objects.get(name=v["thema"])
except Thema.DoesNotExist:
self.stdout.write(self.style.WARNING(
f"Thema '{v['thema']}' not found, skipping Vorgabe {v['nummer']}"
))
continue
if dry_run:
self.stdout.write(self.style.SUCCESS(
f"[DRY RUN] Would create Vorgabe {v['nummer']}: '{v['titel']}' (Thema: {v['thema']})"
))
if v["stichworte"]:
self.stdout.write(self.style.SUCCESS(
f"[DRY RUN] Stichworte: {', '.join(sorted(v['stichworte']))}"
))
if v["checkliste"]:
for frage in v["checkliste"]:
self.stdout.write(self.style.SUCCESS(f"[DRY RUN] Checkliste: {frage}"))
for s in v["kurztext"]:
self.stdout.write(self.style.SUCCESS(
f"[DRY RUN] Kurztext ({s['typ']}): {s['inhalt'][:50]}..."
))
for s in v["langtext"]:
self.stdout.write(self.style.SUCCESS(
f"[DRY RUN] Langtext ({s['typ']}): {s['inhalt'][:50]}..."
))
else:
vorgabe = Vorgabe.objects.create(
nummer=v["nummer"],
dokument=standard,
thema=thema,
titel=v["titel"],
gueltigkeit_von=timezone.now().date(),
)
# Stichworte
for kw in sorted(v["stichworte"]):
stw, _ = Stichwort.objects.get_or_create(stichwort=kw)
vorgabe.stichworte.add(stw)
# Checklistenfragen
for frage in v["checkliste"]:
Checklistenfrage.objects.create(vorgabe=vorgabe, frage=frage)
# Kurztext sections
for s in v["kurztext"]:
VorgabeKurztext.objects.create(
abschnitt=vorgabe,
abschnitttyp=s["typ"],
inhalt=s["inhalt"],
)
# Langtext sections
for s in v["langtext"]:
VorgabeLangtext.objects.create(
abschnitt=vorgabe,
abschnitttyp=s["typ"],
inhalt=s["inhalt"],
)
self.stdout.write(self.style.SUCCESS(
"Dry run complete" if dry_run else f"Imported document {nummer} {name} with {len(vorgaben_data)} Vorgaben"
))

View File

@@ -0,0 +1,169 @@
# Generated by Django 5.2.5 on 2025-08-26 09:34
import django.db.models.deletion
import mptt.fields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('abschnitte', '0001_initial'),
('stichworte', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Dokumententyp',
fields=[
('name', models.CharField(max_length=100, primary_key=True, serialize=False)),
('verantwortliche_ve', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='Person',
fields=[
('name', models.CharField(max_length=100, primary_key=True, serialize=False)),
('funktion', models.CharField(max_length=255)),
],
options={
'verbose_name_plural': 'Personen',
},
),
migrations.CreateModel(
name='Thema',
fields=[
('name', models.CharField(max_length=100, primary_key=True, serialize=False)),
('erklaerung', models.TextField(blank=True)),
],
options={
'verbose_name_plural': 'Themen',
},
),
migrations.CreateModel(
name='Referenz',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('name_nummer', models.CharField(max_length=100)),
('name_text', models.CharField(blank=True, max_length=255)),
('url', models.URLField(blank=True)),
('lft', models.PositiveIntegerField(editable=False)),
('rght', models.PositiveIntegerField(editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(editable=False)),
('oberreferenz', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='unterreferenzen', to='dokumente.referenz')),
],
options={
'verbose_name_plural': 'Referenzen',
},
),
migrations.CreateModel(
name='Referenzerklaerung',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('inhalt', models.TextField(blank=True, null=True)),
('abschnitttyp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='abschnitte.abschnitttyp')),
('erklaerung', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dokumente.referenz')),
],
options={
'verbose_name': 'Erklärung',
},
),
migrations.CreateModel(
name='Standard',
fields=[
('nummer', models.CharField(max_length=50, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('gueltigkeit_von', models.DateField(blank=True, null=True)),
('gueltigkeit_bis', models.DateField(blank=True, null=True)),
('signatur_cso', models.CharField(blank=True, max_length=255)),
('anhaenge', models.TextField(blank=True)),
('autoren', models.ManyToManyField(related_name='verfasste_dokumente', to='dokumente.person')),
('dokumententyp', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dokumente.dokumententyp')),
('pruefende', models.ManyToManyField(related_name='gepruefte_dokumente', to='dokumente.person')),
],
options={
'verbose_name': 'Standard',
'verbose_name_plural': 'Standards',
},
),
migrations.CreateModel(
name='Geltungsbereich',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('inhalt', models.TextField(blank=True, null=True)),
('abschnitttyp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='abschnitte.abschnitttyp')),
('geltungsbereich', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dokumente.standard')),
],
options={
'verbose_name': 'Geltungsbereichs-Abschnitt',
'verbose_name_plural': 'Geltungsbereich',
},
),
migrations.CreateModel(
name='Changelog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('datum', models.DateField()),
('aenderung', models.TextField()),
('autoren', models.ManyToManyField(to='dokumente.person')),
('dokument', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='changelog', to='dokumente.standard')),
],
),
migrations.CreateModel(
name='Vorgabe',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nummer', models.IntegerField()),
('titel', models.CharField(max_length=255)),
('gueltigkeit_von', models.DateField()),
('gueltigkeit_bis', models.DateField(blank=True, null=True)),
('dokument', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vorgaben', to='dokumente.standard')),
('referenzen', models.ManyToManyField(blank=True, to='dokumente.referenz')),
('stichworte', models.ManyToManyField(blank=True, to='stichworte.stichwort')),
('thema', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dokumente.thema')),
],
options={
'verbose_name_plural': 'Vorgaben',
},
),
migrations.CreateModel(
name='Checklistenfrage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('frage', models.CharField(max_length=255)),
('vorgabe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checklistenfragen', to='dokumente.vorgabe')),
],
options={
'verbose_name_plural': 'Fragen für Checkliste',
},
),
migrations.CreateModel(
name='VorgabeKurztext',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('inhalt', models.TextField(blank=True, null=True)),
('abschnitt', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dokumente.vorgabe')),
('abschnitttyp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='abschnitte.abschnitttyp')),
],
options={
'verbose_name': 'Kurztext-Abschnitt',
'verbose_name_plural': 'Kurztext',
},
),
migrations.CreateModel(
name='VorgabeLangtext',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('inhalt', models.TextField(blank=True, null=True)),
('abschnitt', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dokumente.vorgabe')),
('abschnitttyp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='abschnitte.abschnitttyp')),
],
options={
'verbose_name': 'Langtext-Abschnitt',
'verbose_name_plural': 'Langtext-Abschnitte',
},
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.5 on 2025-08-26 09:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('abschnitte', '0001_initial'),
('dokumente', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Einleitung',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('inhalt', models.TextField(blank=True, null=True)),
('abschnitttyp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='abschnitte.abschnitttyp')),
('einleitung', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dokumente.standard')),
],
options={
'verbose_name': 'Einleitungs-Abschnitt',
'verbose_name_plural': 'Einleitung',
},
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.2.5 on 2025-08-27 09:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dokumente', '0002_einleitung'),
]
operations = [
migrations.AddField(
model_name='einleitung',
name='order',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='geltungsbereich',
name='order',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='referenzerklaerung',
name='order',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='vorgabekurztext',
name='order',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='vorgabelangtext',
name='order',
field=models.PositiveIntegerField(default=0),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.5 on 2025-09-04 13:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('referenzen', '0001_initial'),
('dokumente', '0003_einleitung_order_geltungsbereich_order_and_more'),
]
operations = [
migrations.RemoveField(
model_name='referenzerklaerung',
name='erklaerung',
),
migrations.RemoveField(
model_name='referenzerklaerung',
name='abschnitttyp',
),
migrations.AlterField(
model_name='vorgabe',
name='referenzen',
field=models.ManyToManyField(blank=True, to='referenzen.referenz'),
),
migrations.DeleteModel(
name='Referenz',
),
migrations.DeleteModel(
name='Referenzerklaerung',
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.5 on 2025-09-05 08:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rollen', '0001_initial'),
('dokumente', '0004_remove_referenzerklaerung_erklaerung_and_more'),
]
operations = [
migrations.AddField(
model_name='vorgabe',
name='relevanz',
field=models.ManyToManyField(blank=True, to='rollen.rolle'),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.2.5 on 2025-10-02 12:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dokumente', '0005_vorgabe_relevanz'),
]
operations = [
migrations.RenameModel(
old_name='Standard',
new_name='Dokument',
),
migrations.AlterModelOptions(
name='dokument',
options={'verbose_name': 'Dokument', 'verbose_name_plural': 'Dokumente'},
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.2.5 on 2025-10-06 11:29
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dokumente', '0006_rename_standard_dokument_alter_dokument_options'),
]
operations = [
migrations.AlterModelOptions(
name='changelog',
options={'verbose_name': 'Changelog-Eintrag', 'verbose_name_plural': 'Changelog'},
),
migrations.AlterModelOptions(
name='checklistenfrage',
options={'verbose_name': 'Frage für Checkliste', 'verbose_name_plural': 'Fragen für Checkliste'},
),
migrations.AlterModelOptions(
name='dokumententyp',
options={'verbose_name': 'Dokumententyp', 'verbose_name_plural': 'Dokumententypen'},
),
migrations.AlterModelOptions(
name='vorgabelangtext',
options={'verbose_name': 'Langtext-Abschnitt', 'verbose_name_plural': 'Langtext'},
),
]

View File

137
dokumente/models.py Normal file
View File

@@ -0,0 +1,137 @@
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from abschnitte.models import Textabschnitt
from stichworte.models import Stichwort
from referenzen.models import Referenz
from rollen.models import Rolle
import datetime
class Dokumententyp(models.Model):
name = models.CharField(max_length=100, primary_key=True)
verantwortliche_ve = models.CharField(max_length=255)
def __str__(self):
return self.name
class Meta:
verbose_name="Dokumententyp"
verbose_name_plural="Dokumententypen"
class Person(models.Model):
name = models.CharField(max_length=100, primary_key=True)
funktion = models.CharField(max_length=255)
def __str__(self):
return self.name
class Meta:
verbose_name_plural="Personen"
class Thema(models.Model):
name = models.CharField(max_length=100, primary_key=True)
erklaerung = models.TextField(blank=True)
def __str__(self):
return self.name
class Meta:
verbose_name_plural="Themen"
class Dokument(models.Model):
nummer = models.CharField(max_length=50, primary_key=True)
dokumententyp = models.ForeignKey(Dokumententyp, on_delete=models.PROTECT)
name = models.CharField(max_length=255)
autoren = models.ManyToManyField(Person, related_name='verfasste_dokumente')
pruefende = models.ManyToManyField(Person, related_name='gepruefte_dokumente')
gueltigkeit_von = models.DateField(null=True, blank=True)
gueltigkeit_bis = models.DateField(null=True, blank=True)
signatur_cso = models.CharField(max_length=255, blank=True)
anhaenge = models.TextField(blank=True)
def __str__(self):
return f"{self.nummer} {self.name}"
class Meta:
verbose_name_plural="Dokumente"
verbose_name="Dokument"
class Vorgabe(models.Model):
nummer = models.IntegerField()
dokument = models.ForeignKey(Dokument, on_delete=models.CASCADE, related_name='vorgaben')
thema = models.ForeignKey(Thema, on_delete=models.PROTECT)
titel = models.CharField(max_length=255)
referenzen = models.ManyToManyField(Referenz, blank=True)
gueltigkeit_von = models.DateField()
gueltigkeit_bis = models.DateField(blank=True,null=True)
stichworte = models.ManyToManyField(Stichwort, blank=True)
relevanz = models.ManyToManyField(Rolle,blank=True)
def Vorgabennummer(self):
return str(self.dokument.nummer)+"."+self.thema.name[0]+"."+str(self.nummer)
def get_status(self, check_date: datetime.date = datetime.date.today(), verbose: bool = False) -> str:
if self.gueltigkeit_von > check_date:
return "future" if not verbose else "Ist erst ab dem "+self.gueltigkeit_von.strftime('%d.%m.%Y')+" in Kraft."
if not self.gueltigkeit_bis:
return "active"
if self.gueltigkeit_bis > check_date:
return "active"
return "expired" if not verbose else "Ist seit dem "+self.gueltigkeit_bis.strftime('%d.%m.%Y')+" nicht mehr in Kraft."
def __str__(self):
return f"{self.Vorgabennummer()}: {self.titel}"
class Meta:
verbose_name_plural="Vorgaben"
class VorgabeLangtext(Textabschnitt):
abschnitt=models.ForeignKey(Vorgabe,on_delete=models.CASCADE)
class Meta:
verbose_name_plural="Langtext"
verbose_name="Langtext-Abschnitt"
class VorgabeKurztext(Textabschnitt):
abschnitt=models.ForeignKey(Vorgabe,on_delete=models.CASCADE)
class Meta:
verbose_name_plural="Kurztext"
verbose_name="Kurztext-Abschnitt"
class Geltungsbereich(Textabschnitt):
geltungsbereich=models.ForeignKey(Dokument,on_delete=models.CASCADE)
class Meta:
verbose_name_plural="Geltungsbereich"
verbose_name="Geltungsbereichs-Abschnitt"
class Einleitung(Textabschnitt):
einleitung=models.ForeignKey(Dokument,on_delete=models.CASCADE)
class Meta:
verbose_name_plural="Einleitung"
verbose_name="Einleitungs-Abschnitt"
class Checklistenfrage(models.Model):
vorgabe=models.ForeignKey(Vorgabe, on_delete=models.CASCADE, related_name="checklistenfragen")
frage = models.CharField(max_length=255)
def __str__(self):
return self.frage
class Meta:
verbose_name_plural="Fragen für Checkliste"
verbose_name="Frage für Checkliste"
class Changelog(models.Model):
dokument = models.ForeignKey(Dokument, on_delete=models.CASCADE, related_name='changelog')
autoren = models.ManyToManyField(Person)
datum = models.DateField()
aenderung = models.TextField()
def __str__(self):
return f"{self.datum} {self.dokument.nummer}"
class Meta:
verbose_name_plural="Changelog"
verbose_name="Changelog-Eintrag"

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ standard }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
{% load static %}
</head>
<body class="container py-4">
<h1>{{ standard.nummer }} {{ standard.name }}</h1>
<h2>Checkliste</h2>
<ul class="list-group">
{% for vorgabe in vorgaben %}
{% if vorgabe.checklistenfragen.all %}
{% for frage in vorgabe.checklistenfragen.all %}
<li class="list-group-item">{{ vorgabe.Vorgabennummer }}: {{ frage.frage }}</li>
{% endfor %}
{% endif %}
{% endfor %}
</ul>
</body>
</html>

View File

@@ -0,0 +1,109 @@
{% extends "base.html" %}
{% block title %}{{ standard }}{% endblock %}
{% block content %}
<h1>{{ standard.nummer }} {{ standard.name }}</h1>
{% if standard.history == True %}
<h2>Version vom {{ standard.check_date }}</h2>
{% endif %}
<!-- Autoren, Prüfende etc. -->
<p><strong>Autoren:</strong> {{ standard.autoren.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 }}</p>
<!-- Start Einleitung -->
{% if standard.einleitung_html %}
<h2>Einleitung</h2>
{% for typ, html in standard.einleitung_html %}
<div>{{ html|safe }}</div>
{% endfor %}
{% endif %}
<!-- End Einleitung -->
<!-- Start Geltungsbereich -->
{% if standard.geltungsbereich_html %}
<h2>Geltungsbereich</h2>
{% for typ, html in standard.geltungsbereich_html %}
<div>{{ html|safe }}</div>
{% endfor %}
{% endif %}
<!-- End Geltungsbereich -->
<h2>Vorgaben</h2>
{% for vorgabe in vorgaben %}
<!-- Start Vorgabe -->
{% if standard.history == True or vorgabe.long_status == "active" %}
<a id="{{ vorgabe.Vorgabennummer }}"></a><div class="card mb-4">
{% if vorgabe.long_status == "active"%}
<div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
{% elif standard.history == True %}
<div class="card-header d-flex justify-content-between align-items-center bg-danger-subtle">
{% endif %}
<h3 class="h5 m-0">{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}
{% if vorgabe.long_status != "active" and standard.history == True %}<span class="text-danger"> ({{ vorgabe.long_status}})</span>{% endif %}
</h3>
{% if vorgabe.relevanzset %}
<span class="badge bg-light text-black"> Relevanz:
{{ vorgabe.relevanzset|join:", " }}
</span>
{% endif %}
<span class="badge bg-light text-black">{{ vorgabe.thema }}</span>
</div>
<div class="card-body p-0">
<!-- Start Kurztext -->
{% comment %} KURZTEXT BLOCK {% endcomment %}
{% if vorgabe.kurztext_html.0.1 %}
<div class="p-3 mb-3 bg-light border-3" style="width: 100%;">
{% for typ, html in vorgabe.kurztext_html %}
{% if html %}
<div class="mb-2">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- Langtext -->
<div class="p-3 mb-3">
{% comment %} LANGTEXT BLOCK {% endcomment %}
{# <h5>Langtext</h5> #}
{% for typ, html in vorgabe.langtext_html %}
{% if html %}<div class="mb-3">{{ html|safe }}</div>{% endif %}
{% endfor %}
<!-- Checklistenfragen -->
{% comment %} CHECKLISTENFRAGEN BLOCK {% endcomment %}
<h5>Checklistenfragen</h5>
{% if vorgabe.checklistenfragen.all %}
<ul class="list-group">
{% for frage in vorgabe.checklistenfragen.all %}
<li class="list-group-item">{{ frage.frage }}</li>
{% endfor %}
</ul>
{% else %}
<p><em>Keine Checklistenfragen</em></p>
{% endif %}
{% comment %} STICHWORTE + REFERENZEN AT BOTTOM {% endcomment %}
<div class="mt-4 small text-muted">
<strong>Stichworte:</strong>
{% if vorgabe.stichworte.all %}
{% for s in vorgabe.stichworte.all %}
<a href="{% url 'stichwort_detail' stichwort=s %}">{{ s }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
<em>Keine</em>
{% endif %}
<br>
<strong>Referenzen:</strong>
{% if vorgabe.referenzpfade %}
{% for ref in vorgabe.referenzpfade %}
{{ ref|safe }}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
<em>Keine</em>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,35 @@
<h1>{{ standard.nummer }} {{ standard.name }}</h1>
<p><strong>Autoren:</strong> {{ standard.autoren.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 }}</p>
{% if standard.geltungsbereich_html %}
<h2>Geltungsbereich</h2>
{% for typ, html in standard.geltungsbereich_html %}
<div>{{ html|safe }}</div>
{% endfor %}
{% endif %}
<h2>Vorgaben</h2>
{% for vorgabe in vorgaben %}
<h3>{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}</h3>
{% if vorgabe.referenzpfade %}
<h4>Referenzen:</h4>
{% endif %}
{% for ref in vorgabe.referenzpfade %}
{{ref | safe}}<br>
{% empty %}
Keine Referenzen angegeben
{% endfor %}
{# <h4>Kurztext</h4> #}
{% for typ, html in vorgabe.kurztext_html %}
{% if html %}<div>{{ html|safe }}</div>{% endif %}
{% endfor %}
<h4>Langtext</h4>
{% for typ, html in vorgabe.langtext_html %}
{% if html %}<div>{{ html|safe }}</div>{% endif %}
{% endfor %}
{% endfor %}

View File

@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<h1>Standards Informatiksicherheit</h1>
<ul>
{% for standard in standards %}
<li>
<a href="{% url 'standard_detail' nummer=standard.nummer %}">
{{ standard.nummer }} {{ standard.name }}
</a>
</li>
{% endfor %}
</ul>
{% endblock %}

3
dokumente/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
dokumente/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.standard_list, name='standard_list'),
path('<str:nummer>/', views.standard_detail, name='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>/checkliste/', views.standard_checkliste, name='standard_checkliste')
]

58
dokumente/views.py Normal file
View File

@@ -0,0 +1,58 @@
from django.shortcuts import render, get_object_or_404
from .models import Dokument
from abschnitte.utils import render_textabschnitte
from datetime import date
import parsedatetime
calendar=parsedatetime.Calendar()
def standard_list(request):
standards = Dokument.objects.all()
return render(request, 'standards/standard_list.html',
{'dokumente': standards}
)
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
else:
check_date = date.today()
standard.history = 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
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,
})