diff --git a/api/src/db/migrations/versions/2024_12_19_create_user_saved_opportunity_table.py b/api/src/db/migrations/versions/2024_12_19_create_user_saved_opportunity_table.py new file mode 100644 index 000000000..fc702bde3 --- /dev/null +++ b/api/src/db/migrations/versions/2024_12_19_create_user_saved_opportunity_table.py @@ -0,0 +1,56 @@ +"""Create user saved opportunity table + +Revision ID: 232a9223ed9b +Revises: 6a23520d2c3c +Create Date: 2024-12-19 22:41:02.487364 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "232a9223ed9b" +down_revision = "f8058a6c0a66" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "user_saved_opportunity", + sa.Column("user_id", sa.UUID(), nullable=False), + sa.Column("opportunity_id", sa.BigInteger(), nullable=False), + sa.Column( + "created_at", + sa.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.TIMESTAMP(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.ForeignKeyConstraint( + ["user_id"], ["api.user.user_id"], name=op.f("user_saved_opportunity_user_id_user_fkey") + ), + sa.ForeignKeyConstraint( + ["opportunity_id"], + ["api.opportunity.opportunity_id"], + name=op.f("user_saved_opportunity_opportunity_id_opportunity_fkey"), + ), + sa.PrimaryKeyConstraint( + "user_id", "opportunity_id", name=op.f("user_saved_opportunity_pkey") + ), + schema="api", + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("user_saved_opportunity", schema="api") + # ### end Alembic commands ### diff --git a/api/src/db/models/opportunity_models.py b/api/src/db/models/opportunity_models.py index c71362e0e..4e8b944fd 100644 --- a/api/src/db/models/opportunity_models.py +++ b/api/src/db/models/opportunity_models.py @@ -1,4 +1,5 @@ from datetime import date +from typing import TYPE_CHECKING from sqlalchemy import BigInteger, ForeignKey, UniqueConstraint from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy @@ -24,6 +25,9 @@ LkOpportunityStatus, ) +if TYPE_CHECKING: + from src.db.models.user_models import UserSavedOpportunity + class Opportunity(ApiSchemaTable, TimestampMixin): __tablename__ = "opportunity" @@ -77,6 +81,13 @@ def agency(self) -> str | None: back_populates="opportunity", uselist=True, cascade="all, delete-orphan" ) + saved_opportunities_by_users: Mapped[list["UserSavedOpportunity"]] = relationship( + "UserSavedOpportunity", + back_populates="opportunity", + uselist=True, + cascade="all, delete-orphan", + ) + agency_record: Mapped[Agency | None] = relationship( Agency, primaryjoin="Opportunity.agency_code == foreign(Agency.agency_code)", diff --git a/api/src/db/models/user_models.py b/api/src/db/models/user_models.py index 29ce9761a..c2ed8158b 100644 --- a/api/src/db/models/user_models.py +++ b/api/src/db/models/user_models.py @@ -1,7 +1,7 @@ import uuid from datetime import datetime -from sqlalchemy import ForeignKey +from sqlalchemy import BigInteger, ForeignKey from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -9,6 +9,7 @@ from src.constants.lookup_constants import ExternalUserType from src.db.models.base import ApiSchemaTable, TimestampMixin from src.db.models.lookup_models import LkExternalUserType +from src.db.models.opportunity_models import Opportunity class User(ApiSchemaTable, TimestampMixin): @@ -16,6 +17,13 @@ class User(ApiSchemaTable, TimestampMixin): user_id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4) + saved_opportunities: Mapped[list["UserSavedOpportunity"]] = relationship( + "UserSavedOpportunity", + back_populates="user", + uselist=True, + cascade="all, delete-orphan", + ) + class LinkExternalUser(ApiSchemaTable, TimestampMixin): __tablename__ = "link_external_user" @@ -60,3 +68,17 @@ class LoginGovState(ApiSchemaTable, TimestampMixin): # https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes nonce: Mapped[uuid.UUID] + + +class UserSavedOpportunity(ApiSchemaTable, TimestampMixin): + __tablename__ = "user_saved_opportunity" + + user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey(User.user_id), primary_key=True) + opportunity_id: Mapped[int] = mapped_column( + BigInteger, ForeignKey(Opportunity.opportunity_id), primary_key=True + ) + + user: Mapped[User] = relationship(User, back_populates="saved_opportunities") + opportunity: Mapped[Opportunity] = relationship( + "Opportunity", back_populates="saved_opportunities_by_users" + ) diff --git a/documentation/api/database/erds/api-schema.png b/documentation/api/database/erds/api-schema.png index e0e62dcca..5f97fd5fa 100644 Binary files a/documentation/api/database/erds/api-schema.png and b/documentation/api/database/erds/api-schema.png differ