Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions app/classrooms/routes/classrooms_int.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
from collections.abc import Sequence
from typing import Annotated, assert_never

from fastapi import Path
from fastapi import Path, Query
from sqlalchemy import or_, select

from app.classrooms.dependencies.classrooms_dep import ClassroomByID
from app.classrooms.models.classrooms_db import (
AnyClassroom,
Classroom,
GroupClassroom,
IndividualClassroom,
)
from app.classrooms.models.enrollments_db import Enrollment
from app.common.fastapi_ext import APIRouterExt
from app.common.schemas.classrooms_sch import ClassroomRole
from app.common.sqlalchemy_ext import db

router = APIRouterExt(tags=["classrooms internal"])


@router.get(
path="/classrooms/{classroom_id}/students/",
summary="List all student ids in a classroom by id",
)
async def list_classroom_student_ids(classroom: ClassroomByID) -> Sequence[int]:
async def list_classroom_student_ids(classroom: AnyClassroom) -> Sequence[int]:
match classroom:
case IndividualClassroom():
return [classroom.student_id]
Expand All @@ -33,6 +31,28 @@ async def list_classroom_student_ids(classroom: ClassroomByID) -> Sequence[int]:
assert_never(classroom)


@router.get(
path="/classrooms/{classroom_id}/participant-ids/",
summary="List participant ids in a classroom by id filtered by role",
)
async def list_classroom_participant_ids(
classroom: ClassroomByID,
role: Annotated[ClassroomRole | None, Query()] = None,
) -> Sequence[int]:
match role:
case ClassroomRole.TUTOR:
return [classroom.tutor_id]
case ClassroomRole.STUDENT:
return await list_classroom_student_ids(classroom=classroom)
case None:
return [
classroom.tutor_id,
*await list_classroom_student_ids(classroom=classroom),
]
case _:
assert_never(role)


@router.get(
path="/tutors/{tutor_id}/classroom-ids/",
summary="List all classroom ids for a tutor by id",
Expand Down
9 changes: 6 additions & 3 deletions app/classrooms/routes/enrollments_tutor_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
from app.common.responses import LimitedListResponses
from app.common.schemas.notifications_sch import (
EnrollmentNotificationPayloadSchema,
NotificationInputSchema,
NotificationInputV2Schema,
NotificationKind,
SingleUserRecipientFilterSchema,
)
from app.common.schemas.users_sch import UserProfileWithIDSchema

Expand Down Expand Up @@ -85,13 +86,15 @@ async def add_classroom_student(
)

await notifications_bridge.send_notification(
NotificationInputSchema(
NotificationInputV2Schema(
payload=EnrollmentNotificationPayloadSchema(
kind=NotificationKind.ENROLLMENT_CREATED_V1,
classroom_id=group_classroom.id,
student_id=tutorship.student_id,
),
recipient_user_ids=[tutorship.student_id],
recipient_filters=[
SingleUserRecipientFilterSchema(user_id=tutorship.student_id),
],
)
)

Expand Down
17 changes: 12 additions & 5 deletions app/classrooms/routes/invitations_student_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
from app.common.responses import LimitedListResponses
from app.common.schemas.notifications_sch import (
InvitationAcceptanceNotificationPayloadSchema,
NotificationInputSchema,
NotificationInputV2Schema,
NotificationKind,
SingleUserRecipientFilterSchema,
)
from app.common.schemas.users_sch import UserProfileWithIDSchema

Expand Down Expand Up @@ -146,14 +147,16 @@ async def accept_individual_invitation(
)

await notifications_bridge.send_notification(
NotificationInputSchema(
NotificationInputV2Schema(
payload=InvitationAcceptanceNotificationPayloadSchema(
kind=NotificationKind.INDIVIDUAL_INVITATION_ACCEPTED_V1,
invitation_id=individual_invitation.id,
classroom_id=individual_classroom.id,
student_id=student_id,
),
recipient_user_ids=[individual_invitation.tutor_id],
recipient_filters=[
SingleUserRecipientFilterSchema(user_id=individual_invitation.tutor_id),
],
)
)

Expand Down Expand Up @@ -185,14 +188,18 @@ async def accept_group_invitation(
)

await notifications_bridge.send_notification(
NotificationInputSchema(
NotificationInputV2Schema(
payload=InvitationAcceptanceNotificationPayloadSchema(
kind=NotificationKind.GROUP_INVITATION_ACCEPTED_V1,
invitation_id=group_invitation.id,
classroom_id=group_invitation.group_classroom_id,
student_id=student_id,
),
recipient_user_ids=[group_invitation.group_classroom.tutor_id],
recipient_filters=[
SingleUserRecipientFilterSchema(
user_id=group_invitation.group_classroom.tutor_id
),
],
)
)

Expand Down
10 changes: 8 additions & 2 deletions app/common/bridges/classrooms_bdg.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from app.common.bridges.base_bdg import BaseBridge
from app.common.bridges.utils import validate_external_json_response
from app.common.config import settings
from app.common.schemas.classrooms_sch import ClassroomRole


class ClassroomsBridge(BaseBridge):
Expand All @@ -14,9 +15,14 @@ def __init__(self) -> None:
)

@validate_external_json_response(TypeAdapter(list[int]))
async def list_classroom_student_ids(self, classroom_id: int) -> Response:
async def list_classroom_participant_ids(
self,
classroom_id: int,
role: ClassroomRole | None = None,
) -> Response:
return await self.client.get(
f"/classrooms/{classroom_id}/students/",
f"/classrooms/{classroom_id}/participant-ids/",
params=None if role is None else {"role": role},
)

@validate_external_json_response(TypeAdapter(list[int]))
Expand Down
7 changes: 5 additions & 2 deletions app/common/bridges/notifications_bdg.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from app.common.bridges.base_bdg import BaseBridge
from app.common.bridges.utils import validate_external_json_response
from app.common.config import settings
from app.common.schemas.notifications_sch import NotificationInputSchema
from app.common.schemas.notifications_sch import NotificationInputV2Schema
from app.common.schemas.user_contacts_sch import UserContactSchema


Expand Down Expand Up @@ -37,7 +37,10 @@ async def create_or_update_email_connection(
json={"email": email},
)

async def send_notification(self, data: NotificationInputSchema) -> None:
async def send_notification(
self,
data: NotificationInputV2Schema,
) -> None:
await self.broker.publish(
message=data.model_dump(mode="json"),
stream=settings.notifications_send_stream_name,
Expand Down
6 changes: 6 additions & 0 deletions app/common/schemas/classrooms_sch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from enum import StrEnum, auto


class ClassroomRole(StrEnum):
TUTOR = auto()
STUDENT = auto()
35 changes: 33 additions & 2 deletions app/common/schemas/notifications_sch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from pydantic import BaseModel, Field

from app.common.schemas.classrooms_sch import ClassroomRole


class NotificationKind(StrEnum):
INDIVIDUAL_INVITATION_ACCEPTED_V1 = auto()
Expand Down Expand Up @@ -72,6 +74,35 @@ class CustomNotificationPayloadSchema(BaseModel):
]


class NotificationInputSchema(BaseModel):
class RecipientKind(StrEnum):
SINGLE_USER = auto()
CLASSROOM_PARTICIPANT = auto()


class SingleUserRecipientFilterSchema(BaseModel):
kind: Literal[RecipientKind.SINGLE_USER] = RecipientKind.SINGLE_USER

user_id: int


class ClassroomParticipantRecipientFilterSchema(BaseModel):
kind: Literal[RecipientKind.CLASSROOM_PARTICIPANT] = (
RecipientKind.CLASSROOM_PARTICIPANT
)

classroom_id: int
role: ClassroomRole | None


AnyRecipientFilterSchema = Annotated[
SingleUserRecipientFilterSchema | ClassroomParticipantRecipientFilterSchema,
Field(discriminator="kind"),
]


class NotificationInputV2Schema(BaseModel):
payload: AnyNotificationPayloadSchema
recipient_user_ids: Annotated[list[int], Field(min_length=1, max_length=100)]
recipient_filters: Annotated[
list[AnyRecipientFilterSchema],
Field(min_length=1, max_length=100),
]
22 changes: 11 additions & 11 deletions app/conferences/routes/classroom_conferences_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from fastapi import Path
from starlette import status

from app.common.config_bdg import classrooms_bridge, notifications_bridge
from app.common.config_bdg import notifications_bridge
from app.common.dependencies.authorization_dep import AuthorizationData
from app.common.fastapi_ext import APIRouterExt, Responses
from app.common.schemas.classrooms_sch import ClassroomRole
from app.common.schemas.notifications_sch import (
ClassroomNotificationPayloadSchema,
NotificationInputSchema,
ClassroomParticipantRecipientFilterSchema,
NotificationInputV2Schema,
NotificationKind,
)
from app.conferences.dependencies.conferences_dep import (
Expand Down Expand Up @@ -36,20 +38,18 @@ async def reactivate_classroom_conference(
) -> None:
await conferences_svc.reactivate_room(livekit_room_name=livekit_room_name)

# TODO: make notifications service know classrooms instead
classroom_student_ids = await classrooms_bridge.list_classroom_student_ids(
classroom_id=classroom_id
)
if len(classroom_student_ids) == 0:
return

await notifications_bridge.send_notification(
NotificationInputSchema(
NotificationInputV2Schema(
payload=ClassroomNotificationPayloadSchema(
kind=NotificationKind.CLASSROOM_CONFERENCE_STARTED_V1,
classroom_id=classroom_id,
),
recipient_user_ids=classroom_student_ids,
recipient_filters=[
ClassroomParticipantRecipientFilterSchema(
classroom_id=classroom_id,
role=ClassroomRole.STUDENT,
)
],
)
)

Expand Down
9 changes: 6 additions & 3 deletions app/invoices/routes/invoices_student_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
from app.common.dependencies.authorization_dep import AuthorizationData
from app.common.fastapi_ext import APIRouterExt
from app.common.schemas.notifications_sch import (
NotificationInputSchema,
NotificationInputV2Schema,
NotificationKind,
RecipientInvoiceNotificationPayloadSchema,
SingleUserRecipientFilterSchema,
)
from app.invoices.dependencies.recipient_invoices_dep import (
PaymentStatusResponses,
Expand Down Expand Up @@ -91,11 +92,13 @@ async def confirm_student_recipient_invoice_payment_with_payment_type(
recipient_invoice.status = PaymentStatus.WF_RECEIVER_CONFIRMATION

await notifications_bridge.send_notification(
NotificationInputSchema(
NotificationInputV2Schema(
payload=RecipientInvoiceNotificationPayloadSchema(
kind=NotificationKind.STUDENT_RECIPIENT_INVOICE_PAYMENT_CONFIRMED_V1,
recipient_invoice_id=recipient_invoice.id,
),
recipient_user_ids=[recipient_invoice.tutor_id],
recipient_filters=[
SingleUserRecipientFilterSchema(user_id=recipient_invoice.tutor_id),
],
)
)
15 changes: 10 additions & 5 deletions app/invoices/routes/invoices_tutor_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from app.common.config_bdg import classrooms_bridge, notifications_bridge
from app.common.dependencies.authorization_dep import AuthorizationData
from app.common.fastapi_ext import APIRouterExt, Responses
from app.common.schemas.classrooms_sch import ClassroomRole
from app.common.schemas.notifications_sch import (
NotificationInputSchema,
NotificationInputV2Schema,
NotificationKind,
RecipientInvoiceNotificationPayloadSchema,
SingleUserRecipientFilterSchema,
)
from app.invoices.dependencies.recipient_invoices_dep import (
PaymentStatusResponses,
Expand Down Expand Up @@ -82,8 +84,9 @@ async def create_invoice(
auth_data: AuthorizationData,
classroom_id: int,
) -> Invoice:
classroom_student_ids = await classrooms_bridge.list_classroom_student_ids(
classroom_id=classroom_id
classroom_student_ids = await classrooms_bridge.list_classroom_participant_ids(
classroom_id=classroom_id,
role=ClassroomRole.STUDENT,
)
included_student_ids: list[int]
if data.student_ids is None:
Expand Down Expand Up @@ -121,12 +124,14 @@ async def create_invoice(

# TODO: batch sending or use invoice_ids instead
await notifications_bridge.send_notification(
NotificationInputSchema(
NotificationInputV2Schema(
payload=RecipientInvoiceNotificationPayloadSchema(
kind=NotificationKind.RECIPIENT_INVOICE_CREATED_V1,
recipient_invoice_id=recipient_invoice.id,
),
recipient_user_ids=[student_id],
recipient_filters=[
SingleUserRecipientFilterSchema(user_id=student_id),
],
)
)

Expand Down
4 changes: 2 additions & 2 deletions app/notifications/routes/notifications_mub.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from app.common.config_bdg import notifications_bridge
from app.common.fastapi_ext import APIRouterExt
from app.common.schemas.notifications_sch import NotificationInputSchema
from app.common.schemas.notifications_sch import NotificationInputV2Schema

router = APIRouterExt(tags=["notifications mub"])

Expand All @@ -12,5 +12,5 @@
status_code=status.HTTP_204_NO_CONTENT,
summary="Queue sending a new notification to multiple users by ids",
)
async def queue_notification_sending(data: NotificationInputSchema) -> None:
async def queue_notification_sending(data: NotificationInputV2Schema) -> None:
await notifications_bridge.send_notification(data)
Loading
Loading