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
1 change: 1 addition & 0 deletions .github/workflows/backend-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
POSTGRES_PORT: ${{ secrets.POSTGRES_PORT }}
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
uv run pytest --maxfail=1 --disable-warnings -v
working-directory: backend/app
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/backend_prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
POSTGRES_PORT: ${{ secrets.POSTGRES_PORT }}
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
uv run pytest --maxfail=1 --disable-warnings -v
working-directory: backend/app
Expand Down
3 changes: 2 additions & 1 deletion backend/app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ POSTGRES_PASSWORD=postgres
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=db_name
JWT_SECRET_KEY=your_jwt_secret_key
JWT_SECRET_KEY=your_jwt_secret_key
OPENAI_API_KEY=your_api_key
1 change: 1 addition & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Settings(BaseSettings):
POSTGRES_PORT: int
POSTGRES_DB: str
JWT_SECRET_KEY: str
OPENAI_API_KEY: str

class Config:
env_file = ".env"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add relationship for project table

Revision ID: 2214619bb37b
Revises: e846313bfb35
Create Date: 2025-07-18 01:34:04.336961

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "2214619bb37b"
down_revision: Union[str, Sequence[str], None] = "e846313bfb35"
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! ###
pass
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""add brief description and create 2 tables

Revision ID: e846313bfb35
Revises: 43d4892b1544
Create Date: 2025-07-16 18:37:32.706199

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "e846313bfb35"
down_revision: Union[str, Sequence[str], None] = "43d4892b1544"
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.create_table(
"tags",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
op.create_index(op.f("ix_tags_id"), "tags", ["id"], unique=False)
op.create_table(
"project_tags",
sa.Column("project_id", sa.Integer(), nullable=False),
sa.Column("tag_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["project_id"],
["projects.id"],
),
sa.ForeignKeyConstraint(
["tag_id"],
["tags.id"],
),
sa.PrimaryKeyConstraint("project_id", "tag_id"),
)
op.create_index(
"ix_project_tags_project_id_tag_id",
"project_tags",
["project_id", "tag_id"],
unique=False,
)
op.add_column(
"projects", sa.Column("brief_description", sa.String(), nullable=True)
)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("projects", "brief_description")
op.drop_index("ix_project_tags_project_id_tag_id", table_name="project_tags")
op.drop_table("project_tags")
op.drop_index(op.f("ix_tags_id"), table_name="tags")
op.drop_table("tags")
# ### end Alembic commands ###
9 changes: 3 additions & 6 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
from .users import User
from .project_member import ProjectMember
from .applications import Application
from .tags import Tag
from .project_tags import ProjectTag

__all__ = [
"Project",
"User",
"ProjectMember",
"Application",
]
__all__ = ["Project", "User", "ProjectMember", "Application", "Tag", "ProjectTag"]
20 changes: 20 additions & 0 deletions backend/app/models/project_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from sqlalchemy import (
Column,
Integer,
Index,
ForeignKey,
PrimaryKeyConstraint,
)
from dependencies.database import base


class ProjectTag(base):
__tablename__ = "project_tags"

project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
tag_id = Column(Integer, ForeignKey("tags.id"), nullable=False)

__table_args__ = (
PrimaryKeyConstraint("project_id", "tag_id"),
Index("ix_project_tags_project_id_tag_id", "project_id", "tag_id"),
)
5 changes: 4 additions & 1 deletion backend/app/models/projects.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from sqlalchemy import Column, Integer, String, DateTime, func, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from dependencies.database import base


Expand All @@ -7,13 +8,15 @@ class Project(base):

id = Column(Integer, primary_key=True, index=True, autoincrement=True)
title = Column(String, unique=True, nullable=False)
description = Column(String)
description = Column(String, nullable=True)
brief_description = Column(String)
created_at = Column(DateTime, default=func.current_timestamp())
is_public = Column(Boolean, nullable=True, default=True)
status = Column(String)
ceo_id = Column(Integer, ForeignKey("users.id"), nullable=True, server_default="1")
is_opensource = Column(Boolean, nullable=True, default=True, server_default="TRUE")
is_dead = Column(Boolean, nullable=True, default=False, server_default="FALSE")
tags = relationship("Tag", secondary="project_tags", back_populates="projects")

def __repr__(self):
return f"<Project({', '.join(f'{k}={getattr(self, k)!r}' for k in self.__table__.columns.keys())})>"
16 changes: 16 additions & 0 deletions backend/app/models/tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from sqlalchemy import (
Column,
Integer,
String,
)
from sqlalchemy.orm import relationship
from dependencies.database import base


class Tag(base):
__tablename__ = "tags"

id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String, unique=True, nullable=False)

projects = relationship("Project", secondary="project_tags", back_populates="tags")
1 change: 1 addition & 0 deletions backend/app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies = [
"fastapi>=0.115.12",
"greenlet>=3.2.3",
"passlib[bcrypt]>=1.7.4",
"pydantic-ai>=0.4.2",
"pydantic-settings>=2.10.1",
"pyjwt>=2.10.1",
"pytest>=8.4.1",
Expand Down
7 changes: 4 additions & 3 deletions backend/app/routes/api/project/__tests__/test_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def sample_projects():
ProjectSchema(
id=1,
title="Project 1",
description="Description 1",
brief_description="Description 1",
description=None,
is_public=True,
is_dead=False,
is_opensource=True,
Expand All @@ -36,7 +37,8 @@ def sample_projects():
ProjectSchema(
id=2,
title="Project 2",
description="Description 2",
brief_description="Description 2",
description=None,
is_public=True,
is_dead=False,
is_opensource=True,
Expand Down Expand Up @@ -67,7 +69,6 @@ async def test_get_project_not_found(mock_service, mock_user):
await get_project_by_id(
project_id,
mock_service,
mock_user,
)

assert exc_info.value.status_code == 404
Expand Down
6 changes: 4 additions & 2 deletions backend/app/routes/api/project/__tests__/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ def sample_projects():
ProjectSchema(
id=1,
title="Project 1",
description="Description 1",
brief_description="Description 1",
description=None,
is_public=True,
is_dead=False,
is_opensource=True,
Expand All @@ -39,7 +40,8 @@ def sample_projects():
ProjectSchema(
id=2,
title="Project 2",
description="Description 2",
brief_description="Description 2",
description=None,
is_public=True,
is_dead=False,
is_opensource=True,
Expand Down
39 changes: 27 additions & 12 deletions backend/app/routes/api/project/data_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dependencies.database import DBSessionDep
from models import Project, Application, ProjectMember
from sqlalchemy import select, update, delete
from sqlalchemy.orm import joinedload

from routes.api.project.exceptions import ApplicationNotFoundError
from routes.api.project.schemas import (
Expand All @@ -22,29 +23,42 @@ def __init__(self, db_session: DBSessionDep):
async def get_project_by_id(
self, project_id: int, ceo_id: int = None
) -> ProjectSchema | None:
query = select(Project).where(Project.id == project_id)
query = (
select(Project)
.where(Project.id == project_id)
.options(joinedload(Project.tags))
)
if ceo_id is not None:
query = query.where(Project.ceo_id == ceo_id)

res = await self.db_session.execute(query)
project = res.scalars().first()
try:
return ProjectSchema.model_validate(project)
except ValidationError as e:
project = res.unique().scalar_one_or_none()

if project is None:
return None

return ProjectSchema.model_validate(project)

async def get_project_by_title(self, title: str) -> ProjectSchema | None:
res = await self.db_session.execute(
select(Project).where(Project.title == title)
(
select(Project)
.where(Project.title == title)
.options(joinedload(Project.tags))
)
)
project = res.scalars().first()
try:
return ProjectSchema.model_validate(project)
except ValidationError as e:
project = res.unique().scalar_one_or_none()

if project is None:
return None

return ProjectSchema.model_validate(project)

async def get_all_projects(self) -> list[ProjectSchema]:
res = await self.db_session.execute(select(Project))
projects = res.scalars().all()
res = await self.db_session.execute(
select(Project).options(joinedload(Project.tags))
)
projects = res.unique().scalars().all()
return [ProjectSchema.model_validate(project) for project in projects]

async def create_project(
Expand Down Expand Up @@ -145,4 +159,5 @@ async def delete_application(self, project_id: int, user_id: int) -> bool:
res = await self.db_session.execute(query)
return res.rowcount > 0


ProjectsDataAccessDep = Annotated[ProjectsDataAccess, Depends(ProjectsDataAccess)]
1 change: 0 additions & 1 deletion backend/app/routes/api/project/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ async def get_user_applications(
async def get_project_by_id(
project_id: int,
service: ProjectServiceDep,
user: AuthUserDep,
) -> ProjectSchema:
return await service.get_project_by_id(project_id)

Expand Down
12 changes: 11 additions & 1 deletion backend/app/routes/api/project/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
from typing import Optional


class Tag(BaseModel):
name: str

class Config:
from_attributes = True


class NewProjectSchema(BaseModel):
title: str
brief_description: str
description: Optional[str]
is_public: Optional[bool]
is_opensource: Optional[bool]
Expand All @@ -15,15 +23,17 @@ class ProjectSchema(NewProjectSchema):
id: int
created_at: datetime
ceo_id: Optional[int]
tags: Optional[list[Tag]] = None

class Config:
from_attributes = True


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


class ApplicationSchema(BaseModel):
project_id: int
user_id: int
Expand Down
7 changes: 6 additions & 1 deletion backend/app/routes/api/project/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,12 @@ async def delete_application(self, project_id, user: UserInDB) -> ActionResponse
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"
msg = (
"Application was successfully deleted"
if res
else "Failed to delete application"
)
return ActionResponse(message=msg, success=res)


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