diff --git a/backend/alembic/versions/398a2154b3d5_add_unique_constraint_for_applications.py b/backend/alembic/versions/398a2154b3d5_add_unique_constraint_for_applications.py new file mode 100644 index 00000000..5f65299e --- /dev/null +++ b/backend/alembic/versions/398a2154b3d5_add_unique_constraint_for_applications.py @@ -0,0 +1,34 @@ +"""add unique constraint for applications + +Revision ID: 398a2154b3d5 +Revises: add_unique_constraint_applicant_project +Create Date: 2026-07-09 21:40:36.723685 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '398a2154b3d5' +down_revision: Union[str, Sequence[str], None] = "c14aa06f723a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_unique_constraint( + "uq_applicant_project", + "applications", + ["applicant_id", "project_id"], + ) + + +def downgrade() -> None: + op.drop_constraint( + "uq_applicant_project", + "applications", + type_="unique", + ) diff --git a/backend/app/models/application.py b/backend/app/models/application.py index ef9ec136..0b6249e3 100644 --- a/backend/app/models/application.py +++ b/backend/app/models/application.py @@ -11,6 +11,7 @@ ForeignKey, String, Text, + UniqueConstraint, func, ) from sqlalchemy.dialects.postgresql import UUID @@ -33,7 +34,13 @@ class Application(Base): """ __tablename__ = "applications" - + __table_args__ = ( + UniqueConstraint( + "applicant_id", + "project_id", + name="uq_applicant_project", + ), + ) # ========================================================== # Primary Key # ========================================================== diff --git a/backend/app/services/application_service.py b/backend/app/services/application_service.py index 63a92435..9c655dc8 100644 --- a/backend/app/services/application_service.py +++ b/backend/app/services/application_service.py @@ -2,7 +2,9 @@ import uuid +from fastapi import HTTPException, status from sqlalchemy import select +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.models.application import ( @@ -28,7 +30,18 @@ def create_application( flare_id: uuid.UUID, application: ApplicationCreate, ) -> Application: + existing_application = db.scalar( + select(Application).where( + Application.applicant_id == applicant_id, + Application.project_id == project_id, + ) + ) + if existing_application: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="You have already applied to this project.", + ) db_application = Application( applicant_id=applicant_id, project_id=project_id, @@ -40,10 +53,17 @@ def create_application( ) db.add(db_application) - db.commit() - db.refresh(db_application) - return db_application + try: + db.commit() + except IntegrityError: + db.rollback() + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="You have already applied to this project.", + ) + db.refresh(db_application) + return db_application @staticmethod def get_application(