diff --git a/lms/services/dashboard.py b/lms/services/dashboard.py index 6a26cbff96..b9d4d61e34 100644 --- a/lms/services/dashboard.py +++ b/lms/services/dashboard.py @@ -17,13 +17,14 @@ ) from lms.models.dashboard_admin import DashboardAdmin from lms.security import Permissions -from lms.services import OrganizationService, RosterService, UserService +from lms.services import OrganizationService, RosterService, UserService, SegmentService class DashboardService: def __init__( # noqa: PLR0913, PLR0917 self, request, + segment_service: SegmentService, assignment_service, course_service, roster_service: RosterService, @@ -33,6 +34,7 @@ def __init__( # noqa: PLR0913, PLR0917 ): self._db = request.db + self._segment_service = segment_service self._assignment_service = assignment_service self._course_service = course_service self._roster_service = roster_service @@ -211,10 +213,46 @@ def get_assignment_roster( # Always return the results, no matter the source, sorted return roster_last_updated, query.order_by(LMSUser.display_name, LMSUser.id) + def get_segment_roster( + self, authority_provided_id: str, h_userids: list[str] | None = None + ) -> Select[tuple[LMSUser, bool]]: + """Return a query that fetches the roster for a segment.""" + segment = self._segment_service.get_segment(authority_provided_id) + rosters_enabled = ( + segment + and segment.lms_course + and segment.lms_course.course.application_instance.settings.get( + "dashboard", "rosters" + ) + ) + roster_last_updated = self._roster_service.segment_roster_exists(segment) + if rosters_enabled and roster_last_updated: + # If rostering is enabled and we do have the data, use it + query = self._roster_service.get_segment_roster( + segment, + role_scope=RoleScope.COURSE, + role_type=RoleType.LEARNER, + h_userids=h_userids, + ) + + else: + # Always fallback to fetch users that have launched the assignment at some point + query = self._user_service.get_users_for_segment( + role_scope=RoleScope.COURSE, + role_type=RoleType.LEARNER, + segment_id=segment.id, + h_userids=h_userids, + # For launch data we always add the "active" column as true for compatibility with the roster query. + ).add_columns(True) + + # Always return the results, no matter the source, sorted + return query.order_by(LMSUser.display_name, LMSUser.id) + def factory(_context, request): return DashboardService( request=request, + segment_service=request.find_service(SegmentService), assignment_service=request.find_service(name="assignment"), course_service=request.find_service(name="course"), organization_service=request.find_service(OrganizationService), diff --git a/lms/services/roster.py b/lms/services/roster.py index e37aca76be..fdfa43d292 100644 --- a/lms/services/roster.py +++ b/lms/services/roster.py @@ -78,6 +78,19 @@ def assignment_roster_exists(self, assignment: Assignment) -> datetime | None: .limit(1) ) + def segment_roster_exists(self, segment: LMSSegment) -> datetime | None: + """ + Check if we have roster data for the given segment. + + In case we have roster data, return the last updated timestamp, None otherwise. + """ + return self._db.scalar( + select(LMSSegmentRoster.updated) + .where(LMSSegmentRoster.lms_segment_id == segment.id) + .order_by(LMSSegmentRoster.updated.desc()) + .limit(1) + ) + def get_assignment_roster( self, assignment: Assignment, @@ -85,7 +98,7 @@ def get_assignment_roster( role_type: RoleType | None = None, h_userids: list[str] | None = None, ) -> Select[tuple[LMSUser, bool]]: - """Get the roster information for a course from our DB.""" + """Get the roster information for an assignment from our DB.""" roster_query = ( select(LMSUser, AssignmentRoster.active) .join(LMSUser, AssignmentRoster.lms_user_id == LMSUser.id) @@ -104,6 +117,34 @@ def get_assignment_roster( return roster_query + def get_segment_roster( + self, + segment: LMSSegment, + role_scope: RoleScope | None = None, + role_type: RoleType | None = None, + h_userids: list[str] | None = None, + ) -> Select[tuple[LMSUser, bool]]: + """Get the roster information for a segment from our DB.""" + + roster_query = ( + (select(LMSUser, LMSSegmentRoster.active).join(LMSUser)) + .join(LMSSegmentRoster, LMSSegmentRoster.lms_user_id == LMSUser.id) + .join(LTIRole, LTIRole.id == LMSSegmentRoster.lti_role_id) + .where(LMSSegmentRoster.lms_segment_id == segment.id) + .distinct() + ) + + if role_scope: + roster_query = roster_query.where(LTIRole.scope == role_scope) + + if role_type: + roster_query = roster_query.where(LTIRole.type == role_type) + + if h_userids: + roster_query = roster_query.where(LMSUser.h_userid.in_(h_userids)) + + return roster_query + def fetch_course_roster(self, lms_course: LMSCourse) -> None: """Fetch the roster information for a course from the LMS.""" assert ( diff --git a/lms/services/segment.py b/lms/services/segment.py index 20ac68b5ae..435fe869b0 100644 --- a/lms/services/segment.py +++ b/lms/services/segment.py @@ -17,6 +17,14 @@ def __init__(self, db, group_set_service: GroupSetService): self._db = db self._group_set_service = group_set_service + def get_segment(self, authority_provided_id: str) -> LMSSegment | None: + """Get a segment by its authority_provided_id.""" + return ( + self._db.query(LMSSegment) + .filter_by(h_authority_provided_id=authority_provided_id) + .one_or_none() + ) + def upsert_segments( self, course: LMSCourse, diff --git a/lms/views/dashboard/api/user.py b/lms/views/dashboard/api/user.py index 79edbbcd1b..f9edddea7b 100644 --- a/lms/views/dashboard/api/user.py +++ b/lms/views/dashboard/api/user.py @@ -212,6 +212,16 @@ def _students_query( h_userids: list[str] | None = None, ) -> tuple[datetime | None, Select]: course_ids = self.request.parsed_params.get("course_ids") + + # Single segment fetch + if segment_authority_provided_ids and len(segment_authority_provided_ids) == 1: + # TODO SEGMETN CHECK + + return self.dashboard_service.get_segment_roster( + segment_authority_provided_id=segment_authority_provided_ids, + h_userids=h_userids, + ) + # Single assigment fetch if ( assignment_ids