Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
3 changes: 2 additions & 1 deletion backend/api/academics/hiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def get_hiring_summary_overview(
page_size: int = 100,
order_by: str = "",
filter: str = "",
flagged: str = "all",
subject: User = Depends(registered_user),
hiring_service: HiringService = Depends(),
) -> Paginated[HiringAssignmentSummaryOverview]:
Expand All @@ -180,7 +181,7 @@ def get_hiring_summary_overview(
page=page, page_size=page_size, order_by=order_by, filter=filter
)
return hiring_service.get_hiring_summary_overview(
subject, term_id, pagination_params
subject, term_id, flagged, pagination_params
)


Expand Down
6 changes: 6 additions & 0 deletions backend/entities/academics/hiring/hiring_assignment_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ class HiringAssignmentEntity(EntityBase):
# Stores the timestamp for the last time the assignment was updated.
modified: Mapped[datetime] = mapped_column(DateTime, nullable=False)

# Stores whether the assignment is flagged for further review in the summary.
flagged: Mapped[bool] = mapped_column(Boolean, default=False)

@classmethod
def from_draft_model(cls, overview: HiringAssignmentDraft) -> Self:
return cls(
Expand All @@ -119,6 +122,7 @@ def from_draft_model(cls, overview: HiringAssignmentDraft) -> Self:
position_number=overview.position_number,
epar=overview.epar,
i9=overview.i9,
flagged=overview.flagged,
notes=overview.notes,
created=overview.created,
modified=overview.modified,
Expand All @@ -134,6 +138,7 @@ def to_overview_model(self) -> HiringAssignmentOverview:
epar=self.epar,
i9=self.i9,
notes=self.notes,
flagged=self.flagged,
)

def to_summary_overview_model(self) -> HiringAssignmentSummaryOverview:
Expand All @@ -159,6 +164,7 @@ def to_summary_overview_model(self) -> HiringAssignmentSummaryOverview:
epar=self.epar,
i9=self.i9,
notes=self.notes,
flagged=self.flagged,
)

def to_csv_row(self) -> HiringAssignmentCsvRow:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Migration for applicant-flagging

Revision ID: 0a57afd03df5
Revises: a9f09b49d862
Create Date: 2025-11-24 16:09:49.309576

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '0a57afd03df5'
down_revision = 'a9f09b49d862'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('academics__hiring__assignment', sa.Column('flagged', sa.Boolean(), nullable=False, default=False))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('academics__hiring__assignment', 'flagged')
# ### end Alembic commands ###
3 changes: 3 additions & 0 deletions backend/models/academics/hiring/hiring_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class HiringAssignmentDraft(BaseModel):
epar: str
i9: bool
notes: str
flagged: bool
created: datetime
modified: datetime

Expand All @@ -44,6 +45,7 @@ class HiringAssignmentOverview(BaseModel):
epar: str
i9: bool
notes: str
flagged: bool


class HiringAssignmentSummaryOverview(BaseModel):
Expand All @@ -61,6 +63,7 @@ class HiringAssignmentSummaryOverview(BaseModel):
epar: str
i9: bool
notes: str
flagged: bool


class HiringAssignmentCsvRow(BaseModel):
Expand Down
24 changes: 18 additions & 6 deletions backend/services/academics/hiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,7 @@ def update_hiring_assignment(
assignment_entity.epar = assignment.epar
assignment_entity.i9 = assignment.i9
assignment_entity.notes = assignment.notes
assignment_entity.flagged = assignment.flagged
assignment_entity.modified = datetime.now()

self._session.commit()
Expand Down Expand Up @@ -807,14 +808,19 @@ def update_hiring_level(self, subject: User, level: HiringLevel) -> HiringLevel:
return level_entity.to_model()

def get_hiring_summary_overview(
self, subject: User, term_id: str, pagination_params: PaginationParams
self,
subject: User,
term_id: str,
flagged: str,
pagination_params: PaginationParams,
) -> Paginated[HiringAssignmentSummaryOverview]:
"""
Returns the hires to show on a summary page for a given term.

Args:
subject: The user making the request
term_id: The term to get assignments for
flagged: Filter for flagged assignments ('flagged', 'not_flagged', or 'all')
pagination_params: Parameters for pagination and filtering

Raises:
Expand Down Expand Up @@ -851,26 +857,32 @@ def get_hiring_summary_overview(
)
base_query = base_query.where(criteria)

# 5. Create count query from base query
# 5. Apply flagged filter if present
if flagged == "flagged":
base_query = base_query.where(HiringAssignmentEntity.flagged.is_(True))
elif flagged == "not_flagged":
base_query = base_query.where(HiringAssignmentEntity.flagged.is_(False))

# 6. Create count query from base query
count_query = select(func.count()).select_from(base_query.subquery())

# 6. Create assignment query with eager loading
# 7. Create assignment query with eager loading
assignment_query = base_query.options(
joinedload(HiringAssignmentEntity.course_site)
.joinedload(CourseSiteEntity.sections)
.joinedload(SectionEntity.staff),
)

# 7. Apply pagination
# 8. Apply pagination
offset = pagination_params.page * pagination_params.page_size
limit = pagination_params.page_size
assignment_query = assignment_query.offset(offset).limit(limit)

# 8. Execute queries
# 9. Execute queries
length = self._session.scalar(count_query) or 0
assignment_entities = self._session.scalars(assignment_query).unique().all()

# 9. Build and return response
# 10. Build and return response
return Paginated(
items=[
assignment.to_summary_overview_model()
Expand Down
37 changes: 36 additions & 1 deletion backend/test/services/academics/hiring/hiring_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,23 @@
notes="Some notes here",
created=datetime.now(),
modified=datetime.now(),
flagged=False,
)

hiring_assignment_flagged = HiringAssignmentDraft(
id=1,
user_id=user_data.student.id,
term_id=term_data.current_term.id,
course_site_id=office_hours_data.comp_110_site.id,
level=uta_level,
status=HiringAssignmentStatus.COMMIT,
position_number="sample",
epar="12345",
i9=True,
notes="Some notes here",
created=datetime.now(),
modified=datetime.now(),
flagged=True,
)

updated_hiring_assignment = HiringAssignmentDraft(
Expand All @@ -267,6 +284,7 @@
notes="Some notes here",
created=datetime.now(),
modified=datetime.now(),
flagged=False,
)

new_hiring_assignment = HiringAssignmentDraft(
Expand All @@ -282,9 +300,26 @@
notes="Some notes here",
created=datetime.now(),
modified=datetime.now(),
flagged=False,
)

new_flagged_hiring_assignment = HiringAssignmentDraft(
id=3,
user_id=user_data.instructor.id,
term_id=term_data.current_term.id,
course_site_id=office_hours_data.comp_110_site.id,
level=uta_level,
status=HiringAssignmentStatus.FINAL,
position_number="sample",
epar="12345",
i9=True,
notes="Some notes here",
created=datetime.now(),
modified=datetime.now(),
flagged=True,
)

hiring_assignments = [hiring_assignment]
hiring_assignments = [hiring_assignment, new_flagged_hiring_assignment]


def insert_fake_data(session: Session):
Expand Down
62 changes: 62 additions & 0 deletions backend/test/services/academics/hiring/hiring_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
from ...office_hours.office_hours_data import fake_data_fixture as insert_order_5
from .hiring_data import fake_data_fixture as insert_order_6

from backend.models.pagination import PaginationParams


# Test data
from ... import user_data
Expand Down Expand Up @@ -207,6 +209,14 @@ def test_update_hiring_assignment_not_found(hiring_svc: HiringService):
)
pytest.fail()

def test_update_hiring_assigment_flag(hiring_svc: HiringService):
"""Ensures that the admin can update the flagged status of a hiring assignment."""
assignment = hiring_svc.update_hiring_assignment(
user_data.root, hiring_data.hiring_assignment_flagged
)
assert assignment is not None
assert assignment.flagged is True


def test_delete_hiring_assignment(hiring_svc: HiringService):
"""Ensures that the admin can delete hiring assignments."""
Expand Down Expand Up @@ -303,3 +313,55 @@ def test_get_phd_applicants(hiring_svc: HiringService):
assert len(applicants) > 0
for applicant in applicants:
assert applicant.program_pursued in {"PhD", "PhD (ABD)"}


def test_get_hiring_summary_overview_all(hiring_svc: HiringService):
"""Test that the hiring summary overview returns all assignments."""
term_id = term_data.current_term.id
pagination_params = PaginationParams(page=0, page_size=10, order_by="", filter="")
summary = hiring_svc.get_hiring_summary_overview(
user_data.root, term_id, "all", pagination_params
)
assert summary is not None
assert len(summary.items) > 0
assert all(
assignment.flagged in [True, False] for assignment in summary.items
)


def test_get_hiring_summary_overview_flagged(hiring_svc: HiringService):
"""Test that the hiring summary overview filters for flagged assignments."""
term_id = term_data.current_term.id
pagination_params = PaginationParams(page=0, page_size=10, order_by="", filter="")
summary = hiring_svc.get_hiring_summary_overview(
user_data.root, term_id, "flagged", pagination_params
)
assert summary is not None
assert len(summary.items) > 0
assert all(assignment.flagged is True for assignment in summary.items)


def test_get_hiring_summary_overview_not_flagged(hiring_svc: HiringService):
"""Test that the hiring summary overview filters for not flagged assignments."""
term_id = term_data.current_term.id
pagination_params = PaginationParams(page=0, page_size=10, order_by="", filter="")
summary = hiring_svc.get_hiring_summary_overview(
user_data.root, term_id, "not_flagged", pagination_params
)
assert summary is not None
assert len(summary.items) > 0
assert all(assignment.flagged is False for assignment in summary.items)


def test_get_hiring_summary_overview_invalid_flagged(hiring_svc: HiringService):
"""Test that an invalid flagged filter returns all flagged/non-flagged assignments."""
term_id = term_data.current_term.id
pagination_params = PaginationParams(page=0, page_size=10, order_by="", filter="")
summary = hiring_svc.get_hiring_summary_overview(
user_data.root, term_id, "invalid_flagged", pagination_params
)

assert len(summary.items) > 0
assert all(assignment.flagged in [True, False] for assignment in summary.items)


Loading