Files
evo-sync/web/routes/catalog.py
mguschin 5e7be16755 feat: remove register, add evo webhooks, admin view-as user
- Remove /register route and nav links (users created via Evotor webhook)
- Fix evotor_webhooks.py: use phone=None instead of phone="" to avoid unique constraint
- Add admin "view as user" feature: POST /admin/users/{id}/view-as sets viewed_user_id
  in session; POST /admin/view-as/stop clears it
- catalog, vk_catalog, sync, connections GET routes use get_viewed_user() so admins
  see another user's data while browsing
- Orange banner shown at top when admin is viewing as another user

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 20:44:25 +03:00

266 lines
9.5 KiB
Python

from datetime import datetime, timezone
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy import func
from sqlalchemy.orm import Session
from web.auth.session import get_current_user, get_viewed_user
from web.config import settings
from web.database import get_db
from web.models.connections import CachedGroup, CachedProduct, CachedStore, SyncConfig, SyncFilter
from web.templates_env import templates
router = APIRouter()
def _get_or_create_sync_config(db: Session, user_id: int) -> SyncConfig:
cfg = db.query(SyncConfig).filter_by(user_id=user_id).first()
if not cfg:
cfg = SyncConfig(user_id=user_id, is_enabled=True)
db.add(cfg)
db.flush()
return cfg
def _enabled_store_ids(db: Session, user_id: int) -> set[str] | None:
"""Return set of enabled store evotor_ids, or None if filters not yet seeded (all enabled)."""
cfg = db.query(SyncConfig).filter_by(user_id=user_id).first()
if not cfg or not cfg.store_filters_seeded:
return None
filters = db.query(SyncFilter).filter_by(
sync_config_id=cfg.id, entity_type="store", filter_mode="include"
).all()
return {f.entity_id for f in filters}
def _enabled_group_ids(db: Session, user_id: int, store_evotor_id: str) -> set[str] | None:
"""Return set of enabled group evotor_ids for a store, or None if filters not yet seeded (all enabled)."""
cfg = db.query(SyncConfig).filter_by(user_id=user_id).first()
if not cfg or not cfg.group_filters_seeded:
return None
filters = db.query(SyncFilter).filter_by(
sync_config_id=cfg.id, entity_type="group", filter_mode="include",
parent_entity_id=store_evotor_id,
).all()
return {f.entity_id for f in filters}
def _render(request: Request, template: str, ctx: dict) -> HTMLResponse:
ctx["request"] = request
ctx.setdefault("jivosite_widget_id", settings.JIVOSITE_WIDGET_ID)
return templates.TemplateResponse(ctx.pop("request"), template, ctx)
@router.get("/catalog/stores")
async def catalog_stores(request: Request, db: Session = Depends(get_db)):
try:
real_user, viewed_user = get_viewed_user(request, db)
except Exception:
return RedirectResponse("/login", 303)
stores = (
db.query(CachedStore)
.filter(CachedStore.user_id == viewed_user.id)
.order_by(CachedStore.name)
.all()
)
enabled_ids = _enabled_store_ids(db, viewed_user.id)
return _render(request, "catalog/stores.html", {
"user": real_user,
"viewed_user": viewed_user if viewed_user.id != real_user.id else None,
"stores": stores,
"enabled_ids": enabled_ids,
"refresh_interval": settings.CATALOG_REFRESH_INTERVAL_SECONDS,
})
@router.get("/catalog/stores/{store_evotor_id}/groups")
async def catalog_groups(store_evotor_id: str, request: Request, db: Session = Depends(get_db)):
try:
real_user, viewed_user = get_viewed_user(request, db)
except Exception:
return RedirectResponse("/login", 303)
store = (
db.query(CachedStore)
.filter(CachedStore.user_id == viewed_user.id, CachedStore.evotor_id == store_evotor_id)
.first()
)
if not store:
return RedirectResponse("/catalog/stores", 303)
groups = (
db.query(CachedGroup)
.filter(CachedGroup.user_id == viewed_user.id, CachedGroup.store_evotor_id == store_evotor_id)
.order_by(CachedGroup.name)
.all()
)
enabled_ids = _enabled_group_ids(db, viewed_user.id, store_evotor_id)
counts_q = (
db.query(CachedProduct.group_evotor_id, func.count().label("cnt"))
.filter(CachedProduct.user_id == viewed_user.id, CachedProduct.store_evotor_id == store_evotor_id)
.group_by(CachedProduct.group_evotor_id)
.all()
)
product_counts = {row.group_evotor_id: row.cnt for row in counts_q}
return _render(request, "catalog/groups.html", {
"user": real_user,
"viewed_user": viewed_user if viewed_user.id != real_user.id else None,
"store": store, "groups": groups,
"enabled_ids": enabled_ids,
"product_counts": product_counts,
})
@router.get("/catalog/stores/{store_evotor_id}/products")
async def catalog_products(store_evotor_id: str, request: Request, db: Session = Depends(get_db)):
try:
real_user, viewed_user = get_viewed_user(request, db)
except Exception:
return RedirectResponse("/login", 303)
store = (
db.query(CachedStore)
.filter(CachedStore.user_id == viewed_user.id, CachedStore.evotor_id == store_evotor_id)
.first()
)
if not store:
return RedirectResponse("/catalog/stores", 303)
group_id = request.query_params.get("group")
q = db.query(CachedProduct).filter(
CachedProduct.user_id == viewed_user.id,
CachedProduct.store_evotor_id == store_evotor_id,
)
if group_id:
q = q.filter(CachedProduct.group_evotor_id == group_id)
products = q.order_by(CachedProduct.name).all()
groups = (
db.query(CachedGroup)
.filter(CachedGroup.user_id == viewed_user.id, CachedGroup.store_evotor_id == store_evotor_id)
.order_by(CachedGroup.name)
.all()
)
group_map = {g.evotor_id: g.name for g in groups}
return _render(request, "catalog/products.html", {
"user": real_user,
"viewed_user": viewed_user if viewed_user.id != real_user.id else None,
"store": store,
"products": products,
"groups": groups,
"group_id": group_id,
"group_map": group_map,
})
@router.post("/catalog/stores/{store_evotor_id}/toggle")
async def catalog_store_toggle(store_evotor_id: str, request: Request, db: Session = Depends(get_db)):
try:
user = get_current_user(request, db)
except Exception:
return RedirectResponse("/login", 303)
cfg = _get_or_create_sync_config(db, user.id)
existing = db.query(SyncFilter).filter_by(
sync_config_id=cfg.id, entity_type="store", filter_mode="include"
).all()
existing_ids = {f.entity_id for f in existing}
if cfg.store_filters_seeded:
if store_evotor_id in existing_ids:
# Currently enabled → disable: remove its filter
db.query(SyncFilter).filter_by(
sync_config_id=cfg.id, entity_type="store",
entity_id=store_evotor_id, filter_mode="include",
).delete()
else:
# Currently disabled → re-enable: add its filter back
db.add(SyncFilter(
sync_config_id=cfg.id,
entity_type="store",
entity_id=store_evotor_id,
filter_mode="include",
created_at=datetime.now(timezone.utc).replace(tzinfo=None),
))
else:
# First toggle ever: seed include-filters for all OTHER stores, mark seeded
all_stores = db.query(CachedStore).filter_by(user_id=user.id).all()
now = datetime.now(timezone.utc).replace(tzinfo=None)
for s in all_stores:
if s.evotor_id == store_evotor_id:
continue
db.add(SyncFilter(
sync_config_id=cfg.id,
entity_type="store",
entity_id=s.evotor_id,
entity_name=s.name,
filter_mode="include",
created_at=now,
))
cfg.store_filters_seeded = True
db.commit()
return RedirectResponse("/catalog/stores", 303)
@router.post("/catalog/stores/{store_evotor_id}/groups/{group_evotor_id}/toggle")
async def catalog_group_toggle(
store_evotor_id: str, group_evotor_id: str,
request: Request, db: Session = Depends(get_db),
):
try:
user = get_current_user(request, db)
except Exception:
return RedirectResponse("/login", 303)
cfg = _get_or_create_sync_config(db, user.id)
existing = db.query(SyncFilter).filter_by(
sync_config_id=cfg.id, entity_type="group", filter_mode="include",
parent_entity_id=store_evotor_id,
).all()
existing_ids = {f.entity_id for f in existing}
if cfg.group_filters_seeded:
if group_evotor_id in existing_ids:
db.query(SyncFilter).filter_by(
sync_config_id=cfg.id, entity_type="group",
entity_id=group_evotor_id, filter_mode="include",
).delete()
else:
db.add(SyncFilter(
sync_config_id=cfg.id,
entity_type="group",
entity_id=group_evotor_id,
filter_mode="include",
parent_entity_id=store_evotor_id,
created_at=datetime.now(timezone.utc).replace(tzinfo=None),
))
else:
# First toggle ever: seed include-filters for all OTHER groups in this store, mark seeded
all_groups = db.query(CachedGroup).filter_by(
user_id=user.id, store_evotor_id=store_evotor_id,
).all()
now = datetime.now(timezone.utc).replace(tzinfo=None)
for g in all_groups:
if g.evotor_id == group_evotor_id:
continue
db.add(SyncFilter(
sync_config_id=cfg.id,
entity_type="group",
entity_id=g.evotor_id,
entity_name=g.name,
filter_mode="include",
parent_entity_id=store_evotor_id,
created_at=now,
))
cfg.group_filters_seeded = True
db.commit()
return RedirectResponse(f"/catalog/stores/{store_evotor_id}/groups", 303)