generated from coulomb/repo-seed
chore(workplan): WP-0016 — Issue-Facade Integration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
574
workplans/WP-0016-issue-facade-integration.md
Normal file
574
workplans/WP-0016-issue-facade-integration.md
Normal file
@@ -0,0 +1,574 @@
|
||||
---
|
||||
id: WP-0016
|
||||
title: Issue-Facade Integration — lokale Aufgabenverfolgung + Remote-Delegation
|
||||
status: todo
|
||||
phase: 16-of-n
|
||||
created: "2026-05-14"
|
||||
depends_on: WP-0015
|
||||
---
|
||||
|
||||
# WP-0016 — Issue-Facade Integration
|
||||
|
||||
Ersetzt den Platzhalter-`issue_facade.py` aus WP-0015 durch eine echte
|
||||
Integration mit dem Sister-Projekt `issue-facade` (`/home/worsch/issue-facade`,
|
||||
Package-Name `universal-issue-tracker`).
|
||||
|
||||
**Leitgedanke:** Aufgaben werden zunächst lokal im SQLite-Backend von
|
||||
`issue-facade` verwaltet (offline-fähig, keine externe Abhängigkeit). Optional
|
||||
können sie in ein entferntes System (zunächst Gitea) delegiert werden. Der
|
||||
Status wird von dort zurückgelesen.
|
||||
|
||||
**Key interfaces aus dem Sister-Projekt:**
|
||||
|
||||
- `issue_tracker.core.models.Issue` — `id` (UUID), `number`, `title`,
|
||||
`description`, `state` (IssueState: open/closed/in_progress/blocked)
|
||||
- `issue_tracker.core.interfaces.IssueBackend` ABC — `create_issue()`,
|
||||
`get_issue()`, `update_issue()`, `list_issues()`
|
||||
- `issue_tracker.backends.local.LocalSQLiteBackend` —
|
||||
`connect({'db_path': ...})`
|
||||
- `issue_tracker.backends.gitea.GiteaBackend` —
|
||||
`connect({'base_url', 'token', 'owner', 'repo'})`
|
||||
|
||||
---
|
||||
|
||||
```task
|
||||
id: WP-0016-T01
|
||||
title: Package-Installation + Django-Settings
|
||||
status: todo
|
||||
|
||||
**`pyproject.toml`** — Dependency ergänzen:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
...
|
||||
"universal-issue-tracker @ file:///home/worsch/issue-facade",
|
||||
]
|
||||
```
|
||||
|
||||
Danach:
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
**`vergabe_teilnahme/settings/base.py`** — neue Optionen am Ende:
|
||||
|
||||
```python
|
||||
# Issue Facade — lokales SQLite-Backend (immer aktiv)
|
||||
ISSUE_FACADE_LOCAL_DB = BASE_DIR / '.issue-facade' / 'issues.db'
|
||||
|
||||
# Issue Facade — Gitea-Remote (optional, None = deaktiviert)
|
||||
ISSUE_FACADE_GITEA: dict | None = None
|
||||
# Beispiel:
|
||||
# ISSUE_FACADE_GITEA = {
|
||||
# 'base_url': 'https://gitea.example.com',
|
||||
# 'token': env('GITEA_TOKEN', default=''),
|
||||
# 'owner': 'org',
|
||||
# 'repo': 'vergabe',
|
||||
# }
|
||||
```
|
||||
|
||||
**`.gitignore`** — SQLite-DB ausschließen:
|
||||
|
||||
```
|
||||
.issue-facade/
|
||||
```
|
||||
|
||||
Smoke-Check:
|
||||
```bash
|
||||
uv run python -c "from issue_tracker.backends.local import LocalSQLiteBackend; print('ok')"
|
||||
```
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0016-T02
|
||||
title: Backend-Utility — issue_backends.py
|
||||
status: todo
|
||||
|
||||
Neues Modul `vergabe_teilnahme/apps/aufgaben/issue_backends.py`:
|
||||
|
||||
```python
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@contextmanager
|
||||
def local_backend():
|
||||
from django.conf import settings
|
||||
from issue_tracker.backends.local import LocalSQLiteBackend
|
||||
|
||||
db_path = str(getattr(settings, 'ISSUE_FACADE_LOCAL_DB', '.issue-facade/issues.db'))
|
||||
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
b = LocalSQLiteBackend()
|
||||
b.connect({'db_path': db_path})
|
||||
try:
|
||||
yield b
|
||||
finally:
|
||||
b.disconnect()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def remote_backend():
|
||||
"""Yields GiteaBackend wenn konfiguriert, sonst None."""
|
||||
from django.conf import settings
|
||||
from issue_tracker.backends.gitea import GiteaBackend
|
||||
|
||||
cfg = getattr(settings, 'ISSUE_FACADE_GITEA', None)
|
||||
if not cfg:
|
||||
yield None
|
||||
return
|
||||
|
||||
b = GiteaBackend()
|
||||
b.connect(cfg)
|
||||
try:
|
||||
yield b
|
||||
finally:
|
||||
b.disconnect()
|
||||
|
||||
|
||||
def gitea_configured() -> bool:
|
||||
from django.conf import settings
|
||||
cfg = getattr(settings, 'ISSUE_FACADE_GITEA', None)
|
||||
return bool(cfg and cfg.get('token'))
|
||||
```
|
||||
|
||||
Kontext-Manager-Pattern (kein Prozess-globaler Singleton, SQLite ist
|
||||
pro Request cheap genug für eine Einzelbenutzer-App).
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0016-T03
|
||||
title: ExternalIssue-Modell erweitern + Migration
|
||||
status: todo
|
||||
|
||||
In `apps/aufgaben/models.py` das Modell `ExternalIssue` anpassen:
|
||||
|
||||
```python
|
||||
class ExternalIssue(models.Model):
|
||||
BACKEND_CHOICES = [
|
||||
('local', 'Lokal (SQLite)'),
|
||||
('gitea', 'Gitea'),
|
||||
]
|
||||
SYNC_STATUS_CHOICES = [
|
||||
('open', 'Offen'),
|
||||
('in_progress', 'In Bearbeitung'),
|
||||
('blocked', 'Blockiert'),
|
||||
('closed', 'Geschlossen'),
|
||||
('error', 'Sync-Fehler'),
|
||||
]
|
||||
|
||||
aufgabe = models.OneToOneField(
|
||||
Aufgabe, on_delete=models.CASCADE, related_name='external_issue'
|
||||
)
|
||||
issue_facade_backend = models.CharField(
|
||||
max_length=20, choices=BACKEND_CHOICES, default='local'
|
||||
)
|
||||
issue_facade_id = models.CharField(
|
||||
max_length=200, blank=True,
|
||||
help_text='UUID (lokal) oder Issue-Number (Gitea)'
|
||||
)
|
||||
issue_url = models.URLField(blank=True)
|
||||
issue_key = models.CharField(max_length=100, blank=True,
|
||||
help_text='z.B. "#42" oder "PROJ-1234"')
|
||||
sync_status = models.CharField(
|
||||
max_length=20, choices=SYNC_STATUS_CHOICES, default='open'
|
||||
)
|
||||
letzter_sync = models.DateTimeField(null=True, blank=True)
|
||||
notizen = models.TextField(blank=True)
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'External Issue'
|
||||
verbose_name_plural = 'External Issues'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.get_issue_facade_backend_display()} #{self.issue_key or self.issue_facade_id[:8]}'
|
||||
```
|
||||
|
||||
**Änderungen vs. WP-0015:**
|
||||
- `system` (github/jira/linear/…) entfällt — ersetzt durch `issue_facade_backend`
|
||||
- `issue_facade_id` neu — UUID für lokale Issues, Issue-Number-String für Gitea
|
||||
- `sync_status`-Choices auf IssueState-Werte ausgerichtet
|
||||
|
||||
Migration erstellen und ausführen:
|
||||
|
||||
```bash
|
||||
uv run python manage.py makemigrations aufgaben --name external_issue_redesign
|
||||
uv run python manage.py migrate
|
||||
```
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0016-T04
|
||||
title: Service-Schicht — issue_facade.py ersetzen
|
||||
status: todo
|
||||
|
||||
`vergabe_teilnahme/apps/aufgaben/issue_facade.py` **komplett ersetzen**:
|
||||
|
||||
```python
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from issue_tracker.core.models import (
|
||||
Issue, IssueState, IssueType, Label, Priority
|
||||
)
|
||||
|
||||
from .issue_backends import gitea_configured, local_backend, remote_backend
|
||||
|
||||
|
||||
def aufgabe_zu_issue(aufgabe) -> Issue:
|
||||
"""Konvertiert eine Aufgabe in ein issue-facade Issue-Objekt."""
|
||||
prioritaet_map = {1: Priority.HIGH, 2: Priority.MEDIUM, 3: Priority.LOW}
|
||||
labels = [
|
||||
Label(name='task'),
|
||||
Label(name=f'priority:{prioritaet_map.get(aufgabe.prioritaet, Priority.MEDIUM).value}'),
|
||||
]
|
||||
if aufgabe.typ:
|
||||
labels.append(Label(name=aufgabe.typ))
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
return Issue(
|
||||
id=None, number=0,
|
||||
title=aufgabe.titel,
|
||||
description=aufgabe.beschreibung or '',
|
||||
state=IssueState.OPEN,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
labels=labels,
|
||||
)
|
||||
|
||||
|
||||
def lokales_issue_erstellen(aufgabe) -> dict:
|
||||
"""
|
||||
Legt ein Issue im lokalen SQLite-Backend an.
|
||||
Gibt {'issue_facade_id', 'issue_key', 'sync_status'} zurück.
|
||||
"""
|
||||
issue = aufgabe_zu_issue(aufgabe)
|
||||
with local_backend() as b:
|
||||
created = b.create_issue(issue)
|
||||
return {
|
||||
'issue_facade_id': created.id,
|
||||
'issue_key': f'#{created.number}',
|
||||
'sync_status': created.state.value,
|
||||
}
|
||||
|
||||
|
||||
def an_gitea_delegieren(aufgabe, external_issue) -> dict:
|
||||
"""
|
||||
Schiebt ein lokales Issue nach Gitea.
|
||||
Gibt {'issue_facade_id', 'issue_url', 'issue_key', 'sync_status'} zurück.
|
||||
Wirft ValueError wenn Gitea nicht konfiguriert ist.
|
||||
"""
|
||||
with remote_backend() as b:
|
||||
if b is None:
|
||||
raise ValueError('Gitea nicht konfiguriert (ISSUE_FACADE_GITEA fehlt in settings)')
|
||||
issue = aufgabe_zu_issue(aufgabe)
|
||||
created = b.create_issue(issue)
|
||||
return {
|
||||
'issue_facade_backend': 'gitea',
|
||||
'issue_facade_id': str(created.number),
|
||||
'issue_url': created.sync_metadata.get('url', '') if created.sync_metadata else '',
|
||||
'issue_key': f'#{created.number}',
|
||||
'sync_status': created.state.value,
|
||||
}
|
||||
|
||||
|
||||
def status_synchronisieren(external_issue) -> str:
|
||||
"""
|
||||
Liest den aktuellen Status aus dem konfigurierten Backend.
|
||||
Gibt den neuen sync_status-String zurück.
|
||||
"""
|
||||
from django.utils import timezone as dj_tz
|
||||
|
||||
backend_ctx = (
|
||||
remote_backend() if external_issue.issue_facade_backend == 'gitea'
|
||||
else local_backend()
|
||||
)
|
||||
with backend_ctx as b:
|
||||
if b is None:
|
||||
raise ValueError('Backend nicht verfügbar')
|
||||
issue = b.get_issue(external_issue.issue_facade_id)
|
||||
|
||||
neuer_status = issue.state.value if issue else 'error'
|
||||
external_issue.sync_status = neuer_status
|
||||
external_issue.letzter_sync = dj_tz.now()
|
||||
external_issue.save(update_fields=['sync_status', 'letzter_sync'])
|
||||
return neuer_status
|
||||
|
||||
|
||||
def issue_schliessen(external_issue) -> None:
|
||||
"""Setzt das Issue im Backend auf CLOSED."""
|
||||
backend_ctx = (
|
||||
remote_backend() if external_issue.issue_facade_backend == 'gitea'
|
||||
else local_backend()
|
||||
)
|
||||
with backend_ctx as b:
|
||||
if b is None:
|
||||
return
|
||||
issue = b.get_issue(external_issue.issue_facade_id)
|
||||
if issue:
|
||||
issue.state = IssueState.CLOSED
|
||||
b.update_issue(issue)
|
||||
external_issue.sync_status = 'closed'
|
||||
external_issue.save(update_fields=['sync_status'])
|
||||
|
||||
|
||||
def get_adapter(system: str):
|
||||
"""Rückwärtskompatible Stub — gibt None zurück (kein manuelles Adapter-Dict mehr)."""
|
||||
return None
|
||||
```
|
||||
|
||||
Der ABC `IssueAdapter` aus WP-0015 entfällt damit vollständig —
|
||||
`issue-facade` übernimmt diese Rolle.
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0016-T05
|
||||
title: ExternalIssueForm anpassen + Views verkabeln
|
||||
status: todo
|
||||
|
||||
**Form** (`forms.py`) — vereinfacht, da `system` entfällt:
|
||||
|
||||
```python
|
||||
class ExternalIssueForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ExternalIssue
|
||||
fields = ['notizen']
|
||||
widgets = {
|
||||
'notizen': forms.Textarea(attrs={'class': 'form-input', 'rows': 2,
|
||||
'placeholder': 'Notizen (optional)'}),
|
||||
}
|
||||
```
|
||||
|
||||
(Backend und issue_facade_id werden vom Service gesetzt, nicht vom User.)
|
||||
|
||||
**`views.py`** — `external_issue_bearbeiten` anpassen:
|
||||
|
||||
```python
|
||||
def external_issue_bearbeiten(request, ausschreibung_id, pk):
|
||||
from .issue_facade import lokales_issue_erstellen
|
||||
from .forms import ExternalIssueForm
|
||||
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
issue = getattr(aufgabe, 'external_issue', None)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ExternalIssueForm(request.POST, instance=issue)
|
||||
if form.is_valid():
|
||||
ei = form.save(commit=False)
|
||||
ei.aufgabe = aufgabe
|
||||
if not issue: # Neues Issue — im lokalen Backend anlegen
|
||||
daten = lokales_issue_erstellen(aufgabe)
|
||||
ei.issue_facade_backend = 'local'
|
||||
ei.issue_facade_id = daten['issue_facade_id']
|
||||
ei.issue_key = daten['issue_key']
|
||||
ei.sync_status = daten['sync_status']
|
||||
ei.save()
|
||||
if _is_htmx(request):
|
||||
return render(request,
|
||||
'aufgaben/partials/external_issue_panel.html',
|
||||
{'aufgabe': aufgabe, 'ausschreibung': ausschreibung})
|
||||
return redirect('ausschreibungen:aufgaben:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
else:
|
||||
form = ExternalIssueForm(instance=issue)
|
||||
|
||||
if _is_htmx(request):
|
||||
return render(request, 'aufgaben/partials/external_issue_form.html',
|
||||
{'form': form, 'aufgabe': aufgabe, 'ausschreibung': ausschreibung})
|
||||
return redirect('ausschreibungen:aufgaben:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
```
|
||||
|
||||
Zwei neue Views ergänzen:
|
||||
|
||||
```python
|
||||
def external_issue_push_remote(request, ausschreibung_id, pk):
|
||||
"""Schiebt ein lokales Issue nach Gitea."""
|
||||
from .issue_facade import an_gitea_delegieren
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
ei = get_object_or_404(ExternalIssue, aufgabe=aufgabe)
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
daten = an_gitea_delegieren(aufgabe, ei)
|
||||
for k, v in daten.items():
|
||||
setattr(ei, k, v)
|
||||
ei.save()
|
||||
except ValueError as exc:
|
||||
# Gitea nicht konfiguriert — ignorieren, Fehlermeldung im Panel
|
||||
pass
|
||||
if _is_htmx(request):
|
||||
return render(request,
|
||||
'aufgaben/partials/external_issue_panel.html',
|
||||
{'aufgabe': aufgabe, 'ausschreibung': ausschreibung,
|
||||
'push_error': str(exc) if 'exc' in dir() else None})
|
||||
return redirect('ausschreibungen:aufgaben:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
|
||||
|
||||
def external_issue_sync(request, ausschreibung_id, pk):
|
||||
"""Aktualisiert sync_status aus dem Backend."""
|
||||
from .issue_facade import status_synchronisieren
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
aufgabe = get_object_or_404(Aufgabe, pk=pk, ausschreibung=ausschreibung)
|
||||
ei = get_object_or_404(ExternalIssue, aufgabe=aufgabe)
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
status_synchronisieren(ei)
|
||||
except Exception:
|
||||
ei.sync_status = 'error'
|
||||
ei.save(update_fields=['sync_status'])
|
||||
if _is_htmx(request):
|
||||
return render(request,
|
||||
'aufgaben/partials/external_issue_panel.html',
|
||||
{'aufgabe': aufgabe, 'ausschreibung': ausschreibung})
|
||||
return redirect('ausschreibungen:aufgaben:detail',
|
||||
ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
```
|
||||
|
||||
URLs in `urls.py` ergänzen:
|
||||
|
||||
```python
|
||||
path('<int:pk>/issue/push/', views.external_issue_push_remote, name='external_issue_push'),
|
||||
path('<int:pk>/issue/sync/', views.external_issue_sync, name='external_issue_sync'),
|
||||
```
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0016-T06
|
||||
title: Admin + ExternalIssueAdmin aktualisieren
|
||||
status: todo
|
||||
|
||||
`apps/aufgaben/admin.py` — `ExternalIssueAdmin` an neue Felder anpassen:
|
||||
|
||||
```python
|
||||
@admin.register(ExternalIssue)
|
||||
class ExternalIssueAdmin(admin.ModelAdmin):
|
||||
list_display = ['aufgabe', 'issue_facade_backend', 'issue_key',
|
||||
'sync_status', 'letzter_sync']
|
||||
list_filter = ['issue_facade_backend', 'sync_status']
|
||||
readonly_fields = ['issue_facade_id', 'issue_key', 'issue_url',
|
||||
'sync_status', 'letzter_sync', 'erstellt_am']
|
||||
```
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0016-T07
|
||||
title: UI — ExternalIssue-Panel aktualisieren
|
||||
status: todo
|
||||
|
||||
`templates/aufgaben/partials/external_issue_card.html` überarbeiten:
|
||||
|
||||
```html
|
||||
{% with ei=aufgabe.external_issue %}
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<!-- Backend-Badge -->
|
||||
{% if ei.issue_facade_backend == 'local' %}
|
||||
<span class="text-xs bg-slate-100 text-slate-600 rounded px-1.5 py-0.5 font-medium">LOKAL</span>
|
||||
{% else %}
|
||||
<span class="text-xs bg-blue-100 text-blue-700 rounded px-1.5 py-0.5 font-medium">GITEA</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Issue-Key -->
|
||||
{% if ei.issue_url %}
|
||||
<a href="{{ ei.issue_url }}" target="_blank" rel="noopener"
|
||||
class="text-xs font-mono text-blue-600 hover:underline">{{ ei.issue_key }}</a>
|
||||
{% else %}
|
||||
<span class="text-xs font-mono text-slate-600">{{ ei.issue_key }}</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Status-Badge -->
|
||||
<span class="text-xs rounded px-1.5 py-0.5
|
||||
{% if ei.sync_status == 'closed' %}bg-green-100 text-green-700
|
||||
{% elif ei.sync_status == 'in_progress' %}bg-amber-100 text-amber-700
|
||||
{% elif ei.sync_status == 'blocked' %}bg-red-100 text-red-700
|
||||
{% elif ei.sync_status == 'error' %}bg-red-200 text-red-800
|
||||
{% else %}bg-slate-100 text-slate-600{% endif %}">
|
||||
{{ ei.get_sync_status_display }}
|
||||
</span>
|
||||
|
||||
<!-- Letzter Sync -->
|
||||
{% if ei.letzter_sync %}
|
||||
<span class="text-xs text-slate-400 ml-auto">Sync: {{ ei.letzter_sync|date:"d.m.Y H:i" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if ei.notizen %}
|
||||
<p class="text-xs text-slate-500 whitespace-pre-wrap">{{ ei.notizen }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex gap-2 flex-wrap mt-1">
|
||||
<!-- Bearbeiten -->
|
||||
<button class="btn-ghost text-xs"
|
||||
hx-get="{% url 'ausschreibungen:aufgaben:external_issue' ausschreibung.pk aufgabe.pk %}"
|
||||
hx-target="#external-issue-panel" hx-swap="innerHTML">Bearbeiten</button>
|
||||
|
||||
<!-- Push to Gitea (nur wenn lokal und Gitea konfiguriert) -->
|
||||
{% if ei.issue_facade_backend == 'local' and gitea_configured %}
|
||||
<form hx-post="{% url 'ausschreibungen:aufgaben:external_issue_push' ausschreibung.pk aufgabe.pk %}"
|
||||
hx-target="#external-issue-panel" hx-swap="innerHTML">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-ghost text-xs text-blue-600">↑ Nach Gitea</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<!-- Status syncen -->
|
||||
<form hx-post="{% url 'ausschreibungen:aufgaben:external_issue_sync' ausschreibung.pk aufgabe.pk %}"
|
||||
hx-target="#external-issue-panel" hx-swap="innerHTML">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-ghost text-xs">⟳ Status syncen</button>
|
||||
</form>
|
||||
|
||||
<!-- Entfernen -->
|
||||
<form hx-post="{% url 'ausschreibungen:aufgaben:external_issue_loeschen' ausschreibung.pk aufgabe.pk %}"
|
||||
hx-target="#external-issue-panel" hx-swap="innerHTML"
|
||||
hx-confirm="Externe Verknüpfung entfernen?">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-ghost text-xs text-red-500">Entfernen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
```
|
||||
|
||||
`gitea_configured` im Template benötigt einen Context-Processor oder muss im
|
||||
View als Kontext-Variable übergeben werden. Einfachste Lösung: in
|
||||
`external_issue_panel.html` und den Views direkt via
|
||||
`from .issue_backends import gitea_configured` hinzufügen.
|
||||
|
||||
`external_issue_form.html` vereinfachen — nur noch Notizen-Feld + Hinweis
|
||||
"Issue wird automatisch im lokalen Backend angelegt".
|
||||
```
|
||||
|
||||
```task
|
||||
id: WP-0016-T08
|
||||
title: Tests + Smoke-Check
|
||||
status: todo
|
||||
|
||||
Alle 76 bestehenden Tests müssen grün bleiben.
|
||||
|
||||
Neue Tests in `apps/aufgaben/tests.py` — lokales Backend per tmp-Datei:
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def tmp_issue_db(tmp_path, settings):
|
||||
settings.ISSUE_FACADE_LOCAL_DB = tmp_path / 'test_issues.db'
|
||||
settings.ISSUE_FACADE_GITEA = None
|
||||
return settings.ISSUE_FACADE_LOCAL_DB
|
||||
```
|
||||
|
||||
- `test_aufgabe_zu_issue_mapping` — title, description, priority-Label korrekt
|
||||
- `test_lokales_issue_erstellen` — ExternalIssue bekommt issue_facade_id + issue_key
|
||||
- `test_status_synchronisieren` — sync_status wird aktualisiert
|
||||
- `test_external_issue_bearbeiten_view_erstellt_issue` — POST → Issue in DB
|
||||
- `test_external_issue_sync_view` — POST → sync_status geändert
|
||||
- `test_get_adapter_stub` — gibt None zurück (Rückwärtskompatibilität)
|
||||
|
||||
```bash
|
||||
uv run pytest vergabe_teilnahme/apps/aufgaben/tests.py -v
|
||||
uv run pytest vergabe_teilnahme/ -q # alle Tests
|
||||
```
|
||||
```
|
||||
Reference in New Issue
Block a user