issue-facade integration

This commit is contained in:
tegwick 2026-05-14 11:30:30 +02:00
parent c2b7cc64d6
commit a907ed6f74
16 changed files with 642 additions and 95 deletions

View file

@ -1,28 +1,111 @@
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from issue_tracker.core.models import Issue, IssueState, Label, Priority
from .issue_backends import gitea_configured, local_backend, remote_backend
class IssueAdapter(ABC):
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='',
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:
"""
Adapter-Basisklasse. Jeder externe Issue-Tracker implementiert diese
Schnittstelle. Registrierung via ISSUE_ADAPTERS-Dict in settings.
Legt ein Issue im lokalen SQLite-Backend an.
Gibt {'issue_facade_id', 'issue_key', 'sync_status'} zurück.
"""
@abstractmethod
def create_issue(self, aufgabe) -> dict:
"""Legt ein Issue im externen System an. Gibt {'url', 'key'} zurück."""
@abstractmethod
def fetch_status(self, external_issue) -> str:
"""Liest den aktuellen Status aus dem externen System."""
@abstractmethod
def close_issue(self, external_issue) -> None:
"""Schließt das Issue im externen System."""
issue = aufgabe_zu_issue(aufgabe)
with local_backend() as b:
created = b.create_issue(issue)
return {
'issue_facade_id': str(created.id),
'issue_key': f'#{created.number}',
'sync_status': created.state.value,
}
def get_adapter(system: str) -> 'IssueAdapter | None':
"""Gibt den registrierten Adapter für `system` zurück, oder None."""
from django.conf import settings
adapters = getattr(settings, 'ISSUE_ADAPTERS', {})
cls = adapters.get(system)
return cls() if cls else None
def an_gitea_delegieren(aufgabe, external_issue) -> dict:
"""
Schiebt ein lokales Issue nach Gitea.
Gibt {'issue_facade_backend', '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)
url = ''
if created.sync_metadata:
url = created.sync_metadata.get('url', '')
return {
'issue_facade_backend': 'gitea',
'issue_facade_id': str(created.number),
'issue_url': url,
'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