Skip to content
Open
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
22 changes: 21 additions & 1 deletion backend/api/academics/hiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
from ...models.academics.hiring.hiring_assignment import *
from ...models.academics.hiring.hiring_level import *
from ...models.academics.hiring.conflict_check import ConflictCheck
from ...models.academics.hiring.hiring_assignment_audit import (
HiringAssignmentAuditOverview,
)

from ...api.authentication import registered_user
from ...models.user import User
Expand Down Expand Up @@ -170,6 +173,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 +184,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 Expand Up @@ -407,3 +411,19 @@ def row_iter():
f"attachment; filename=applicants_{term_id}.csv"
)
return response


@api.get(
"/assignments/{assignment_id}/history",
tags=["Hiring"],
response_model=list[HiringAssignmentAuditOverview],
)
def get_assignment_history(
assignment_id: int,
subject: User = Depends(registered_user),
hiring_service: HiringService = Depends(),
) -> list[HiringAssignmentAuditOverview]:
"""
Get the change history for a specific hiring assignment.
"""
return hiring_service.get_audit_history(subject, assignment_id)
7 changes: 4 additions & 3 deletions backend/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
1. Loads all entities into the application derived from `entities.EntityBase`. In doing so,
many of SQLAlchemy's features around metadata (creation/updating/dropping of tables)
are possible by virtue of importing from this file directly.

2. An index module of all entities which makes importing entities easier. Rather than importing
from the modules directly, you can import them from the entities `package`, e.g. `from entities import UserEntity`.
When adding a new entity to the application be sure to import it here. As a reminder, all identifiers

When adding a new entity to the application be sure to import it here. As a reminder, all identifiers
global to a module are available for import from other modules."""

from .entity_base import EntityBase
Expand All @@ -33,6 +33,7 @@
from .article_author_entity import article_author_table

from .academics.hiring.hiring_assignment_entity import HiringAssignmentEntity
from .academics.hiring.hiring_assignment_audit_entity import HiringAssignmentAuditEntity
from .academics.hiring.hiring_level_entity import HiringLevelEntity

__authors__ = ["Kris Jordan"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from sqlalchemy import Integer, String, ForeignKey, DateTime
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
from ...entity_base import EntityBase

__authors__ = ["Christian Lee"]
__copyright__ = "Copyright 2025"
__license__ = "MIT"


class HiringAssignmentAuditEntity(EntityBase):
"""Schema for the `academics__hiring__assignment_audit` table."""

__tablename__ = "academics__hiring__assignment_audit"

id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

# The assignment being modified
hiring_assignment_id: Mapped[int] = mapped_column(
ForeignKey("academics__hiring__assignment.id")
)

# Who made the change
changed_by_user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
changed_by_user: Mapped["UserEntity"] = relationship("UserEntity")

# When it happened
change_timestamp: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)

# What changed (e.g. "Status: Draft -> Commit")
change_details: Mapped[str] = mapped_column(String)
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 ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Add hiring assignment audit table

Revision ID: 4fb402ba2e0d
Revises: 0a57afd03df5
Create Date: 2025-12-07 19:02:21.685299

"""

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "4fb402ba2e0d"
down_revision = "0a57afd03df5"


def upgrade() -> None:
op.create_table(
"academics__hiring__assignment_audit",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("hiring_assignment_id", sa.Integer(), nullable=False),
sa.Column("changed_by_user_id", sa.Integer(), nullable=False),
sa.Column("change_timestamp", sa.DateTime(), nullable=False),
sa.Column("change_details", sa.String(), nullable=False),
sa.ForeignKeyConstraint(
["changed_by_user_id"],
["user.id"],
name=op.f("fk_academics__hiring__assignment_audit_changed_by_user_id_user"),
),
sa.ForeignKeyConstraint(
["hiring_assignment_id"],
["academics__hiring__assignment.id"],
name=op.f(
"fk_academics__hiring__assignment_audit_hiring_assignment_id_assignment"
),
),
sa.PrimaryKeyConstraint(
"id", name=op.f("pk_academics__hiring__assignment_audit")
),
)


def downgrade() -> None:
op.drop_table("academics__hiring__assignment_audit")
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
31 changes: 31 additions & 0 deletions backend/models/academics/hiring/hiring_assignment_audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from pydantic import BaseModel
from datetime import datetime
from ... import PublicUser

__authors__ = ["Christian Lee"]
__copyright__ = "Copyright 2025"
__license__ = "MIT"


class HiringAssignmentAudit(BaseModel):
"""
Pydantic model to represent a snapshot of changes to a Hiring Assignment.
"""

id: int | None = None
hiring_assignment_id: int
changed_by_user_id: int
change_timestamp: datetime
change_details: str


class HiringAssignmentAuditOverview(BaseModel):
"""
Model for displaying audit logs in the UI.
Includes the full user details instead of just an ID.
"""

id: int
change_timestamp: datetime
change_details: str
changed_by_user: PublicUser
Loading