add newsletter
This commit is contained in:
@@ -11,6 +11,8 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
from app.config import Settings
|
||||
from app.db.base import utcnow
|
||||
from app.db.models import (
|
||||
Guide,
|
||||
GuidePhoto,
|
||||
InternalSquad,
|
||||
PromoCode,
|
||||
PromoCodeApplication,
|
||||
@@ -65,6 +67,19 @@ class PromoCodeView:
|
||||
return self.is_active and not self.is_expired and self.discount_percent > 0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GuideView:
|
||||
id: int
|
||||
title: str
|
||||
short_description: str
|
||||
body_text: str
|
||||
photo_file_ids: list[str] = field(default_factory=list)
|
||||
is_active: bool = True
|
||||
created_by_telegram_id: int | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AdminStats:
|
||||
total_telegram_users: int = 0
|
||||
@@ -268,6 +283,101 @@ class SyncService:
|
||||
promo = await self._get_active_promo_for_user(session, telegram_id=telegram_id)
|
||||
return self._promo_code_view(promo) if promo is not None else None
|
||||
|
||||
async def create_guide(
|
||||
self,
|
||||
*,
|
||||
title: str,
|
||||
short_description: str,
|
||||
body_text: str,
|
||||
photo_file_ids: list[str],
|
||||
created_by_telegram_id: int,
|
||||
) -> GuideView:
|
||||
resolved_title = " ".join((title or "").split()).strip()
|
||||
if not resolved_title:
|
||||
raise ValueError("Введите название гайда.")
|
||||
if len(resolved_title) > 255:
|
||||
raise ValueError("Название гайда должно быть не длиннее 255 символов.")
|
||||
|
||||
resolved_description = " ".join((short_description or "").split()).strip()
|
||||
if len(resolved_description) > 512:
|
||||
raise ValueError("Короткое описание должно быть не длиннее 512 символов.")
|
||||
|
||||
resolved_body = (body_text or "").strip()
|
||||
if not resolved_body:
|
||||
raise ValueError("Введите текст гайда.")
|
||||
|
||||
resolved_photos: list[str] = []
|
||||
for file_id in photo_file_ids:
|
||||
normalized_file_id = str(file_id or "").strip()
|
||||
if normalized_file_id and normalized_file_id not in resolved_photos:
|
||||
resolved_photos.append(normalized_file_id)
|
||||
|
||||
async with self._session_factory() as session:
|
||||
guide = Guide(
|
||||
title=resolved_title,
|
||||
short_description=resolved_description,
|
||||
body_text=resolved_body,
|
||||
is_active=True,
|
||||
created_by_telegram_id=created_by_telegram_id,
|
||||
)
|
||||
session.add(guide)
|
||||
await session.flush()
|
||||
|
||||
for position, file_id in enumerate(resolved_photos):
|
||||
session.add(
|
||||
GuidePhoto(
|
||||
guide_id=guide.id,
|
||||
file_id=file_id,
|
||||
position=position,
|
||||
)
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
return self._guide_view(guide, resolved_photos)
|
||||
|
||||
async def get_guides(self, *, active_only: bool = True, limit: int = 50) -> list[GuideView]:
|
||||
resolved_limit = max(1, min(limit, 100))
|
||||
async with self._session_factory() as session:
|
||||
query = (
|
||||
select(Guide, GuidePhoto.file_id)
|
||||
.outerjoin(GuidePhoto, GuidePhoto.guide_id == Guide.id)
|
||||
.order_by(Guide.created_at.desc(), Guide.id.desc(), GuidePhoto.position.asc())
|
||||
.limit(resolved_limit * 10)
|
||||
)
|
||||
if active_only:
|
||||
query = query.where(Guide.is_active == True) # noqa: E712
|
||||
rows = (await session.execute(query)).all()
|
||||
|
||||
guides = self._build_guide_views(rows)
|
||||
return guides[:resolved_limit]
|
||||
|
||||
async def get_guide(self, guide_id: int, *, active_only: bool = True) -> GuideView | None:
|
||||
async with self._session_factory() as session:
|
||||
query = (
|
||||
select(Guide, GuidePhoto.file_id)
|
||||
.outerjoin(GuidePhoto, GuidePhoto.guide_id == Guide.id)
|
||||
.where(Guide.id == guide_id)
|
||||
.order_by(GuidePhoto.position.asc())
|
||||
)
|
||||
if active_only:
|
||||
query = query.where(Guide.is_active == True) # noqa: E712
|
||||
rows = (await session.execute(query)).all()
|
||||
|
||||
guides = self._build_guide_views(rows)
|
||||
return guides[0] if guides else None
|
||||
|
||||
async def delete_guide(self, guide_id: int) -> GuideView | None:
|
||||
guide = await self.get_guide(guide_id, active_only=False)
|
||||
if guide is None:
|
||||
return None
|
||||
|
||||
async with self._session_factory() as session:
|
||||
await session.execute(delete(GuidePhoto).where(GuidePhoto.guide_id == guide_id))
|
||||
await session.execute(delete(Guide).where(Guide.id == guide_id))
|
||||
await session.commit()
|
||||
|
||||
return guide
|
||||
|
||||
async def consume_discount_for_user(self, telegram_id: int) -> None:
|
||||
"""Burn the applied promo / referral discount so it cannot be reused.
|
||||
|
||||
@@ -452,6 +562,15 @@ class SyncService:
|
||||
active_cached_users=int(active_cached_users or 0),
|
||||
)
|
||||
|
||||
async def get_broadcast_telegram_ids(self) -> list[int]:
|
||||
async with self._session_factory() as session:
|
||||
rows = await session.scalars(
|
||||
select(TelegramUser.telegram_id)
|
||||
.where(TelegramUser.is_blocked == False) # noqa: E712
|
||||
.order_by(TelegramUser.id.asc())
|
||||
)
|
||||
return list(rows.all())
|
||||
|
||||
async def get_admin_telegram_users_page(
|
||||
self,
|
||||
*,
|
||||
@@ -1073,6 +1192,32 @@ class SyncService:
|
||||
updated_at=promo.updated_at,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _guide_view(guide: Guide, photo_file_ids: list[str]) -> GuideView:
|
||||
return GuideView(
|
||||
id=guide.id,
|
||||
title=guide.title,
|
||||
short_description=guide.short_description,
|
||||
body_text=guide.body_text,
|
||||
photo_file_ids=photo_file_ids,
|
||||
is_active=bool(guide.is_active),
|
||||
created_by_telegram_id=guide.created_by_telegram_id,
|
||||
created_at=guide.created_at,
|
||||
updated_at=guide.updated_at,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _build_guide_views(cls, rows: list[tuple[Guide, str | None]]) -> list[GuideView]:
|
||||
guide_map: dict[int, GuideView] = {}
|
||||
for guide, file_id in rows:
|
||||
view = guide_map.get(guide.id)
|
||||
if view is None:
|
||||
view = cls._guide_view(guide, [])
|
||||
guide_map[guide.id] = view
|
||||
if file_id and file_id not in view.photo_file_ids:
|
||||
view.photo_file_ids.append(file_id)
|
||||
return list(guide_map.values())
|
||||
|
||||
@staticmethod
|
||||
def _build_views(rows: list[tuple[RemnawaveUser, str | None]]) -> list[CachedUserView]:
|
||||
view_map: dict[int, CachedUserView] = {}
|
||||
|
||||
Reference in New Issue
Block a user