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
2 changes: 1 addition & 1 deletion .github/workflows/development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
&& contains(github.event.pull_request.labels.*.name, 'ci:deployable')

runs-on: ubuntu-latest
environment: ${{ needs.namer.outputs.branch == 'staging' && github.actor == github.triggering_actor && 'staging' || 'manual-staging' }}
environment: manual-staging

env:
pull_image: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_USERNAME }}:${{ needs.namer.outputs.tag }}
Expand Down
46 changes: 46 additions & 0 deletions alembic/versions/055_classroom_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""classroom_events

Revision ID: 055
Revises: 054
Create Date: 2025-11-21 00:41:26.686639

"""

from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "055"
down_revision: Union[str, None] = "054"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

EventKind = sa.Enum("CLASSROOM", name="eventkind")


def upgrade() -> None:
op.execute("TRUNCATE TABLE xi_back_2.scheduler_events")
EventKind.create(bind=op.get_bind())
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"scheduler_events",
sa.Column("kind", EventKind, nullable=False),
schema="xi_back_2",
)
op.add_column(
"scheduler_events",
sa.Column("classroom_id", sa.Integer(), nullable=True),
schema="xi_back_2",
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("scheduler_events", "classroom_id", schema="xi_back_2")
op.drop_column("scheduler_events", "kind", schema="xi_back_2")
# ### end Alembic commands ###
EventKind.drop(bind=op.get_bind())
7 changes: 6 additions & 1 deletion app/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def is_testing_mode(self) -> bool:

cookie_domain: str = "localhost"
frontend_app_base_url: str = "https://app.sovlium.ru"
frontend_vacancies_base_url: str = "https://vacancy.sovlium.ru/vacancy"

password_reset_keys: FernetSettings = FernetSettings(encryption_ttl=60 * 60)
email_confirmation_keys: FernetSettings = FernetSettings(
Expand Down Expand Up @@ -208,9 +209,13 @@ def redis_supbot_dsn(self) -> str:
FaststreamIntegration(),
# other integrations are automatic
],
before_breadcrumb=before_breadcrumb,
traces_sample_rate=0,
profiles_sample_rate=0,
before_breadcrumb=before_breadcrumb,
send_default_pii=True,
max_request_body_size="always",
send_client_reports=False,
auto_session_tracking=False,
)


Expand Down
16 changes: 15 additions & 1 deletion app/common/schemas/notifications_sch.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class NotificationKind(StrEnum):
RECIPIENT_INVOICE_CREATED_V1 = auto()
STUDENT_RECIPIENT_INVOICE_PAYMENT_CONFIRMED_V1 = auto()

CUSTOM_V1 = auto()


class InvitationAcceptanceNotificationPayloadSchema(BaseModel):
kind: Literal[
Expand Down Expand Up @@ -49,11 +51,23 @@ class RecipientInvoiceNotificationPayloadSchema(BaseModel):
recipient_invoice_id: int


class CustomNotificationPayloadSchema(BaseModel):
kind: Literal[NotificationKind.CUSTOM_V1]

theme: str
pre_header: str
header: str
content: str
button_text: str
button_link: str


AnyNotificationPayloadSchema = Annotated[
InvitationAcceptanceNotificationPayloadSchema
| EnrollmentNotificationPayloadSchema
| ClassroomNotificationPayloadSchema
| RecipientInvoiceNotificationPayloadSchema,
| RecipientInvoiceNotificationPayloadSchema
| CustomNotificationPayloadSchema,
Field(discriminator="kind"),
]

Expand Down
16 changes: 15 additions & 1 deletion app/common/schemas/pochta_sch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class EmailMessageKind(StrEnum):
CUSTOM_V1 = auto()

EMAIL_CONFIRMATION_V2 = auto()
EMAIL_CHANGE_V2 = auto()
PASSWORD_RESET_V2 = auto()
Expand All @@ -21,6 +23,17 @@ class EmailMessageKind(StrEnum):
STUDENT_RECIPIENT_INVOICE_PAYMENT_CONFIRMED_V1 = auto()


class CustomEmailMessagePayloadSchema(BaseModel):
kind: Literal[EmailMessageKind.CUSTOM_V1]

theme: str
pre_header: str
header: str
content: str
button_text: str
button_link: str


class TokenEmailMessagePayloadSchema(BaseModel):
kind: Literal[
EmailMessageKind.EMAIL_CONFIRMATION_V2,
Expand Down Expand Up @@ -60,7 +73,8 @@ class RecipientInvoiceNotificationEmailMessagePayloadSchema(


AnyEmailMessagePayload = Annotated[
TokenEmailMessagePayloadSchema
CustomEmailMessagePayloadSchema
| TokenEmailMessagePayloadSchema
| ClassroomNotificationEmailMessagePayloadSchema
| RecipientInvoiceNotificationEmailMessagePayloadSchema,
Field(discriminator="kind"),
Expand Down
12 changes: 12 additions & 0 deletions app/notifications/services/adapters/base_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from app.common.schemas.notifications_sch import (
ClassroomNotificationPayloadSchema,
CustomNotificationPayloadSchema,
EnrollmentNotificationPayloadSchema,
InvitationAcceptanceNotificationPayloadSchema,
NotificationKind,
Expand Down Expand Up @@ -57,6 +58,13 @@ def adapt_student_recipient_invoice_payment_confirmed_v1(
) -> T:
raise NotImplementedError

@abstractmethod
def adapt_custom_v1(
self,
payload: CustomNotificationPayloadSchema,
) -> T:
raise NotImplementedError

def adapt(self) -> T:
# cast is used because mypy doesn't understand pydantic's discriminated unions
payload = self.notification.payload
Expand Down Expand Up @@ -85,5 +93,9 @@ def adapt(self) -> T:
return self.adapt_student_recipient_invoice_payment_confirmed_v1(
cast(RecipientInvoiceNotificationPayloadSchema, payload)
)
case NotificationKind.CUSTOM_V1:
return self.adapt_custom_v1(
cast(CustomNotificationPayloadSchema, payload)
)
case _:
assert_never(payload.kind)
15 changes: 15 additions & 0 deletions app/notifications/services/adapters/email_message_adapter.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from app.common.schemas.notifications_sch import (
ClassroomNotificationPayloadSchema,
CustomNotificationPayloadSchema,
EnrollmentNotificationPayloadSchema,
InvitationAcceptanceNotificationPayloadSchema,
RecipientInvoiceNotificationPayloadSchema,
)
from app.common.schemas.pochta_sch import (
AnyEmailMessagePayload,
ClassroomNotificationEmailMessagePayloadSchema,
CustomEmailMessagePayloadSchema,
EmailMessageKind,
RecipientInvoiceNotificationEmailMessagePayloadSchema,
)
Expand Down Expand Up @@ -75,3 +77,16 @@ def adapt_student_recipient_invoice_payment_confirmed_v1(
recipient_invoice_id=payload.recipient_invoice_id,
notification_id=self.notification.id,
)

def adapt_custom_v1(
self, payload: CustomNotificationPayloadSchema
) -> CustomEmailMessagePayloadSchema:
return CustomEmailMessagePayloadSchema(
kind=EmailMessageKind.CUSTOM_V1,
theme=payload.theme,
pre_header=payload.pre_header,
header=payload.header,
content=payload.content,
button_text=payload.button_text,
button_link=payload.button_link,
)
11 changes: 11 additions & 0 deletions app/notifications/services/adapters/telegram_message_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from app.common.config import settings
from app.common.schemas.notifications_sch import (
ClassroomNotificationPayloadSchema,
CustomNotificationPayloadSchema,
EnrollmentNotificationPayloadSchema,
InvitationAcceptanceNotificationPayloadSchema,
RecipientInvoiceNotificationPayloadSchema,
Expand Down Expand Up @@ -117,3 +118,13 @@ def adapt_student_recipient_invoice_payment_confirmed_v1(
},
),
)

def adapt_custom_v1(
self,
payload: CustomNotificationPayloadSchema,
) -> TelegramMessagePayloadSchema:
return TelegramMessagePayloadSchema(
message_text=f"{payload.header}\n\n{payload.content}",
button_text=payload.button_text,
button_link=payload.button_link,
)
1 change: 1 addition & 0 deletions app/pochta/routes/email_messages_sub.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
}

KIND_TO_TEMPLATE_ID: dict[EmailMessageKind, str] = {
EmailMessageKind.CUSTOM_V1: "228a8576-b802-11f0-bca7-d2544595dc68",
EmailMessageKind.EMAIL_CONFIRMATION_V2: "05b83984-bd89-11f0-81f1-122da0a24080",
EmailMessageKind.EMAIL_CHANGE_V2: "a25aced8-bd88-11f0-b8e4-122da0a24080",
EmailMessageKind.PASSWORD_RESET_V2: "3b5242e2-bd89-11f0-8132-025779db5bd3",
Expand Down
44 changes: 44 additions & 0 deletions app/scheduler/dependencies/classroom_events_dep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import Annotated

from fastapi import Depends, Path
from starlette import status

from app.common.fastapi_ext import Responses, with_responses
from app.scheduler.models.events_db import ClassroomEvent


class ClassroomEventResponses(Responses):
CLASSROOM_EVENT_NOT_FOUND = status.HTTP_404_NOT_FOUND, "Classroom event not found"


@with_responses(ClassroomEventResponses)
async def get_classroom_event_by_id(event_id: Annotated[int, Path()]) -> ClassroomEvent:
classroom_event = await ClassroomEvent.find_first_by_id(event_id)
if classroom_event is None:
raise ClassroomEventResponses.CLASSROOM_EVENT_NOT_FOUND
return classroom_event


ClassroomEventByID = Annotated[ClassroomEvent, Depends(get_classroom_event_by_id)]


class MyClassroomEventResponses(Responses):
CLASSROOM_EVENT_ACCESS_DENIED = (
status.HTTP_403_FORBIDDEN,
"Classroom event access denied",
)


@with_responses(MyClassroomEventResponses)
async def get_my_classroom_event_by_ids(
classroom_event: ClassroomEventByID,
classroom_id: Annotated[int, Path()],
) -> ClassroomEvent:
if classroom_event.classroom_id != classroom_id:
raise MyClassroomEventResponses.CLASSROOM_EVENT_ACCESS_DENIED
return classroom_event


MyClassroomEventByIDs = Annotated[
ClassroomEvent, Depends(get_my_classroom_event_by_ids)
]
30 changes: 14 additions & 16 deletions app/scheduler/dependencies/events_dep.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
from typing import Annotated
from typing import Annotated, Self

from fastapi import Depends, Path
from starlette import status
from fastapi import Query
from pydantic import AwareDatetime, BaseModel, model_validator

from app.common.fastapi_ext import Responses, with_responses
from app.scheduler.models.events_db import Event

class EventTimeFrameSchema(BaseModel):
happens_after: AwareDatetime
happens_before: AwareDatetime

class EventResponses(Responses):
EVENT_NOT_FOUND = status.HTTP_404_NOT_FOUND, "Event not found"
@model_validator(mode="after")
def validate_happens_after_and_happens_before(self) -> Self:
if self.happens_after >= self.happens_before:
raise ValueError(
"parameter happens_before must be later in time than happens_after"
)
return self


@with_responses(EventResponses)
async def get_event_by_id(event_id: Annotated[int, Path()]) -> Event:
event = await Event.find_first_by_id(event_id)
if event is None:
raise EventResponses.EVENT_NOT_FOUND
return event


EventById = Annotated[Event, Depends(get_event_by_id)]
EventTimeFrameQuery = Annotated[EventTimeFrameSchema, Query()]
8 changes: 6 additions & 2 deletions app/scheduler/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@
from app.common.dependencies.authorization_dep import ProxyAuthorized
from app.common.dependencies.mub_dep import MUBProtection
from app.common.fastapi_ext import APIRouterExt
from app.scheduler.routes import events_mub
from app.scheduler.routes import (
classroom_events_student_rst,
classroom_events_tutor_rst,
)

outside_router = APIRouterExt(prefix="/api/public/scheduler-service")

authorized_router = APIRouterExt(
dependencies=[ProxyAuthorized],
prefix="/api/protected/scheduler-service",
)
authorized_router.include_router(classroom_events_tutor_rst.router)
authorized_router.include_router(classroom_events_student_rst.router)

mub_router = APIRouterExt(
dependencies=[MUBProtection],
prefix="/mub/scheduler-service",
)
mub_router.include_router(events_mub.router)

internal_router = APIRouterExt(
dependencies=[APIKeyProtection],
Expand Down
Loading