diff --git a/backend/migrations/versions/2026_03_10_1618-983168a63271_add_notifications.py b/backend/migrations/versions/2026_03_10_1618-983168a63271_add_notifications.py new file mode 100644 index 000000000..9c20f6b2d --- /dev/null +++ b/backend/migrations/versions/2026_03_10_1618-983168a63271_add_notifications.py @@ -0,0 +1,63 @@ +# Copyright 2026 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License version 3, as +# published by the Free Software Foundation. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# SPDX-FileCopyrightText: Copyright 2026 Canonical Ltd. +# SPDX-License-Identifier: AGPL-3.0-only + +"""Add notifications + +Revision ID: 983168a63271 +Revises: 2f7a130290ea +Create Date: 2026-03-10 16:18:40.152277+00:00 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "983168a63271" +down_revision = "2f7a130290ea" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "notification", + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column( + "notification_type", + sa.Enum("USER_ASSIGNED_ARTEFACT_REVIEW", "USER_ASSIGNED_ENVIRONMENT_REVIEW", name="notificationtype"), + nullable=False, + ), + sa.Column("target_url", sa.String(), nullable=True), + sa.Column("dismissed_at", sa.DateTime(), nullable=True), + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], ["app_user.id"], name=op.f("notification_user_id_fkey"), ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("id", name=op.f("notification_pkey")), + ) + op.create_index(op.f("notification_user_id_ix"), "notification", ["user_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("notification_user_id_ix"), table_name="notification") + op.drop_table("notification") + op.execute("DROP TYPE IF EXISTS notificationtype") + # ### end Alembic commands ### diff --git a/backend/test_observer/controllers/notifications/models.py b/backend/test_observer/controllers/notifications/models.py new file mode 100644 index 000000000..6324a2432 --- /dev/null +++ b/backend/test_observer/controllers/notifications/models.py @@ -0,0 +1,35 @@ +# Copyright 2024 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License version 3, as +# published by the Free Software Foundation. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# SPDX-FileCopyrightText: Copyright 2024 Canonical Ltd. +# SPDX-License-Identifier: AGPL-3.0-only + +from datetime import datetime + +from pydantic import BaseModel, ConfigDict + +from test_observer.data_access.models_enums import NotificationType + + +class NotificationResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + user_id: int + notification_type: NotificationType + target_url: str | None + created_at: datetime + dismissed_at: datetime | None + + +class NotificationsResponse(BaseModel): + notifications: list[NotificationResponse] diff --git a/backend/test_observer/data_access/models.py b/backend/test_observer/data_access/models.py index e3403b657..a96c34c14 100644 --- a/backend/test_observer/data_access/models.py +++ b/backend/test_observer/data_access/models.py @@ -51,6 +51,7 @@ FamilyName, IssueSource, IssueStatus, + NotificationType, TestExecutionStatus, TestResultStatus, ) @@ -233,6 +234,20 @@ class UserSession(Base): user: Mapped[User] = relationship(back_populates="sessions", foreign_keys=[user_id]) +class Notification(Base): + __tablename__ = "notification" + + user_id: Mapped[int] = mapped_column(ForeignKey("app_user.id", ondelete="CASCADE"), index=True) + notification_type: Mapped[NotificationType] + target_url: Mapped[str | None] = mapped_column(default=None) + dismissed_at: Mapped[datetime | None] = mapped_column(default=None) + + user: Mapped[User] = relationship(foreign_keys=[user_id]) + + def __repr__(self) -> str: + return data_model_repr(self, "user_id", "notification_type") + + class Artefact(Base): """A model to represent artefacts (snaps, debs, images)""" diff --git a/backend/test_observer/data_access/models_enums.py b/backend/test_observer/data_access/models_enums.py index e0b20bf13..5f5d61b83 100644 --- a/backend/test_observer/data_access/models_enums.py +++ b/backend/test_observer/data_access/models_enums.py @@ -118,3 +118,8 @@ class IssueStatus(StrEnum): UNKNOWN = "unknown" OPEN = "open" CLOSED = "closed" + + +class NotificationType(StrEnum): + USER_ASSIGNED_ARTEFACT_REVIEW = "USER_ASSIGNED_ARTEFACT_REVIEW" + USER_ASSIGNED_ENVIRONMENT_REVIEW = "USER_ASSIGNED_ENVIRONMENT_REVIEW"