Skip to content

Commit

Permalink
First version of the roster service
Browse files Browse the repository at this point in the history
  • Loading branch information
marcospri committed Aug 29, 2024
1 parent 8f8de6c commit 4146044
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 2 deletions.
5 changes: 4 additions & 1 deletion lms/models/lms_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
"""

import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship

from lms.db import Base
from lms.models import ApplicationInstance
from lms.models._mixins import CreatedUpdatedMixin


Expand Down Expand Up @@ -53,10 +54,12 @@ class LMSCourseApplicationInstance(CreatedUpdatedMixin, Base):
application_instance_id: Mapped[int] = mapped_column(
sa.ForeignKey("application_instances.id", ondelete="cascade"), index=True
)
application_instance: Mapped[ApplicationInstance] = relationship()

lms_course_id: Mapped[int] = mapped_column(
sa.ForeignKey("lms_course.id", ondelete="cascade"), index=True
)
lms_course: Mapped[LMSCourse] = relationship()


class LMSCourseMembership(CreatedUpdatedMixin, Base):
Expand Down
4 changes: 4 additions & 0 deletions lms/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from lms.services.assignment import AssignmentService
from lms.services.canvas import CanvasService
from lms.services.canvas_studio import CanvasStudioService
from lms.services.course_roster import CourseRosterService
from lms.services.d2l_api.client import D2LAPIClient
from lms.services.digest import DigestService
from lms.services.email_preferences import EmailPreferencesService, EmailPrefs
Expand Down Expand Up @@ -149,6 +150,9 @@ def includeme(config):
"lms.services.youtube.factory", iface=YouTubeService
)
config.register_service_factory(MoodleAPIClient.factory, iface=MoodleAPIClient)
config.register_service_factory(
"lms.services.course_roster.factory", iface=CourseRosterService
)

# Plugins are not the same as top level services but we want to register them as pyramid services too
# Importing them here to:
Expand Down
49 changes: 49 additions & 0 deletions lms/services/course_roster.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from logging import getLogger

from sqlalchemy import select

from lms.models import (
ApplicationInstance,
LMSCourse,
LMSCourseApplicationInstance,
LTIRegistration,
)
from lms.services.lti_names_roles import LTINamesRolesService

LOG = getLogger(__name__)


class CourseRosterService:
def __init__(self, db, lti_names_roles_service: LTINamesRolesService):
self._db = db
self._lti_names_roles_service = lti_names_roles_service

def fetch_roster(self, lms_course: LMSCourse) -> None:
lti_registration = self._db.scalars(
select(LTIRegistration)
.join(ApplicationInstance)
.join(LMSCourseApplicationInstance)
.where(LMSCourseApplicationInstance.lms_course_id == lms_course.id)
.order_by(LTIRegistration.updated.desc())
).first()

assert lti_registration, "No LTI registration found for LMSCourse."
assert (
lms_course.lti_context_memberships_url
), "Trying fetch roster for course without service URL."

roster = self._lti_names_roles_service.get_context_memberships(
lti_registration, lms_course.lti_context_memberships_url
)
LOG.info(
"Roster for %s. Users returned %s",
lms_course.h_authority_provided_id,
len(roster),
)


def factory(_context, request):
return CourseRosterService(
db=request.db,
lti_names_roles_service=request.find_service(LTINamesRolesService),
)
2 changes: 1 addition & 1 deletion tests/factories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from tests.factories.h_user import HUser
from tests.factories.hubspot_company import HubSpotCompany
from tests.factories.jwt_oauth2_token import JWTOAuth2Token
from tests.factories.lms_course import LMSCourse
from tests.factories.lms_course import LMSCourse, LMSCourseApplicationInstance
from tests.factories.lms_user import LMSUser
from tests.factories.lti_registration import LTIRegistration
from tests.factories.lti_role import LTIRole, LTIRoleOverride
Expand Down
4 changes: 4 additions & 0 deletions tests/factories/lms_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@
h_authority_provided_id=Faker("hexify", text="^" * 40),
name=Sequence(lambda n: f"Course {n}"),
)

LMSCourseApplicationInstance = make_factory(
models.LMSCourseApplicationInstance, FACTORY_CLASS=SQLAlchemyModelFactory
)
45 changes: 45 additions & 0 deletions tests/unit/lms/services/course_roster_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from unittest.mock import sentinel

import pytest

from lms.services.course_roster import CourseRosterService, factory
from tests import factories


class TestLTINameRolesServices:
def test_fetch_roster(
self, svc, lti_names_roles_service, lti_v13_application_instance, db_session
):
lms_course = factories.LMSCourse(lti_context_memberships_url="SERVICE_URL")
factories.LMSCourseApplicationInstance(
lms_course=lms_course, application_instance=lti_v13_application_instance
)
db_session.flush()

svc.fetch_roster(lms_course)

lti_names_roles_service.get_context_memberships.assert_called_once_with(
lti_v13_application_instance.lti_registration, "SERVICE_URL"
)

@pytest.fixture
def svc(self, lti_names_roles_service, db_session):
return CourseRosterService(
db_session, lti_names_roles_service=lti_names_roles_service
)


class TestFactory:
def test_it(
self, pyramid_request, db_session, CourseRosterService, lti_names_roles_service
):
service = factory(sentinel.context, pyramid_request)

CourseRosterService.assert_called_once_with(
db=db_session, lti_names_roles_service=lti_names_roles_service
)
assert service == CourseRosterService.return_value

@pytest.fixture
def CourseRosterService(self, patch):
return patch("lms.services.course_roster.CourseRosterService")
7 changes: 7 additions & 0 deletions tests/unit/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from lms.services.launch_verifier import LaunchVerifier
from lms.services.lti_grading import LTIGradingService
from lms.services.lti_h import LTIHService
from lms.services.lti_names_roles import LTINamesRolesService
from lms.services.lti_registration import LTIRegistrationService
from lms.services.lti_user import LTIUserService
from lms.services.ltia_http import LTIAHTTPService
Expand Down Expand Up @@ -84,6 +85,7 @@
"launch_verifier",
"lti_grading_service",
"lti_h_service",
"lti_names_roles_service",
"lti_registration_service",
"lti_role_service",
"lti_user_service",
Expand Down Expand Up @@ -316,6 +318,11 @@ def lti_h_service(mock_service):
return mock_service(LTIHService, service_name="lti_h")


@pytest.fixture
def lti_names_roles_service(mock_service):
return mock_service(LTINamesRolesService)


@pytest.fixture
def lti_registration_service(mock_service):
return mock_service(LTIRegistrationService)
Expand Down

0 comments on commit 4146044

Please sign in to comment.