diff --git a/api/src/constants/lookup_constants.py b/api/src/constants/lookup_constants.py index f686f95f2..3876cfe0f 100644 --- a/api/src/constants/lookup_constants.py +++ b/api/src/constants/lookup_constants.py @@ -116,3 +116,8 @@ class AgencySubmissionNotificationSetting(StrEnum): NEVER = "never" FIRST_APPLICATION_ONLY = "first_application_only" ALWAYS = "always" + + +class OpportunityAttachmentType(StrEnum): + NOTICE_OF_FUNDING_OPPORTUNITY = "notice_of_funding_opportunity" + OTHER = "other" diff --git a/api/src/db/migrations/versions/2024_10_16_add_opportunity_attachment_tables.py b/api/src/db/migrations/versions/2024_10_16_add_opportunity_attachment_tables.py new file mode 100644 index 000000000..1b79f5b60 --- /dev/null +++ b/api/src/db/migrations/versions/2024_10_16_add_opportunity_attachment_tables.py @@ -0,0 +1,113 @@ +"""Add opportunity attachment tables + +Revision ID: 56448a3ecb8f +Revises: fa38970d0cef +Create Date: 2024-10-16 22:00:42.274537 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "56448a3ecb8f" +down_revision = "fa38970d0cef" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "lk_opportunity_attachment_type", + sa.Column("opportunity_attachment_type_id", sa.Integer(), nullable=False), + sa.Column("description", sa.Text(), 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.PrimaryKeyConstraint( + "opportunity_attachment_type_id", name=op.f("lk_opportunity_attachment_type_pkey") + ), + schema="api", + ) + op.create_table( + "opportunity_attachment", + sa.Column("attachment_id", sa.BigInteger(), nullable=False), + sa.Column("opportunity_id", sa.BigInteger(), nullable=False), + sa.Column("opportunity_attachment_type_id", sa.Integer(), nullable=False), + sa.Column("file_location", sa.Text(), nullable=False), + sa.Column("mime_type", sa.Text(), nullable=False), + sa.Column("file_name", sa.Text(), nullable=False), + sa.Column("file_description", sa.Text(), nullable=False), + sa.Column("file_size_bytes", sa.BigInteger(), nullable=False), + sa.Column("created_by", sa.Text(), nullable=True), + sa.Column("updated_by", sa.Text(), nullable=True), + sa.Column("legacy_folder_id", sa.BigInteger(), nullable=True), + 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( + ["opportunity_attachment_type_id"], + ["api.lk_opportunity_attachment_type.opportunity_attachment_type_id"], + name=op.f( + "opportunity_attachment_opportunity_attachment_type_id_lk_opportunity_attachment_type_fkey" + ), + ), + sa.ForeignKeyConstraint( + ["opportunity_id"], + ["api.opportunity.opportunity_id"], + name=op.f("opportunity_attachment_opportunity_id_opportunity_fkey"), + ), + sa.PrimaryKeyConstraint("attachment_id", name=op.f("opportunity_attachment_pkey")), + schema="api", + ) + op.create_index( + op.f("opportunity_attachment_opportunity_attachment_type_id_idx"), + "opportunity_attachment", + ["opportunity_attachment_type_id"], + unique=False, + schema="api", + ) + op.create_index( + op.f("opportunity_attachment_opportunity_id_idx"), + "opportunity_attachment", + ["opportunity_id"], + unique=False, + schema="api", + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index( + op.f("opportunity_attachment_opportunity_id_idx"), + table_name="opportunity_attachment", + schema="api", + ) + op.drop_index( + op.f("opportunity_attachment_opportunity_attachment_type_id_idx"), + table_name="opportunity_attachment", + schema="api", + ) + op.drop_table("opportunity_attachment", schema="api") + op.drop_table("lk_opportunity_attachment_type", schema="api") + # ### end Alembic commands ### diff --git a/api/src/db/models/lookup_models.py b/api/src/db/models/lookup_models.py index d954d6422..8282f6fc5 100644 --- a/api/src/db/models/lookup_models.py +++ b/api/src/db/models/lookup_models.py @@ -6,6 +6,7 @@ ApplicantType, FundingCategory, FundingInstrument, + OpportunityAttachmentType, OpportunityCategory, OpportunityStatus, ) @@ -21,6 +22,12 @@ ] ) +OPPORTUNITY_ATTACHMENT_TYPE_CONFIG = LookupConfig( + [ + LookupStr(OpportunityAttachmentType.NOTICE_OF_FUNDING_OPPORTUNITY, 1), + LookupStr(OpportunityAttachmentType.OTHER, 2), + ] +) OPPORTUNITY_CATEGORY_CONFIG = LookupConfig( [ @@ -205,3 +212,18 @@ def from_lookup(cls, lookup: Lookup) -> "LkAgencySubmissionNotificationSetting": agency_submission_notification_setting_id=lookup.lookup_val, description=lookup.get_description(), ) + + +@LookupRegistry.register_lookup(OPPORTUNITY_ATTACHMENT_TYPE_CONFIG) +class LkOpportunityAttachmentType(LookupTable, TimestampMixin): + __tablename__ = "lk_opportunity_attachment_type" + + opportunity_attachment_type_id: Mapped[int] = mapped_column(primary_key=True) + description: Mapped[str] + + @classmethod + def from_lookup(cls, lookup: Lookup) -> "LkOpportunityAttachmentType": + return LkOpportunityAttachmentType( + opportunity_attachment_type_id=lookup.lookup_val, + description=lookup.get_description(), + ) diff --git a/api/src/db/models/opportunity_models.py b/api/src/db/models/opportunity_models.py index e57bce5e3..9864f1faf 100644 --- a/api/src/db/models/opportunity_models.py +++ b/api/src/db/models/opportunity_models.py @@ -9,6 +9,7 @@ ApplicantType, FundingCategory, FundingInstrument, + OpportunityAttachmentType, OpportunityCategory, OpportunityStatus, ) @@ -18,6 +19,7 @@ LkApplicantType, LkFundingCategory, LkFundingInstrument, + LkOpportunityAttachmentType, LkOpportunityCategory, LkOpportunityStatus, ) @@ -51,6 +53,10 @@ class Opportunity(ApiSchemaTable, TimestampMixin): publisher_user_id: Mapped[str | None] publisher_profile_id: Mapped[int | None] = mapped_column(BigInteger) + opportunity_attachments: Mapped[list["OpportunityAttachment"]] = relationship( + back_populates="opportunity", uselist=True, cascade="all, delete-orphan" + ) + opportunity_assistance_listings: Mapped[list["OpportunityAssistanceListing"]] = relationship( back_populates="opportunity", uselist=True, cascade="all, delete-orphan" ) @@ -385,3 +391,29 @@ class CurrentOpportunitySummary(ApiSchemaTable, TimestampMixin): ForeignKey(LkOpportunityStatus.opportunity_status_id), index=True, ) + + +class OpportunityAttachment(ApiSchemaTable, TimestampMixin): + __tablename__ = "opportunity_attachment" + + attachment_id: Mapped[int] = mapped_column(BigInteger, primary_key=True) + + opportunity_id: Mapped[int] = mapped_column( + BigInteger, ForeignKey(Opportunity.opportunity_id), index=True + ) + opportunity: Mapped[Opportunity] = relationship(single_parent=True) + opportunity_attachment_type: Mapped[OpportunityAttachmentType] = mapped_column( + "opportunity_attachment_type_id", + LookupColumn(LkOpportunityAttachmentType), + ForeignKey(LkOpportunityAttachmentType.opportunity_attachment_type_id), + index=True, + ) + + file_location: Mapped[str] + mime_type: Mapped[str] + file_name: Mapped[str] + file_description: Mapped[str] + file_size_bytes: Mapped[int] = mapped_column(BigInteger) + created_by: Mapped[str | None] + updated_by: Mapped[str | None] + legacy_folder_id: Mapped[int | None] = mapped_column(BigInteger)