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 - - - Assignments -
- -
- - - Instructor Preferences - - -
+ View Applicants + + +
+ + Draft + Commit + Final + + +
+ + + + + + + + + Instructor Preferences + + @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 );