Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 3 additions & 6 deletions alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,10 @@

from app.core.config import settings
from app.db.base import Base
import app.models.application # noqa: F401
import app.models.email_verification # noqa: F401
import app.models.event # noqa: F401
import app.models.review_image # noqa: F401
import app.models.review_submission # noqa: F401
import app.application.models # noqa: F401
import app.auth.models # noqa: F401
import app.event.models # noqa: F401
import app.store.models # noqa: F401
import app.models.user # noqa: F401

target_metadata = Base.metadata

Expand Down
6 changes: 0 additions & 6 deletions app/api/v1/router.py

This file was deleted.

Empty file added app/application/__init__.py
Empty file.
20 changes: 20 additions & 0 deletions app/application/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

from app.application.repository import ApplicationRepository
from app.application.repository_impl import ApplicationRepositoryImpl
from app.application.service import ApplicationService
from app.application.service_impl import ApplicationServiceImpl
from app.db.session import get_db


def get_application_repository(
db: AsyncSession = Depends(get_db),
) -> ApplicationRepository:
return ApplicationRepositoryImpl(db)


def get_application_service(
repo: ApplicationRepository = Depends(get_application_repository),
) -> ApplicationService:
return ApplicationServiceImpl(repo)
32 changes: 31 additions & 1 deletion app/models/application.py → app/application/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
from datetime import datetime
from uuid import UUID, uuid4

from sqlalchemy import DateTime, Enum, ForeignKey, String, UniqueConstraint
from sqlalchemy import (
DateTime,
Enum,
ForeignKey,
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func

Expand All @@ -24,3 +32,25 @@ class Application(Base):
applied_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)


class ReviewSubmission(Base):
__tablename__ = "review_submissions"

id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
application_id: Mapped[UUID] = mapped_column(
ForeignKey("applications.id"), unique=True
)
message: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)


class ReviewImage(Base):
__tablename__ = "review_images"

id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
submission_id: Mapped[UUID] = mapped_column(ForeignKey("review_submissions.id"))
image_key: Mapped[str] = mapped_column(String)
order: Mapped[int] = mapped_column(Integer)
48 changes: 48 additions & 0 deletions app/application/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from abc import ABC, abstractmethod
from uuid import UUID

from app.application.models import Application, ReviewImage, ReviewSubmission
from app.auth.models import User
from app.event.models import Event


class ApplicationRepository(ABC):
@abstractmethod
async def find_event_by_id(self, event_id: UUID) -> Event | None: ...

@abstractmethod
async def find_by_id(self, application_id: UUID) -> Application | None: ...

@abstractmethod
async def find_by_event_and_reviewer(
self, event_id: UUID, reviewer_id: UUID
) -> Application | None: ...

@abstractmethod
async def find_by_reviewer_id(
self, reviewer_id: UUID, *, offset: int, limit: int
) -> tuple[list[Application], int]: ...

@abstractmethod
async def find_user_by_id(self, user_id: UUID) -> User | None: ...

@abstractmethod
async def find_submission_by_application_id(
self, application_id: UUID
) -> ReviewSubmission | None: ...

@abstractmethod
async def find_images_by_submission_id(
self, submission_id: UUID
) -> list[ReviewImage]: ...

@abstractmethod
async def save_application(self, application: Application) -> None: ...

@abstractmethod
async def delete(self, application: Application) -> None: ...

@abstractmethod
async def save_review(
self, application_id: UUID, message: str, image_keys: list[str]
) -> None: ...
112 changes: 112 additions & 0 deletions app/application/repository_impl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from typing import cast
from uuid import UUID

from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from app.application.models import Application, ReviewImage, ReviewSubmission
from app.application.repository import ApplicationRepository
from app.auth.models import User
from app.core.exceptions import APPLICATION_003_APPLY, AppException
from app.event.models import Event


class ApplicationRepositoryImpl(ApplicationRepository):
def __init__(self, db: AsyncSession) -> None:
self.db = db

async def find_event_by_id(self, event_id: UUID) -> Event | None:
return cast(
Event | None,
await self.db.scalar(select(Event).where(Event.id == event_id)),
)

async def find_by_id(self, application_id: UUID) -> Application | None:
return cast(
Application | None,
await self.db.scalar(
select(Application).where(Application.id == application_id)
),
)

async def find_by_event_and_reviewer(
self, event_id: UUID, reviewer_id: UUID
) -> Application | None:
return cast(
Application | None,
await self.db.scalar(
select(Application).where(
Application.event_id == event_id,
Application.reviewer_id == reviewer_id,
)
),
)

async def find_by_reviewer_id(
self, reviewer_id: UUID, *, offset: int, limit: int
) -> tuple[list[Application], int]:
q = select(Application).where(Application.reviewer_id == reviewer_id)
total = (
await self.db.scalar(select(func.count()).select_from(q.subquery())) or 0
)
apps = list((await self.db.scalars(q.offset(offset).limit(limit))).all())
return apps, int(total)

async def find_user_by_id(self, user_id: UUID) -> User | None:
return cast(
User | None,
await self.db.scalar(select(User).where(User.id == user_id)),
)

async def find_submission_by_application_id(
self, application_id: UUID
) -> ReviewSubmission | None:
return cast(
ReviewSubmission | None,
await self.db.scalar(
select(ReviewSubmission).where(
ReviewSubmission.application_id == application_id
)
),
)

async def find_images_by_submission_id(
self, submission_id: UUID
) -> list[ReviewImage]:
return list(
(
await self.db.scalars(
select(ReviewImage)
.where(ReviewImage.submission_id == submission_id)
.order_by(ReviewImage.order)
)
).all()
)

async def save_application(self, application: Application) -> None:
self.db.add(application)
try:
await self.db.commit()
except IntegrityError as e:
await self.db.rollback()
raise AppException(APPLICATION_003_APPLY) from e

async def delete(self, application: Application) -> None:
await self.db.delete(application)
await self.db.commit()

async def save_review(
self, application_id: UUID, message: str, image_keys: list[str]
) -> None:
submission = ReviewSubmission(
application_id=application_id,
message=message,
)
self.db.add(submission)
await self.db.flush()
for i, key in enumerate(image_keys):
self.db.add(
ReviewImage(submission_id=submission.id, image_key=key, order=i)
)
await self.db.commit()
27 changes: 11 additions & 16 deletions app/api/v1/endpoints/application.py → app/application/router.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from fastapi import APIRouter, BackgroundTasks, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.v1.deps import require_login
from app.db.session import get_db
from app.schemas.application import (
from app.application.dependencies import get_application_service
from app.application.schemas import (
ApplicationCreateReq,
ApplicationListResp,
ReviewSubmissionReq,
)
from app.services import application as application_service
from app.application.service import ApplicationService

router = APIRouter(tags=["Application"])

Expand All @@ -17,24 +16,20 @@
async def create_application(
body: ApplicationCreateReq,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db),
reviewer_id: str = Depends(require_login),
service: ApplicationService = Depends(get_application_service),
) -> None:
await application_service.create_application(
db, reviewer_id, body, background_tasks
)
await service.create_application(reviewer_id, body, background_tasks)


@router.get("/application", response_model=ApplicationListResp)
async def get_my_applications(
page: int = Query(default=0, ge=0),
size: int = Query(default=20, ge=1),
db: AsyncSession = Depends(get_db),
reviewer_id: str = Depends(require_login),
service: ApplicationService = Depends(get_application_service),
) -> ApplicationListResp:
applications, total = await application_service.list_my_applications(
db, reviewer_id, page, size
)
applications, total = await service.list_my_applications(reviewer_id, page, size)
total_pages = max(1, (total + size - 1) // size)
return ApplicationListResp(
applications=applications,
Expand All @@ -48,17 +43,17 @@ async def get_my_applications(
@router.delete("/application/{applicationId}", response_model=None)
async def cancel_application(
applicationId: str,
db: AsyncSession = Depends(get_db),
user_id: str = Depends(require_login),
service: ApplicationService = Depends(get_application_service),
) -> None:
await application_service.cancel_application(db, applicationId, user_id)
await service.cancel_application(applicationId, user_id)


@router.post("/application/{applicationId}/submission", response_model=None)
async def submit_review(
applicationId: str,
body: ReviewSubmissionReq,
db: AsyncSession = Depends(get_db),
user_id: str = Depends(require_login),
service: ApplicationService = Depends(get_application_service),
) -> None:
await application_service.submit_review(db, applicationId, user_id, body)
await service.submit_review(applicationId, user_id, body)
File renamed without changes.
32 changes: 32 additions & 0 deletions app/application/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from abc import ABC, abstractmethod

from fastapi import BackgroundTasks

from app.application.schemas import (
ApplicationCreateReq,
ApplicationItem,
ReviewSubmissionReq,
)


class ApplicationService(ABC):
@abstractmethod
async def create_application(
self,
reviewer_id: str,
data: ApplicationCreateReq,
background_tasks: BackgroundTasks,
) -> None: ...

@abstractmethod
async def list_my_applications(
self, reviewer_id: str, page: int, size: int
) -> tuple[list[ApplicationItem], int]: ...

@abstractmethod
async def cancel_application(self, application_id: str, user_id: str) -> None: ...

@abstractmethod
async def submit_review(
self, application_id: str, user_id: str, data: ReviewSubmissionReq
) -> None: ...
Loading
Loading