From 34921f5c193b7811b22209e6baab189478fc3269 Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 3 Mar 2026 07:06:44 -0500 Subject: [PATCH 01/18] t \:wq :wq :wq ydantic DTOs --- backend/app/schemas/AccountDTO.py | 15 + backend/app/schemas/BudgetDataDTO.py | 19 ++ backend/app/schemas/CalendarEventDTO.py | 15 + backend/app/schemas/CarouselSlideDTO.py | 13 + backend/app/schemas/CommitteeAssignmentDTO.py | 11 + backend/app/schemas/CommitteeDTO.py | 19 ++ backend/app/schemas/DistrictDTO.py | 14 + .../app/schemas/FinanceHearingConfigDTO.py | 15 + backend/app/schemas/FinanceHearingDateDTO.py | 15 + backend/app/schemas/LeadershipDTO.py | 16 + backend/app/schemas/LegislationActionDTO.py | 12 + backend/app/schemas/LegislationDTO.py | 23 ++ backend/app/schemas/NewsDTO.py | 25 ++ backend/app/schemas/SenatorDTO.py | 21 ++ backend/app/schemas/StaffDTO.py | 14 + backend/app/schemas/StaticPageDTO.py | 13 + backend/app/schemas/__init__.py | 85 ++--- backend/requirements.txt | 3 + backend/tests/schemas/test_schemas.py | 302 ++++++++++++++++++ 19 files changed, 585 insertions(+), 65 deletions(-) create mode 100644 backend/app/schemas/AccountDTO.py create mode 100644 backend/app/schemas/BudgetDataDTO.py create mode 100644 backend/app/schemas/CalendarEventDTO.py create mode 100644 backend/app/schemas/CarouselSlideDTO.py create mode 100644 backend/app/schemas/CommitteeAssignmentDTO.py create mode 100644 backend/app/schemas/CommitteeDTO.py create mode 100644 backend/app/schemas/DistrictDTO.py create mode 100644 backend/app/schemas/FinanceHearingConfigDTO.py create mode 100644 backend/app/schemas/FinanceHearingDateDTO.py create mode 100644 backend/app/schemas/LeadershipDTO.py create mode 100644 backend/app/schemas/LegislationActionDTO.py create mode 100644 backend/app/schemas/LegislationDTO.py create mode 100644 backend/app/schemas/NewsDTO.py create mode 100644 backend/app/schemas/SenatorDTO.py create mode 100644 backend/app/schemas/StaffDTO.py create mode 100644 backend/app/schemas/StaticPageDTO.py create mode 100644 backend/tests/schemas/test_schemas.py diff --git a/backend/app/schemas/AccountDTO.py b/backend/app/schemas/AccountDTO.py new file mode 100644 index 0000000..51efe33 --- /dev/null +++ b/backend/app/schemas/AccountDTO.py @@ -0,0 +1,15 @@ +from typing import Literal + +from pydantic import BaseModel, ConfigDict + + +class AccountDTO(BaseModel): + id: int + email: str + pid: str + first_name: str + last_name: str + role: Literal["admin", "staff"] + + model_config = ConfigDict(from_attributes=True) + diff --git a/backend/app/schemas/BudgetDataDTO.py b/backend/app/schemas/BudgetDataDTO.py new file mode 100644 index 0000000..ed69796 --- /dev/null +++ b/backend/app/schemas/BudgetDataDTO.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, ConfigDict + + +class BudgetDataDTO(BaseModel): + id: int + fiscal_year: str + category: str + amount: float + description: Optional[str] = None + children: List["BudgetDataDTO"] = [] + + model_config = ConfigDict(from_attributes=True) + + +BudgetDataDTO.model_rebuild() diff --git a/backend/app/schemas/CalendarEventDTO.py b/backend/app/schemas/CalendarEventDTO.py new file mode 100644 index 0000000..cd25749 --- /dev/null +++ b/backend/app/schemas/CalendarEventDTO.py @@ -0,0 +1,15 @@ +from datetime import datetime + +from pydantic import BaseModel, ConfigDict + + +class CalendarEventDTO(BaseModel): + id: int + title: str + description: str + start_datetime: datetime + end_datetime: datetime + location: str | None = None + event_type: str + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/CarouselSlideDTO.py b/backend/app/schemas/CarouselSlideDTO.py new file mode 100644 index 0000000..9b6f722 --- /dev/null +++ b/backend/app/schemas/CarouselSlideDTO.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel, ConfigDict + + +class CarouselSlideDTO(BaseModel): + id: int + image_url: str + overlay_text: str + link_url: str + display_order: int + is_active: bool + + model_config = ConfigDict(from_attributes=True) + diff --git a/backend/app/schemas/CommitteeAssignmentDTO.py b/backend/app/schemas/CommitteeAssignmentDTO.py new file mode 100644 index 0000000..b72f180 --- /dev/null +++ b/backend/app/schemas/CommitteeAssignmentDTO.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict + + +class CommitteeAssignmentDTO(BaseModel): + committee_id: int + committee_name: str + role: str + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/CommitteeDTO.py b/backend/app/schemas/CommitteeDTO.py new file mode 100644 index 0000000..1ca6f13 --- /dev/null +++ b/backend/app/schemas/CommitteeDTO.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import List + +from pydantic import BaseModel, ConfigDict + +from .SenatorDTO import SenatorDTO + + +class CommitteeDTO(BaseModel): + id: int + name: str + description: str + chair_name: str + chair_email: str + members: List[SenatorDTO] + is_active: bool + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/DistrictDTO.py b/backend/app/schemas/DistrictDTO.py new file mode 100644 index 0000000..ab04342 --- /dev/null +++ b/backend/app/schemas/DistrictDTO.py @@ -0,0 +1,14 @@ +from typing import List, Optional + +from pydantic import BaseModel, ConfigDict + +from .SenatorDTO import SenatorDTO + + +class DistrictDTO(BaseModel): + id: int + district_name: str + description: Optional[str] = None + senator: Optional[List[SenatorDTO]] = None + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/FinanceHearingConfigDTO.py b/backend/app/schemas/FinanceHearingConfigDTO.py new file mode 100644 index 0000000..a68e8fc --- /dev/null +++ b/backend/app/schemas/FinanceHearingConfigDTO.py @@ -0,0 +1,15 @@ +from datetime import date +from typing import List, Optional + +from pydantic import BaseModel, ConfigDict + +from .FinanceHearingDateDTO import FinanceHearingDateDTO + + +class FinanceHearingConfigDTO(BaseModel): + is_active: bool + season_start: Optional[date] = None + season_end: Optional[date] = None + dates: List[FinanceHearingDateDTO] + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/FinanceHearingDateDTO.py b/backend/app/schemas/FinanceHearingDateDTO.py new file mode 100644 index 0000000..b7b46e4 --- /dev/null +++ b/backend/app/schemas/FinanceHearingDateDTO.py @@ -0,0 +1,15 @@ +from datetime import date, time +from typing import Optional + +from pydantic import BaseModel, ConfigDict + + +class FinanceHearingDateDTO(BaseModel): + id: int + hearing_date: date + hearing_time: time + location: Optional[str] = None + description: Optional[str] = None + is_full: bool + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/LeadershipDTO.py b/backend/app/schemas/LeadershipDTO.py new file mode 100644 index 0000000..2589a85 --- /dev/null +++ b/backend/app/schemas/LeadershipDTO.py @@ -0,0 +1,16 @@ +from typing import Optional + +from pydantic import BaseModel, ConfigDict + + +class LeadershipDTO(BaseModel): + id: int + title: str + first_name: str + last_name: str + email: str + photo_url: Optional[str] = None + session_number: int + is_current: bool + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/LegislationActionDTO.py b/backend/app/schemas/LegislationActionDTO.py new file mode 100644 index 0000000..1dbce0d --- /dev/null +++ b/backend/app/schemas/LegislationActionDTO.py @@ -0,0 +1,12 @@ +from datetime import date + +from pydantic import BaseModel, ConfigDict + + +class LegislationActionDTO(BaseModel): + id: int + action_date: date + description: str + action_type: str + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/LegislationDTO.py b/backend/app/schemas/LegislationDTO.py new file mode 100644 index 0000000..3c850ae --- /dev/null +++ b/backend/app/schemas/LegislationDTO.py @@ -0,0 +1,23 @@ +from datetime import date +from typing import List + +from pydantic import BaseModel, ConfigDict + +from .LegislationActionDTO import LegislationActionDTO + + +class LegislationDTO(BaseModel): + id: int + title: str + bill_number: str + session_number: int + sponsor_name: str + summary: str + full_text: str + status: str + type: str + date_introduced: date + date_last_action: date + actions: List[LegislationActionDTO] + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/NewsDTO.py b/backend/app/schemas/NewsDTO.py new file mode 100644 index 0000000..3ad1aea --- /dev/null +++ b/backend/app/schemas/NewsDTO.py @@ -0,0 +1,25 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, computed_field + + +class NewsDTO(BaseModel): + id: int + title: str + summary: str + body: str + image_url: Optional[str] = None + date_published: datetime + date_last_edited: datetime + + admin: Optional[object] = None + + model_config = ConfigDict(from_attributes=True) + + @computed_field + @property + def author_name(self) -> str: + if self.admin: + return f"{self.admin.first_name} {self.admin.last_name}" + return "Unknown" diff --git a/backend/app/schemas/SenatorDTO.py b/backend/app/schemas/SenatorDTO.py new file mode 100644 index 0000000..6b5fa3e --- /dev/null +++ b/backend/app/schemas/SenatorDTO.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, ConfigDict + +from .CommitteeAssignmentDTO import CommitteeAssignmentDTO + + +class SenatorDTO(BaseModel): + id: int + first_name: str + last_name: str + email: str + headshot_url: Optional[str] = None + district_id: int + is_active: bool + session_number: int + committees: List[CommitteeAssignmentDTO] + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/StaffDTO.py b/backend/app/schemas/StaffDTO.py new file mode 100644 index 0000000..b320eac --- /dev/null +++ b/backend/app/schemas/StaffDTO.py @@ -0,0 +1,14 @@ +from typing import Optional + +from pydantic import BaseModel, ConfigDict + + +class StaffDTO(BaseModel): + id: int + first_name: str + last_name: str + title: str + email: str + photo_url: Optional[str] = None + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/StaticPageDTO.py b/backend/app/schemas/StaticPageDTO.py new file mode 100644 index 0000000..a411354 --- /dev/null +++ b/backend/app/schemas/StaticPageDTO.py @@ -0,0 +1,13 @@ +from datetime import datetime + +from pydantic import BaseModel, ConfigDict + + +class StaticPageDTO(BaseModel): + id: int + page_slug: str + title: str + body: str + updated_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 28acb24..f4aa191 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,82 +1,37 @@ """Pydantic schemas""" -from .account import AccountDTO, CreateAccountDTO -from .budget import BudgetDataDTO, CreateBudgetDataDTO -from .calendar_event import CreateCalendarEventDTO -from .carousel import CarouselSlideDTO, CreateCarouselSlideDTO -from .committee import ( - AssignCommitteeMemberDTO, - CommitteeDTO, - CreateCommitteeDTO, -) -from .district import DistrictDTO, DistrictLookupDTO -from .finance import ( - CreateFinanceHearingDateDTO, - FinanceHearingConfigDTO, - FinanceHearingDateDTO, - UpdateFinanceHearingConfigDTO, -) -from .leadership import LeadershipDTO -from .legislation import ( - CreateLegislationActionDTO, - CreateLegislationDTO, - LegislationActionDTO, - LegislationDTO, -) -from .news import CreateNewsDTO, NewsDTO, UpdateNewsDTO -from .senator import ( - CommitteeAssignmentDTO, - CreateSenatorDTO, - SenatorDTO, - UpdateSenatorDTO, -) -from .staff import CreateStaffDTO, StaffDTO -from .static_page import StaticPageDTO, UpdateStaticPageDTO +from .AccountDTO import AccountDTO +from .BudgetDataDTO import BudgetDataDTO +from .CalendarEventDTO import CalendarEventDTO +from .CarouselSlideDTO import CarouselSlideDTO +from .CommitteeAssignmentDTO import CommitteeAssignmentDTO +from .CommitteeDTO import CommitteeDTO +from .DistrictDTO import DistrictDTO +from .FinanceHearingConfigDTO import FinanceHearingConfigDTO +from .FinanceHearingDateDTO import FinanceHearingDateDTO +from .LeadershipDTO import LeadershipDTO +from .LegislationActionDTO import LegislationActionDTO +from .LegislationDTO import LegislationDTO +from .NewsDTO import NewsDTO +from .SenatorDTO import SenatorDTO +from .StaffDTO import StaffDTO +from .StaticPageDTO import StaticPageDTO __all__ = [ - # Account + "SenatorDTO", "AccountDTO", - "CreateAccountDTO", - # Budget "BudgetDataDTO", - "CreateBudgetDataDTO", - # Calendar Event - "CreateCalendarEventDTO", - # Carousel + "CalendarEventDTO", "CarouselSlideDTO", - "CreateCarouselSlideDTO", - # Committee - "AssignCommitteeMemberDTO", + "CommitteeAssignmentDTO", "CommitteeDTO", - "CreateCommitteeDTO", - # District "DistrictDTO", - "DistrictLookupDTO", - # Finance - "CreateFinanceHearingDateDTO", "FinanceHearingConfigDTO", - "FinanceHearingDateDTO", - "UpdateFinanceHearingConfigDTO", - # Leadership "LeadershipDTO", - # Legislation - "CreateLegislationActionDTO", - "CreateLegislationDTO", + "FinanceHearingDateDTO", "LegislationActionDTO", "LegislationDTO", - # News - "CreateNewsDTO", "NewsDTO", - "UpdateNewsDTO", - # Senator - "CommitteeAssignmentDTO", - "CreateSenatorDTO", - "SenatorDTO", - "UpdateSenatorDTO", - # Staff - "CreateStaffDTO", "StaffDTO", - # Static Page "StaticPageDTO", - "UpdateStaticPageDTO", ] diff --git a/backend/requirements.txt b/backend/requirements.txt index 1beecc7..9f811b5 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -16,3 +16,6 @@ httpx==0.28.1 pre-commit==3.7.1 ruff==0.9.3 junitparser==3.2.0 + +# Pydantic (ships with fastapi) +pydantic==2.7.1 diff --git a/backend/tests/schemas/test_schemas.py b/backend/tests/schemas/test_schemas.py new file mode 100644 index 0000000..cb67522 --- /dev/null +++ b/backend/tests/schemas/test_schemas.py @@ -0,0 +1,302 @@ +from datetime import date, datetime, time + +import pytest + +from app.schemas import ( + AccountDTO, + BudgetDataDTO, + CalendarEventDTO, + CarouselSlideDTO, + CommitteeAssignmentDTO, + CommitteeDTO, + DistrictDTO, + FinanceHearingConfigDTO, + FinanceHearingDateDTO, + LeadershipDTO, + LegislationActionDTO, + LegislationDTO, + NewsDTO, + SenatorDTO, + StaffDTO, + StaticPageDTO, +) + + +# ------------------------ +# Mock ORM helper +# ------------------------ +class MockAdmin: + def __init__(self, first_name: str, last_name: str): + self.first_name = first_name + self.last_name = last_name + + +# ------------------------ +# Test CommitteeDTO +# ------------------------ +def test_committee_dto_serialization(): + committee_assignment = CommitteeAssignmentDTO( + committee_id=1, + committee_name="Finance", + role="Member" + ) + + senator = SenatorDTO( + id=1, + first_name="John", + last_name="Doe", + email="john@example.com", + headshot_url=None, + district_id=100, + is_active=True, + session_number=2026, + committees=[committee_assignment] + ) + + committee = CommitteeDTO( + id=10, + name="Finance", + description="Finance Committee", + chair_name="Jane Chair", + chair_email="chair@example.com", + members=[senator], + is_active=True + ) + + data = committee.model_dump() + assert data["id"] == 10 + assert data["members"][0]["first_name"] == "John" + + +# ------------------------ +# Test NewsDTO with computed field +# ------------------------ +def test_news_dto_computed_author(): + admin = MockAdmin(first_name="Alice", last_name="Admin") + + news = NewsDTO( + id=1, + title="News Title", + summary="Summary", + body="Body", + image_url=None, + date_published=datetime(2026, 3, 3, 10, 0), + date_last_edited=datetime(2026, 3, 3, 11, 0), + admin=admin + ) + + data = news.model_dump() + assert data["author_name"] == "Alice Admin" + + +# ------------------------ +# Test LeadershipDTO +# ------------------------ +def test_leadership_dto(): + leadership = LeadershipDTO( + id=1, + title="Governor", + first_name="Bob", + last_name="Leader", + email="bob@example.com", + photo_url=None, + session_number=2026, + is_current=True + ) + + data = leadership.model_dump() + assert data["title"] == "Governor" + assert data["is_current"] is True + + +# ------------------------ +# Test LegislationDTO +# ------------------------ +def test_legislation_dto(): + action = LegislationActionDTO( + id=1, + action_date=date(2026, 1, 1), + description="Introduced", + action_type="Bill Introduction" + ) + + legislation = LegislationDTO( + id=1, + title="Bill A", + bill_number="A123", + session_number=2026, + sponsor_name="John Sponsor", + summary="Summary", + full_text="Full text", + status="Active", + type="Bill", + date_introduced=date(2026, 1, 1), + date_last_action=date(2026, 1, 2), + actions=[action] + ) + + data = legislation.model_dump() + assert data["actions"][0]["description"] == "Introduced" + + +# ------------------------ +# Test CarouselSlideDTO +# ------------------------ +def test_carousel_slide_dto(): + slide = CarouselSlideDTO( + id=1, + image_url="https://example.com/image.png", + overlay_text="Overlay", + link_url="https://example.com", + display_order=1, + is_active=True + ) + + data = slide.model_dump() + assert data["overlay_text"] == "Overlay" + + +# ------------------------ +# Test Finance DTOs +# ------------------------ +def test_finance_hearing_dtos(): + hearing_date = FinanceHearingDateDTO( + id=1, + hearing_date=date(2026, 3, 3), + hearing_time=time(14, 0), + location="Room 101", + description="Budget Hearing", + is_full=False + ) + + config = FinanceHearingConfigDTO( + is_active=True, + season_start=date(2026, 1, 1), + season_end=date(2026, 12, 31), + dates=[hearing_date] + ) + + data = config.model_dump() + assert data["dates"][0]["location"] == "Room 101" + + +# ------------------------ +# Test StaffDTO +# ------------------------ +def test_staff_dto(): + staff = StaffDTO( + id=1, + first_name="Sally", + last_name="Staff", + title="Manager", + email="sally@example.com", + photo_url=None + ) + + data = staff.model_dump() + assert data["title"] == "Manager" + + +# ------------------------ +# Test DistrictDTO +# ------------------------ +def test_district_dto(): + senator = SenatorDTO( + id=1, + first_name="John", + last_name="Doe", + email="john@example.com", + headshot_url=None, + district_id=100, + is_active=True, + session_number=2026, + committees=[] + ) + + district = DistrictDTO( + id=1, + district_name="District 1", + description=None, + senator=[senator] + ) + + data = district.model_dump() + assert data["senator"][0]["last_name"] == "Doe" + + +# ------------------------ +# Test BudgetDataDTO recursive +# ------------------------ +def test_budget_dto_recursive(): + child_budget = BudgetDataDTO( + id=2, + fiscal_year="2026", + category="Subcategory", + amount=100.0, + description=None, + children=[] + ) + + parent_budget = BudgetDataDTO( + id=1, + fiscal_year="2026", + category="Main", + amount=1000.0, + description="Main budget", + children=[child_budget] + ) + + data = parent_budget.model_dump() + assert data["children"][0]["category"] == "Subcategory" + + +# ------------------------ +# Test StaticPageDTO +# ------------------------ +def test_static_page_dto(): + page = StaticPageDTO( + id=1, + page_slug="about", + title="About Us", + body="Content", + updated_at=datetime.now() + ) + + data = page.model_dump() + assert data["page_slug"] == "about" + + +# ------------------------ +# Test AccountDTO +# ------------------------ +def test_account_dto(): + account = AccountDTO( + id=1, + email="user@example.com", + pid="pid123", + first_name="User", + last_name="Example", + role="admin" + ) + + data = account.model_dump() + assert data["role"] == "admin" + +# ------------------------ +# Test CalendarEventDTO +# ------------------------ +def test_calendar_event_dto(): + event = CalendarEventDTO( + id=1, + title="Budget Meeting", + description="Discuss budget allocations", + start_datetime=datetime(2026, 3, 10, 9, 0), + end_datetime=datetime(2026, 3, 10, 11, 0), + location="Room 101", + event_type="Hearing" + ) + + data = event.model_dump() + assert data["title"] == "Budget Meeting" + assert data["start_datetime"] == datetime(2026, 3, 10, 9, 0) + assert data["event_type"] == "Hearing" From 32fadfbcf43e7fbb945867df683bcf123312e401 Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 3 Mar 2026 07:22:48 -0500 Subject: [PATCH 02/18] pydantic ver upd --- backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 9f811b5..ff18e4f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,4 +18,4 @@ ruff==0.9.3 junitparser==3.2.0 # Pydantic (ships with fastapi) -pydantic==2.7.1 +pydantic==2.6.2 From 5d8158b15e4afbe582a9e41f2c089b0d31847b3f Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 3 Mar 2026 07:29:00 -0500 Subject: [PATCH 03/18] changed pydantic version again --- backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index ff18e4f..9829c64 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,4 +18,4 @@ ruff==0.9.3 junitparser==3.2.0 # Pydantic (ships with fastapi) -pydantic==2.6.2 +pydantic>=2.8,<3.0.0 From fc0d0ab1b7d6c3832751f5f3306ef4197b42278c Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 3 Mar 2026 07:31:42 -0500 Subject: [PATCH 04/18] rmved unused import --- backend/tests/schemas/test_schemas.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/tests/schemas/test_schemas.py b/backend/tests/schemas/test_schemas.py index cb67522..b9d8898 100644 --- a/backend/tests/schemas/test_schemas.py +++ b/backend/tests/schemas/test_schemas.py @@ -1,7 +1,5 @@ from datetime import date, datetime, time -import pytest - from app.schemas import ( AccountDTO, BudgetDataDTO, From 36c804589ef86409f825c525d5df36f3487f4876 Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 3 Mar 2026 14:50:41 -0500 Subject: [PATCH 05/18] added AccountDTO to NewsDTO --- backend/app/schemas/CalendarEventDTO.py | 4 +++- backend/app/schemas/NewsDTO.py | 3 ++- backend/tests/schemas/test_schemas.py | 21 +++++++++------------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/backend/app/schemas/CalendarEventDTO.py b/backend/app/schemas/CalendarEventDTO.py index cd25749..6bf5426 100644 --- a/backend/app/schemas/CalendarEventDTO.py +++ b/backend/app/schemas/CalendarEventDTO.py @@ -1,5 +1,7 @@ from datetime import datetime +from typing import Optional + from pydantic import BaseModel, ConfigDict @@ -9,7 +11,7 @@ class CalendarEventDTO(BaseModel): description: str start_datetime: datetime end_datetime: datetime - location: str | None = None + location: Optional[str] = None event_type: str model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/NewsDTO.py b/backend/app/schemas/NewsDTO.py index 3ad1aea..db6283e 100644 --- a/backend/app/schemas/NewsDTO.py +++ b/backend/app/schemas/NewsDTO.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, ConfigDict, computed_field +from .AccountDTO import AccountDTO class NewsDTO(BaseModel): id: int @@ -13,7 +14,7 @@ class NewsDTO(BaseModel): date_published: datetime date_last_edited: datetime - admin: Optional[object] = None + admin: Optional["AccountDTO"] = None model_config = ConfigDict(from_attributes=True) diff --git a/backend/tests/schemas/test_schemas.py b/backend/tests/schemas/test_schemas.py index b9d8898..ade3e08 100644 --- a/backend/tests/schemas/test_schemas.py +++ b/backend/tests/schemas/test_schemas.py @@ -19,16 +19,6 @@ StaticPageDTO, ) - -# ------------------------ -# Mock ORM helper -# ------------------------ -class MockAdmin: - def __init__(self, first_name: str, last_name: str): - self.first_name = first_name - self.last_name = last_name - - # ------------------------ # Test CommitteeDTO # ------------------------ @@ -70,7 +60,14 @@ def test_committee_dto_serialization(): # Test NewsDTO with computed field # ------------------------ def test_news_dto_computed_author(): - admin = MockAdmin(first_name="Alice", last_name="Admin") + admin = AccountDTO( + id=1, + email="user@example.com", + pid="pid123", + first_name="Mock", + last_name="Admin", + role="admin" + ) news = NewsDTO( id=1, @@ -84,7 +81,7 @@ def test_news_dto_computed_author(): ) data = news.model_dump() - assert data["author_name"] == "Alice Admin" + assert data["author_name"] == "Mock Admin" # ------------------------ From 6a4939c26e9afd93f95951d50eb2a1105c8d6899 Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 3 Mar 2026 14:54:34 -0500 Subject: [PATCH 06/18] ruff check fix --- backend/app/schemas/CalendarEventDTO.py | 1 - backend/app/schemas/NewsDTO.py | 1 + backend/tests/schemas/test_schemas.py | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/app/schemas/CalendarEventDTO.py b/backend/app/schemas/CalendarEventDTO.py index 6bf5426..4a215be 100644 --- a/backend/app/schemas/CalendarEventDTO.py +++ b/backend/app/schemas/CalendarEventDTO.py @@ -1,5 +1,4 @@ from datetime import datetime - from typing import Optional from pydantic import BaseModel, ConfigDict diff --git a/backend/app/schemas/NewsDTO.py b/backend/app/schemas/NewsDTO.py index db6283e..a13cec2 100644 --- a/backend/app/schemas/NewsDTO.py +++ b/backend/app/schemas/NewsDTO.py @@ -5,6 +5,7 @@ from .AccountDTO import AccountDTO + class NewsDTO(BaseModel): id: int title: str diff --git a/backend/tests/schemas/test_schemas.py b/backend/tests/schemas/test_schemas.py index ade3e08..88dfeeb 100644 --- a/backend/tests/schemas/test_schemas.py +++ b/backend/tests/schemas/test_schemas.py @@ -19,6 +19,7 @@ StaticPageDTO, ) + # ------------------------ # Test CommitteeDTO # ------------------------ From 5877d408bf3454781a2bf7fd331e480d7650786e Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 10 Mar 2026 13:08:57 -0400 Subject: [PATCH 07/18] raw ticket --- backend/app/main.py | 5 + backend/app/models/Senator.py | 2 +- backend/app/routers/__init__.py | 2 +- backend/app/routers/committees.py | 101 ++++++++++++++++ backend/app/routers/leadership.py | 44 +++++++ backend/conftest.py | 14 +-- backend/tests/routers/test_committees.py | 140 +++++++++++++++++++++++ backend/tests/routers/test_leadership.py | 114 ++++++++++++++++++ 8 files changed, 413 insertions(+), 9 deletions(-) create mode 100644 backend/app/routers/committees.py create mode 100644 backend/app/routers/leadership.py create mode 100644 backend/tests/routers/test_committees.py create mode 100644 backend/tests/routers/test_leadership.py diff --git a/backend/app/main.py b/backend/app/main.py index 50fb40f..1a21182 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -4,6 +4,8 @@ from fastapi.middleware.cors import CORSMiddleware from app.routers import health, news, senators +from app.routers import leadership +from app.routers import committees app = FastAPI( title="Senate API", @@ -24,6 +26,9 @@ app.include_router(health.router) app.include_router(news.router) app.include_router(senators.router) +app.include_router(leadership.router) +app.include_router(committees.router) + @app.get("/") diff --git a/backend/app/models/Senator.py b/backend/app/models/Senator.py index 7cedd34..1cc6db6 100644 --- a/backend/app/models/Senator.py +++ b/backend/app/models/Senator.py @@ -18,7 +18,7 @@ class Senator(Base): last_name: Mapped[str] = mapped_column(String(100), nullable=False) email: Mapped[str] = mapped_column(String(255), nullable=False) headshot_url: Mapped[Optional[str]] = mapped_column(CHAR(500), nullable=True) - district: Mapped[int] = mapped_column(Integer, ForeignKey("district.id"), nullable=False) + district_id: Mapped[int] = mapped_column(Integer, ForeignKey("district.id"), nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) session_number: Mapped[int] = mapped_column(Integer, nullable=False) created_at: Mapped[datetime] = mapped_column( diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py index 58a660e..7120b66 100644 --- a/backend/app/routers/__init__.py +++ b/backend/app/routers/__init__.py @@ -1 +1 @@ -"""API routers""" +"""API routers""" \ No newline at end of file diff --git a/backend/app/routers/committees.py b/backend/app/routers/committees.py new file mode 100644 index 0000000..c021581 --- /dev/null +++ b/backend/app/routers/committees.py @@ -0,0 +1,101 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session, selectinload + +from app.database import get_db +from app.models import Committee, CommitteeMembership +from app.schemas import CommitteeDTO + + +router = APIRouter(prefix="/committees", tags=["committees"]) + +@router.get("/", response_model=list[CommitteeDTO]) +def get_committees(db: Session = Depends(get_db)): + committees = ( + db.query(Committee) + .filter(Committee.is_active) + .order_by(Committee.name) + .all() + ) + + result = [] + for committee in committees: + members = [] + for membership in committee.memberships: + senator = membership.senator + committees_list = [ + { + "committee_id": membership.committee.id, + "committee_name": membership.committee.name, + "role": membership.role + } + ] + members.append({ + "id": senator.id, + "first_name": senator.first_name, + "last_name": senator.last_name, + "email": senator.email, + "headshot_url": senator.headshot_url, + "district_id": senator.district_id, + "is_active": senator.is_active, + "session_number": senator.session_number, + "committees": committees_list + }) + result.append({ + "id": committee.id, + "name": committee.name, + "description": committee.description, + "chair_name": committee.chair_name, + "chair_email": committee.chair_email, + "is_active": committee.is_active, + "members": members + }) + + return result + +@router.get("/{id}", response_model=CommitteeDTO) +def get_committee(id: int, db: Session = Depends(get_db)): + committee = ( + db.query(Committee) + .options( + selectinload(Committee.memberships) + .selectinload(CommitteeMembership.senator) + ) + .filter(Committee.id == id) + .first() + ) + + if not committee: + raise HTTPException(status_code=404, detail="Committee not found") + + members = [] + + for membership in committee.memberships: + senator = membership.senator + + members.append({ + "id": senator.id, + "first_name": senator.first_name, + "last_name": senator.last_name, + "email": senator.email, + "headshot_url": senator.headshot_url, + "district_id": senator.district_id, + "is_active": senator.is_active, + "session_number": senator.session_number, + "committees": [ + { + "committee_id": membership.committee.id, + "committee_name": membership.committee.name, + "role": membership.role + } + ] + }) + + return { + "id": committee.id, + "name": committee.name, + "description": committee.description, + "chair_name": committee.chair_name, + "chair_email": committee.chair_email, + "members": members, + "is_active": committee.is_active + } \ No newline at end of file diff --git a/backend/app/routers/leadership.py b/backend/app/routers/leadership.py new file mode 100644 index 0000000..9f09c76 --- /dev/null +++ b/backend/app/routers/leadership.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from app.database import get_db +from app.models import Leadership +from app.schemas import LeadershipDTO + + +router = APIRouter(prefix="/leadership", tags=["leadership"]) + +@router.get("/", response_model=list[LeadershipDTO]) +def get_leadership(session_number: int | None = None, db: Session = Depends(get_db)): + + query = db.query(Leadership) + + if session_number: + query = query.filter(Leadership.session_number == session_number) + else: + query = query.filter(Leadership.is_active) + + leadership = query.order_by(Leadership.title).all() + + # dynamically add is_current based on is_active + for l in leadership: + l.is_current = l.is_active + + return leadership + + +@router.get("/{id}", response_model=LeadershipDTO) +def get_leadership_by_id(id: int, db: Session = Depends(get_db)): + + leadership = ( + db.query(Leadership) + .filter(Leadership.id == id) + .first() + ) + + if leadership is None: + raise HTTPException(status_code=404, detail="Leadership record not found") + + leadership.is_current = leadership.is_active + + return leadership \ No newline at end of file diff --git a/backend/conftest.py b/backend/conftest.py index 81c3666..b4a5b10 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -5,13 +5,13 @@ so unit tests can run without a SQL Server connection (e.g., in CI). """ -import pytest -from fastapi.testclient import TestClient +# import pytest +# from fastapi.testclient import TestClient -from app.main import app +# from app.main import app -@pytest.fixture() -def client(): - """FastAPI test client (no DB required). This can be removed upon actual test implementations.""" - return TestClient(app) +# @pytest.fixture() +# def client(): +# """FastAPI test client (no DB required). This can be removed upon actual test implementations.""" +# return TestClient(app) diff --git a/backend/tests/routers/test_committees.py b/backend/tests/routers/test_committees.py new file mode 100644 index 0000000..ad36fd7 --- /dev/null +++ b/backend/tests/routers/test_committees.py @@ -0,0 +1,140 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + +from app.database import Base, get_db +from app.main import app # your FastAPI app +from app.models import Committee, CommitteeMembership, Senator + +# --- Setup in-memory database for tests --- +SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# --- Override get_db for tests --- +def override_get_db(): + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + +app.dependency_overrides[get_db] = override_get_db + +# --- Fixtures --- +@pytest.fixture(scope="module") +def test_db(): + # Create tables + Base.metadata.create_all(bind=engine) + db = TestingSessionLocal() + yield db + db.close() + Base.metadata.drop_all(bind=engine) + + +@pytest.fixture +def seeded_data(test_db): + + # --- Create Senators --- + s1 = Senator( + first_name="John", + last_name="Doe", + email="john@example.com", + headshot_url=None, + district_id=1, + is_active=True, + session_number=2016, + ) + + s2 = Senator( + first_name="Jane", + last_name="Doe", + email="jane@example.com", + headshot_url=None, + district_id=2, + is_active=True, + session_number=2025 + ) + test_db.add_all([s1, s2]) + test_db.commit() + + # --- Create Committees --- + c1 = Committee( + name="Finance Committee", + description="Handles budget matters", + chair_name="John Doe", + chair_email="john@example.com", + chair_senator_id=s1.id, + is_active=True + ) + c2 = Committee( + name="Education Committee", + description="Handles education policy", + chair_name="Jane Smith", + chair_email="jane@example.com", + chair_senator_id=s2.id, + is_active=False + ) + test_db.add_all([c1, c2]) + test_db.commit() + + # --- Create Memberships --- + m1 = CommitteeMembership( + senator_id=s1.id, + committee_id=c1.id, + role="Chair", + committee=c1, + senator=s1 + ) + m2 = CommitteeMembership( + senator_id=s2.id, + committee_id=c1.id, + role="Member", + committee=c1, + senator=s2 + ) + test_db.add_all([m1, m2]) + test_db.commit() + + return {"senators": [s1, s2], "committees": [c1, c2], "memberships": [m1, m2]} + + +@pytest.fixture +def client(): + return TestClient(app) + + +# --- Tests --- +def test_get_committee_by_id(client, seeded_data): + committee = seeded_data["committees"][0] + + response = client.get(f"/committees/{committee.id}") + assert response.status_code == 200 + + data = response.json() + assert data["id"] == committee.id + assert data["name"] == "Finance Committee" + assert len(data["members"]) == 2 + roles = {c["role"] for m in data["members"] for c in m["committees"]} + assert roles == {"Chair", "Member"} + +def test_get_committee_not_found(client): + response = client.get("/committees/999") + assert response.status_code == 404 + assert response.json()["detail"] == "Committee not found" + +def test_get_all_active_committees(client, seeded_data): + response = client.get("/committees/") + assert response.status_code == 200 + + data = response.json() + # Only active committees + names = [c["name"] for c in data] + assert "Finance Committee" in names + assert "Education Committee" not in names \ No newline at end of file diff --git a/backend/tests/routers/test_leadership.py b/backend/tests/routers/test_leadership.py new file mode 100644 index 0000000..3b63605 --- /dev/null +++ b/backend/tests/routers/test_leadership.py @@ -0,0 +1,114 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + + +from app.database import Base, get_db +from app.main import app # your FastAPI app +from app.models import Leadership + +# --- Setup in-memory SQLite for tests --- +SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# --- Override get_db for testing --- +def override_get_db(): + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + +app.dependency_overrides[get_db] = override_get_db + +# --- Fixtures --- +@pytest.fixture(scope="module") +def test_db(): + # Create tables + Base.metadata.create_all(bind=engine) + db = TestingSessionLocal() + yield db + db.close() + Base.metadata.drop_all(bind=engine) + +@pytest.fixture +def seeded_data(test_db): + # Seed Leadership records + l1 = Leadership( + title="Speaker", + first_name="John", + last_name="Doe", + email="john@example.com", + headshot_url=None, + is_active=True, + session_number=2025 + ) + l2 = Leadership( + title="Minority Leader", + first_name="Jane", + last_name="Smith", + email="jane@example.com", + headshot_url=None, + is_active=False, + session_number=2023 + ) + l3 = Leadership( + title="Whip", + first_name="Alice", + last_name="Brown", + email="alice@example.com", + headshot_url=None, + is_active=True, + session_number=2025 + ) + + test_db.add_all([l1, l2, l3]) + test_db.commit() + + return {"records": [l1, l2, l3]} + +@pytest.fixture +def client(): + return TestClient(app) + +# --- Tests --- +def test_get_all_active_leadership(client, seeded_data): + response = client.get("/leadership/") + assert response.status_code == 200 + data = response.json() + + # Only active records should be returned + titles = [l["title"] for l in data] + assert "Speaker" in titles + assert "Whip" in titles + assert "Minority Leader" not in titles + +def test_get_leadership_by_session_number(client, seeded_data): + response = client.get("/leadership/?session_number=2025") + assert response.status_code == 200 + data = response.json() + + # Should return only records with session_number=2025 + assert all(l["session_number"] == 2025 for l in data) + titles = [l["title"] for l in data] + assert set(titles) == {"Speaker", "Whip"} + +def test_get_leadership_by_id_success(client, seeded_data): + leadership = seeded_data["records"][0] # Speaker + response = client.get(f"/leadership/{leadership.id}") + assert response.status_code == 200 + data = response.json() + assert data["id"] == leadership.id + assert data["title"] == leadership.title + +def test_get_leadership_by_id_not_found(client): + response = client.get("/leadership/999") + assert response.status_code == 404 + assert response.json()["detail"] == "Leadership record not found" \ No newline at end of file From 5d81176ea572393bf7020b05d3fdc11082ca2b78 Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 10 Mar 2026 13:11:05 -0400 Subject: [PATCH 08/18] raw ticket --- backend/conftest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/conftest.py b/backend/conftest.py index b4a5b10..81c3666 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -5,13 +5,13 @@ so unit tests can run without a SQL Server connection (e.g., in CI). """ -# import pytest -# from fastapi.testclient import TestClient +import pytest +from fastapi.testclient import TestClient -# from app.main import app +from app.main import app -# @pytest.fixture() -# def client(): -# """FastAPI test client (no DB required). This can be removed upon actual test implementations.""" -# return TestClient(app) +@pytest.fixture() +def client(): + """FastAPI test client (no DB required). This can be removed upon actual test implementations.""" + return TestClient(app) From 8a802c7f5832f45f5aa7f8cfdf0fe31e0e594ca9 Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 10 Mar 2026 13:39:58 -0400 Subject: [PATCH 09/18] ruff lint fix --- backend/app/main.py | 4 +--- backend/app/routers/__init__.py | 2 +- backend/app/routers/committees.py | 5 ++--- backend/app/routers/leadership.py | 9 ++++----- backend/tests/routers/test_committees.py | 14 +++++++------- backend/tests/routers/test_leadership.py | 9 ++++----- 6 files changed, 19 insertions(+), 24 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 1a21182..5e4582b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,9 +3,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.routers import health, news, senators -from app.routers import leadership -from app.routers import committees +from app.routers import committees, health, leadership, news, senators app = FastAPI( title="Senate API", diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py index 7120b66..58a660e 100644 --- a/backend/app/routers/__init__.py +++ b/backend/app/routers/__init__.py @@ -1 +1 @@ -"""API routers""" \ No newline at end of file +"""API routers""" diff --git a/backend/app/routers/committees.py b/backend/app/routers/committees.py index c021581..859aab6 100644 --- a/backend/app/routers/committees.py +++ b/backend/app/routers/committees.py @@ -5,7 +5,6 @@ from app.models import Committee, CommitteeMembership from app.schemas import CommitteeDTO - router = APIRouter(prefix="/committees", tags=["committees"]) @router.get("/", response_model=list[CommitteeDTO]) @@ -81,7 +80,7 @@ def get_committee(id: int, db: Session = Depends(get_db)): "district_id": senator.district_id, "is_active": senator.is_active, "session_number": senator.session_number, - "committees": [ + "committees": [ { "committee_id": membership.committee.id, "committee_name": membership.committee.name, @@ -98,4 +97,4 @@ def get_committee(id: int, db: Session = Depends(get_db)): "chair_email": committee.chair_email, "members": members, "is_active": committee.is_active - } \ No newline at end of file + } diff --git a/backend/app/routers/leadership.py b/backend/app/routers/leadership.py index 9f09c76..13c53be 100644 --- a/backend/app/routers/leadership.py +++ b/backend/app/routers/leadership.py @@ -5,7 +5,6 @@ from app.models import Leadership from app.schemas import LeadershipDTO - router = APIRouter(prefix="/leadership", tags=["leadership"]) @router.get("/", response_model=list[LeadershipDTO]) @@ -21,8 +20,8 @@ def get_leadership(session_number: int | None = None, db: Session = Depends(get_ leadership = query.order_by(Leadership.title).all() # dynamically add is_current based on is_active - for l in leadership: - l.is_current = l.is_active + for leader in leadership: + leader.is_current = leader.is_active return leadership @@ -38,7 +37,7 @@ def get_leadership_by_id(id: int, db: Session = Depends(get_db)): if leadership is None: raise HTTPException(status_code=404, detail="Leadership record not found") - + leadership.is_current = leadership.is_active - return leadership \ No newline at end of file + return leadership diff --git a/backend/tests/routers/test_committees.py b/backend/tests/routers/test_committees.py index ad36fd7..5fc00f4 100644 --- a/backend/tests/routers/test_committees.py +++ b/backend/tests/routers/test_committees.py @@ -86,16 +86,16 @@ def seeded_data(test_db): # --- Create Memberships --- m1 = CommitteeMembership( - senator_id=s1.id, - committee_id=c1.id, - role="Chair", + senator_id=s1.id, + committee_id=c1.id, + role="Chair", committee=c1, senator=s1 ) m2 = CommitteeMembership( - senator_id=s2.id, - committee_id=c1.id, - role="Member", + senator_id=s2.id, + committee_id=c1.id, + role="Member", committee=c1, senator=s2 ) @@ -137,4 +137,4 @@ def test_get_all_active_committees(client, seeded_data): # Only active committees names = [c["name"] for c in data] assert "Finance Committee" in names - assert "Education Committee" not in names \ No newline at end of file + assert "Education Committee" not in names diff --git a/backend/tests/routers/test_leadership.py b/backend/tests/routers/test_leadership.py index 3b63605..cc6c656 100644 --- a/backend/tests/routers/test_leadership.py +++ b/backend/tests/routers/test_leadership.py @@ -4,7 +4,6 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool - from app.database import Base, get_db from app.main import app # your FastAPI app from app.models import Leadership @@ -85,7 +84,7 @@ def test_get_all_active_leadership(client, seeded_data): data = response.json() # Only active records should be returned - titles = [l["title"] for l in data] + titles = [leader["title"] for leader in data] assert "Speaker" in titles assert "Whip" in titles assert "Minority Leader" not in titles @@ -96,8 +95,8 @@ def test_get_leadership_by_session_number(client, seeded_data): data = response.json() # Should return only records with session_number=2025 - assert all(l["session_number"] == 2025 for l in data) - titles = [l["title"] for l in data] + assert all(leader["session_number"] == 2025 for leader in data) + titles = [leader["title"] for leader in data] assert set(titles) == {"Speaker", "Whip"} def test_get_leadership_by_id_success(client, seeded_data): @@ -111,4 +110,4 @@ def test_get_leadership_by_id_success(client, seeded_data): def test_get_leadership_by_id_not_found(client): response = client.get("/leadership/999") assert response.status_code == 404 - assert response.json()["detail"] == "Leadership record not found" \ No newline at end of file + assert response.json()["detail"] == "Leadership record not found" From 1a0242317289dcf34f75de4aa5412c567ca0cafd Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 10 Mar 2026 16:36:44 -0400 Subject: [PATCH 10/18] unfix --- backend/app/models/Senator.py | 2 +- backend/app/routers/committees.py | 4 ++-- backend/tests/routers/test_committees.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/app/models/Senator.py b/backend/app/models/Senator.py index 1cc6db6..7cedd34 100644 --- a/backend/app/models/Senator.py +++ b/backend/app/models/Senator.py @@ -18,7 +18,7 @@ class Senator(Base): last_name: Mapped[str] = mapped_column(String(100), nullable=False) email: Mapped[str] = mapped_column(String(255), nullable=False) headshot_url: Mapped[Optional[str]] = mapped_column(CHAR(500), nullable=True) - district_id: Mapped[int] = mapped_column(Integer, ForeignKey("district.id"), nullable=False) + district: Mapped[int] = mapped_column(Integer, ForeignKey("district.id"), nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) session_number: Mapped[int] = mapped_column(Integer, nullable=False) created_at: Mapped[datetime] = mapped_column( diff --git a/backend/app/routers/committees.py b/backend/app/routers/committees.py index 859aab6..30cce7a 100644 --- a/backend/app/routers/committees.py +++ b/backend/app/routers/committees.py @@ -34,7 +34,7 @@ def get_committees(db: Session = Depends(get_db)): "last_name": senator.last_name, "email": senator.email, "headshot_url": senator.headshot_url, - "district_id": senator.district_id, + "district_id": senator.district, "is_active": senator.is_active, "session_number": senator.session_number, "committees": committees_list @@ -77,7 +77,7 @@ def get_committee(id: int, db: Session = Depends(get_db)): "last_name": senator.last_name, "email": senator.email, "headshot_url": senator.headshot_url, - "district_id": senator.district_id, + "district_id": senator.district, "is_active": senator.is_active, "session_number": senator.session_number, "committees": [ diff --git a/backend/tests/routers/test_committees.py b/backend/tests/routers/test_committees.py index 5fc00f4..240972c 100644 --- a/backend/tests/routers/test_committees.py +++ b/backend/tests/routers/test_committees.py @@ -47,7 +47,7 @@ def seeded_data(test_db): last_name="Doe", email="john@example.com", headshot_url=None, - district_id=1, + district=1, is_active=True, session_number=2016, ) @@ -57,7 +57,7 @@ def seeded_data(test_db): last_name="Doe", email="jane@example.com", headshot_url=None, - district_id=2, + district=2, is_active=True, session_number=2025 ) From 56ea9d862fa79d730f8c6937245dbdfb163459db Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 10 Mar 2026 17:09:53 -0400 Subject: [PATCH 11/18] added conftest --- backend/tests/routers/conftest.py | 135 +++++++++++++++++++++++ backend/tests/routers/test_committees.py | 112 +------------------ backend/tests/routers/test_leadership.py | 79 +------------ 3 files changed, 145 insertions(+), 181 deletions(-) create mode 100644 backend/tests/routers/conftest.py diff --git a/backend/tests/routers/conftest.py b/backend/tests/routers/conftest.py new file mode 100644 index 0000000..32950f8 --- /dev/null +++ b/backend/tests/routers/conftest.py @@ -0,0 +1,135 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + +from app.database import Base, get_db +from app.main import app +from app.models import Committee, CommitteeMembership, Leadership, Senator + +# --- Setup shared in-memory database --- +SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# --- Override FastAPI get_db dependency --- +def override_get_db(): + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + +app.dependency_overrides[get_db] = override_get_db + +# --- Fixtures --- + +@pytest.fixture(scope="module") +def test_db(): + """Create all tables once per test module.""" + Base.metadata.create_all(bind=engine) + db = TestingSessionLocal() + yield db + db.close() + Base.metadata.drop_all(bind=engine) + +@pytest.fixture +def client(test_db): + """FastAPI TestClient using the test_db.""" + return TestClient(app) + +@pytest.fixture +def seeded_committees(test_db): + """Seed test data for committees, senators, and memberships.""" + # --- Senators --- + s1 = Senator( + first_name="John", + last_name="Doe", + email="john@example.com", + headshot_url=None, + district=1, + is_active=True, + session_number=2016, + ) + s2 = Senator( + first_name="Jane", + last_name="Doe", + email="jane@example.com", + headshot_url=None, + district=2, + is_active=True, + session_number=2025, + ) + test_db.add_all([s1, s2]) + test_db.commit() + + # --- Committees --- + c1 = Committee( + name="Finance Committee", + description="Handles budget matters", + chair_name="John Doe", + chair_email="john@example.com", + chair_senator_id=s1.id, + is_active=True, + ) + c2 = Committee( + name="Education Committee", + description="Handles education policy", + chair_name="Jane Smith", + chair_email="jane@example.com", + chair_senator_id=s2.id, + is_active=False, + ) + test_db.add_all([c1, c2]) + test_db.commit() + + # --- Memberships --- + m1 = CommitteeMembership(senator_id=s1.id, committee_id=c1.id, role="Chair") + m2 = CommitteeMembership(senator_id=s2.id, committee_id=c1.id, role="Member") + test_db.add_all([m1, m2]) + test_db.commit() + + yield {"senators": [s1, s2], "committees": [c1, c2], "memberships": [m1, m2]} + + +@pytest.fixture +def seeded_leadership(test_db): + """Seed test data for leadership.""" + l1 = Leadership( + title="Speaker", + first_name="John", + last_name="Doe", + email="john@example.com", + headshot_url=None, + is_active=True, + session_number=2025, + ) + l2 = Leadership( + title="Minority Leader", + first_name="Jane", + last_name="Smith", + email="jane@example.com", + headshot_url=None, + is_active=False, + session_number=2023, + ) + l3 = Leadership( + title="Whip", + first_name="Alice", + last_name="Brown", + email="alice@example.com", + headshot_url=None, + is_active=True, + session_number=2025, + ) + + test_db.add_all([l1, l2, l3]) + test_db.commit() + + yield {"records": [l1, l2, l3]} diff --git a/backend/tests/routers/test_committees.py b/backend/tests/routers/test_committees.py index 240972c..2ee3e3d 100644 --- a/backend/tests/routers/test_committees.py +++ b/backend/tests/routers/test_committees.py @@ -1,118 +1,16 @@ import pytest from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from sqlalchemy.pool import StaticPool -from app.database import Base, get_db from app.main import app # your FastAPI app -from app.models import Committee, CommitteeMembership, Senator - -# --- Setup in-memory database for tests --- -SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" -engine = create_engine( - SQLALCHEMY_DATABASE_URL, - connect_args={"check_same_thread": False}, - poolclass=StaticPool -) -TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -# --- Override get_db for tests --- -def override_get_db(): - db = TestingSessionLocal() - try: - yield db - finally: - db.close() - -app.dependency_overrides[get_db] = override_get_db - -# --- Fixtures --- -@pytest.fixture(scope="module") -def test_db(): - # Create tables - Base.metadata.create_all(bind=engine) - db = TestingSessionLocal() - yield db - db.close() - Base.metadata.drop_all(bind=engine) @pytest.fixture -def seeded_data(test_db): - - # --- Create Senators --- - s1 = Senator( - first_name="John", - last_name="Doe", - email="john@example.com", - headshot_url=None, - district=1, - is_active=True, - session_number=2016, - ) - - s2 = Senator( - first_name="Jane", - last_name="Doe", - email="jane@example.com", - headshot_url=None, - district=2, - is_active=True, - session_number=2025 - ) - test_db.add_all([s1, s2]) - test_db.commit() - - # --- Create Committees --- - c1 = Committee( - name="Finance Committee", - description="Handles budget matters", - chair_name="John Doe", - chair_email="john@example.com", - chair_senator_id=s1.id, - is_active=True - ) - c2 = Committee( - name="Education Committee", - description="Handles education policy", - chair_name="Jane Smith", - chair_email="jane@example.com", - chair_senator_id=s2.id, - is_active=False - ) - test_db.add_all([c1, c2]) - test_db.commit() - - # --- Create Memberships --- - m1 = CommitteeMembership( - senator_id=s1.id, - committee_id=c1.id, - role="Chair", - committee=c1, - senator=s1 - ) - m2 = CommitteeMembership( - senator_id=s2.id, - committee_id=c1.id, - role="Member", - committee=c1, - senator=s2 - ) - test_db.add_all([m1, m2]) - test_db.commit() - - return {"senators": [s1, s2], "committees": [c1, c2], "memberships": [m1, m2]} - - -@pytest.fixture -def client(): +def client(test_db): return TestClient(app) - # --- Tests --- -def test_get_committee_by_id(client, seeded_data): - committee = seeded_data["committees"][0] +def test_get_committee_by_id(client, seeded_committees): + committee = seeded_committees["committees"][0] response = client.get(f"/committees/{committee.id}") assert response.status_code == 200 @@ -124,12 +22,12 @@ def test_get_committee_by_id(client, seeded_data): roles = {c["role"] for m in data["members"] for c in m["committees"]} assert roles == {"Chair", "Member"} -def test_get_committee_not_found(client): +def test_get_committee_not_found(client, test_db): response = client.get("/committees/999") assert response.status_code == 404 assert response.json()["detail"] == "Committee not found" -def test_get_all_active_committees(client, seeded_data): +def test_get_all_active_committees(client, seeded_committees): response = client.get("/committees/") assert response.status_code == 200 diff --git a/backend/tests/routers/test_leadership.py b/backend/tests/routers/test_leadership.py index cc6c656..2e7a0da 100644 --- a/backend/tests/routers/test_leadership.py +++ b/backend/tests/routers/test_leadership.py @@ -1,84 +1,15 @@ import pytest from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from sqlalchemy.pool import StaticPool -from app.database import Base, get_db from app.main import app # your FastAPI app -from app.models import Leadership -# --- Setup in-memory SQLite for tests --- -SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" -engine = create_engine( - SQLALCHEMY_DATABASE_URL, - connect_args={"check_same_thread": False}, - poolclass=StaticPool -) -TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -# --- Override get_db for testing --- -def override_get_db(): - db = TestingSessionLocal() - try: - yield db - finally: - db.close() - -app.dependency_overrides[get_db] = override_get_db - -# --- Fixtures --- -@pytest.fixture(scope="module") -def test_db(): - # Create tables - Base.metadata.create_all(bind=engine) - db = TestingSessionLocal() - yield db - db.close() - Base.metadata.drop_all(bind=engine) - -@pytest.fixture -def seeded_data(test_db): - # Seed Leadership records - l1 = Leadership( - title="Speaker", - first_name="John", - last_name="Doe", - email="john@example.com", - headshot_url=None, - is_active=True, - session_number=2025 - ) - l2 = Leadership( - title="Minority Leader", - first_name="Jane", - last_name="Smith", - email="jane@example.com", - headshot_url=None, - is_active=False, - session_number=2023 - ) - l3 = Leadership( - title="Whip", - first_name="Alice", - last_name="Brown", - email="alice@example.com", - headshot_url=None, - is_active=True, - session_number=2025 - ) - - test_db.add_all([l1, l2, l3]) - test_db.commit() - - return {"records": [l1, l2, l3]} @pytest.fixture -def client(): +def client(test_db): return TestClient(app) # --- Tests --- -def test_get_all_active_leadership(client, seeded_data): +def test_get_all_active_leadership(client, seeded_leadership): response = client.get("/leadership/") assert response.status_code == 200 data = response.json() @@ -89,7 +20,7 @@ def test_get_all_active_leadership(client, seeded_data): assert "Whip" in titles assert "Minority Leader" not in titles -def test_get_leadership_by_session_number(client, seeded_data): +def test_get_leadership_by_session_number(client, seeded_leadership): response = client.get("/leadership/?session_number=2025") assert response.status_code == 200 data = response.json() @@ -99,8 +30,8 @@ def test_get_leadership_by_session_number(client, seeded_data): titles = [leader["title"] for leader in data] assert set(titles) == {"Speaker", "Whip"} -def test_get_leadership_by_id_success(client, seeded_data): - leadership = seeded_data["records"][0] # Speaker +def test_get_leadership_by_id_success(client, seeded_leadership): + leadership = seeded_leadership["records"][0] # Speaker response = client.get(f"/leadership/{leadership.id}") assert response.status_code == 200 data = response.json() From 618359936fa88ce7c3a7b61238d6700cd82344d0 Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 10 Mar 2026 17:23:58 -0400 Subject: [PATCH 12/18] lint merge --- backend/app/main.py | 10 ++----- backend/app/schemas/__init__.py | 49 +++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 42f47eb..4e7ff22 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,22 +3,20 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -<<<<<<< gabriel -from app.routers import committees, health, leadership, news, senators -======= from app.routers import ( budget, carousel, + committees, districts, events, finance, health, + leadership, news, pages, senators, staff, ) ->>>>>>> main app = FastAPI( title="Senate API", @@ -39,11 +37,8 @@ app.include_router(health.router) app.include_router(news.router) app.include_router(senators.router) -<<<<<<< gabriel app.include_router(leadership.router) app.include_router(committees.router) - -======= app.include_router(carousel.router) app.include_router(districts.router) app.include_router(staff.router) @@ -51,7 +46,6 @@ app.include_router(budget.router) app.include_router(pages.router) app.include_router(events.router) ->>>>>>> main @app.get("/") diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 3c3883d..37c3a63 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,21 +1,5 @@ """Pydantic schemas""" -from .AccountDTO import AccountDTO -from .BudgetDataDTO import BudgetDataDTO -from .CalendarEventDTO import CalendarEventDTO -from .CarouselSlideDTO import CarouselSlideDTO -from .CommitteeAssignmentDTO import CommitteeAssignmentDTO -from .CommitteeDTO import CommitteeDTO -from .DistrictDTO import DistrictDTO -from .FinanceHearingConfigDTO import FinanceHearingConfigDTO -from .FinanceHearingDateDTO import FinanceHearingDateDTO -from .LeadershipDTO import LeadershipDTO -from .LegislationActionDTO import LegislationActionDTO -from .LegislationDTO import LegislationDTO -from .NewsDTO import NewsDTO -from .SenatorDTO import SenatorDTO -from .StaffDTO import StaffDTO -from .StaticPageDTO import StaticPageDTO from .account import AccountDTO, CreateAccountDTO from .budget import BudgetDataDTO, CreateBudgetDataDTO from .calendar_event import CalendarEventDTO, CreateCalendarEventDTO @@ -50,25 +34,50 @@ from .static_page import StaticPageDTO, UpdateStaticPageDTO __all__ = [ - "SenatorDTO", + # Account "AccountDTO", + "CreateAccountDTO", + # Budget "BudgetDataDTO", - "CalendarEventDTO", "CreateBudgetDataDTO", # Calendar Event "CalendarEventDTO", "CreateCalendarEventDTO", # Carousel "CarouselSlideDTO", - "CommitteeAssignmentDTO", + "CreateCarouselSlideDTO", + # Committee + "AssignCommitteeMemberDTO", "CommitteeDTO", + "CreateCommitteeDTO", + # District "DistrictDTO", + "DistrictLookupDTO", + # Finance + "CreateFinanceHearingDateDTO", "FinanceHearingConfigDTO", - "LeadershipDTO", "FinanceHearingDateDTO", + "UpdateFinanceHearingConfigDTO", + # Leadership + "LeadershipDTO", + # Legislation + "CreateLegislationActionDTO", + "CreateLegislationDTO", "LegislationActionDTO", "LegislationDTO", + # News + "CreateNewsDTO", "NewsDTO", + "UpdateNewsDTO", + # Senator + "CommitteeAssignmentDTO", + "CreateSenatorDTO", + "SenatorDTO", + "UpdateSenatorDTO", + # Staff + "CreateStaffDTO", "StaffDTO", + # Static Page "StaticPageDTO", + "UpdateStaticPageDTO", ] From 62e4353b2ccb908e904af4b2dbdca33918ac8aab Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 10 Mar 2026 17:33:02 -0400 Subject: [PATCH 13/18] headshot to photo --- backend/app/routers/leadership.py | 2 ++ backend/conftest.py | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/backend/app/routers/leadership.py b/backend/app/routers/leadership.py index 13c53be..cdaab4b 100644 --- a/backend/app/routers/leadership.py +++ b/backend/app/routers/leadership.py @@ -22,6 +22,7 @@ def get_leadership(session_number: int | None = None, db: Session = Depends(get_ # dynamically add is_current based on is_active for leader in leadership: leader.is_current = leader.is_active + leader.photo_url = leader.headshot_url return leadership @@ -39,5 +40,6 @@ def get_leadership_by_id(id: int, db: Session = Depends(get_db)): raise HTTPException(status_code=404, detail="Leadership record not found") leadership.is_current = leadership.is_active + leadership.photo_url = leadership.headshot_url return leadership diff --git a/backend/conftest.py b/backend/conftest.py index 81c3666..b4a5b10 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -5,13 +5,13 @@ so unit tests can run without a SQL Server connection (e.g., in CI). """ -import pytest -from fastapi.testclient import TestClient +# import pytest +# from fastapi.testclient import TestClient -from app.main import app +# from app.main import app -@pytest.fixture() -def client(): - """FastAPI test client (no DB required). This can be removed upon actual test implementations.""" - return TestClient(app) +# @pytest.fixture() +# def client(): +# """FastAPI test client (no DB required). This can be removed upon actual test implementations.""" +# return TestClient(app) From ece68c0955eb6154d9c84fd377fff20e45c75ccc Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 10 Mar 2026 17:35:13 -0400 Subject: [PATCH 14/18] is this the dagger???? --- backend/conftest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/conftest.py b/backend/conftest.py index b4a5b10..81c3666 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -5,13 +5,13 @@ so unit tests can run without a SQL Server connection (e.g., in CI). """ -# import pytest -# from fastapi.testclient import TestClient +import pytest +from fastapi.testclient import TestClient -# from app.main import app +from app.main import app -# @pytest.fixture() -# def client(): -# """FastAPI test client (no DB required). This can be removed upon actual test implementations.""" -# return TestClient(app) +@pytest.fixture() +def client(): + """FastAPI test client (no DB required). This can be removed upon actual test implementations.""" + return TestClient(app) From 11fa624114b23592d6213129b6915e563daf0a60 Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 10 Mar 2026 17:43:14 -0400 Subject: [PATCH 15/18] it was the dagger, this is a newsDTO check --- backend/app/schemas/NewsDTO.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/app/schemas/NewsDTO.py b/backend/app/schemas/NewsDTO.py index a13cec2..7962586 100644 --- a/backend/app/schemas/NewsDTO.py +++ b/backend/app/schemas/NewsDTO.py @@ -20,7 +20,6 @@ class NewsDTO(BaseModel): model_config = ConfigDict(from_attributes=True) @computed_field - @property def author_name(self) -> str: if self.admin: return f"{self.admin.first_name} {self.admin.last_name}" From 376acb0edc8ef85635d31d7f1a634ff5288262fe Mon Sep 17 00:00:00 2001 From: ggreat317 Date: Tue, 10 Mar 2026 17:45:04 -0400 Subject: [PATCH 16/18] unsure of random news computed error --- backend/app/schemas/NewsDTO.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/app/schemas/NewsDTO.py b/backend/app/schemas/NewsDTO.py index 7962586..a13cec2 100644 --- a/backend/app/schemas/NewsDTO.py +++ b/backend/app/schemas/NewsDTO.py @@ -20,6 +20,7 @@ class NewsDTO(BaseModel): model_config = ConfigDict(from_attributes=True) @computed_field + @property def author_name(self) -> str: if self.admin: return f"{self.admin.first_name} {self.admin.last_name}" From 24cb701795050fb74c3d11cdf3374cba1fa5cbe2 Mon Sep 17 00:00:00 2001 From: calebyhan Date: Thu, 12 Mar 2026 16:46:50 -0400 Subject: [PATCH 17/18] fix: compute author_name from admin relationship in NewsDTO --- backend/app/routers/news.py | 8 +++----- backend/app/schemas/news.py | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/app/routers/news.py b/backend/app/routers/news.py index 39169c4..a14a742 100644 --- a/backend/app/routers/news.py +++ b/backend/app/routers/news.py @@ -29,9 +29,9 @@ def _news_to_dict(news: News) -> dict[str, Any]: - """Convert a News ORM row to a dict compatible with PR #37's NewsDTO. + """Convert a News ORM row to a dict compatible with NewsDTO. - PR #37's NewsDTO expects a field called ``admin`` (not ``author``) so we + NewsDTO expects a field called ``admin`` (not ``author``) so we remap the relationship here rather than in the model. """ return { @@ -42,9 +42,7 @@ def _news_to_dict(news: News) -> dict[str, Any]: "image_url": news.image_url, "date_published": news.date_published, "date_last_edited": news.date_last_edited, - # PR #37's NewsDTO has a computed ``author_name`` field; provide it here - # so model_validate() can pick it up once the schema is available. - "author_name": f"{news.author.first_name} {news.author.last_name}" if news.author else "Unknown", + "admin": news.author, } diff --git a/backend/app/schemas/news.py b/backend/app/schemas/news.py index b2cb228..c4abc94 100644 --- a/backend/app/schemas/news.py +++ b/backend/app/schemas/news.py @@ -1,8 +1,11 @@ """News schemas — input and output DTOs.""" from datetime import datetime +from typing import Optional -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, computed_field + +from .account import AccountDTO class NewsDTO(BaseModel): @@ -11,12 +14,19 @@ class NewsDTO(BaseModel): summary: str body: str image_url: str | None - author_name: str date_published: datetime date_last_edited: datetime + admin: Optional[AccountDTO] = None model_config = ConfigDict(from_attributes=True) + @computed_field + @property + def author_name(self) -> str: + if self.admin: + return f"{self.admin.first_name} {self.admin.last_name}" + return "Unknown" + class CreateNewsDTO(BaseModel): title: str From 185ed2f2af1676783a9b9fc60b96c10b408c6d62 Mon Sep 17 00:00:00 2001 From: calebyhan Date: Thu, 12 Mar 2026 18:12:38 -0400 Subject: [PATCH 18/18] fix: align leadership/committees public APIs with TDD (/api routes + current-session default) --- backend/app/routers/committees.py | 6 +++++- backend/app/routers/leadership.py | 17 ++++++++++------- backend/tests/routers/test_committees.py | 6 +++--- backend/tests/routers/test_leadership.py | 8 ++++---- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/backend/app/routers/committees.py b/backend/app/routers/committees.py index 30cce7a..7793595 100644 --- a/backend/app/routers/committees.py +++ b/backend/app/routers/committees.py @@ -5,12 +5,16 @@ from app.models import Committee, CommitteeMembership from app.schemas import CommitteeDTO -router = APIRouter(prefix="/committees", tags=["committees"]) +router = APIRouter(prefix="/api/committees", tags=["committees"]) @router.get("/", response_model=list[CommitteeDTO]) def get_committees(db: Session = Depends(get_db)): committees = ( db.query(Committee) + .options( + selectinload(Committee.memberships) + .selectinload(CommitteeMembership.senator) + ) .filter(Committee.is_active) .order_by(Committee.name) .all() diff --git a/backend/app/routers/leadership.py b/backend/app/routers/leadership.py index cdaab4b..73b658c 100644 --- a/backend/app/routers/leadership.py +++ b/backend/app/routers/leadership.py @@ -1,21 +1,24 @@ from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import func from sqlalchemy.orm import Session from app.database import get_db from app.models import Leadership from app.schemas import LeadershipDTO -router = APIRouter(prefix="/leadership", tags=["leadership"]) +router = APIRouter(prefix="/api/leadership", tags=["leadership"]) + + +def _current_session(db: Session) -> int: + """Return the highest session_number in the leadership table.""" + result = db.query(func.max(Leadership.session_number)).scalar() + return result or 1 @router.get("/", response_model=list[LeadershipDTO]) def get_leadership(session_number: int | None = None, db: Session = Depends(get_db)): - query = db.query(Leadership) - - if session_number: - query = query.filter(Leadership.session_number == session_number) - else: - query = query.filter(Leadership.is_active) + target_session = session_number if session_number is not None else _current_session(db) + query = db.query(Leadership).filter(Leadership.session_number == target_session) leadership = query.order_by(Leadership.title).all() diff --git a/backend/tests/routers/test_committees.py b/backend/tests/routers/test_committees.py index 2ee3e3d..8e4da53 100644 --- a/backend/tests/routers/test_committees.py +++ b/backend/tests/routers/test_committees.py @@ -12,7 +12,7 @@ def client(test_db): def test_get_committee_by_id(client, seeded_committees): committee = seeded_committees["committees"][0] - response = client.get(f"/committees/{committee.id}") + response = client.get(f"/api/committees/{committee.id}") assert response.status_code == 200 data = response.json() @@ -23,12 +23,12 @@ def test_get_committee_by_id(client, seeded_committees): assert roles == {"Chair", "Member"} def test_get_committee_not_found(client, test_db): - response = client.get("/committees/999") + response = client.get("/api/committees/999") assert response.status_code == 404 assert response.json()["detail"] == "Committee not found" def test_get_all_active_committees(client, seeded_committees): - response = client.get("/committees/") + response = client.get("/api/committees/") assert response.status_code == 200 data = response.json() diff --git a/backend/tests/routers/test_leadership.py b/backend/tests/routers/test_leadership.py index 2e7a0da..db18eb0 100644 --- a/backend/tests/routers/test_leadership.py +++ b/backend/tests/routers/test_leadership.py @@ -10,7 +10,7 @@ def client(test_db): # --- Tests --- def test_get_all_active_leadership(client, seeded_leadership): - response = client.get("/leadership/") + response = client.get("/api/leadership/") assert response.status_code == 200 data = response.json() @@ -21,7 +21,7 @@ def test_get_all_active_leadership(client, seeded_leadership): assert "Minority Leader" not in titles def test_get_leadership_by_session_number(client, seeded_leadership): - response = client.get("/leadership/?session_number=2025") + response = client.get("/api/leadership/?session_number=2025") assert response.status_code == 200 data = response.json() @@ -32,13 +32,13 @@ def test_get_leadership_by_session_number(client, seeded_leadership): def test_get_leadership_by_id_success(client, seeded_leadership): leadership = seeded_leadership["records"][0] # Speaker - response = client.get(f"/leadership/{leadership.id}") + response = client.get(f"/api/leadership/{leadership.id}") assert response.status_code == 200 data = response.json() assert data["id"] == leadership.id assert data["title"] == leadership.title def test_get_leadership_by_id_not_found(client): - response = client.get("/leadership/999") + response = client.get("/api/leadership/999") assert response.status_code == 404 assert response.json()["detail"] == "Leadership record not found"