Compare commits
5 Commits
developmen
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
6c2c76859b
|
|||
|
44de29a9da
|
|||
|
e39a65d5b2
|
|||
|
137ed9d1a0
|
|||
|
3faa88fcea
|
@@ -12,7 +12,11 @@ RUN pip install --no-cache-dir -r requirements.txt && \
|
|||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
FROM python:3.15.0a5-slim-trixie
|
FROM python:3.15.0a5-slim-trixie
|
||||||
RUN useradd -m -r -u 99 appuser && \
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends libxslt1.1 libxml2 && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
useradd -m -r -u 99 appuser && \
|
||||||
mkdir /app && \
|
mkdir /app && \
|
||||||
chown -R appuser /app
|
chown -R appuser /app
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ data:
|
|||||||
MEDIA_URL: "/media/"
|
MEDIA_URL: "/media/"
|
||||||
|
|
||||||
# Application Version
|
# Application Version
|
||||||
VERSION: "0.990"
|
VERSION: "0.992"
|
||||||
|
|
||||||
# Database Configuration (for future use)
|
# Database Configuration (for future use)
|
||||||
# DATABASE_ENGINE: "django.db.backends.sqlite3"
|
# DATABASE_ENGINE: "django.db.backends.sqlite3"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ spec:
|
|||||||
fsGroupChangePolicy: "OnRootMismatch"
|
fsGroupChangePolicy: "OnRootMismatch"
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: loader
|
- name: loader
|
||||||
image: git.baumann.gr/adebaumann/vui-data-loader:0.11
|
image: git.baumann.gr/adebaumann/vui-data-loader:0.12
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsUser: 99
|
runAsUser: 99
|
||||||
command: [ "sh","-c","if [ ! -f /data/db.sqlite3 ] || [ ! -s /data/db.sqlite3 ]; then cp preload/preload.sqlite3 /data/db.sqlite3 && echo 'Database copied from preload'; else echo 'Existing database preserved'; fi" ]
|
command: [ "sh","-c","if [ ! -f /data/db.sqlite3 ] || [ ! -s /data/db.sqlite3 ]; then cp preload/preload.sqlite3 /data/db.sqlite3 && echo 'Database copied from preload'; else echo 'Existing database preserved'; fi" ]
|
||||||
@@ -27,7 +27,7 @@ spec:
|
|||||||
mountPath: /data
|
mountPath: /data
|
||||||
containers:
|
containers:
|
||||||
- name: web
|
- name: web
|
||||||
image: git.baumann.gr/adebaumann/vui:0.990
|
image: git.baumann.gr/adebaumann/vui:0.993
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsUser: 99
|
runAsUser: 99
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
FROM alpine:3.22.1
|
FROM alpine:3.22
|
||||||
RUN addgroup -S appuser && \
|
RUN apk upgrade --no-cache && \
|
||||||
|
addgroup -S appuser && \
|
||||||
adduser -S appuser -G appuser && \
|
adduser -S appuser -G appuser && \
|
||||||
mkdir /preload && \
|
mkdir /preload && \
|
||||||
chown -R appuser:appuser /preload
|
chown -R appuser:appuser /preload
|
||||||
|
|||||||
BIN
dokumente/docx_template/template.docx
Normal file
BIN
dokumente/docx_template/template.docx
Normal file
Binary file not shown.
207
dokumente/docx_utils.py
Normal file
207
dokumente/docx_utils.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
"""
|
||||||
|
Utility functions for exporting Dokument instances as Word (.docx) files.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from docx import Document
|
||||||
|
from docx.shared import Pt
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
|
||||||
|
|
||||||
|
TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docx_template', 'template.docx')
|
||||||
|
|
||||||
|
|
||||||
|
def build_standard_docx(dokument):
|
||||||
|
"""
|
||||||
|
Generate a Word Document object for the given Dokument.
|
||||||
|
|
||||||
|
Opens the template .docx to inherit custom styles (Hermes, Kurztext,
|
||||||
|
Tabelle, etc.), clears the body, then writes the document structure.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dokument: a Dokument instance (should be prefetched with related data)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
docx.Document instance ready to be saved
|
||||||
|
"""
|
||||||
|
doc = Document(TEMPLATE_PATH)
|
||||||
|
|
||||||
|
# Clear all body content while preserving page-layout section properties
|
||||||
|
body = doc.element.body
|
||||||
|
for element in list(body):
|
||||||
|
if element.tag != qn('w:sectPr'):
|
||||||
|
body.remove(element)
|
||||||
|
|
||||||
|
# 1. Title
|
||||||
|
title_para = doc.add_paragraph(style='Normal')
|
||||||
|
run = title_para.add_run(f'Standard\nIT-Sicherheit-{dokument.name}')
|
||||||
|
run.bold = True
|
||||||
|
run.font.size = Pt(22)
|
||||||
|
|
||||||
|
# 2. Metadata table (uses the "Hermes" table style from the template)
|
||||||
|
meta_table = doc.add_table(rows=0, cols=2, style='Hermes')
|
||||||
|
meta_rows = [
|
||||||
|
('Identifikation:', dokument.nummer),
|
||||||
|
('Dokumentationsklasse:', dokument.dokumententyp.name if dokument.dokumententyp else ''),
|
||||||
|
('Gültig ab:', dokument.gueltigkeit_von.strftime('%d.%m.%Y') if dokument.gueltigkeit_von else ''),
|
||||||
|
('Gültig bis:', dokument.gueltigkeit_bis.strftime('%d.%m.%Y') if dokument.gueltigkeit_bis else ''),
|
||||||
|
('Klassifizierung:', ''),
|
||||||
|
('Verantwortliche Stelle:', 'Information Security Management BIT'),
|
||||||
|
('Autoren:', ', '.join(a.name for a in dokument.autoren.all())),
|
||||||
|
('Prüfende:', ', '.join(p.name for p in dokument.pruefende.all())),
|
||||||
|
]
|
||||||
|
for label, value in meta_rows:
|
||||||
|
row = meta_table.add_row()
|
||||||
|
row.cells[0].text = label
|
||||||
|
row.cells[1].text = value
|
||||||
|
|
||||||
|
# 3. Einleitung
|
||||||
|
doc.add_heading('Einleitung', level=1)
|
||||||
|
for abschnitt in dokument.einleitung_set.order_by('order'):
|
||||||
|
_add_abschnitt(doc, abschnitt)
|
||||||
|
|
||||||
|
# 4. Geltungsbereich
|
||||||
|
doc.add_heading('Geltungsbereich', level=1)
|
||||||
|
for abschnitt in dokument.geltungsbereich_set.order_by('order'):
|
||||||
|
_add_abschnitt(doc, abschnitt)
|
||||||
|
|
||||||
|
# 5. Vorgaben
|
||||||
|
doc.add_heading('Vorgaben', level=1)
|
||||||
|
|
||||||
|
vorgaben = list(
|
||||||
|
dokument.vorgaben
|
||||||
|
.order_by('thema__name', 'nummer')
|
||||||
|
.select_related('thema')
|
||||||
|
.prefetch_related(
|
||||||
|
'vorgabekurztext_set__abschnitttyp',
|
||||||
|
'vorgabelangtext_set__abschnitttyp',
|
||||||
|
'checklistenfragen',
|
||||||
|
'referenzen',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
current_thema = None
|
||||||
|
for vorgabe in vorgaben:
|
||||||
|
thema_name = vorgabe.thema.name if vorgabe.thema else ''
|
||||||
|
if thema_name != current_thema:
|
||||||
|
current_thema = thema_name
|
||||||
|
doc.add_heading(thema_name, level=2)
|
||||||
|
|
||||||
|
doc.add_heading(f'{vorgabe.Vorgabennummer()} \u2013 {vorgabe.titel}', level=4)
|
||||||
|
|
||||||
|
for kt in vorgabe.vorgabekurztext_set.order_by('order'):
|
||||||
|
_add_abschnitt(doc, kt, default_style='Kurztext')
|
||||||
|
|
||||||
|
for lt in vorgabe.vorgabelangtext_set.order_by('order'):
|
||||||
|
_add_abschnitt(doc, lt)
|
||||||
|
|
||||||
|
fragen = list(vorgabe.checklistenfragen.all())
|
||||||
|
if fragen:
|
||||||
|
doc.add_paragraph('Checklistenfragen', style='Normal')
|
||||||
|
for frage in fragen:
|
||||||
|
doc.add_paragraph(frage.frage, style='Normal')
|
||||||
|
|
||||||
|
refs = list(vorgabe.referenzen.all())
|
||||||
|
if refs:
|
||||||
|
doc.add_paragraph('Referenzen: ' + ', '.join(r.Path() for r in refs), style='Normal')
|
||||||
|
|
||||||
|
# 6. Checkliste
|
||||||
|
doc.add_heading('Checkliste', level=1)
|
||||||
|
|
||||||
|
all_fragen = [
|
||||||
|
(vorgabe.Vorgabennummer(), frage.frage)
|
||||||
|
for vorgabe in vorgaben
|
||||||
|
for frage in vorgabe.checklistenfragen.all()
|
||||||
|
]
|
||||||
|
if all_fragen:
|
||||||
|
checklist_table = doc.add_table(rows=1, cols=3, style='Normal Table')
|
||||||
|
header = checklist_table.rows[0].cells
|
||||||
|
header[0].text = '#'
|
||||||
|
header[1].text = 'Bezeichnung (WAS)'
|
||||||
|
header[2].text = 'Richtlinieregel (WIE)'
|
||||||
|
for vorgabe_num, frage_text in all_fragen:
|
||||||
|
row = checklist_table.add_row()
|
||||||
|
row.cells[0].text = vorgabe_num
|
||||||
|
row.cells[1].text = ''
|
||||||
|
row.cells[2].text = frage_text
|
||||||
|
|
||||||
|
# 7. Changelog
|
||||||
|
changelog_entries = list(
|
||||||
|
dokument.changelog.order_by('-datum').prefetch_related('autoren')
|
||||||
|
)
|
||||||
|
if changelog_entries:
|
||||||
|
doc.add_heading('Änderungskontrolle', level=1)
|
||||||
|
changelog_table = doc.add_table(rows=1, cols=4, style='Tabelle')
|
||||||
|
header = changelog_table.rows[0].cells
|
||||||
|
header[0].text = 'Wann'
|
||||||
|
header[1].text = 'Version'
|
||||||
|
header[2].text = 'Wer'
|
||||||
|
header[3].text = 'Beschreibung'
|
||||||
|
for cl in changelog_entries:
|
||||||
|
row = changelog_table.add_row()
|
||||||
|
row.cells[0].text = cl.datum.strftime('%d.%m.%Y') if cl.datum else ''
|
||||||
|
row.cells[1].text = ''
|
||||||
|
row.cells[2].text = ', '.join(a.name for a in cl.autoren.all())
|
||||||
|
row.cells[3].text = cl.aenderung
|
||||||
|
|
||||||
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
def _add_abschnitt(doc, abschnitt, default_style='Normal'):
|
||||||
|
"""
|
||||||
|
Add a single Textabschnitt to the document, respecting its section type.
|
||||||
|
"""
|
||||||
|
typ = abschnitt.abschnitttyp.abschnitttyp if abschnitt.abschnitttyp else 'text'
|
||||||
|
inhalt = abschnitt.inhalt or ''
|
||||||
|
|
||||||
|
if not inhalt.strip():
|
||||||
|
return
|
||||||
|
|
||||||
|
if typ == 'text':
|
||||||
|
doc.add_paragraph(inhalt, style=default_style)
|
||||||
|
|
||||||
|
elif typ in ('liste ungeordnet', 'liste geordnet'):
|
||||||
|
for line in inhalt.strip().split('\n'):
|
||||||
|
line = line.strip().lstrip('- ').lstrip('* ')
|
||||||
|
if line:
|
||||||
|
doc.add_paragraph(line, style='Normal')
|
||||||
|
|
||||||
|
elif typ == 'tabelle':
|
||||||
|
_add_markdown_table(doc, inhalt)
|
||||||
|
|
||||||
|
else:
|
||||||
|
doc.add_paragraph(inhalt, style='Normal')
|
||||||
|
|
||||||
|
|
||||||
|
def _add_markdown_table(doc, markdown_content):
|
||||||
|
"""
|
||||||
|
Parse a markdown table and add it to the document as a Word table.
|
||||||
|
Falls back to a plain paragraph if parsing fails.
|
||||||
|
"""
|
||||||
|
lines = [line.strip() for line in markdown_content.strip().split('\n') if line.strip()]
|
||||||
|
if len(lines) < 2:
|
||||||
|
doc.add_paragraph(markdown_content, style='Normal')
|
||||||
|
return
|
||||||
|
|
||||||
|
header_cells = [c.strip() for c in lines[0].split('|') if c.strip()]
|
||||||
|
if not header_cells:
|
||||||
|
doc.add_paragraph(markdown_content, style='Normal')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Skip the separator row (dashes), collect data rows
|
||||||
|
data_lines = [
|
||||||
|
line for line in lines[2:]
|
||||||
|
if not all(c in '-|: ' for c in line)
|
||||||
|
]
|
||||||
|
|
||||||
|
table = doc.add_table(rows=1, cols=len(header_cells), style='Table Grid')
|
||||||
|
header_row = table.rows[0].cells
|
||||||
|
for i, cell_text in enumerate(header_cells):
|
||||||
|
if i < len(header_row):
|
||||||
|
header_row[i].text = cell_text
|
||||||
|
|
||||||
|
for line in data_lines:
|
||||||
|
row_cells = [c.strip() for c in line.split('|') if c.strip()]
|
||||||
|
row = table.add_row()
|
||||||
|
for i, cell_text in enumerate(row_cells):
|
||||||
|
if i < len(row.cells):
|
||||||
|
row.cells[i].text = cell_text
|
||||||
55
dokumente/management/commands/export_docx.py
Normal file
55
dokumente/management/commands/export_docx.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from dokumente.models import Dokument
|
||||||
|
from dokumente.docx_utils import build_standard_docx
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Export a Dokument (or all active Dokumente) as Word (.docx) files'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--nummer',
|
||||||
|
type=str,
|
||||||
|
help='Document number to export (e.g. R0129). Omit to export all active documents.',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--output',
|
||||||
|
type=str,
|
||||||
|
default='.',
|
||||||
|
help='Output directory (default: current directory)',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
output_dir = options['output']
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
if options['nummer']:
|
||||||
|
try:
|
||||||
|
dokumente = [Dokument.objects.prefetch_related(
|
||||||
|
'autoren', 'pruefende', 'vorgaben__thema',
|
||||||
|
'vorgaben__referenzen', 'vorgaben__checklistenfragen',
|
||||||
|
'vorgaben__vorgabekurztext_set__abschnitttyp',
|
||||||
|
'vorgaben__vorgabelangtext_set__abschnitttyp',
|
||||||
|
'geltungsbereich_set__abschnitttyp',
|
||||||
|
'einleitung_set__abschnitttyp',
|
||||||
|
'changelog__autoren',
|
||||||
|
).get(nummer=options['nummer'])]
|
||||||
|
except Dokument.DoesNotExist:
|
||||||
|
raise CommandError(f"Dokument with nummer '{options['nummer']}' not found.")
|
||||||
|
else:
|
||||||
|
dokumente = Dokument.objects.filter(aktiv=True).prefetch_related(
|
||||||
|
'autoren', 'pruefende', 'vorgaben__thema',
|
||||||
|
'vorgaben__referenzen', 'vorgaben__checklistenfragen',
|
||||||
|
'vorgaben__vorgabekurztext_set__abschnitttyp',
|
||||||
|
'vorgaben__vorgabelangtext_set__abschnitttyp',
|
||||||
|
'geltungsbereich_set__abschnitttyp',
|
||||||
|
'einleitung_set__abschnitttyp',
|
||||||
|
'changelog__autoren',
|
||||||
|
).order_by('nummer')
|
||||||
|
|
||||||
|
for dokument in dokumente:
|
||||||
|
doc = build_standard_docx(dokument)
|
||||||
|
filename = os.path.join(output_dir, f'{dokument.nummer}.docx')
|
||||||
|
doc.save(filename)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f'Exported {dokument.nummer} → {filename}'))
|
||||||
@@ -214,6 +214,11 @@
|
|||||||
download="{{ standard.nummer }}.xml">
|
download="{{ standard.nummer }}.xml">
|
||||||
XML herunterladen
|
XML herunterladen
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'standard_docx' standard.nummer %}"
|
||||||
|
class="btn btn-secondary icon icon--before icon--download"
|
||||||
|
download="{{ standard.nummer }}.docx">
|
||||||
|
DOCX herunterladen (Prototyp)
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ urlpatterns = [
|
|||||||
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
|
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
|
||||||
path('<str:nummer>/json/', views.standard_json, name='standard_json'),
|
path('<str:nummer>/json/', views.standard_json, name='standard_json'),
|
||||||
path('<str:nummer>/xml/', views.standard_xml, name='standard_xml'),
|
path('<str:nummer>/xml/', views.standard_xml, name='standard_xml'),
|
||||||
|
path('<str:nummer>/docx/', views.standard_docx, name='standard_docx'),
|
||||||
path('comments/<int:vorgabe_id>/', views.get_vorgabe_comments, name='get_vorgabe_comments'),
|
path('comments/<int:vorgabe_id>/', views.get_vorgabe_comments, name='get_vorgabe_comments'),
|
||||||
path('comments/<int:vorgabe_id>/add/', views.add_vorgabe_comment, name='add_vorgabe_comment'),
|
path('comments/<int:vorgabe_id>/add/', views.add_vorgabe_comment, name='add_vorgabe_comment'),
|
||||||
path('comments/delete/<int:comment_id>/', views.delete_vorgabe_comment, name='delete_vorgabe_comment'),
|
path('comments/delete/<int:comment_id>/', views.delete_vorgabe_comment, name='delete_vorgabe_comment'),
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ from django.views.decorators.http import require_POST
|
|||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.utils.html import escape, mark_safe
|
from django.utils.html import escape, mark_safe
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
|
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
|
||||||
from .utils import build_dokument_xml_element, prettify_xml
|
from .utils import build_dokument_xml_element, prettify_xml
|
||||||
|
from .docx_utils import build_standard_docx
|
||||||
from abschnitte.utils import render_textabschnitte
|
from abschnitte.utils import render_textabschnitte
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@@ -256,6 +258,37 @@ def standard_json(request, nummer):
|
|||||||
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
|
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
|
||||||
|
|
||||||
|
|
||||||
|
def standard_docx(request, nummer):
|
||||||
|
"""
|
||||||
|
Export a single Dokument as a Word (.docx) file.
|
||||||
|
"""
|
||||||
|
dokument = get_object_or_404(
|
||||||
|
Dokument.objects.prefetch_related(
|
||||||
|
'autoren', 'pruefende', 'vorgaben__thema',
|
||||||
|
'vorgaben__referenzen', 'vorgaben__checklistenfragen',
|
||||||
|
'vorgaben__vorgabekurztext_set__abschnitttyp',
|
||||||
|
'vorgaben__vorgabelangtext_set__abschnitttyp',
|
||||||
|
'geltungsbereich_set__abschnitttyp',
|
||||||
|
'einleitung_set__abschnitttyp',
|
||||||
|
'changelog__autoren',
|
||||||
|
),
|
||||||
|
nummer=nummer,
|
||||||
|
)
|
||||||
|
|
||||||
|
doc = build_standard_docx(dokument)
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
doc.save(buffer)
|
||||||
|
buffer.seek(0)
|
||||||
|
|
||||||
|
response = HttpResponse(
|
||||||
|
buffer.read(),
|
||||||
|
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
)
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="{dokument.nummer}.docx"'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def standard_xml(request, nummer):
|
def standard_xml(request, nummer):
|
||||||
"""
|
"""
|
||||||
Export a single Dokument as XML
|
Export a single Dokument as XML
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ charset-normalizer==3.4.4
|
|||||||
coverage==7.13.1
|
coverage==7.13.1
|
||||||
curtsies==0.4.3
|
curtsies==0.4.3
|
||||||
cwcwidth==0.1.12
|
cwcwidth==0.1.12
|
||||||
Django==6.0.1
|
Django==6.0.3
|
||||||
django-admin-sortable2==2.3
|
django-admin-sortable2==2.3
|
||||||
django-js-asset==3.1.2
|
django-js-asset==3.1.2
|
||||||
django-mptt==0.18.0
|
django-mptt==0.18.0
|
||||||
@@ -19,6 +19,7 @@ greenlet==3.3.0
|
|||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
idna==3.11
|
idna==3.11
|
||||||
jedi==0.19.2
|
jedi==0.19.2
|
||||||
|
lxml==6.0.2
|
||||||
jproperties==2.1.2
|
jproperties==2.1.2
|
||||||
Markdown==3.10
|
Markdown==3.10
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
@@ -30,6 +31,7 @@ pyfakefs==5.9.3
|
|||||||
Pygments==2.19.2
|
Pygments==2.19.2
|
||||||
pysonar==1.2.1.3951
|
pysonar==1.2.1.3951
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
|
python-docx==1.2.0
|
||||||
python-monkey-business==1.1.0
|
python-monkey-business==1.1.0
|
||||||
pyxdg==0.28
|
pyxdg==0.28
|
||||||
PyYAML==6.0.3
|
PyYAML==6.0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user