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
45 changes: 45 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Summary
<!-- Briefly describe what this PR does -->

## Type of Change
<!-- Mark all that apply -->
- [ ] Feature
- [ ] Fix
- [ ] Improvement
- [ ] Refactor
- [ ] Documentation

## Included Changes
<!-- If a section does not apply, delete it -->

### Features
<!-- List new features introduced in this release -->
-

### Fixes
<!-- List bug fixes included in this release -->
-

### Improvements
<!-- List non-breaking improvements and refactors -->
-

## Related Issues
<!-- Use "Refs #<issue_number>" to reference issues -->
<!-- Use "Closes #<issue_number>" ONLY when merging into main -->

## Testing
<!-- Describe how this was tested or mark N/A -->
- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual testing
- [ ] N/A

## Notes
<!-- Optional: delete this section if not applicable -->
-

## Checklist
- [ ] Self-review completed
- [ ] CI passing
- [ ] Ready to merge
4 changes: 2 additions & 2 deletions .github/workflows/backend-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,5 @@ jobs:
with:
name: coverage-reports
path: |
coverage.xml
htmlcov/
backend/coverage.xml
backend/htmlcov/
5 changes: 4 additions & 1 deletion backend/app/application/dto/application.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import date

from pydantic import BaseModel
from pydantic import BaseModel, HttpUrl
from typing_extensions import Literal

from app.application.dto import BaseSchema
Expand All @@ -13,6 +13,7 @@ class ApplicationCreateDTO(BaseModel):
mode: Literal['active', 'passive']
platform_id: int
application_date: date
link_to_job: HttpUrl | None
observation: str | None = None
expected_salary: float | None = None
salary_range_min: float | None = None
Expand All @@ -26,6 +27,7 @@ class ApplicationUpdateDTO(BaseModel):
mode: Literal['active', 'passive']
platform_id: int
application_date: date
link_to_job: HttpUrl | None
observation: str | None = None
expected_salary: float | None = None
salary_range_min: float | None = None
Expand Down Expand Up @@ -60,6 +62,7 @@ class ApplicationDTO(BaseSchema):
mode: Literal['active', 'passive']
platform_id: int
application_date: date
link_to_job: HttpUrl | None = None
observation: str | None = None
expected_salary: float | None = None
salary_range_min: float | None = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ def __init__(self, app_repo: ApplicationRepository):

async def execute(self, user_id: int) -> List[ApplicationDTO]:
applications = await self.app_repo.get_all_by_user_id(user_id)
return [ApplicationDTO.model_validate(app) for app in applications]
active, finalized = [], []
for application in applications:
application = ApplicationDTO.model_validate(application)
if application.feedback is None:
active.append(application)
else:
finalized.append(application)
return active + finalized
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ async def execute(
application.mode = data.mode
application.platform_id = data.platform_id
application.application_date = data.application_date
application.link_to_job = (str(data.link_to_job)
if data.link_to_job else None)
application.observation = data.observation
application.expected_salary = data.expected_salary
application.salary_range_min = data.salary_range_min
Expand Down
6 changes: 6 additions & 0 deletions backend/app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,11 @@ class Settings(BaseSettings):

LOGIN_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/docs'

@property
def openapi_url(self):
if self.ENVIRONMENT == "PROD":
return None
return "/openapi.json"


envs = Settings()
5 changes: 4 additions & 1 deletion backend/app/domain/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ class BaseMixin:
sa.DateTime(timezone=True), default=sa.func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
sa.DateTime(timezone=True), onupdate=sa.func.now(), nullable=True
sa.DateTime(timezone=True), default=None,
onupdate=sa.func.now(), nullable=True
)


# TODO: Configure MappedAsDataclass and set default_factory for each model field
class Base(DeclarativeBase): ...


Expand Down Expand Up @@ -161,6 +163,7 @@ class ApplicationModel(BaseMixin, Base):
sa.String(10), nullable=False
)
observation: Mapped[Optional[str]] = mapped_column(sa.Text)
link_to_job: Mapped[Optional[str]] = mapped_column(sa.String(2083))

salary_offer: Mapped[Optional[float]] = mapped_column(sa.Numeric(10, 2))
expected_salary: Mapped[Optional[float]] = mapped_column(sa.Numeric(10, 2))
Expand Down
6 changes: 5 additions & 1 deletion backend/app/domain/repositories/application_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ async def create(
self, application: ApplicationCreateDTO
) -> ApplicationModel:
try:
db_application = ApplicationModel(**application.model_dump())
db_application = ApplicationModel(
**application.model_dump(exclude={'link_to_job'}),
link_to_job=(str(application.link_to_job)
if application.link_to_job else None),
)
self.session.add(db_application)
await self.session.commit()
await self.session.refresh(db_application)
Expand Down
1 change: 1 addition & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
title='Application Panel',
version='0.1.0',
root_path=envs.API_PREFIX,
openapi_url=envs.openapi_url,
)


Expand Down
5 changes: 4 additions & 1 deletion backend/app/presentation/schemas/application.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import date

from pydantic import BaseModel
from pydantic import BaseModel, HttpUrl
from typing_extensions import Literal

from app.presentation.schemas import BaseSchema, TimeSchema
Expand All @@ -12,6 +12,7 @@ class CreateApplication(BaseSchema):
mode: Literal['active', 'passive']
platform_id: int
application_date: date
link_to_job: HttpUrl | None = None
observation: str | None = None
expected_salary: float | None = None
salary_range_min: float | None = None
Expand All @@ -24,6 +25,7 @@ class UpdateApplication(BaseModel):
mode: Literal['active', 'passive']
platform_id: int
application_date: date
link_to_job: HttpUrl | None = None
observation: str | None = None
expected_salary: float | None = None
salary_range_min: float | None = None
Expand Down Expand Up @@ -51,6 +53,7 @@ class Application(BaseSchema, TimeSchema):
mode: Literal['active', 'passive']
platform_id: int
application_date: date
link_to_job: HttpUrl | None = None
observation: str | None = None
expected_salary: float | None = None
salary_range_min: float | None = None
Expand Down
2 changes: 2 additions & 0 deletions backend/app/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def msg(expected, actual):
return f"Expected {expected} but got {actual}"
23 changes: 23 additions & 0 deletions backend/app/tests/base_db_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import TypedDict

from app.domain.models import FeedbackDefinitionModel, PlatformModel, UserModel


class BaseDataType(TypedDict):
user: UserModel
plat_linkedin: PlatformModel
fb_denied: FeedbackDefinitionModel


def base_data() -> BaseDataType:
return {
'user': UserModel(
id=1, github_id=1, username="testuser", email="test@user.com"
),
'plat_linkedin': PlatformModel(
id=1, name="Linkedin", url="https://www.linkedin.com/"
),
'fb_denied': FeedbackDefinitionModel(
id=1, name="Denied", color="#a80000"
),
}
61 changes: 45 additions & 16 deletions backend/app/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from datetime import datetime, timezone

import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import (
AsyncSession,
AsyncEngine,
async_sessionmaker,
create_async_engine,
)
Expand All @@ -10,49 +12,76 @@
from app.config.db import get_session
from app.domain.models import Base
from app.main import app as main_app
from app.presentation.dependencies import get_current_user
from app.tests.base_db_setup import base_data


@pytest_asyncio.fixture(scope="session")
async def db_container():
container = PostgresContainer('postgres:14', driver='asyncpg')
container = PostgresContainer("postgres:14", driver="asyncpg")
container.start()
yield container
container.stop()


@pytest_asyncio.fixture
async def db_session(db_container):
async def async_engine(db_container: PostgresContainer):
db_url = db_container.get_connection_url().replace(
"postgresql://", "postgresql+asyncpg://"
)
async_engine = create_async_engine(db_url, echo=False, future=True)
async_engine = create_async_engine(db_url, echo=False)

# Reset database tables before each test
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)

async_session_maker = async_sessionmaker(
async_engine, autoflush=False, expire_on_commit=False
yield async_engine
await async_engine.dispose()


@pytest_asyncio.fixture
async def db_session(async_engine: AsyncEngine):
SessionLocal = async_sessionmaker(
bind=async_engine,
expire_on_commit=False,
)
async with async_session_maker() as session:

async with SessionLocal() as session:
session.add_all(base_data().values())
await session.commit()
yield session


@pytest_asyncio.fixture
async def async_client(db_session: AsyncSession):
"""
Async HTTP client for the FastAPI app, using the test DB session.
"""
# Dependency override to inject the test session
async def async_client(async_engine: AsyncEngine):
SessionLocal = async_sessionmaker(
bind=async_engine,
expire_on_commit=False,
)

async def override_get_session():
yield db_session
async with SessionLocal() as session:
yield session

main_app.dependency_overrides[get_session] = override_get_session

async def override_get_current_user():
from app.application.dto.user import UserDTO

return UserDTO(
id=base_data()["user"].id,
github_id=base_data()["user"].github_id,
username=base_data()["user"].username,
email=base_data()["user"].email,
created_at=datetime.now(timezone.utc),
)

main_app.dependency_overrides[get_current_user] = override_get_current_user

transport = ASGITransport(app=main_app)
async with AsyncClient(transport=transport,
base_url="http://test/api") as client:
async with AsyncClient(
transport=transport, base_url="http://test/api"
) as client:
yield client

main_app.dependency_overrides.clear()
Loading