Add Profile/Channel entities with CRUD and remove legacy parser builder.

Introduce reusable ParserProfile and ParseChannel, pair jobs with enqueue flatten, Events admin CRUD, and drop inline/legacy parser-builder UI and aliases.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-16 14:51:03 +03:00
co-authored by Cursor
parent e196de7320
commit 71f8cf169e
30 changed files with 2157 additions and 954 deletions
@@ -0,0 +1,98 @@
"""Admin CRUD for parse channels (Telegram handles in MVP)."""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import ParseChannel, ParseJob
from ..schemas import ParseChannelCreate, ParseChannelRead, ParseChannelUpdate
router = APIRouter(prefix="/admin/parse-channels", tags=["parse-channels"])
ALLOWED_SOURCE_TYPES = {"telegram"}
@router.get("", response_model=list[ParseChannelRead])
def list_channels(db: Session = Depends(get_db)):
return db.query(ParseChannel).order_by(ParseChannel.id.desc()).all()
@router.post("", response_model=ParseChannelRead, status_code=201)
def create_channel(payload: ParseChannelCreate, db: Session = Depends(get_db)):
source_type = (payload.source_type or "telegram").strip()
if source_type not in ALLOWED_SOURCE_TYPES:
raise HTTPException(
status_code=400,
detail=f"source_type must be one of: {', '.join(sorted(ALLOWED_SOURCE_TYPES))}",
)
channel = ParseChannel(
name=payload.name.strip(),
source_type=source_type,
channel=payload.channel.strip().lstrip("@"),
is_active=payload.is_active,
)
db.add(channel)
db.commit()
db.refresh(channel)
return channel
@router.get("/{channel_id}", response_model=ParseChannelRead)
def get_channel(channel_id: int, db: Session = Depends(get_db)):
channel = db.query(ParseChannel).filter(ParseChannel.id == channel_id).first()
if not channel:
raise HTTPException(status_code=404, detail="Channel not found")
return channel
@router.patch("/{channel_id}", response_model=ParseChannelRead)
def update_channel(
channel_id: int,
payload: ParseChannelUpdate,
db: Session = Depends(get_db),
):
channel = db.query(ParseChannel).filter(ParseChannel.id == channel_id).first()
if not channel:
raise HTTPException(status_code=404, detail="Channel not found")
if payload.name is not None:
channel.name = payload.name.strip()
if payload.source_type is not None:
source_type = payload.source_type.strip()
if source_type not in ALLOWED_SOURCE_TYPES:
raise HTTPException(
status_code=400,
detail=f"source_type must be one of: {', '.join(sorted(ALLOWED_SOURCE_TYPES))}",
)
channel.source_type = source_type
if payload.channel is not None:
channel.channel = payload.channel.strip().lstrip("@")
if payload.is_active is not None:
channel.is_active = payload.is_active
db.commit()
db.refresh(channel)
return channel
@router.delete("/{channel_id}", status_code=204)
def delete_channel(channel_id: int, db: Session = Depends(get_db)):
channel = db.query(ParseChannel).filter(ParseChannel.id == channel_id).first()
if not channel:
raise HTTPException(status_code=404, detail="Channel not found")
linked = (
db.query(ParseJob)
.filter(ParseJob.channel_id == channel_id, ParseJob.status.in_(("queued", "running")))
.count()
)
if linked:
raise HTTPException(
status_code=409,
detail="Cannot delete channel used by queued/running jobs",
)
db.delete(channel)
db.commit()