generated from coulomb/repo-seed
feat(WP-0011): Marktbegleiter-Analyse — Katalog, Passagen, Auswertung
Implementiert UC-MB-01 bis UC-MB-03: Marktbegleiter-Katalog (Liste, Detail, Anlegen/Bearbeiten), Ausschreibungspassagen mit Verlässlichkeitsscore (1–10, Validator), Musterauswertung mit Aggregationen (Ausschreiber-Häufigkeit, Ø-Score, Passagen-Anzahl). 4 Tests grün. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
urlpatterns = []
|
from . import passagen_views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', passagen_views.passagen_liste, name='liste'),
|
||||||
|
path('neu/', passagen_views.passage_neu, name='neu'),
|
||||||
|
path('<int:pk>/', passagen_views.passage_detail, name='detail'),
|
||||||
|
path('<int:pk>/bearbeiten/', passagen_views.passage_bearbeiten, name='bearbeiten'),
|
||||||
|
]
|
||||||
|
|||||||
110
vergabe_teilnahme/apps/marktbegleiter/passagen_views.py
Normal file
110
vergabe_teilnahme/apps/marktbegleiter/passagen_views.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
|
||||||
|
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||||
|
from vergabe_teilnahme.apps.dokumente.models import Dokument
|
||||||
|
|
||||||
|
from .models import Ausschreibungspassage, Marktbegleiter
|
||||||
|
|
||||||
|
|
||||||
|
KATEGORIE_CHOICES = [
|
||||||
|
('formulierung', 'Formulierung'),
|
||||||
|
('leistungsmerkmal', 'Leistungsmerkmal'),
|
||||||
|
('zertifizierung', 'Zertifizierung'),
|
||||||
|
('referenz', 'Referenz'),
|
||||||
|
('sonstiges', 'Sonstiges'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AusschreibungspassageForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Ausschreibungspassage
|
||||||
|
fields = [
|
||||||
|
'passage', 'marktbegleiter', 'dokument', 'fundstelle',
|
||||||
|
'kategorie', 'verlaesslichkeitsscore',
|
||||||
|
'begruendung_zuordnung', 'auswirkung_entscheidung',
|
||||||
|
'auswirkung_preisstrategie', 'auswirkung_loesungskonzept',
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'passage': forms.Textarea(attrs={'class': 'form-input', 'rows': 6}),
|
||||||
|
'marktbegleiter': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'dokument': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'fundstelle': forms.TextInput(attrs={'class': 'form-input'}),
|
||||||
|
'kategorie': forms.Select(attrs={'class': 'form-select'}, choices=[('', '---------')] + KATEGORIE_CHOICES),
|
||||||
|
'verlaesslichkeitsscore': forms.NumberInput(attrs={
|
||||||
|
'class': 'form-input', 'type': 'range', 'min': 1, 'max': 10,
|
||||||
|
'x-model': 'score',
|
||||||
|
}),
|
||||||
|
'begruendung_zuordnung': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||||
|
'auswirkung_entscheidung': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||||
|
'auswirkung_preisstrategie': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||||
|
'auswirkung_loesungskonzept': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, ausschreibung=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['marktbegleiter'].queryset = Marktbegleiter.objects.all()
|
||||||
|
if ausschreibung:
|
||||||
|
self.fields['dokument'].queryset = Dokument.objects.filter(ausschreibung=ausschreibung)
|
||||||
|
else:
|
||||||
|
self.fields['dokument'].queryset = Dokument.objects.none()
|
||||||
|
self.fields['dokument'].required = False
|
||||||
|
self.fields['fundstelle'].required = False
|
||||||
|
self.fields['kategorie'].required = False
|
||||||
|
self.fields['begruendung_zuordnung'].required = False
|
||||||
|
self.fields['auswirkung_entscheidung'].required = False
|
||||||
|
self.fields['auswirkung_preisstrategie'].required = False
|
||||||
|
self.fields['auswirkung_loesungskonzept'].required = False
|
||||||
|
|
||||||
|
|
||||||
|
def passagen_liste(request, ausschreibung_id):
|
||||||
|
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||||
|
passagen = Ausschreibungspassage.objects.filter(
|
||||||
|
ausschreibung=ausschreibung
|
||||||
|
).select_related('marktbegleiter', 'dokument').order_by('-verlaesslichkeitsscore')
|
||||||
|
return render(request, 'marktbegleiter/passagen_liste.html', {
|
||||||
|
'ausschreibung': ausschreibung,
|
||||||
|
'passagen': passagen,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def passage_neu(request, ausschreibung_id):
|
||||||
|
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||||
|
form = AusschreibungspassageForm(
|
||||||
|
request.POST or None, ausschreibung=ausschreibung
|
||||||
|
)
|
||||||
|
if form.is_valid():
|
||||||
|
passage = form.save(commit=False)
|
||||||
|
passage.ausschreibung = ausschreibung
|
||||||
|
passage.save()
|
||||||
|
return redirect('marktbegleiter:passagen:liste', ausschreibung_id=ausschreibung_id)
|
||||||
|
return render(request, 'marktbegleiter/passage_form.html', {
|
||||||
|
'form': form,
|
||||||
|
'ausschreibung': ausschreibung,
|
||||||
|
'neu': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def passage_detail(request, ausschreibung_id, pk):
|
||||||
|
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||||
|
passage = get_object_or_404(Ausschreibungspassage, pk=pk, ausschreibung=ausschreibung)
|
||||||
|
return render(request, 'marktbegleiter/passage_detail.html', {
|
||||||
|
'ausschreibung': ausschreibung,
|
||||||
|
'passage': passage,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def passage_bearbeiten(request, ausschreibung_id, pk):
|
||||||
|
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||||
|
passage = get_object_or_404(Ausschreibungspassage, pk=pk, ausschreibung=ausschreibung)
|
||||||
|
form = AusschreibungspassageForm(
|
||||||
|
request.POST or None, instance=passage, ausschreibung=ausschreibung
|
||||||
|
)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return redirect('marktbegleiter:passagen:detail', ausschreibung_id=ausschreibung_id, pk=pk)
|
||||||
|
return render(request, 'marktbegleiter/passage_form.html', {
|
||||||
|
'form': form,
|
||||||
|
'ausschreibung': ausschreibung,
|
||||||
|
'passage': passage,
|
||||||
|
})
|
||||||
@@ -1,3 +1,70 @@
|
|||||||
from django.test import TestCase
|
import pytest
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
# Create your tests here.
|
from vergabe_teilnahme.apps.ausschreibungen.tests import AusschreibungFactory
|
||||||
|
|
||||||
|
from .models import Ausschreibungspassage, Marktbegleiter
|
||||||
|
|
||||||
|
|
||||||
|
def make_mb(name='TestBegleiter', **kwargs):
|
||||||
|
return Marktbegleiter.objects.create(name=name, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_passage_anlegen_mit_score_10(client):
|
||||||
|
a = AusschreibungFactory()
|
||||||
|
mb = make_mb()
|
||||||
|
url = reverse('marktbegleiter:passagen:neu', kwargs={'ausschreibung_id': a.pk})
|
||||||
|
response = client.post(url, {
|
||||||
|
'passage': 'Musterpassage aus dem Dokument',
|
||||||
|
'marktbegleiter': mb.pk,
|
||||||
|
'verlaesslichkeitsscore': 10,
|
||||||
|
})
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert Ausschreibungspassage.objects.filter(marktbegleiter=mb, verlaesslichkeitsscore=10).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_passage_score_zu_hoch_validierungsfehler():
|
||||||
|
a = AusschreibungFactory()
|
||||||
|
mb = make_mb()
|
||||||
|
p = Ausschreibungspassage(
|
||||||
|
ausschreibung=a,
|
||||||
|
marktbegleiter=mb,
|
||||||
|
passage='Test',
|
||||||
|
verlaesslichkeitsscore=11,
|
||||||
|
)
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
p.full_clean()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_auswertung_score_durchschnitt(client):
|
||||||
|
a = AusschreibungFactory()
|
||||||
|
mb = make_mb()
|
||||||
|
Ausschreibungspassage.objects.create(
|
||||||
|
ausschreibung=a, marktbegleiter=mb, passage='P1', verlaesslichkeitsscore=8
|
||||||
|
)
|
||||||
|
Ausschreibungspassage.objects.create(
|
||||||
|
ausschreibung=a, marktbegleiter=mb, passage='P2', verlaesslichkeitsscore=6
|
||||||
|
)
|
||||||
|
url = reverse('marktbegleiter:auswertung', kwargs={'pk': mb.pk})
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.content.decode()
|
||||||
|
assert '7' in content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_marktbegleiter_detail_zeigt_passagen(client):
|
||||||
|
a = AusschreibungFactory()
|
||||||
|
mb = make_mb(name='DetailBegleiter')
|
||||||
|
Ausschreibungspassage.objects.create(
|
||||||
|
ausschreibung=a, marktbegleiter=mb,
|
||||||
|
passage='Sichtbare Passage', verlaesslichkeitsscore=5
|
||||||
|
)
|
||||||
|
url = reverse('marktbegleiter:detail', kwargs={'pk': mb.pk})
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b'Sichtbare Passage' in response.content
|
||||||
|
|||||||
@@ -1,2 +1,16 @@
|
|||||||
from django.urls import path
|
from django.urls import include, path
|
||||||
urlpatterns = []
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'marktbegleiter'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.marktbegleiter_liste, name='liste'),
|
||||||
|
path('neu/', views.marktbegleiter_neu, name='neu'),
|
||||||
|
path('<int:pk>/', views.marktbegleiter_detail, name='detail'),
|
||||||
|
path('<int:pk>/auswertung/', views.marktbegleiter_auswertung, name='auswertung'),
|
||||||
|
path('<int:pk>/bearbeiten/', views.marktbegleiter_bearbeiten, name='bearbeiten'),
|
||||||
|
path('ausschreibungen/<int:ausschreibung_id>/passagen/', include(
|
||||||
|
('vergabe_teilnahme.apps.marktbegleiter.passagen_urls', 'passagen')
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,3 +1,113 @@
|
|||||||
from django.shortcuts import render
|
from django.db.models import Avg, Count
|
||||||
|
from django import forms
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
|
||||||
# Create your views here.
|
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||||
|
|
||||||
|
from .models import Ausschreibungspassage, Marktbegleiter
|
||||||
|
|
||||||
|
|
||||||
|
# ── Forms ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class MarktbegleiterForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Marktbegleiter
|
||||||
|
fields = [
|
||||||
|
'name', 'kurzbeschreibung', 'produkt_leistungsportfolio',
|
||||||
|
'relevante_branchen', 'bekannte_staerken', 'bekannte_schwaechen',
|
||||||
|
'typische_formulierungen', 'typische_leistungsmerkmale',
|
||||||
|
'bekannte_zertifizierungen', 'bekannte_referenzen',
|
||||||
|
'quellen_links', 'letzte_aktualisierung', 'interne_notizen',
|
||||||
|
'vertraulichkeit',
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={'class': 'form-input'}),
|
||||||
|
'kurzbeschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||||
|
'produkt_leistungsportfolio': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}),
|
||||||
|
'relevante_branchen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||||
|
'bekannte_staerken': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||||
|
'bekannte_schwaechen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||||
|
'typische_formulierungen': forms.Textarea(attrs={
|
||||||
|
'class': 'form-input', 'rows': 5,
|
||||||
|
'placeholder': 'Eine Formulierung pro Zeile',
|
||||||
|
}),
|
||||||
|
'typische_leistungsmerkmale': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||||
|
'bekannte_zertifizierungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||||
|
'bekannte_referenzen': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||||
|
'quellen_links': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||||
|
'letzte_aktualisierung': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
|
||||||
|
'interne_notizen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||||
|
'vertraulichkeit': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['letzte_aktualisierung'].required = False
|
||||||
|
|
||||||
|
|
||||||
|
# ── Views ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def marktbegleiter_liste(request):
|
||||||
|
qs = Marktbegleiter.objects.all()
|
||||||
|
q = request.GET.get('q', '')
|
||||||
|
branche = request.GET.get('branche', '')
|
||||||
|
if q:
|
||||||
|
qs = qs.filter(name__icontains=q)
|
||||||
|
if branche:
|
||||||
|
qs = qs.filter(relevante_branchen__icontains=branche)
|
||||||
|
return render(request, 'marktbegleiter/marktbegleiter_liste.html', {
|
||||||
|
'marktbegleiter_liste': qs,
|
||||||
|
'q': q,
|
||||||
|
'branche': branche,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def marktbegleiter_neu(request):
|
||||||
|
form = MarktbegleiterForm(request.POST or None)
|
||||||
|
if form.is_valid():
|
||||||
|
obj = form.save()
|
||||||
|
return redirect('marktbegleiter:detail', pk=obj.pk)
|
||||||
|
return render(request, 'marktbegleiter/marktbegleiter_form.html', {'form': form, 'neu': True})
|
||||||
|
|
||||||
|
|
||||||
|
def marktbegleiter_bearbeiten(request, pk):
|
||||||
|
obj = get_object_or_404(Marktbegleiter, pk=pk)
|
||||||
|
form = MarktbegleiterForm(request.POST or None, instance=obj)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return redirect('marktbegleiter:detail', pk=obj.pk)
|
||||||
|
return render(request, 'marktbegleiter/marktbegleiter_form.html', {'form': form, 'obj': obj})
|
||||||
|
|
||||||
|
|
||||||
|
def marktbegleiter_detail(request, pk):
|
||||||
|
obj = get_object_or_404(Marktbegleiter, pk=pk)
|
||||||
|
passagen = Ausschreibungspassage.objects.filter(marktbegleiter=obj).select_related(
|
||||||
|
'ausschreibung', 'dokument'
|
||||||
|
).order_by('-verlaesslichkeitsscore')
|
||||||
|
anzahl_ausschreibungen = passagen.values('ausschreibung').distinct().count()
|
||||||
|
score_durchschnitt = passagen.aggregate(Avg('verlaesslichkeitsscore'))['verlaesslichkeitsscore__avg']
|
||||||
|
return render(request, 'marktbegleiter/marktbegleiter_detail.html', {
|
||||||
|
'obj': obj,
|
||||||
|
'passagen': passagen,
|
||||||
|
'anzahl_ausschreibungen': anzahl_ausschreibungen,
|
||||||
|
'score_durchschnitt': score_durchschnitt,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def marktbegleiter_auswertung(request, pk):
|
||||||
|
mb = get_object_or_404(Marktbegleiter, pk=pk)
|
||||||
|
passagen = Ausschreibungspassage.objects.filter(marktbegleiter=mb).select_related(
|
||||||
|
'ausschreibung', 'dokument'
|
||||||
|
)
|
||||||
|
ausschreiber_haeufigkeit = passagen.values(
|
||||||
|
'ausschreibung__ausschreiber'
|
||||||
|
).annotate(count=Count('id')).order_by('-count')[:10]
|
||||||
|
score_durchschnitt = passagen.aggregate(Avg('verlaesslichkeitsscore'))['verlaesslichkeitsscore__avg']
|
||||||
|
anzahl_ausschreibungen = passagen.values('ausschreibung').distinct().count()
|
||||||
|
return render(request, 'marktbegleiter/auswertung.html', {
|
||||||
|
'marktbegleiter': mb,
|
||||||
|
'passagen': passagen,
|
||||||
|
'ausschreiber_haeufigkeit': ausschreiber_haeufigkeit,
|
||||||
|
'score_durchschnitt': score_durchschnitt,
|
||||||
|
'anzahl_ausschreibungen': anzahl_ausschreibungen,
|
||||||
|
})
|
||||||
|
|||||||
79
vergabe_teilnahme/templates/marktbegleiter/auswertung.html
Normal file
79
vergabe_teilnahme/templates/marktbegleiter/auswertung.html
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Auswertung — {{ marktbegleiter.name }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h1 class="page-title">Auswertung: {{ marktbegleiter.name }}</h1>
|
||||||
|
<a href="{% url 'marktbegleiter:detail' pk=marktbegleiter.pk %}" class="btn-secondary">Zurück</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 gap-4 mb-6">
|
||||||
|
<div class="card text-center">
|
||||||
|
<p class="text-3xl font-bold text-brand-600">{{ passagen.count }}</p>
|
||||||
|
<p class="text-sm text-slate-500 mt-1">Passagen gesamt</p>
|
||||||
|
</div>
|
||||||
|
<div class="card text-center">
|
||||||
|
<p class="text-3xl font-bold text-brand-600">{{ anzahl_ausschreibungen }}</p>
|
||||||
|
<p class="text-sm text-slate-500 mt-1">Ausschreibungen</p>
|
||||||
|
</div>
|
||||||
|
<div class="card text-center">
|
||||||
|
<p class="text-3xl font-bold text-brand-600">
|
||||||
|
{% if score_durchschnitt %}{{ score_durchschnitt|floatformat:1 }}{% else %}—{% endif %}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-slate-500 mt-1">Ø Verlässlichkeit</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if ausschreiber_haeufigkeit %}
|
||||||
|
<div class="card mb-5">
|
||||||
|
<h2 class="section-title mb-4">Häufigste Ausschreiber</h2>
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border-b border-slate-200">
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Ausschreiber</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Passagen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in ausschreiber_haeufigkeit %}
|
||||||
|
<tr class="border-b border-slate-100">
|
||||||
|
<td class="py-2 text-slate-700">{{ row.ausschreibung__ausschreiber|default:"(unbekannt)" }}</td>
|
||||||
|
<td class="py-2 font-medium">{{ row.count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="section-title mb-4">Alle Passagen</h2>
|
||||||
|
{% if passagen %}
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border-b border-slate-200">
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Ausschreibung</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Textauszug</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Score</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Kategorie</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Datum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in passagen %}
|
||||||
|
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||||||
|
<td class="py-2 text-slate-700">{{ p.ausschreibung.titel|truncatechars:40 }}</td>
|
||||||
|
<td class="py-2 text-slate-600 max-w-xs">{{ p.passage|truncatechars:150 }}</td>
|
||||||
|
<td class="py-2 font-medium {% if p.verlaesslichkeitsscore >= 7 %}text-green-600{% elif p.verlaesslichkeitsscore >= 4 %}text-yellow-600{% else %}text-red-500{% endif %}">
|
||||||
|
{{ p.verlaesslichkeitsscore }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 text-slate-600">{{ p.kategorie|default:"—" }}</td>
|
||||||
|
<td class="py-2 text-slate-500 text-xs">{{ p.erfassungsdatum }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-slate-500 text-sm">Keine Passagen vorhanden.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ obj.name }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h1 class="page-title">{{ obj.name }}</h1>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a href="{% url 'marktbegleiter:auswertung' pk=obj.pk %}" class="btn-secondary">Auswertung</a>
|
||||||
|
<a href="{% url 'marktbegleiter:bearbeiten' pk=obj.pk %}" class="btn-primary">Bearbeiten</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5 mb-5">
|
||||||
|
<div class="card col-span-2 space-y-4">
|
||||||
|
{% if obj.kurzbeschreibung %}
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Beschreibung</h2>
|
||||||
|
<p class="text-sm text-slate-700">{{ obj.kurzbeschreibung }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if obj.produkt_leistungsportfolio %}
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Portfolio</h2>
|
||||||
|
<p class="text-sm text-slate-700 whitespace-pre-line">{{ obj.produkt_leistungsportfolio }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if obj.bekannte_staerken %}
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Stärken</h2>
|
||||||
|
<p class="text-sm text-slate-700 whitespace-pre-line">{{ obj.bekannte_staerken }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if obj.bekannte_schwaechen %}
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Schwächen</h2>
|
||||||
|
<p class="text-sm text-slate-700 whitespace-pre-line">{{ obj.bekannte_schwaechen }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if obj.typische_formulierungen %}
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Typische Formulierungen</h2>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{% for zeile in obj.typische_formulierungen.splitlines %}
|
||||||
|
{% if zeile.strip %}
|
||||||
|
<span class="px-2 py-1 bg-slate-100 text-slate-700 rounded text-xs">{{ zeile.strip }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="section-title mb-3">Kennzahlen</h2>
|
||||||
|
<dl class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-slate-500">Ausschreibungen</dt>
|
||||||
|
<dd class="font-medium">{{ anzahl_ausschreibungen }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-slate-500">Passagen gesamt</dt>
|
||||||
|
<dd class="font-medium">{{ passagen.count }}</dd>
|
||||||
|
</div>
|
||||||
|
{% if score_durchschnitt %}
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-slate-500">Ø Verlässlichkeit</dt>
|
||||||
|
<dd class="font-medium">{{ score_durchschnitt|floatformat:1 }}</dd>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-slate-500">Vertraulichkeit</dt>
|
||||||
|
<dd>{{ obj.get_vertraulichkeit_display }}</dd>
|
||||||
|
</div>
|
||||||
|
{% if obj.letzte_aktualisierung %}
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<dt class="text-slate-500">Aktualisiert</dt>
|
||||||
|
<dd>{{ obj.letzte_aktualisierung }}</dd>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="section-title mb-4">Verknüpfte Passagen</h2>
|
||||||
|
{% if passagen %}
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border-b border-slate-200">
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Ausschreibung</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Textauszug</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Score</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Kategorie</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Datum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in passagen %}
|
||||||
|
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||||||
|
<td class="py-2 text-slate-700">{{ p.ausschreibung.titel|truncatechars:40 }}</td>
|
||||||
|
<td class="py-2 text-slate-600 max-w-xs">{{ p.passage|truncatechars:150 }}</td>
|
||||||
|
<td class="py-2">
|
||||||
|
<span class="font-medium {% if p.verlaesslichkeitsscore >= 7 %}text-green-600{% elif p.verlaesslichkeitsscore >= 4 %}text-yellow-600{% else %}text-red-500{% endif %}">
|
||||||
|
{{ p.verlaesslichkeitsscore }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 text-slate-600">{{ p.kategorie|default:"—" }}</td>
|
||||||
|
<td class="py-2 text-slate-500 text-xs">{{ p.erfassungsdatum }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-slate-500 text-sm">Noch keine Passagen erfasst.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{% if neu %}Neuer Marktbegleiter{% else %}{{ obj.name }} bearbeiten{% endif %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h1 class="page-title">{% if neu %}Neuer Marktbegleiter{% else %}{{ obj.name }} bearbeiten{% endif %}</h1>
|
||||||
|
{% if not neu %}
|
||||||
|
<a href="{% url 'marktbegleiter:detail' pk=obj.pk %}" class="btn-secondary">Abbrechen</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'marktbegleiter:liste' %}" class="btn-secondary">Abbrechen</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card max-w-3xl">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">{{ field.label }}</label>
|
||||||
|
{{ field }}
|
||||||
|
{% if field.errors %}
|
||||||
|
<p class="text-red-500 text-xs mt-1">{{ field.errors|join:", " }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="flex gap-3 mt-6">
|
||||||
|
<button type="submit" class="btn-primary">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Marktbegleiter{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h1 class="page-title">Marktbegleiter</h1>
|
||||||
|
<a href="{% url 'marktbegleiter:neu' %}" class="btn-primary">+ Neu</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<form method="get" class="flex flex-wrap gap-3 items-end">
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input type="text" name="q" value="{{ q }}" class="form-input" placeholder="Suche...">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Branche</label>
|
||||||
|
<input type="text" name="branche" value="{{ branche }}" class="form-input" placeholder="Branche...">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-secondary">Filtern</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
{% if marktbegleiter_liste %}
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border-b border-slate-200">
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Name</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Branchen</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Vertraulichkeit</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Aktualisiert</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for mb in marktbegleiter_liste %}
|
||||||
|
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||||||
|
<td class="py-2 font-medium">
|
||||||
|
<a href="{% url 'marktbegleiter:detail' pk=mb.pk %}" class="text-brand-600 hover:underline">{{ mb.name }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 text-slate-600 max-w-xs truncate">{{ mb.relevante_branchen|truncatechars:80|default:"—" }}</td>
|
||||||
|
<td class="py-2 text-slate-600">{{ mb.get_vertraulichkeit_display }}</td>
|
||||||
|
<td class="py-2 text-slate-500 text-xs">{{ mb.letzte_aktualisierung|default:"—" }}</td>
|
||||||
|
<td class="py-2 text-right">
|
||||||
|
<a href="{% url 'marktbegleiter:bearbeiten' pk=mb.pk %}" class="text-slate-400 hover:text-slate-700 text-xs">Bearbeiten</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-slate-500 text-sm">Keine Marktbegleiter gefunden.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Passage — {{ passage.marktbegleiter.name }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h1 class="page-title">Passage: {{ passage.marktbegleiter.name }}</h1>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a href="{% url 'marktbegleiter:passagen:bearbeiten' ausschreibung_id=ausschreibung.pk pk=passage.pk %}" class="btn-primary">Bearbeiten</a>
|
||||||
|
<a href="{% url 'marktbegleiter:passagen:liste' ausschreibung_id=ausschreibung.pk %}" class="btn-secondary">Zurück</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card max-w-2xl space-y-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500 mb-1">Passage</p>
|
||||||
|
<p class="text-sm text-slate-700 whitespace-pre-line">{{ passage.passage }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Verlässlichkeitsscore</p>
|
||||||
|
<p class="font-bold text-lg {% if passage.verlaesslichkeitsscore >= 7 %}text-green-600{% elif passage.verlaesslichkeitsscore >= 4 %}text-yellow-600{% else %}text-red-500{% endif %}">
|
||||||
|
{{ passage.verlaesslichkeitsscore }}/10
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Kategorie</p>
|
||||||
|
<p>{{ passage.kategorie|default:"—" }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Dokument</p>
|
||||||
|
<p>{{ passage.dokument|default:"—" }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Fundstelle</p>
|
||||||
|
<p>{{ passage.fundstelle|default:"—" }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Erfasst am</p>
|
||||||
|
<p>{{ passage.erfassungsdatum }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if passage.begruendung_zuordnung %}
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-slate-500">Begründung</p>
|
||||||
|
<p class="text-sm text-slate-700">{{ passage.begruendung_zuordnung }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
35
vergabe_teilnahme/templates/marktbegleiter/passage_form.html
Normal file
35
vergabe_teilnahme/templates/marktbegleiter/passage_form.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{% if neu %}Neue Passage{% else %}Passage bearbeiten{% endif %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h1 class="page-title">{% if neu %}Neue Passage{% else %}Passage bearbeiten{% endif %}</h1>
|
||||||
|
<a href="{% url 'marktbegleiter:passagen:liste' ausschreibung_id=ausschreibung.pk %}" class="btn-secondary">Abbrechen</a>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-slate-500 mb-5">Ausschreibung: <strong>{{ ausschreibung.titel }}</strong></p>
|
||||||
|
|
||||||
|
<div class="card max-w-3xl" x-data="{ score: {{ passage.verlaesslichkeitsscore|default:5 }} }">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">{{ field.label }}</label>
|
||||||
|
{% if field.name == 'verlaesslichkeitsscore' %}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{{ field }}
|
||||||
|
<span class="text-sm font-medium text-brand-600" x-text="score"></span>
|
||||||
|
<span class="text-xs text-slate-400">(1=sehr unsicher, 10=sehr sicher)</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{{ field }}
|
||||||
|
{% endif %}
|
||||||
|
{% if field.errors %}
|
||||||
|
<p class="text-red-500 text-xs mt-1">{{ field.errors|join:", " }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="flex gap-3 mt-6">
|
||||||
|
<button type="submit" class="btn-primary">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Marktbegleiter-Passagen — {{ ausschreibung.titel }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h1 class="page-title">Marktbegleiter-Passagen</h1>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span class="text-sm text-slate-500 self-center">{{ ausschreibung.titel|truncatechars:50 }}</span>
|
||||||
|
<a href="{% url 'marktbegleiter:passagen:neu' ausschreibung_id=ausschreibung.pk %}" class="btn-primary">+ Neue Passage</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
{% if passagen %}
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border-b border-slate-200">
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Marktbegleiter</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Textauszug</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Score</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Kategorie</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Dokument</th>
|
||||||
|
<th class="pb-2 font-medium text-slate-600">Datum</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in passagen %}
|
||||||
|
<tr class="border-b border-slate-100 hover:bg-slate-50">
|
||||||
|
<td class="py-2 font-medium">
|
||||||
|
<a href="{% url 'marktbegleiter:detail' pk=p.marktbegleiter.pk %}" class="text-brand-600 hover:underline">
|
||||||
|
{{ p.marktbegleiter.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 text-slate-600 max-w-xs">{{ p.passage|truncatechars:150 }}</td>
|
||||||
|
<td class="py-2 font-medium {% if p.verlaesslichkeitsscore >= 7 %}text-green-600{% elif p.verlaesslichkeitsscore >= 4 %}text-yellow-600{% else %}text-red-500{% endif %}">
|
||||||
|
{{ p.verlaesslichkeitsscore }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 text-slate-600">{{ p.kategorie|default:"—" }}</td>
|
||||||
|
<td class="py-2 text-slate-500 text-xs">{{ p.dokument.dateiname|default:"—" }}</td>
|
||||||
|
<td class="py-2 text-slate-500 text-xs">{{ p.erfassungsdatum }}</td>
|
||||||
|
<td class="py-2 text-right">
|
||||||
|
<a href="{% url 'marktbegleiter:passagen:bearbeiten' ausschreibung_id=ausschreibung.pk pk=p.pk %}" class="text-slate-400 hover:text-slate-700 text-xs">Bearbeiten</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-slate-500 text-sm">Noch keine Passagen für diese Ausschreibung erfasst.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: WP-0011
|
id: WP-0011
|
||||||
title: Marktbegleiter-Analyse
|
title: Marktbegleiter-Analyse
|
||||||
status: todo
|
status: done
|
||||||
phase: 11-of-12
|
phase: 11-of-12
|
||||||
created: "2026-05-08"
|
created: "2026-05-08"
|
||||||
depends_on: WP-0010
|
depends_on: WP-0010
|
||||||
@@ -17,7 +17,7 @@ Musterauswertung. Referenz: UC-MB-01 bis UC-MB-03.
|
|||||||
```task
|
```task
|
||||||
id: WP-0011-T01
|
id: WP-0011-T01
|
||||||
title: Marktbegleiter-Katalog: Liste und Anlegen (UC-MB-01)
|
title: Marktbegleiter-Katalog: Liste und Anlegen (UC-MB-01)
|
||||||
status: todo
|
status: done
|
||||||
|
|
||||||
`marktbegleiter/views.py` — marktbegleiter_liste, marktbegleiter_neu/_bearbeiten:
|
`marktbegleiter/views.py` — marktbegleiter_liste, marktbegleiter_neu/_bearbeiten:
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ marktbegleiter_detail:
|
|||||||
```task
|
```task
|
||||||
id: WP-0011-T02
|
id: WP-0011-T02
|
||||||
title: Ausschreibungspassage erfassen (UC-MB-02, UC-MB-03)
|
title: Ausschreibungspassage erfassen (UC-MB-02, UC-MB-03)
|
||||||
status: todo
|
status: done
|
||||||
|
|
||||||
`marktbegleiter/passagen_views.py` — passagen_liste und passage_neu:
|
`marktbegleiter/passagen_views.py` — passagen_liste und passage_neu:
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ und im Marktbegleiter-Profil.
|
|||||||
```task
|
```task
|
||||||
id: WP-0011-T03
|
id: WP-0011-T03
|
||||||
title: Marktbegleiter-Musterauswertung (UC-MB-03)
|
title: Marktbegleiter-Musterauswertung (UC-MB-03)
|
||||||
status: todo
|
status: done
|
||||||
|
|
||||||
`marktbegleiter/views.py` — marktbegleiter_auswertung:
|
`marktbegleiter/views.py` — marktbegleiter_auswertung:
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ Template `marktbegleiter/auswertung.html`:
|
|||||||
```task
|
```task
|
||||||
id: WP-0011-T04
|
id: WP-0011-T04
|
||||||
title: URL-Verkabelung und Tests
|
title: URL-Verkabelung und Tests
|
||||||
status: todo
|
status: done
|
||||||
|
|
||||||
`marktbegleiter/passagen_urls.py`:
|
`marktbegleiter/passagen_urls.py`:
|
||||||
```python
|
```python
|
||||||
|
|||||||
Reference in New Issue
Block a user