135 lines
3.9 KiB
Python
135 lines
3.9 KiB
Python
import asyncio
|
|
import logging
|
|
|
|
from sqlalchemy import select, func, update
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.db.models import ClassMembership, Notification, User
|
|
from app.services.email_service import send_class_notification_email
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def create_notification(
|
|
db: AsyncSession,
|
|
user_id: int,
|
|
type: str,
|
|
title: str,
|
|
content: str | None = None,
|
|
related_id: int | None = None,
|
|
) -> Notification:
|
|
notification = Notification(
|
|
user_id=user_id,
|
|
type=type,
|
|
title=title,
|
|
content=content,
|
|
related_id=related_id,
|
|
)
|
|
db.add(notification)
|
|
await db.commit()
|
|
await db.refresh(notification)
|
|
return notification
|
|
|
|
|
|
async def create_notifications_for_class(
|
|
db: AsyncSession,
|
|
class_id: int,
|
|
type: str,
|
|
title: str,
|
|
content: str | None = None,
|
|
related_id: int | None = None,
|
|
exclude_user_id: int | None = None,
|
|
email_subject: str | None = None,
|
|
email_body: str | None = None,
|
|
email_action_path: str | None = None,
|
|
):
|
|
"""Create in-app notifications + send email for all active users in a class."""
|
|
result = await db.execute(
|
|
select(User.id, User.email)
|
|
.join(ClassMembership, ClassMembership.user_id == User.id)
|
|
.where(
|
|
ClassMembership.class_id == class_id,
|
|
User.status == "approved",
|
|
)
|
|
)
|
|
rows = result.all()
|
|
|
|
emails: list[str] = []
|
|
for uid, email in rows:
|
|
notification = Notification(
|
|
user_id=uid,
|
|
type=type,
|
|
title=title,
|
|
content=content,
|
|
related_id=related_id,
|
|
)
|
|
db.add(notification)
|
|
emails.append(email)
|
|
|
|
await db.commit()
|
|
|
|
# Send email notification in background (fire-and-forget)
|
|
if email_subject and emails:
|
|
from app.config import settings
|
|
action_url = f"{settings.frontend_url}{email_action_path}" if email_action_path else None
|
|
asyncio.create_task(_safe_send_emails(emails, email_subject, title, email_body or content or "", action_url))
|
|
|
|
|
|
async def _safe_send_emails(
|
|
emails: list[str], subject: str, title: str, body: str, action_url: str | None
|
|
):
|
|
"""Fire-and-forget email sending with error logging."""
|
|
try:
|
|
await send_class_notification_email(emails, subject, title, body, action_url)
|
|
except Exception as e:
|
|
logger.error(f"Failed to send class notification emails: {e}")
|
|
|
|
|
|
async def list_notifications(
|
|
db: AsyncSession, user_id: int, page: int = 1, page_size: int = 20
|
|
) -> tuple[list[Notification], int]:
|
|
total_result = await db.execute(
|
|
select(func.count(Notification.id)).where(Notification.user_id == user_id)
|
|
)
|
|
total = total_result.scalar() or 0
|
|
|
|
result = await db.execute(
|
|
select(Notification)
|
|
.where(Notification.user_id == user_id)
|
|
.order_by(Notification.created_at.desc())
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
)
|
|
notifications = list(result.scalars().all())
|
|
return notifications, total
|
|
|
|
|
|
async def get_unread_count(db: AsyncSession, user_id: int) -> int:
|
|
result = await db.execute(
|
|
select(func.count(Notification.id)).where(
|
|
Notification.user_id == user_id,
|
|
Notification.is_read == False,
|
|
)
|
|
)
|
|
return result.scalar() or 0
|
|
|
|
|
|
async def mark_as_read(db: AsyncSession, notification_id: int, user_id: int) -> bool:
|
|
result = await db.execute(
|
|
update(Notification)
|
|
.where(Notification.id == notification_id, Notification.user_id == user_id)
|
|
.values(is_read=True)
|
|
)
|
|
await db.commit()
|
|
return result.rowcount > 0
|
|
|
|
|
|
async def mark_all_as_read(db: AsyncSession, user_id: int) -> int:
|
|
result = await db.execute(
|
|
update(Notification)
|
|
.where(Notification.user_id == user_id, Notification.is_read == False)
|
|
.values(is_read=True)
|
|
)
|
|
await db.commit()
|
|
return result.rowcount
|