Quellenverwaltung: Boulevard-Kategorie, Duplikat-Prüfung, Domain-Normalisierung
- Boulevard als Kategorie in HTML-Dropdowns, JS-Labels und Pydantic-Validierung - create_source: URL-Duplikat-Prüfung (409 Conflict bei existierender URL) - create_source + update_source: Domain via _DOMAIN_ALIASES normalisieren - System-Quellen (auto-entdeckt) sind jetzt von allen Nutzern editierbar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -96,7 +96,7 @@ class SourceCreate(BaseModel):
|
|||||||
url: Optional[str] = None
|
url: Optional[str] = None
|
||||||
domain: Optional[str] = None
|
domain: Optional[str] = None
|
||||||
source_type: str = Field(default="rss_feed", pattern="^(rss_feed|web_source|excluded)$")
|
source_type: str = Field(default="rss_feed", pattern="^(rss_feed|web_source|excluded)$")
|
||||||
category: str = Field(default="sonstige", pattern="^(nachrichtenagentur|oeffentlich-rechtlich|qualitaetszeitung|behoerde|fachmedien|think-tank|international|regional|sonstige)$")
|
category: str = Field(default="sonstige", pattern="^(nachrichtenagentur|oeffentlich-rechtlich|qualitaetszeitung|behoerde|fachmedien|think-tank|international|regional|boulevard|sonstige)$")
|
||||||
status: str = Field(default="active", pattern="^(active|inactive)$")
|
status: str = Field(default="active", pattern="^(active|inactive)$")
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ class SourceUpdate(BaseModel):
|
|||||||
url: Optional[str] = None
|
url: Optional[str] = None
|
||||||
domain: Optional[str] = None
|
domain: Optional[str] = None
|
||||||
source_type: Optional[str] = Field(default=None, pattern="^(rss_feed|web_source|excluded)$")
|
source_type: Optional[str] = Field(default=None, pattern="^(rss_feed|web_source|excluded)$")
|
||||||
category: Optional[str] = Field(default=None, pattern="^(nachrichtenagentur|oeffentlich-rechtlich|qualitaetszeitung|behoerde|fachmedien|think-tank|international|regional|sonstige)$")
|
category: Optional[str] = Field(default=None, pattern="^(nachrichtenagentur|oeffentlich-rechtlich|qualitaetszeitung|behoerde|fachmedien|think-tank|international|regional|boulevard|sonstige)$")
|
||||||
status: Optional[str] = Field(default=None, pattern="^(active|inactive)$")
|
status: Optional[str] = Field(default=None, pattern="^(active|inactive)$")
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from models import SourceCreate, SourceUpdate, SourceResponse, DiscoverRequest, DiscoverResponse, DiscoverMultiResponse, DomainActionRequest
|
from models import SourceCreate, SourceUpdate, SourceResponse, DiscoverRequest, DiscoverResponse, DiscoverMultiResponse, DomainActionRequest
|
||||||
from auth import get_current_user
|
from auth import get_current_user
|
||||||
from database import db_dependency, refresh_source_counts
|
from database import db_dependency, refresh_source_counts
|
||||||
from source_rules import discover_source, discover_all_feeds, evaluate_feeds_with_claude, _extract_domain, _detect_category, domain_to_display_name
|
from source_rules import discover_source, discover_all_feeds, evaluate_feeds_with_claude, _extract_domain, _detect_category, domain_to_display_name, _DOMAIN_ALIASES
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
|
|
||||||
logger = logging.getLogger("osint.sources")
|
logger = logging.getLogger("osint.sources")
|
||||||
@@ -16,14 +16,13 @@ SOURCE_UPDATE_COLUMNS = {"name", "url", "domain", "source_type", "category", "st
|
|||||||
|
|
||||||
|
|
||||||
def _check_source_ownership(source: dict, username: str):
|
def _check_source_ownership(source: dict, username: str):
|
||||||
"""Prueft ob der Nutzer die Quelle bearbeiten/loeschen darf."""
|
"""Prueft ob der Nutzer die Quelle bearbeiten/loeschen darf.
|
||||||
|
|
||||||
|
System-Quellen (auto-entdeckt) duerfen von jedem bearbeitet werden.
|
||||||
|
Nutzer-Quellen nur vom Ersteller.
|
||||||
|
"""
|
||||||
added_by = source.get("added_by", "")
|
added_by = source.get("added_by", "")
|
||||||
if added_by == "system":
|
if added_by and added_by != "system" and added_by != username:
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="System-Quellen koennen nicht veraendert werden",
|
|
||||||
)
|
|
||||||
if added_by and added_by != username:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Nur der Ersteller kann diese Quelle bearbeiten",
|
detail="Nur der Ersteller kann diese Quelle bearbeiten",
|
||||||
@@ -442,13 +441,32 @@ async def create_source(
|
|||||||
):
|
):
|
||||||
"""Neue Quelle hinzufuegen (org-spezifisch)."""
|
"""Neue Quelle hinzufuegen (org-spezifisch)."""
|
||||||
tenant_id = current_user.get("tenant_id")
|
tenant_id = current_user.get("tenant_id")
|
||||||
|
|
||||||
|
# Domain normalisieren (Subdomain-Aliase auflösen)
|
||||||
|
domain = data.domain
|
||||||
|
if domain:
|
||||||
|
domain = _DOMAIN_ALIASES.get(domain.lower(), domain.lower())
|
||||||
|
|
||||||
|
# Duplikat-Prüfung: gleiche URL bereits vorhanden?
|
||||||
|
if data.url:
|
||||||
|
cursor = await db.execute(
|
||||||
|
"SELECT id, name FROM sources WHERE url = ? AND status = 'active'",
|
||||||
|
(data.url,),
|
||||||
|
)
|
||||||
|
existing = await cursor.fetchone()
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=f"Feed-URL bereits vorhanden: {existing['name']} (ID {existing['id']})",
|
||||||
|
)
|
||||||
|
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"""INSERT INTO sources (name, url, domain, source_type, category, status, notes, added_by, tenant_id)
|
"""INSERT INTO sources (name, url, domain, source_type, category, status, notes, added_by, tenant_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||||
(
|
(
|
||||||
data.name,
|
data.name,
|
||||||
data.url,
|
data.url,
|
||||||
data.domain,
|
domain,
|
||||||
data.source_type,
|
data.source_type,
|
||||||
data.category,
|
data.category,
|
||||||
data.status,
|
data.status,
|
||||||
@@ -483,6 +501,9 @@ async def update_source(
|
|||||||
for field, value in data.model_dump(exclude_none=True).items():
|
for field, value in data.model_dump(exclude_none=True).items():
|
||||||
if field not in SOURCE_UPDATE_COLUMNS:
|
if field not in SOURCE_UPDATE_COLUMNS:
|
||||||
continue
|
continue
|
||||||
|
# Domain normalisieren
|
||||||
|
if field == "domain" and value:
|
||||||
|
value = _DOMAIN_ALIASES.get(value.lower(), value.lower())
|
||||||
updates[field] = value
|
updates[field] = value
|
||||||
|
|
||||||
if not updates:
|
if not updates:
|
||||||
|
|||||||
@@ -420,6 +420,7 @@
|
|||||||
<option value="think-tank">Think Tank</option>
|
<option value="think-tank">Think Tank</option>
|
||||||
<option value="international">International</option>
|
<option value="international">International</option>
|
||||||
<option value="regional">Regional</option>
|
<option value="regional">Regional</option>
|
||||||
|
<option value="boulevard">Boulevard</option>
|
||||||
<option value="sonstige">Sonstige</option>
|
<option value="sonstige">Sonstige</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="sources-search" class="sr-only">Quellen durchsuchen</label>
|
<label for="sources-search" class="sr-only">Quellen durchsuchen</label>
|
||||||
@@ -475,6 +476,7 @@
|
|||||||
<option value="think-tank">Think Tank</option>
|
<option value="think-tank">Think Tank</option>
|
||||||
<option value="international">International</option>
|
<option value="international">International</option>
|
||||||
<option value="regional">Regional</option>
|
<option value="regional">Regional</option>
|
||||||
|
<option value="boulevard">Boulevard</option>
|
||||||
<option value="sonstige" selected>Sonstige</option>
|
<option value="sonstige" selected>Sonstige</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -506,6 +506,7 @@ const UI = {
|
|||||||
'think-tank': 'Think Tank',
|
'think-tank': 'Think Tank',
|
||||||
'international': 'Intl.',
|
'international': 'Intl.',
|
||||||
'regional': 'Regional',
|
'regional': 'Regional',
|
||||||
|
'boulevard': 'Boulevard',
|
||||||
'sonstige': 'Sonstige',
|
'sonstige': 'Sonstige',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren