diff --git a/backend/api/academics/hiring.py b/backend/api/academics/hiring.py
index 1792cc14f..bc8133c4a 100644
--- a/backend/api/academics/hiring.py
+++ b/backend/api/academics/hiring.py
@@ -42,6 +42,18 @@ def get_hiring_admin_overview(
return hiring_service.get_hiring_admin_overview(subject, term_id)
+@api.get("/admin/course/{course_site_id}", tags=["Hiring"])
+def get_hiring_admin_course_overview(
+ course_site_id: int,
+ subject: User = Depends(registered_user),
+ hiring_service: HiringService = Depends(),
+) -> HiringAdminCourseOverview:
+ """
+ Returns the state of hiring to the admin.
+ """
+ return hiring_service.get_hiring_admin_course_overview(subject, course_site_id)
+
+
@api.post("/assignment", tags=["Hiring"])
def create_hiring_assignment(
assignment: HiringAssignmentDraft,
diff --git a/backend/models/academics/hiring/hiring_assignment.py b/backend/models/academics/hiring/hiring_assignment.py
index f3e81027a..358ac8dc6 100644
--- a/backend/models/academics/hiring/hiring_assignment.py
+++ b/backend/models/academics/hiring/hiring_assignment.py
@@ -89,9 +89,13 @@ class HiringCourseSiteOverview(BaseModel):
total_cost: float
coverage: float
assignments: list[HiringAssignmentOverview]
- # reviews: list[ApplicationReviewOverview]
- # instructor_preferences: list[PublicUser]
class HiringAdminOverview(BaseModel):
sites: list[HiringCourseSiteOverview]
+
+
+class HiringAdminCourseOverview(BaseModel):
+ assignments: list[HiringAssignmentOverview]
+ reviews: list[ApplicationReviewOverview]
+ instructor_preferences: list[PublicUser]
diff --git a/backend/services/academics/hiring.py b/backend/services/academics/hiring.py
index 462cd1083..a2a85945d 100644
--- a/backend/services/academics/hiring.py
+++ b/backend/services/academics/hiring.py
@@ -194,7 +194,7 @@ def get_phd_applicants(
query = select(ApplicationEntity).where(
ApplicationEntity.term_id == term_id,
ApplicationEntity.type == "gta",
- ApplicationEntity.program_pursued.in_({"PhD", "PhD (ABD)"}),
+ ApplicationEntity.program_pursued.in_({"PhD", "PhD (ABD)", "MS", "BS/MS"}),
)
all = self._session.scalars(query).all()
@@ -237,12 +237,13 @@ def get_phd_applicants(
select(ApplicationReviewEntity)
.where(ApplicationReviewEntity.application_id.in_(application_ids))
.where(ApplicationReviewEntity.status == ApplicationReviewStatus.PREFERRED)
+ .order_by(ApplicationReviewEntity.preference)
.options(joinedload(ApplicationReviewEntity.course_site))
)
instructor_preferences = self._session.scalars(instructor_review_query).all()
for review in instructor_preferences:
phd_applications[review.application_id].instructor_preferences.append(
- f"{review.course_site.sections[0].course_id}.{review.course_site.sections[0].number}"
+ f"({review.preference}) {review.course_site.sections[0].course_id}.{review.course_site.sections[0].number}"
)
return list(phd_applications.values())
@@ -588,12 +589,6 @@ def get_hiring_admin_overview(
.where(TermEntity.id == term_id)
.options(
joinedload(CourseSiteEntity.sections),
- # # .joinedload(SectionEntity.staff)
- # # .joinedload(SectionMemberEntity.user),
- # joinedload(CourseSiteEntity.application_reviews)
- # .joinedload(ApplicationReviewEntity.application),
- # # .joinedload(ApplicationEntity.user),
- # joinedload(CourseSiteEntity.hiring_assignments),
)
)
course_site_entities = self._session.scalars(course_site_query).unique().all()
@@ -616,25 +611,6 @@ def get_hiring_admin_overview(
]
total_enrollment += section_entity.enrolled
- # preferred_review_query = (
- # select(ApplicationReviewEntity)
- # .where(
- # ApplicationReviewEntity.course_site_id == course_site_entity.id,
- # ApplicationReviewEntity.status == ApplicationReviewStatus.PREFERRED,
- # )
- # .order_by(ApplicationReviewEntity.preference)
- # )
- # preferred_review_entities = self._session.scalars(
- # preferred_review_query
- # ).all()
- # reviews = [
- # application_review.to_overview_model()
- # for application_review in preferred_review_entities
- # ]
- # instructor_preferences = [
- # application_review.application.user.to_public_model()
- # for application_review in preferred_review_entities
- # ]
assignments = sorted(
[
assignment.to_overview_model()
@@ -656,8 +632,6 @@ def get_hiring_admin_overview(
total_cost=total_cost,
coverage=coverage,
assignments=assignments,
- # reviews=reviews,
- # instructor_preferences=instructor_preferences,
)
# Add overview to the list
@@ -666,6 +640,65 @@ def get_hiring_admin_overview(
# 4. Return hiring adming overview object
return HiringAdminOverview(sites=hiring_course_site_overviews)
+ def get_hiring_admin_course_overview(
+ self, subject: User, course_site_id: str
+ ) -> HiringAdminCourseOverview:
+ self._permission.enforce(subject, "hiring.admin", "*")
+ course_site_entity = self._session.get(CourseSiteEntity, course_site_id)
+ if course_site_entity is None:
+ raise ResourceNotFoundException()
+
+ preferred_review_query = (
+ select(ApplicationReviewEntity)
+ .where(
+ ApplicationReviewEntity.course_site_id == course_site_entity.id,
+ ApplicationReviewEntity.status == ApplicationReviewStatus.PREFERRED,
+ )
+ .order_by(ApplicationReviewEntity.preference)
+ .options(
+ joinedload(ApplicationReviewEntity.application).joinedload(
+ ApplicationEntity.user
+ )
+ )
+ )
+ preferred_review_entities = self._session.scalars(preferred_review_query).all()
+
+ def to_overview(review: ApplicationReviewEntity) -> ApplicationReviewOverview:
+ return ApplicationReviewOverview(
+ id=review.id,
+ application_id=review.application_id,
+ course_site_id=course_site_entity.id,
+ status=review.status,
+ preference=review.preference,
+ notes=review.notes,
+ application=review.application.to_review_overview_model(),
+ applicant_id=review.application.user_id,
+ applicant_course_ranking=0,
+ )
+
+ reviews = [
+ to_overview(application_review)
+ for application_review in preferred_review_entities
+ ]
+
+ instructor_preferences = [review.application.applicant for review in reviews]
+
+ assignments_query = (
+ select(HiringAssignmentEntity)
+ .where(HiringAssignmentEntity.course_site_id == course_site_id)
+ .options(joinedload(HiringAssignmentEntity.user))
+ )
+ assignment_entities = self._session.scalars(assignments_query).all()
+ assignments = [
+ assignment.to_overview_model() for assignment in assignment_entities
+ ]
+
+ return HiringAdminCourseOverview(
+ assignments=assignments,
+ reviews=reviews,
+ instructor_preferences=instructor_preferences,
+ )
+
def create_hiring_assignment(
self, subject: User, assignment: HiringAssignmentDraft
) -> HiringAssignmentOverview:
diff --git a/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.html b/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.html
index c5cc46a4a..a436317d9 100644
--- a/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.html
+++ b/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.html
@@ -24,6 +24,9 @@
Create Assignment
}}
+
+ This student has submitted an I9.
+
Position Number
@@ -36,9 +39,6 @@ Create Assignment
Notes
-
- This student has submitted an I9.
-
diff --git a/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.ts b/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.ts
index 191c7d997..21587ca04 100644
--- a/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.ts
+++ b/frontend/src/app/hiring/dialogs/create-assignment-dialog/create-assignment.dialog.ts
@@ -10,6 +10,7 @@ import { HiringService } from '../../hiring.service';
import { PublicProfile } from 'src/app/profile/profile.service';
import {
ApplicationReviewOverview,
+ HiringAdminCourseOverview,
HiringAssignmentDraft,
HiringAssignmentStatus,
HiringCourseSiteOverview,
@@ -21,6 +22,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
export interface CreateAssignmentDialogData {
termId: string;
courseSite: HiringCourseSiteOverview;
+ courseAdmin: HiringAdminCourseOverview;
}
@Component({
@@ -100,7 +102,7 @@ export class CreateAssignmentDialog {
}
getApplication(): ApplicationReviewOverview | undefined {
- return this.data.courseSite.reviews.find(
+ return this.data.courseAdmin.reviews.find(
(a) => a.applicant_id === this.users[0].id
);
}
diff --git a/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.html b/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.html
index 65e02b6f4..414f27c62 100644
--- a/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.html
+++ b/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.html
@@ -34,6 +34,9 @@ Edit Assignment
}}
+
+ This student has submitted an I9.
+
Position Number
@@ -46,9 +49,6 @@ Edit Assignment
Notes
-
- This student has submitted an I9.
-
diff --git a/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.ts b/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.ts
index e43f1bcfa..93bcbd2d2 100644
--- a/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.ts
+++ b/frontend/src/app/hiring/dialogs/edit-assignment-dialog/edit-assignment.dialog.ts
@@ -14,6 +14,7 @@ import { HiringService } from '../../hiring.service';
import { PublicProfile } from 'src/app/profile/profile.service';
import {
ApplicationReviewOverview,
+ HiringAdminCourseOverview,
HiringAssignmentDraft,
HiringAssignmentOverview,
HiringAssignmentStatus,
@@ -28,6 +29,7 @@ export interface EditAssignmentDialogData {
assignment: HiringAssignmentOverview;
termId: string;
courseSite: HiringCourseSiteOverview;
+ courseAdmin: HiringAdminCourseOverview;
}
@Component({
@@ -116,7 +118,7 @@ export class EditAssignmentDialog {
}
getApplication(): ApplicationReviewOverview | undefined {
- return this.data.courseSite.reviews.find(
+ return this.data.courseAdmin.reviews.find(
(a) => a.applicant_id === this.data.assignment.user.id
);
}
diff --git a/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.html b/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.html
index 89c093f14..f5d333def 100644
--- a/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.html
+++ b/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.html
@@ -34,6 +34,9 @@ Quick Create Assignment
}}
+
+ This student has submitted an I9.
+
Position Number
@@ -46,9 +49,6 @@ Quick Create Assignment
Notes
-
- This student has submitted an I9.
-
diff --git a/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.ts b/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.ts
index 958328428..3ee6e0581 100644
--- a/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.ts
+++ b/frontend/src/app/hiring/dialogs/quick-create-assignment-dialog/quick-create-assignment.dialog.ts
@@ -13,8 +13,8 @@ import {
import { HiringService } from '../../hiring.service';
import { PublicProfile } from 'src/app/profile/profile.service';
import {
- ApplicationOverview,
ApplicationReviewOverview,
+ HiringAdminCourseOverview,
HiringAssignmentDraft,
HiringAssignmentStatus,
HiringCourseSiteOverview,
@@ -28,6 +28,7 @@ export interface QuickCreateAssignmentDialogData {
user: PublicProfile;
termId: string;
courseSite: HiringCourseSiteOverview;
+ courseAdmin: HiringAdminCourseOverview;
}
@Component({
@@ -62,6 +63,35 @@ export class QuickCreateAssignmentDialog {
@Inject(MAT_DIALOG_DATA) public data: QuickCreateAssignmentDialogData
) {
this.user = data.user;
+
+ // Simple hack to automatically populate the level...
+ let review = data.courseAdmin.reviews.find(
+ (r) => r.applicant_id == data.user.id
+ )!;
+ let program = review.application.program_pursued!;
+ let defaultLevelSearch: string | null;
+ switch (program) {
+ case 'PhD':
+ defaultLevelSearch = '1.0 PhD TA';
+ break;
+ case 'PhD (ABD)':
+ defaultLevelSearch = '1.0 PhD (ABD) TA';
+ break;
+ case 'BS/MS':
+ case 'MS':
+ defaultLevelSearch = '1.0 MS TA';
+ break;
+ default:
+ defaultLevelSearch = '10h UTA';
+ break;
+ }
+
+ const level = this.hiringService
+ .hiringLevels()
+ .find((level) => level.title == defaultLevelSearch);
+ if (level) {
+ this.createAssignmentForm.get('level')?.setValue(level);
+ }
}
/** Determines if the form is valid and can be submitted. */
@@ -110,7 +140,7 @@ export class QuickCreateAssignmentDialog {
}
getApplication(): ApplicationReviewOverview | undefined {
- return this.data.courseSite.reviews.find(
+ return this.data.courseAdmin.reviews.find(
(a) => a.applicant_id === this.data.user.id
);
}
diff --git a/frontend/src/app/hiring/hiring-admin/hiring-admin.component.html b/frontend/src/app/hiring/hiring-admin/hiring-admin.component.html
index 8f60803f1..b8cf0112f 100644
--- a/frontend/src/app/hiring/hiring-admin/hiring-admin.component.html
+++ b/frontend/src/app/hiring/hiring-admin/hiring-admin.component.html
@@ -137,10 +137,12 @@
[@detailExpand]="
element === expandedElement ? 'expanded' : 'collapsed'
">
-
+ @if (element === expandedElement) {
+
+ }
diff --git a/frontend/src/app/hiring/hiring.models.ts b/frontend/src/app/hiring/hiring.models.ts
index 7ca822421..fd4c395e1 100644
--- a/frontend/src/app/hiring/hiring.models.ts
+++ b/frontend/src/app/hiring/hiring.models.ts
@@ -134,14 +134,20 @@ export interface HiringCourseSiteOverview {
total_cost: number;
coverage: number;
assignments: HiringAssignmentOverview[];
- reviews: ApplicationReviewOverview[];
- instructor_preferences: PublicProfile[];
+ // reviews: ApplicationReviewOverview[];
+ // instructor_preferences: PublicProfile[];
}
export interface HiringAdminOverview {
sites: HiringCourseSiteOverview[];
}
+export interface HiringAdminCourseOverview {
+ assignments: HiringAssignmentOverview[];
+ reviews: ApplicationReviewOverview[];
+ instructor_preferences: PublicProfile[];
+}
+
export interface HiringAssignmentSummaryOverview {
id: number | null;
application_review_id: number | null;
diff --git a/frontend/src/app/hiring/hiring.service.ts b/frontend/src/app/hiring/hiring.service.ts
index d6f8410f7..a27e47918 100644
--- a/frontend/src/app/hiring/hiring.service.ts
+++ b/frontend/src/app/hiring/hiring.service.ts
@@ -11,6 +11,7 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { computed, Injectable, signal, WritableSignal } from '@angular/core';
import { Observable, tap } from 'rxjs';
import {
+ HiringAdminCourseOverview,
HiringAdminOverview,
HiringAssignmentDraft,
HiringAssignmentOverview,
@@ -59,6 +60,17 @@ export class HiringService {
return this.http.get(`/api/hiring/admin/${termId}`);
}
+ /**
+ * Returns the state of hiring for a course.
+ */
+ getHiringAdminCourseOverview(
+ courseId: number
+ ): Observable {
+ return this.http.get(
+ `/api/hiring/admin/course/${courseId}`
+ );
+ }
+
private hiringLevelsSignal: WritableSignal = signal([]);
hiringLevels = this.hiringLevelsSignal.asReadonly();
activeHiringlevels = computed(() => {
diff --git a/frontend/src/app/hiring/widgets/course-hiring-card/course-hiring-card.widget.html b/frontend/src/app/hiring/widgets/course-hiring-card/course-hiring-card.widget.html
index a428678b1..20a33a280 100644
--- a/frontend/src/app/hiring/widgets/course-hiring-card/course-hiring-card.widget.html
+++ b/frontend/src/app/hiring/widgets/course-hiring-card/course-hiring-card.widget.html
@@ -1,114 +1,110 @@
-
- {{ item().sections[0].subject_code }} {{ item().sections[0].course_number
- }} Hiring
-
-
-
-
-
-
-
-
-
-
+ View Applicants
+
+
+
+
+ Draft
+ Commit
+ Final
+
+
+
+ |
+
+
+
+
+
+
+
+ @if(item()!.instructor_preferences!.length === 0) {
+ The instructor left no preferences.
+ }
+
+
+
+ {{ user.first_name + ' ' + user.last_name }}
+
+
+
+ }
diff --git a/frontend/src/app/hiring/widgets/course-hiring-card/course-hiring-card.widget.ts b/frontend/src/app/hiring/widgets/course-hiring-card/course-hiring-card.widget.ts
index 615a3f1f5..b6ae4e414 100644
--- a/frontend/src/app/hiring/widgets/course-hiring-card/course-hiring-card.widget.ts
+++ b/frontend/src/app/hiring/widgets/course-hiring-card/course-hiring-card.widget.ts
@@ -9,6 +9,7 @@ import {
} from '@angular/core';
import {
ApplicationReviewOverview,
+ HiringAdminCourseOverview,
HiringAssignmentOverview,
hiringAssignmentOverviewToDraft,
HiringAssignmentStatus,
@@ -42,7 +43,7 @@ export class CourseHiringCardWidget implements OnInit {
@Input() itemInput!: HiringCourseSiteOverview;
@Output() updateData = new EventEmitter();
- item: WritableSignal = signal(this.itemInput);
+ item: WritableSignal = signal(null);
/** Store the columns to display in the table */
public displayedColumns: string[] = [
@@ -60,7 +61,11 @@ export class CourseHiringCardWidget implements OnInit {
) {}
ngOnInit(): void {
- this.item = signal(this.itemInput);
+ this.hiringService
+ .getHiringAdminCourseOverview(this.itemInput.course_site_id)
+ .subscribe((courseOverview) => {
+ this.item.set(courseOverview);
+ });
}
/** Changes a status for a single assignment. */
@@ -72,16 +77,16 @@ export class CourseHiringCardWidget implements OnInit {
updatedAssignment.status = newStatus;
let draft = hiringAssignmentOverviewToDraft(
this.termId,
- this.item(),
+ this.itemInput,
updatedAssignment,
null
);
this.hiringService.updateHiringAssignment(draft).subscribe((assignment) => {
- let assignmentIndex = this.item().assignments.findIndex(
+ let assignmentIndex = this.item()!.assignments.findIndex(
(a) => a.id == assignment.id
);
this.item.update((oldItem) => {
- oldItem.assignments[assignmentIndex] = assignment;
+ oldItem!.assignments[assignmentIndex] = assignment;
return oldItem;
});
});
@@ -111,7 +116,8 @@ export class CourseHiringCardWidget implements OnInit {
width: '800px',
data: {
termId: this.termId,
- courseSite: this.item()
+ courseSite: this.itemInput,
+ courseAdmin: this.item()!
} as CreateAssignmentDialogData
});
dialogRef.afterClosed().subscribe((assignment) => {
@@ -130,7 +136,8 @@ export class CourseHiringCardWidget implements OnInit {
data: {
user: user,
termId: this.termId,
- courseSite: this.item()
+ courseSite: this.itemInput,
+ courseAdmin: this.item()!
} as QuickCreateAssignmentDialogData
});
dialogRef.afterClosed().subscribe((assignment) => {
@@ -148,7 +155,8 @@ export class CourseHiringCardWidget implements OnInit {
data: {
assignment: assignment,
termId: this.termId,
- courseSite: this.item()
+ courseSite: this.itemInput,
+ courseAdmin: this.item()!
} as EditAssignmentDialogData
});
dialogRef.afterClosed().subscribe((assignment) => {
@@ -160,7 +168,7 @@ export class CourseHiringCardWidget implements OnInit {
chipSelected(user: PublicProfile): boolean {
return (
- this.item()
+ this.item()!
.assignments.map((assignment) => assignment.user)
.filter((u) => u.id === user.id).length > 0
);