feat: serve isolated companies below a fixed product path
All checks were successful
All checks were successful
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
09e2239bc6
commit
9345a1bb1a
28 changed files with 350 additions and 117 deletions
|
|
@ -1,6 +1,7 @@
|
|||
from django.contrib.auth.middleware import LoginRequiredMiddleware
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.cache import add_never_cache_headers
|
||||
|
||||
|
||||
|
|
@ -10,12 +11,12 @@ class PilotLoginRequiredMiddleware(LoginRequiredMiddleware):
|
|||
def handle_no_permission(self, request, view_func):
|
||||
if request.headers.get('HX-Request') == 'true':
|
||||
# The original URL may render only a fragment or accept only POST.
|
||||
login = redirect_to_login('/', self.get_login_url(view_func))
|
||||
login = redirect_to_login(reverse('home'), self.get_login_url(view_func))
|
||||
return HttpResponse(status=401, headers={'HX-Redirect': login.url})
|
||||
return super().handle_no_permission(request, view_func)
|
||||
|
||||
def process_response(self, request, response):
|
||||
# Company records and account pages must not survive in shared caches.
|
||||
if request.path != '/health/':
|
||||
if request.path_info != '/health/':
|
||||
add_never_cache_headers(response)
|
||||
return response
|
||||
|
|
|
|||
120
vergabe_teilnahme/apps/accounts/test_tenant_path.py
Normal file
120
vergabe_teilnahme/apps/accounts/test_tenant_path.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""Regression for an isolated instance mounted below a fixed tenant path."""
|
||||
from html.parser import HTMLParser
|
||||
from urllib.parse import parse_qs, urlsplit
|
||||
|
||||
import pytest
|
||||
from django.test import Client
|
||||
from django.urls import get_script_prefix, set_script_prefix
|
||||
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
PREFIX = '/demo-company'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tenant_path(settings):
|
||||
previous = get_script_prefix()
|
||||
settings.FORCE_SCRIPT_NAME = PREFIX
|
||||
settings.APP_BASE_PATH = PREFIX
|
||||
settings.STATIC_URL = PREFIX + '/static/'
|
||||
settings.MEDIA_URL = PREFIX + '/media/'
|
||||
settings.SESSION_COOKIE_PATH = settings.CSRF_COOKIE_PATH = PREFIX + '/'
|
||||
settings.SESSION_COOKIE_NAME = 'vergabe_demo_company_sessionid'
|
||||
settings.CSRF_COOKIE_NAME = 'vergabe_demo_company_csrftoken'
|
||||
set_script_prefix(PREFIX)
|
||||
yield settings
|
||||
set_script_prefix(previous)
|
||||
|
||||
|
||||
class LocalLinks(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.links = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
for name, value in attrs:
|
||||
if name in {'href', 'src', 'action', 'hx-get', 'hx-post', 'hx-delete'} and value:
|
||||
if value.startswith('/'):
|
||||
self.links.append(value)
|
||||
|
||||
|
||||
def assert_prefixed(content):
|
||||
parser = LocalLinks()
|
||||
parser.feed(content.decode())
|
||||
assert parser.links
|
||||
assert all(link.startswith(PREFIX + '/') for link in parser.links), parser.links
|
||||
|
||||
|
||||
def test_anonymous_and_expired_fragments_stay_in_tenant_path(client, tenant_path):
|
||||
response = client.get('/ausschreibungen/', HTTP_X_FORWARDED_PREFIX='/other-company')
|
||||
assert response.status_code == 302
|
||||
assert urlsplit(response.url).path == PREFIX + '/accounts/login/'
|
||||
assert parse_qs(urlsplit(response.url).query)['next'] == [PREFIX + '/ausschreibungen/']
|
||||
response = client.get('/suche/?q=private', HTTP_HX_REQUEST='true')
|
||||
assert response.status_code == 401
|
||||
assert urlsplit(response['HX-Redirect']).path == PREFIX + '/accounts/login/'
|
||||
assert parse_qs(urlsplit(response['HX-Redirect']).query)['next'] == [PREFIX + '/']
|
||||
assert client.get('/health/').json() == {'status': 'ok'}
|
||||
|
||||
|
||||
def test_login_cookie_csrf_logout_and_external_redirect(client, mitarbeiter, tenant_path):
|
||||
client = Client(enforce_csrf_checks=True)
|
||||
login = client.get('/accounts/login/')
|
||||
assert_prefixed(login.content)
|
||||
csrf = tenant_path.CSRF_COOKIE_NAME
|
||||
assert client.cookies[csrf]['path'] == PREFIX + '/'
|
||||
credentials = {'username': mitarbeiter.username, 'password': 'testpass'}
|
||||
assert client.post('/accounts/login/', credentials).status_code == 403
|
||||
credentials['csrfmiddlewaretoken'] = client.cookies[csrf].value
|
||||
credentials['next'] = 'https://untrusted.example/private'
|
||||
response = client.post('/accounts/login/', credentials)
|
||||
assert response.status_code == 302
|
||||
assert response.url == PREFIX + '/'
|
||||
assert client.cookies[tenant_path.SESSION_COOKIE_NAME]['path'] == PREFIX + '/'
|
||||
assert client.get('/accounts/logout/').status_code == 405
|
||||
assert client.post('/accounts/logout/').status_code == 403
|
||||
response = client.post('/accounts/logout/', {'csrfmiddlewaretoken': client.cookies[csrf].value})
|
||||
assert response.url == PREFIX + '/accounts/login/'
|
||||
assert client.cookies[tenant_path.SESSION_COOKIE_NAME]['path'] == PREFIX + '/'
|
||||
assert client.get('/ausschreibungen/').status_code == 302
|
||||
|
||||
|
||||
def test_member_navigation_fragments_and_password_change(client, mitarbeiter, tenant_path):
|
||||
tender = Ausschreibung.objects.create(titel='Path test', ausschreiber='Demo')
|
||||
client.force_login(mitarbeiter)
|
||||
for path in [
|
||||
'/ausschreibungen/', f'/ausschreibungen/{tender.pk}/',
|
||||
'/aufgaben/', '/partner/subunternehmer/', '/bibliothek/nachweise/',
|
||||
'/accounts/password-change/',
|
||||
]:
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, path
|
||||
assert_prefixed(response.content)
|
||||
fragment = client.get('/suche/?q=Path', HTTP_HX_REQUEST='true')
|
||||
assert fragment.status_code == 200
|
||||
assert_prefixed(fragment.content)
|
||||
response = client.post('/accounts/password-change/', {
|
||||
'old_password': 'testpass',
|
||||
'new_password1': 'Path-pilot-password-984!',
|
||||
'new_password2': 'Path-pilot-password-984!',
|
||||
})
|
||||
assert response.status_code == 302
|
||||
assert response.url.startswith(PREFIX + '/')
|
||||
assert client.get('/ausschreibungen/').status_code == 200
|
||||
|
||||
|
||||
def test_private_media_remains_authenticated_under_prefix(
|
||||
client, mitarbeiter, tenant_path, tmp_path,
|
||||
):
|
||||
tenant_path.MEDIA_ROOT = tmp_path
|
||||
(tmp_path / 'document.pdf').write_bytes(b'%PDF-private-demo')
|
||||
anonymous = client.get('/media/document.pdf')
|
||||
assert anonymous.status_code == 302
|
||||
assert urlsplit(anonymous.url).path == PREFIX + '/accounts/login/'
|
||||
client.force_login(mitarbeiter)
|
||||
response = client.get('/media/document.pdf')
|
||||
assert response.status_code == 200
|
||||
assert b''.join(response.streaming_content) == b'%PDF-private-demo'
|
||||
assert response['Content-Disposition'] == 'attachment; filename="document.pdf"'
|
||||
assert 'no-store' in response['Cache-Control']
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
from datetime import date, timedelta
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
from .issue_backends import gitea_configured as _gitea_configured
|
||||
from .models import Aufgabe, AufgabenVerknuepfung, Bieterfrage, ExternalIssue
|
||||
|
||||
|
|
@ -65,8 +65,8 @@ def aufgaben_liste(request, ausschreibung_id=None):
|
|||
|
||||
if ausschreibung:
|
||||
breadcrumbs = [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': ausschreibung.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': ausschreibung_id})},
|
||||
{'label': 'Aufgaben', 'url': None},
|
||||
]
|
||||
else:
|
||||
|
|
@ -119,9 +119,9 @@ def aufgabe_neu(request, ausschreibung_id):
|
|||
'ausschreibung': ausschreibung,
|
||||
'titel': 'Aufgabe anlegen',
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': ausschreibung.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': ausschreibung_id})},
|
||||
{'label': 'Aufgaben', 'url': reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': ausschreibung_id})},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -142,9 +142,9 @@ def aufgabe_bearbeiten(request, ausschreibung_id, pk):
|
|||
'aufgabe': aufgabe,
|
||||
'titel': 'Aufgabe bearbeiten',
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': ausschreibung.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': ausschreibung_id})},
|
||||
{'label': 'Aufgaben', 'url': reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': ausschreibung_id})},
|
||||
{'label': aufgabe.titel[:50], 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -161,9 +161,9 @@ def aufgabe_loeschen(request, ausschreibung_id, pk):
|
|||
'aufgabe': aufgabe,
|
||||
'ausschreibung': ausschreibung,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': ausschreibung.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': ausschreibung_id})},
|
||||
{'label': 'Aufgaben', 'url': reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': ausschreibung_id})},
|
||||
{'label': 'Löschen', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -176,9 +176,9 @@ def aufgabe_detail(request, ausschreibung_id, pk):
|
|||
'aufgabe': aufgabe,
|
||||
'ausschreibung': ausschreibung,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Aufgaben', 'url': f'/ausschreibungen/{ausschreibung_id}/aufgaben/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': ausschreibung.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': ausschreibung_id})},
|
||||
{'label': 'Aufgaben', 'url': reverse('ausschreibungen:aufgaben:liste', kwargs={'ausschreibung_id': ausschreibung_id})},
|
||||
{'label': aufgabe.titel[:50], 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -237,17 +237,18 @@ def bieterfragen_liste(request, ausschreibung_id):
|
|||
'mitarbeiter': Mitarbeiter.objects.all(),
|
||||
'current_status': status_filter or '',
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': ausschreibung.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': ausschreibung_id})},
|
||||
{'label': 'Bieterfragen', 'url': None},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def bieterfrage_neu(request, ausschreibung_id):
|
||||
from .forms import BieterfragenForm
|
||||
from vergabe_teilnahme.apps.lose.models import Anforderung
|
||||
|
||||
from .forms import BieterfragenForm
|
||||
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
|
||||
initial = {}
|
||||
|
|
@ -275,9 +276,9 @@ def bieterfrage_neu(request, ausschreibung_id):
|
|||
'ausschreibung': ausschreibung,
|
||||
'titel': 'Bieterfrage anlegen',
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Bieterfragen', 'url': f'/ausschreibungen/{ausschreibung_id}/bieterfragen/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': ausschreibung.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': ausschreibung_id})},
|
||||
{'label': 'Bieterfragen', 'url': reverse('ausschreibungen:bieterfragen:liste', kwargs={'ausschreibung_id': ausschreibung_id})},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -290,9 +291,9 @@ def bieterfrage_detail(request, ausschreibung_id, pk):
|
|||
'bieterfrage': bieterfrage,
|
||||
'ausschreibung': ausschreibung,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung_id}/'},
|
||||
{'label': 'Bieterfragen', 'url': f'/ausschreibungen/{ausschreibung_id}/bieterfragen/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': ausschreibung.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': ausschreibung_id})},
|
||||
{'label': 'Bieterfragen', 'url': reverse('ausschreibungen:bieterfragen:liste', kwargs={'ausschreibung_id': ausschreibung_id})},
|
||||
{'label': str(bieterfrage)[:50], 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from datetime import date, timedelta
|
||||
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from vergabe_teilnahme.apps.accounts.models import Mitarbeiter
|
||||
|
|
@ -101,7 +102,7 @@ def ausschreibung_neu(request):
|
|||
'historisch': historisch,
|
||||
'titel': 'Neue Ausschreibung',
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -111,7 +112,11 @@ def ausschreibung_detail(request, pk):
|
|||
from django.db.models import Count, Q
|
||||
|
||||
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
|
||||
from vergabe_teilnahme.apps.core.services import aufgaben_score, build_phase_nav, get_deadline_warnings
|
||||
from vergabe_teilnahme.apps.core.services import (
|
||||
aufgaben_score,
|
||||
build_phase_nav,
|
||||
get_deadline_warnings,
|
||||
)
|
||||
from vergabe_teilnahme.apps.lose.models import Los
|
||||
|
||||
a = get_object_or_404(Ausschreibung, pk=pk)
|
||||
|
|
@ -136,7 +141,7 @@ def ausschreibung_detail(request, pk):
|
|||
'warnungen': get_deadline_warnings(a),
|
||||
'aufgaben_score': aufgaben_score(Aufgabe.objects.filter(ausschreibung=a)),
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': a.titel, 'url': None},
|
||||
],
|
||||
}
|
||||
|
|
@ -156,8 +161,8 @@ def ausschreibung_bearbeiten(request, pk):
|
|||
'titel': 'Ausschreibung bearbeiten',
|
||||
'ausschreibung': a,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': a.titel, 'url': f'/ausschreibungen/{pk}/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': a.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': pk})},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -197,8 +202,8 @@ def ausschreibung_entscheidung(request, pk):
|
|||
'regelergebnis': entscheidungsregel_auswertung(a),
|
||||
'ausschlusskriterien_nicht_erfuellbar': ausschlusskriterien,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': a.titel, 'url': f'/ausschreibungen/{pk}/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': a.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': pk})},
|
||||
{'label': 'Teilnahmeentscheidung', 'url': None},
|
||||
],
|
||||
}
|
||||
|
|
@ -227,8 +232,8 @@ def freigaben_uebersicht(request, pk):
|
|||
'ct_id': ct.pk,
|
||||
'freigabe_typ_choices': Freigabe.TYP_CHOICES,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': a.titel, 'url': f'/ausschreibungen/{pk}/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': a.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': pk})},
|
||||
{'label': 'Freigaben', 'url': None},
|
||||
],
|
||||
}
|
||||
|
|
@ -245,8 +250,8 @@ def ausschreibung_archivieren(request, pk):
|
|||
return render(request, 'ausschreibungen/archivieren_confirm.html', {
|
||||
'ausschreibung': a,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': a.titel, 'url': f'/ausschreibungen/{pk}/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': a.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': pk})},
|
||||
{'label': 'Archivieren', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ from datetime import date, timedelta
|
|||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
|
||||
from vergabe_teilnahme.apps.accounts.models import Mitarbeiter
|
||||
|
||||
from .models import Entscheidungsregel, Leistungsblatt, Nachweis, Referenz
|
||||
|
||||
|
||||
# ── Forms ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
class NachweisForm(forms.ModelForm):
|
||||
|
|
@ -173,7 +173,7 @@ def nachweis_neu(request):
|
|||
return render(request, 'bibliothek/nachweis_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
|
||||
{'label': 'Nachweise', 'url': reverse('bibliothek:nachweise_liste')},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -184,7 +184,7 @@ def nachweis_detail(request, pk):
|
|||
return render(request, 'bibliothek/nachweis_detail.html', {
|
||||
'nachweis': nachweis,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
|
||||
{'label': 'Nachweise', 'url': reverse('bibliothek:nachweise_liste')},
|
||||
{'label': nachweis.titel, 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -205,8 +205,8 @@ def nachweis_bearbeiten(request, pk):
|
|||
'form': form,
|
||||
'nachweis': nachweis,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
|
||||
{'label': nachweis.titel, 'url': f'/bibliothek/nachweise/{pk}/'},
|
||||
{'label': 'Nachweise', 'url': reverse('bibliothek:nachweise_liste')},
|
||||
{'label': nachweis.titel, 'url': reverse('bibliothek:nachweis_detail', kwargs={'pk': pk})},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -237,8 +237,8 @@ def nachweis_neue_version(request, pk):
|
|||
'nachweis': alter_nachweis,
|
||||
'neue_version': True,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
|
||||
{'label': alter_nachweis.titel, 'url': f'/bibliothek/nachweise/{pk}/'},
|
||||
{'label': 'Nachweise', 'url': reverse('bibliothek:nachweise_liste')},
|
||||
{'label': alter_nachweis.titel, 'url': reverse('bibliothek:nachweis_detail', kwargs={'pk': pk})},
|
||||
{'label': 'Neue Version', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -279,7 +279,7 @@ def referenz_neu(request):
|
|||
return render(request, 'bibliothek/referenz_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Referenzen', 'url': '/bibliothek/referenzen/'},
|
||||
{'label': 'Referenzen', 'url': reverse('bibliothek:referenz_liste')},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -290,7 +290,7 @@ def referenz_detail(request, pk):
|
|||
return render(request, 'bibliothek/referenz_detail.html', {
|
||||
'ref': ref,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Referenzen', 'url': '/bibliothek/referenzen/'},
|
||||
{'label': 'Referenzen', 'url': reverse('bibliothek:referenz_liste')},
|
||||
{'label': ref.referenztitel, 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -311,8 +311,8 @@ def referenz_bearbeiten(request, pk):
|
|||
'form': form,
|
||||
'ref': ref,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Referenzen', 'url': '/bibliothek/referenzen/'},
|
||||
{'label': ref.referenztitel, 'url': f'/bibliothek/referenzen/{pk}/'},
|
||||
{'label': 'Referenzen', 'url': reverse('bibliothek:referenz_liste')},
|
||||
{'label': ref.referenztitel, 'url': reverse('bibliothek:referenz_detail', kwargs={'pk': pk})},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -365,7 +365,7 @@ def leistungsblatt_neu(request):
|
|||
return render(request, 'bibliothek/leistungsblatt_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Leistungsblätter', 'url': '/bibliothek/leistungsblaetter/'},
|
||||
{'label': 'Leistungsblätter', 'url': reverse('bibliothek:leistungsblaetter_liste')},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -386,7 +386,7 @@ def leistungsblatt_bearbeiten(request, pk):
|
|||
'form': form,
|
||||
'obj': obj,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Leistungsblätter', 'url': '/bibliothek/leistungsblaetter/'},
|
||||
{'label': 'Leistungsblätter', 'url': reverse('bibliothek:leistungsblaetter_liste')},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -415,7 +415,7 @@ def entscheidungsregel_neu(request):
|
|||
return render(request, 'bibliothek/entscheidungsregel_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Entscheidungsregeln', 'url': '/bibliothek/entscheidungsregeln/'},
|
||||
{'label': 'Entscheidungsregeln', 'url': reverse('bibliothek:entscheidungsregeln_liste')},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -436,7 +436,7 @@ def entscheidungsregel_bearbeiten(request, pk):
|
|||
'form': form,
|
||||
'obj': obj,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Entscheidungsregeln', 'url': '/bibliothek/entscheidungsregeln/'},
|
||||
{'label': 'Entscheidungsregeln', 'url': reverse('bibliothek:entscheidungsregeln_liste')},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
PHASEN = [
|
||||
(1, 'Recherche & Unterlagen'),
|
||||
(2, 'Teilnahmeentscheidung'),
|
||||
|
|
@ -35,7 +37,7 @@ def aufgaben_score(qs):
|
|||
def build_phase_nav(ausschreibung, current_url=''):
|
||||
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
|
||||
aktuelle_phase = STATUS_TO_PHASE.get(ausschreibung.status, 1)
|
||||
base = f'/ausschreibungen/{ausschreibung.pk}'
|
||||
base = reverse('ausschreibungen:detail', kwargs={'pk': ausschreibung.pk}).rstrip('/')
|
||||
phase_urls = {
|
||||
1: f'{base}/',
|
||||
2: f'{base}/entscheidung/',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
from vergabe_teilnahme.apps.bibliothek.models import Nachweis
|
||||
|
|
@ -13,8 +14,8 @@ def _is_htmx(request):
|
|||
|
||||
def _ausschreibung_breadcrumbs(ausschreibung, *extra):
|
||||
crumbs = [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': ausschreibung.titel, 'url': f'/ausschreibungen/{ausschreibung.pk}/'},
|
||||
{'label': 'Ausschreibungen', 'url': reverse('ausschreibungen:liste')},
|
||||
{'label': ausschreibung.titel, 'url': reverse('ausschreibungen:detail', kwargs={'pk': ausschreibung.pk})},
|
||||
]
|
||||
for label, url in extra:
|
||||
crumbs.append({'label': label, 'url': url})
|
||||
|
|
@ -65,7 +66,7 @@ def los_neu(request, ausschreibung_id):
|
|||
'ausschreibung': ausschreibung,
|
||||
'titel': 'Los hinzufügen',
|
||||
'breadcrumbs': _ausschreibung_breadcrumbs(ausschreibung,
|
||||
('Lose', f'/ausschreibungen/{ausschreibung_id}/lose/'),
|
||||
('Lose', reverse('ausschreibungen:lose:liste', kwargs={'ausschreibung_id': ausschreibung_id})),
|
||||
('Neu', None)),
|
||||
})
|
||||
|
||||
|
|
@ -79,7 +80,7 @@ def los_detail(request, ausschreibung_id, los_pk):
|
|||
'los': los,
|
||||
'anforderungen': anforderungen,
|
||||
'breadcrumbs': _ausschreibung_breadcrumbs(ausschreibung,
|
||||
('Lose', f'/ausschreibungen/{ausschreibung_id}/lose/'),
|
||||
('Lose', reverse('ausschreibungen:lose:liste', kwargs={'ausschreibung_id': ausschreibung_id})),
|
||||
(str(los), None)),
|
||||
})
|
||||
|
||||
|
|
@ -97,8 +98,8 @@ def los_bearbeiten(request, ausschreibung_id, los_pk):
|
|||
'los': los,
|
||||
'titel': 'Los bearbeiten',
|
||||
'breadcrumbs': _ausschreibung_breadcrumbs(ausschreibung,
|
||||
('Lose', f'/ausschreibungen/{ausschreibung_id}/lose/'),
|
||||
(str(los), f'/ausschreibungen/{ausschreibung_id}/lose/{los_pk}/'),
|
||||
('Lose', reverse('ausschreibungen:lose:liste', kwargs={'ausschreibung_id': ausschreibung_id})),
|
||||
(str(los), reverse('ausschreibungen:lose:detail', kwargs={'ausschreibung_id': ausschreibung_id, 'los_pk': los_pk})),
|
||||
('Bearbeiten', None)),
|
||||
})
|
||||
|
||||
|
|
@ -113,8 +114,8 @@ def los_loeschen(request, ausschreibung_id, los_pk):
|
|||
'ausschreibung': ausschreibung,
|
||||
'los': los,
|
||||
'breadcrumbs': _ausschreibung_breadcrumbs(ausschreibung,
|
||||
('Lose', f'/ausschreibungen/{ausschreibung_id}/lose/'),
|
||||
(str(los), f'/ausschreibungen/{ausschreibung_id}/lose/{los_pk}/'),
|
||||
('Lose', reverse('ausschreibungen:lose:liste', kwargs={'ausschreibung_id': ausschreibung_id})),
|
||||
(str(los), reverse('ausschreibungen:lose:detail', kwargs={'ausschreibung_id': ausschreibung_id, 'los_pk': los_pk})),
|
||||
('Löschen', None)),
|
||||
})
|
||||
|
||||
|
|
@ -187,7 +188,7 @@ def anforderung_neu(request, ausschreibung_id):
|
|||
'ausschreibung': ausschreibung,
|
||||
'titel': 'Anforderung anlegen',
|
||||
'breadcrumbs': _ausschreibung_breadcrumbs(ausschreibung,
|
||||
('Anforderungen', f'/ausschreibungen/{ausschreibung_id}/lose/anforderungen/'),
|
||||
('Anforderungen', reverse('ausschreibungen:lose:anforderungen_liste', kwargs={'ausschreibung_id': ausschreibung_id})),
|
||||
('Neu', None)),
|
||||
})
|
||||
|
||||
|
|
@ -201,7 +202,7 @@ def anforderung_detail(request, ausschreibung_id, pk):
|
|||
'anforderung': anforderung,
|
||||
'aufgaben': aufgaben,
|
||||
'breadcrumbs': _ausschreibung_breadcrumbs(ausschreibung,
|
||||
('Anforderungen', f'/ausschreibungen/{ausschreibung_id}/lose/anforderungen/'),
|
||||
('Anforderungen', reverse('ausschreibungen:lose:anforderungen_liste', kwargs={'ausschreibung_id': ausschreibung_id})),
|
||||
(anforderung.titel[:50], None)),
|
||||
})
|
||||
|
||||
|
|
@ -220,8 +221,8 @@ def anforderung_bearbeiten(request, ausschreibung_id, pk):
|
|||
'anforderung': anforderung,
|
||||
'titel': 'Anforderung bearbeiten',
|
||||
'breadcrumbs': _ausschreibung_breadcrumbs(ausschreibung,
|
||||
('Anforderungen', f'/ausschreibungen/{ausschreibung_id}/lose/anforderungen/'),
|
||||
(anforderung.titel[:50], f'/ausschreibungen/{ausschreibung_id}/lose/anforderungen/{pk}/'),
|
||||
('Anforderungen', reverse('ausschreibungen:lose:anforderungen_liste', kwargs={'ausschreibung_id': ausschreibung_id})),
|
||||
(anforderung.titel[:50], reverse('ausschreibungen:lose:anforderung_detail', kwargs={'ausschreibung_id': ausschreibung_id, 'pk': pk})),
|
||||
('Bearbeiten', None)),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
from vergabe_teilnahme.apps.lose.models import Los
|
||||
|
|
@ -97,7 +98,7 @@ def subunternehmer_neu(request):
|
|||
return render(request, 'partner/subunternehmer_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'},
|
||||
{'label': 'Subunternehmer', 'url': reverse('partner:su_liste')},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -113,7 +114,7 @@ def subunternehmer_detail(request, pk):
|
|||
'sub': sub,
|
||||
'zuordnungen': zuordnungen,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'},
|
||||
{'label': 'Subunternehmer', 'url': reverse('partner:su_liste')},
|
||||
{'label': sub.name, 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -134,8 +135,8 @@ def subunternehmer_bearbeiten(request, pk):
|
|||
'form': form,
|
||||
'sub': sub,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'},
|
||||
{'label': sub.name, 'url': f'/partner/subunternehmer/{pk}/'},
|
||||
{'label': 'Subunternehmer', 'url': reverse('partner:su_liste')},
|
||||
{'label': sub.name, 'url': reverse('partner:su_detail', kwargs={'pk': pk})},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -209,7 +210,7 @@ def dienstleistertyp_neu(request):
|
|||
return render(request, 'partner/dienstleistertyp_form.html', {
|
||||
'form': form,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Dienstleistertypen', 'url': '/partner/dienstleistertypen/'},
|
||||
{'label': 'Dienstleistertypen', 'url': reverse('partner:dt_liste')},
|
||||
{'label': 'Neu', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
@ -230,7 +231,7 @@ def dienstleistertyp_bearbeiten(request, pk):
|
|||
'form': form,
|
||||
'obj': obj,
|
||||
'breadcrumbs': [
|
||||
{'label': 'Dienstleistertypen', 'url': '/partner/dienstleistertypen/'},
|
||||
{'label': 'Dienstleistertypen', 'url': reverse('partner:dt_liste')},
|
||||
{'label': 'Bearbeiten', 'url': None},
|
||||
],
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue