diff --git a/backend/api/academics/section_member.py b/backend/api/academics/section_member.py index 0c94d36b4..282026c08 100644 --- a/backend/api/academics/section_member.py +++ b/backend/api/academics/section_member.py @@ -6,7 +6,9 @@ from ..authentication import registered_user from ...models.academics.section_member import SectionMember +from ...models.academics.section_member_details import SectionMemberDetails from ...models.office_hours.section import OfficeHoursSection +from ...models.roster_role import RosterRole from ...models import User from ...services.academics import SectionMemberService @@ -122,3 +124,25 @@ def check_instructor_memberships( """ return section_member_svc.search_instructor_memberships(subject) + + +@api.post( + "/instructor/{section_id}/{user_id}", + response_model=SectionMember, + tags=["Academics"], +) +def add_instructor( + section_id: int, + user_id: int, + subject: User = Depends(registered_user), + section_member_svc: SectionMemberService = Depends(), +) -> SectionMemberDetails: + """ + Gets one section by its id + + Returns: + SectionDetails: Section with the given ID + """ + return section_member_svc.add_section_member( + subject, section_id, user_id, RosterRole.INSTRUCTOR + ) diff --git a/backend/services/academics/section_member.py b/backend/services/academics/section_member.py index eca00d6f4..d62e1896f 100644 --- a/backend/services/academics/section_member.py +++ b/backend/services/academics/section_member.py @@ -101,6 +101,36 @@ def get_section_member_by_user_id_and_oh_section_id( return entity.to_flat_model() + def add_section_member( + self, subject: User, section_id: int, user_id: int, member_role: RosterRole + ) -> SectionMemberDetails: + """Add one member to a section + + Args: + subject (User): The user for whom to add section memberships. + section_id (int): ID of the section to add a member to. + user_id (int): ID of the user to add a member to. + + Returns: + SectionMember: Newly created section member. + + Raises: + ResourceNotFoundException: If no academic section is found for any of the specified office hours sections. + """ + self._permission_svc.enforce( + subject, "academics.section_member.create", f"section/{section_id}" + ) + + draft = SectionMemberDraft( + user_id=user_id, section_id=section_id, member_role=member_role + ) + section_membership = SectionMemberEntity.from_draft_model(draft) + + self._session.add(section_membership) + self._session.commit() + + return section_membership.to_details_model() + def add_user_section_memberships_by_oh_sections( self, subject: User, diff --git a/backend/services/office_hours/section.py b/backend/services/office_hours/section.py index dd2117f43..5ae67e6b6 100644 --- a/backend/services/office_hours/section.py +++ b/backend/services/office_hours/section.py @@ -26,6 +26,7 @@ from ...models.office_hours.section_details import OfficeHoursSectionDetails from ...models.user import User from ..exceptions import ResourceNotFoundException +from ...services.permission import PermissionService __authors__ = ["Sadie Amato", "Madelyn Andrews", "Bailey DeSouza", "Meghan Sun"] @@ -39,9 +40,11 @@ class OfficeHoursSectionService: def __init__( self, session: Session = Depends(db_session), + permission_svc: PermissionService = Depends(), ): """Initializes the database session.""" self._session = session + self._permission_svc = permission_svc def create( self, @@ -75,17 +78,20 @@ def create( if entity.office_hours_id is not None: raise Exception("Office Hours Section Already Exists!") - # 3. Check If User is a Member in a Section and Has Proper Role to Create - for academic_id in academic_section_ids: - section_member_entity = self._check_membership_by_user_id_section_id( - subject.id, academic_id - ) + has_permissions = self._permission_svc.check(subject, "oh_service.create", "*") - if section_member_entity.member_role != RosterRole.INSTRUCTOR: - raise PermissionError( - f"Section Member is not an Instructor. User Does Not Have Permisions Create an Office Hours Section." + # 3. Check If User is a Member in a Section and Has Proper Role to Create + if not has_permissions: + for academic_id in academic_section_ids: + section_member_entity = self._check_membership_by_user_id_section_id( + subject.id, academic_id ) + if section_member_entity.member_role != RosterRole.INSTRUCTOR: + raise PermissionError( + f"Section Member is not an Instructor. User Does Not Have Permisions Create an Office Hours Section." + ) + # Query and Execution to Update All Academic Section With New OH Section ID oh_section_entity = OfficeHoursSectionEntity.from_draft_model(oh_section) diff --git a/backend/test/services/academics/section_test.py b/backend/test/services/academics/section_test.py index 14b6822be..d43916ace 100644 --- a/backend/test/services/academics/section_test.py +++ b/backend/test/services/academics/section_test.py @@ -2,16 +2,17 @@ from unittest.mock import create_autospec import pytest +from backend.models.roster_role import RosterRole from backend.services.exceptions import ( ResourceNotFoundException, UserPermissionException, ) from backend.services.permission import PermissionService -from ....services.academics import SectionService +from ....services.academics import SectionService, SectionMemberService from ....models.academics import SectionDetails # Imported fixtures provide dependencies injected for the tests as parameters. -from .fixtures import permission_svc, section_svc +from .fixtures import permission_svc, section_svc, section_member_svc # Import the setup_teardown fixture explicitly to load entities in database from ..core_data import setup_insert_data_fixture as insert_order_0 @@ -231,3 +232,25 @@ def test_get_sections_with_no_office_hours_by_term(section_svc: SectionService): assert len(sections_with_no_oh) > 0 assert isinstance(sections_with_no_oh[0], SectionDetails) + + +def test_root_add_section_member(section_member_svc: SectionMemberService): + membership = section_member_svc.add_section_member( + subject=user_data.root, + section_id=section_data.comp_101_001.id, + user_id=user_data.root.id, + member_role=RosterRole.INSTRUCTOR, + ) + assert membership is not None + + +def test_user_add_section_member(section_member_svc: SectionMemberService): + + with pytest.raises(UserPermissionException): + section_member_svc.add_section_member( + subject=user_data.student, + section_id=section_data.comp_101_001.id, + user_id=user_data.root.id, + member_role=RosterRole.INSTRUCTOR, + ) + pytest.fail() diff --git a/backend/test/services/office_hours/fixtures.py b/backend/test/services/office_hours/fixtures.py index c3b7d2795..75cbf1d0e 100644 --- a/backend/test/services/office_hours/fixtures.py +++ b/backend/test/services/office_hours/fixtures.py @@ -38,6 +38,6 @@ def oh_ticket_svc( @pytest.fixture() -def oh_section_svc(session: Session): +def oh_section_svc(session: Session, permission_svc: PermissionService): """OfficeHoursSectionService fixture.""" - return OfficeHoursSectionService(session) + return OfficeHoursSectionService(session, permission_svc) diff --git a/backend/test/services/office_hours/section/create_test.py b/backend/test/services/office_hours/section/create_test.py index 30d64a935..c78ca1319 100644 --- a/backend/test/services/office_hours/section/create_test.py +++ b/backend/test/services/office_hours/section/create_test.py @@ -4,12 +4,13 @@ from .....models.office_hours.section_details import OfficeHoursSectionDetails from .....services.academics.section import SectionService - +from .....services.permission import PermissionService from .....services.exceptions import ResourceNotFoundException from .....services.office_hours.section import OfficeHoursSectionService # Imported fixtures provide dependencies injected for the tests as parameters. -from ..fixtures import permission_svc, oh_section_svc +from ..fixtures import permission_svc +from ..fixtures import oh_section_svc from ...academics.fixtures import section_svc # Import the setup_teardown fixture explicitly to load entities in database @@ -22,6 +23,8 @@ # Import the fake model data in a namespace for test assertions from .. import office_hours_data +from ...user_data import root + from ...academics.section_data import ( user__comp110_instructor, user__comp110_student_0, @@ -38,6 +41,73 @@ __license__ = "MIT" +def test_create_by_root( + oh_section_svc: OfficeHoursSectionService, section_svc: SectionService +): + """Test case to validate creation of an office hours section by an instructor.""" + oh_section = oh_section_svc.create( + root, + office_hours_data.oh_section_draft, + [comp_301_001_current_term.id], + ) + + assert isinstance(oh_section, OfficeHoursSectionDetails) + assert oh_section.title == office_hours_data.oh_section_draft.title + + +def test_create_by_root_and_linked_to_academic_section( + oh_section_svc: OfficeHoursSectionService, section_svc: SectionService +): + """Test case to validate creation of an office hours section linked to an academic section by an instructor.""" + oh_section = oh_section_svc.create( + root, + office_hours_data.oh_section_draft, + [comp_301_001_current_term.id], + ) + + assert isinstance(oh_section, OfficeHoursSectionDetails) + + # Check if OH Section Is Linked to Academic Section + academic_section = section_svc.get_by_id(comp_301_001_current_term.id) + assert oh_section.id == academic_section.office_hours_section.id + + +def test_create_by_root_multiple_academic_sections( + oh_section_svc: OfficeHoursSectionService, +): + """Test case to validate creation of an office hours section with multiple academic sections by an instructor.""" + oh_section = oh_section_svc.create( + root, + office_hours_data.oh_section_draft, + [comp_301_001_current_term.id, comp_210_001_current_term.id], + ) + + assert isinstance(oh_section, OfficeHoursSectionDetails) + assert oh_section.title == office_hours_data.oh_section_draft.title + + +def test_create_by_root_multiple_academic_sections_and_linked_to_academic_section( + oh_section_svc: OfficeHoursSectionService, section_svc: SectionService +): + """Test case to validate creation of an office hours section with multiple academic sections + and linked to those academic sections by an instructor.""" + oh_section = oh_section_svc.create( + root, + office_hours_data.oh_section_draft, + [comp_301_001_current_term.id, comp_210_001_current_term.id], + ) + + assert isinstance(oh_section, OfficeHoursSectionDetails) + assert oh_section.title == office_hours_data.oh_section_draft.title + + # Check if OH Section Is Linked to Academic Sections + comp301 = section_svc.get_by_id(comp_301_001_current_term.id) + comp210 = section_svc.get_by_id(comp_210_001_current_term.id) + + assert oh_section.id == comp301.office_hours_section.id + assert oh_section.id == comp210.office_hours_section.id + + def test_create_by_instructor( oh_section_svc: OfficeHoursSectionService, section_svc: SectionService ): @@ -144,7 +214,7 @@ def test_create_exception_if_oh_section_exists( """Test case to validate that creating an office hours section raises an Exception if a duplicate section already exists.""" with pytest.raises(Exception): oh_section_svc.create( - user__comp110_instructor, + root, office_hours_data.oh_section_draft, [comp_110_001_current_term.id], ) @@ -157,7 +227,7 @@ def test_create_exception_if_one_oh_section_exists_and_other_does_not_have_one( """Test case to validate that creating an office hours section raises an Exception if one section exists but the other doesn't.""" with pytest.raises(Exception): oh_section_svc.create( - user__comp110_instructor, + root, office_hours_data.oh_section_draft, [comp_110_001_current_term.id, comp_301_001_current_term.id], ) @@ -170,7 +240,7 @@ def test_create_exception_if_can_not_find_all_academic_sections( """Test case to validate that creating an office hours section raises an ResourceNotFoundException if all academic sections are not found.""" with pytest.raises(ResourceNotFoundException): oh_section_svc.create( - user__comp110_instructor, + root, office_hours_data.oh_section_draft, [comp_301_001_current_term.id, 99], )