diff --git a/backend/api/academics/course.py b/backend/api/academics/course.py index 73048cac8..164638b6f 100644 --- a/backend/api/academics/course.py +++ b/backend/api/academics/course.py @@ -20,13 +20,13 @@ } -@api.get("", response_model=list[CourseDetails], tags=["Academics"]) -def get_courses(course_service: CourseService = Depends()) -> list[CourseDetails]: +@api.get("", tags=["Academics"]) +def get_courses(course_service: CourseService = Depends()) -> list[Course]: """ Get all courses Returns: - list[CourseDetails]: All `Course`s in the `Course` database table + list[Course]: All `Course`s in the `Course` database table """ return course_service.all() diff --git a/backend/services/academics/course.py b/backend/services/academics/course.py index 738908a2b..7a455ea5d 100644 --- a/backend/services/academics/course.py +++ b/backend/services/academics/course.py @@ -33,7 +33,7 @@ def __init__( self._session = session self._permission_svc = permission_svc - def all(self) -> list[CourseDetails]: + def all(self) -> list[Course]: """Retrieves all courses from the table Returns: @@ -45,7 +45,7 @@ def all(self) -> list[CourseDetails]: entities = self._session.scalars(query).all() # Convert entries to a model and return - return [entity.to_details_model() for entity in entities] + return [entity.to_model() for entity in entities] def get_by_id(self, id: str) -> CourseDetails: """Gets the course from the table for an id. diff --git a/backend/services/academics/section.py b/backend/services/academics/section.py index 9c954d8fe..be6b61e9e 100644 --- a/backend/services/academics/section.py +++ b/backend/services/academics/section.py @@ -7,7 +7,7 @@ from fastapi import Depends from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, joinedload from pydantic import BaseModel from ...database import db_session @@ -62,8 +62,15 @@ def get_by_term(self, term_id: str) -> list[CatalogSection]: select(SectionEntity) .where(SectionEntity.term_id == term_id) .order_by(SectionEntity.course_id, SectionEntity.number) + .options( + joinedload(SectionEntity.members).joinedload(SectionMemberEntity.user), + joinedload(SectionEntity.lecture_rooms).joinedload( + SectionRoomEntity.room + ), + joinedload(SectionEntity.course), + ) ) - entities = self._session.scalars(query).all() + entities = self._session.scalars(query).unique().all() # Return the model return [entity.to_catalog_model() for entity in entities] @@ -276,7 +283,7 @@ def update_enrollment_totals(self, subject: User): # Currently active terms. # This is hard-coded based on the availability and representation # of course enrollment data from UNC's course database. - AVAILABLE_TERMS = {"2024+Summer+II": "SuII24", "2024+Fall": "F24"} + AVAILABLE_TERMS = {"2024+Summer+II": "24SSII", "2024+Fall": "24F"} # Store updates to make. updates: dict[tuple[str, str], SectionEnrollmentData] = {} diff --git a/backend/test/services/academics/course_test.py b/backend/test/services/academics/course_test.py index 53b778a88..4ee35df51 100644 --- a/backend/test/services/academics/course_test.py +++ b/backend/test/services/academics/course_test.py @@ -8,7 +8,7 @@ ) from backend.services.permission import PermissionService from ....services.academics import CourseService -from ....models.academics import CourseDetails +from ....models.academics import Course, CourseDetails # Imported fixtures provide dependencies injected for the tests as parameters. from .fixtures import permission_svc, course_svc @@ -29,7 +29,7 @@ def test_all(course_svc: CourseService): courses = course_svc.all() assert len(courses) == len(course_data.courses) - assert isinstance(courses[0], CourseDetails) + assert isinstance(courses[0], Course) def test_get_by_id(course_svc: CourseService): diff --git a/frontend/src/app/academics/academics-admin/section/admin-section.component.css b/frontend/src/app/academics/academics-admin/section/admin-section.component.css index 0ac5784ce..f457bc3b1 100644 --- a/frontend/src/app/academics/academics-admin/section/admin-section.component.css +++ b/frontend/src/app/academics/academics-admin/section/admin-section.component.css @@ -11,7 +11,8 @@ .header { display: flex; align-items: center; - justify-content: space-between; + padding-top: 16px; + padding-bottom: 16px; } .row { @@ -29,3 +30,17 @@ #edit-button { margin-right: 8px; } + +.right-header-container { + display: flex; + flex-direction: row; + margin-left: auto; + gap: 12px; + align-items: center; +} + +.term-selector { + width: 260px; + margin-bottom: -1.25em; + } + \ No newline at end of file diff --git a/frontend/src/app/academics/academics-admin/section/admin-section.component.html b/frontend/src/app/academics/academics-admin/section/admin-section.component.html index ab59c7dba..bb323ee77 100644 --- a/frontend/src/app/academics/academics-admin/section/admin-section.component.html +++ b/frontend/src/app/academics/academics-admin/section/admin-section.component.html @@ -1,20 +1,23 @@ -
+
Sections - - Select Term - - {{ - term.name - }} - - - - +
+ + Select Term + + @for(term of terms; track term.id) { + {{ term.name }} + } + + + +
diff --git a/frontend/src/app/academics/academics-admin/section/admin-section.component.ts b/frontend/src/app/academics/academics-admin/section/admin-section.component.ts index cefa344df..875f00550 100644 --- a/frontend/src/app/academics/academics-admin/section/admin-section.component.ts +++ b/frontend/src/app/academics/academics-admin/section/admin-section.component.ts @@ -44,11 +44,13 @@ export class AdminSectionComponent { public sections: WritableSignal = signal([]); /** Store list of Terms */ - public terms: RxTermList = new RxTermList(); - public terms$: Observable = this.terms.value$; + public terms: Term[]; /** Store the currently selected term from the form */ - public displayTerm: FormControl = new FormControl(); + // NOTE: Separating these fields into an ID and a selected term was required + // for Angular to correctly show the correct term in the initial drop down. + public displayTermId: string | null; + public displayTerm: WritableSignal; public displayedColumns: string[] = ['name']; @@ -64,17 +66,13 @@ export class AdminSectionComponent { currentTerm: Term | undefined; }; - this.terms.set(data.terms); + this.terms = data.terms; - if (data.currentTerm) { - this.displayTerm.setValue(data.currentTerm); - - this.academicsService - .getSectionsByTerm(data.currentTerm!) - .subscribe((sections) => { - this.sections.set(sections); - }); - } + // Set initial display term + this.displayTermId = data.currentTerm?.id ?? null; + this.displayTerm = signal(this.selectedTerm()); + // Initialize the sections list + this.resetSections(); } /** Event handler to open the Section Editor to create a new term */ @@ -104,15 +102,29 @@ export class AdminSectionComponent { ); confirmDelete.onAction().subscribe(() => { this.academicsService.deleteSection(section).subscribe(() => { - let termToUpdate = this.displayTerm.value; this.sections.update((sections) => sections.filter((s) => s.id !== section.id) ); - this.terms.updateTerm(termToUpdate); this.snackBar.open('This Section has been deleted.', '', { duration: 2000 }); }); }); } + + selectedTerm() { + return this.terms.find((term) => term.id == this.displayTermId); + } + + /** Resets the section data based on the selected term. */ + resetSections() { + this.displayTerm.set(this.selectedTerm()); + if (this.displayTerm()) { + this.academicsService + .getSectionsByTerm(this.displayTerm()!) + .subscribe((sections) => { + this.sections.set(sections); + }); + } + } } diff --git a/frontend/src/app/academics/academics.module.ts b/frontend/src/app/academics/academics.module.ts index 612e8b580..8c95cbda4 100644 --- a/frontend/src/app/academics/academics.module.ts +++ b/frontend/src/app/academics/academics.module.ts @@ -11,7 +11,7 @@ import { AcademicsRoutingModule } from './academics-routing.module'; import { MatIconModule } from '@angular/material/icon'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; -import { ReactiveFormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AcademicsHomeComponent } from './academics-home/academics-home.component'; import { AcademicsAdminComponent } from './academics-admin/academics-admin.component'; import { MatTabsModule } from '@angular/material/tabs'; @@ -52,6 +52,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle'; MatTableModule, MatIconModule, MatFormFieldModule, + FormsModule, MatSelectModule, ReactiveFormsModule, MatTabsModule,