Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add feedback column application

Revision ID: 43d4892b1544
Revises: 1722e1790137
Create Date: 2025-07-14 18:38:01.464920

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "43d4892b1544"
down_revision: Union[str, Sequence[str], None] = "1722e1790137"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("applications", sa.Column("feedback", sa.Text(), nullable=True))
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("applications", "feedback")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
Expand Up @@ -43,36 +43,24 @@ def upgrade() -> None:
type_=sa.DateTime(),
existing_nullable=True,
)
op.alter_column(
"projects", "is_public", existing_type=sa.BOOLEAN(), nullable=True
)
op.alter_column(
"projects", "ceo_id", existing_type=sa.INTEGER(), nullable=True
)
op.alter_column("projects", "is_public", existing_type=sa.BOOLEAN(), nullable=True)
op.alter_column("projects", "ceo_id", existing_type=sa.INTEGER(), nullable=True)
op.alter_column(
"projects", "is_opensource", existing_type=sa.BOOLEAN(), nullable=True
)
op.alter_column(
"projects", "is_dead", existing_type=sa.BOOLEAN(), nullable=True
)
op.alter_column("projects", "is_dead", existing_type=sa.BOOLEAN(), nullable=True)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"projects", "is_dead", existing_type=sa.BOOLEAN(), nullable=False
)
op.alter_column("projects", "is_dead", existing_type=sa.BOOLEAN(), nullable=False)
op.alter_column(
"projects", "is_opensource", existing_type=sa.BOOLEAN(), nullable=False
)
op.alter_column(
"projects", "ceo_id", existing_type=sa.INTEGER(), nullable=False
)
op.alter_column(
"projects", "is_public", existing_type=sa.BOOLEAN(), nullable=False
)
op.alter_column("projects", "ceo_id", existing_type=sa.INTEGER(), nullable=False)
op.alter_column("projects", "is_public", existing_type=sa.BOOLEAN(), nullable=False)
op.alter_column(
"projects",
"created_at",
Expand Down
2 changes: 2 additions & 0 deletions backend/app/models/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
func,
Boolean,
ForeignKey,
Text,
PrimaryKeyConstraint,
)
from dependencies.database import base
Expand All @@ -18,6 +19,7 @@ class Application(base):
user_id = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime, default=func.current_timestamp())
is_approved = Column(Boolean, nullable=True)
feedback = Column(Text, nullable=True)

__table_args__ = (
Index("ix_applications_project_user", "project_id", "user_id", unique=True),
Expand Down
39 changes: 26 additions & 13 deletions backend/app/routes/api/project/data_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pydantic_core._pydantic_core import ValidationError
from dependencies.database import DBSessionDep
from models import Project, Application, ProjectMember
from sqlalchemy import select, update
from sqlalchemy import select, update, delete

from routes.api.project.exceptions import ApplicationNotFoundError
from routes.api.project.schemas import (
Expand All @@ -15,7 +15,6 @@
from fastapi import Depends



class ProjectsDataAccess:
def __init__(self, db_session: DBSessionDep):
self.db_session = db_session
Expand Down Expand Up @@ -46,11 +45,7 @@ async def get_project_by_title(self, title: str) -> ProjectSchema | None:
async def get_all_projects(self) -> list[ProjectSchema]:
res = await self.db_session.execute(select(Project))
projects = res.scalars().all()
return (
[ProjectSchema.model_validate(project) for project in projects]
if projects
else []
)
return [ProjectSchema.model_validate(project) for project in projects]

async def create_project(
self,
Expand All @@ -73,11 +68,7 @@ async def get_project_applications(
query = query.where(Application.is_approved.is_(None))
res = await self.db_session.execute(query)
applications = res.scalars().all()
return (
[ApplicationSchema.model_validate(app) for app in applications]
if applications
else []
)
return [ApplicationSchema.model_validate(app) for app in applications]

async def get_application_by_user_and_project_id(
self, project_id: int, user_id: int
Expand Down Expand Up @@ -110,7 +101,9 @@ async def approve_application(
Application.project_id == project_id,
Application.user_id == approve_schema.user_id,
)
.values(is_approved=approve_schema.is_approved)
.values(
is_approved=approve_schema.is_approved, feedback=approve_schema.feedback
)
.returning(Application)
)
res = await self.db_session.execute(query)
Expand All @@ -131,5 +124,25 @@ async def add_user_to_project(
await self.db_session.flush()
return ProjectMemberSchema.model_validate(project_member)

async def delete_project(self, project_id: int) -> bool:
query = delete(Project).where(Project.id == project_id)
res = await self.db_session.execute(query)
return res.rowcount > 0

async def get_user_applications(self, user_id: int) -> list[ApplicationSchema]:
query = select(Application).where(Application.user_id == user_id)
res = await self.db_session.execute(query)
applications = res.scalars().all()
return [
ApplicationSchema.model_validate(application)
for application in applications
]

async def delete_application(self, project_id: int, user_id: int) -> bool:
query = delete(Application).where(
Application.user_id == user_id, Application.project_id == project_id
)
res = await self.db_session.execute(query)
return res.rowcount > 0

ProjectsDataAccessDep = Annotated[ProjectsDataAccess, Depends(ProjectsDataAccess)]
2 changes: 1 addition & 1 deletion backend/app/routes/api/project/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class ApplicationNotFoundError(Exception):
def __init__(self, message="No application found."):
self.message = message
super().__init__(self.message)
super().__init__(self.message)
24 changes: 24 additions & 0 deletions backend/app/routes/api/project/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
ApplicationSchema,
ApproveApplicationSchema,
ProjectMemberSchema,
ActionResponse,
)
from routes.api.project.service import ProjectServiceDep

Expand All @@ -30,6 +31,13 @@ async def create_project(
return await service.create_project(project_data, user)


@router.get("/all-applications")
async def get_user_applications(
user: AuthUserDep, service: ProjectServiceDep
) -> list[ApplicationSchema]:
return await service.get_user_applications(user)


@router.get("/{project_id}")
async def get_project_by_id(
project_id: int,
Expand Down Expand Up @@ -74,3 +82,19 @@ async def approve_application(
user: AuthUserDep,
) -> ProjectMemberSchema:
return await service.approve_application(project_id, user, approve_schema)


@router.delete("/{project_id}/applications/cancel")
async def delete_application(
project_id: int, service: ProjectServiceDep, user: AuthUserDep
) -> ActionResponse:
return await service.delete_application(project_id, user)


@router.delete("/{project_id}")
async def delete_project(
project_id: int,
service: ProjectServiceDep,
user: AuthUserDep,
) -> ActionResponse:
return await service.delete_project(project_id, user)
6 changes: 6 additions & 0 deletions backend/app/routes/api/project/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ class Config:
from_attributes = True


class ActionResponse(BaseModel):
message : str
success: bool

class ApplicationSchema(BaseModel):
project_id: int
user_id: int
is_approved: Optional[bool]
created_at: datetime
feedback: Optional[str]

class Config:
from_attributes = True
Expand All @@ -33,6 +38,7 @@ class Config:
class ApproveApplicationSchema(BaseModel):
is_approved: bool
user_id: int
feedback: Optional[str]


class ProjectMemberSchema(BaseModel):
Expand Down
35 changes: 35 additions & 0 deletions backend/app/routes/api/project/service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from email.message import Message
from typing import Annotated
from fastapi import Depends
from fastapi.exceptions import HTTPException
from pyexpat.errors import messages

from models.users import UserRole
from routes.api.project.data_access import ProjectsDataAccessDep
from routes.api.project.exceptions import ApplicationNotFoundError
Expand All @@ -9,6 +12,7 @@
NewProjectSchema,
ApplicationSchema,
ApproveApplicationSchema,
ActionResponse,
)
from schemas.user import UserInDB

Expand Down Expand Up @@ -133,5 +137,36 @@ async def approve_application(
application.user_id,
)

async def delete_project(self, project_id: int, user: UserInDB) -> ActionResponse:
project = await self.data_access.get_project_by_id(project_id)
if project is None:
raise HTTPException(
status_code=404, detail=f"Project with ID {project_id} not found."
)
if project.ceo_id != user.id:
raise HTTPException(
status_code=403, detail=f"Only the project CEO can delete."
)
res = await self.data_access.delete_project(project_id)
msg = "Project was successfully deleted" if res else "Project not found"
return ActionResponse(message=msg, success=res)

async def get_user_applications(self, user: UserInDB) -> list[ApplicationSchema]:
return await self.data_access.get_user_applications(user.id)

async def delete_application(self, project_id, user: UserInDB) -> ActionResponse:
application = await self.data_access.get_application_by_user_and_project_id(
project_id, user.id
)
if application is None:
raise HTTPException(
status_code=404,
detail=f"Application not found in project {project_id}.",
)
if application.is_approved:
raise HTTPException(status_code=400, detail="Application was approved")
res = await self.data_access.detele_application(project_id, user.id)
msg = "Application was successfully deleted" if res else "Failed to delete application"
return ActionResponse(message=msg, success=res)

ProjectServiceDep = Annotated[ProjectService, Depends(ProjectService)]
Loading