Skip to content

Commit

Permalink
Follow pagination links on the LTI names and roles API responses
Browse files Browse the repository at this point in the history
  • Loading branch information
marcospri committed Jan 31, 2025
1 parent 60e5731 commit fcc7f12
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 8 deletions.
32 changes: 27 additions & 5 deletions lms/services/lti_names_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
https://www.imsglobal.org/ltiadvantage
"""

from typing import TypedDict
import logging
from typing import Any, TypedDict

from lms.models import LTIRegistration
from lms.services.ltia_http import LTIAHTTPService

LOG = logging.getLogger(__name__)


class Member(TypedDict):
"""Structure of the members returned by the name and roles LTI API."""
Expand Down Expand Up @@ -37,6 +40,8 @@ def get_context_memberships(
lti_registration: LTIRegistration,
service_url: str,
resource_link_id: str | None = None,
max_pages: int = 10,
limit: int = 100,
) -> list[Member]:
"""
Get the roster for a course or assignment.
Expand All @@ -45,12 +50,31 @@ def get_context_memberships(
from a LTI launch parameter and is always linked to an specific context.
Optionally, using the same service_url the API allows to get the roster of an assignment identified by `resource_link_id`.
max_pages and limit control the default pagination limits.
"""
query = {}

query: dict[str, Any] = {"limit": limit}
if resource_link_id:
query["rlid"] = resource_link_id

response = self._ltia_service.request(
response = self._make_request(lti_registration, service_url, query)

members = response.json()["members"]

while response.links.get("next") and max_pages:
LOG.info("Fetching next page of members %s", response.links["next"]["url"])
response = self._make_request(
lti_registration, response.links["next"]["url"], query
)
members.extend(response.json()["members"])

max_pages -= 1

return members

def _make_request(self, lti_registration, service_url, query):
return self._ltia_service.request(
lti_registration,
"GET",
service_url,
Expand All @@ -61,8 +85,6 @@ def get_context_memberships(
params=query,
)

return response.json()["members"]


def factory(_context, request):
return LTINamesRolesService(ltia_http_service=request.find_service(LTIAHTTPService))
54 changes: 51 additions & 3 deletions tests/unit/lms/services/lti_names_roles_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import sentinel
from unittest.mock import Mock, call, sentinel

import pytest

Expand All @@ -7,6 +7,8 @@

class TestLTINameRolesServices:
def test_get_context_memberships(self, svc, ltia_http_service, lti_registration):
ltia_http_service.request.return_value.links = {}

memberships = svc.get_context_memberships(
lti_registration, sentinel.service_url
)
Expand All @@ -19,16 +21,62 @@ def test_get_context_memberships(self, svc, ltia_http_service, lti_registration)
headers={
"Accept": "application/vnd.ims.lti-nrps.v2.membershipcontainer+json"
},
params={},
params={"limit": 100},
)
assert (
memberships
== ltia_http_service.request.return_value.json.return_value["members"]
)

def test_get_context_memberships_multiple_pages(
self, svc, ltia_http_service, lti_registration
):
ltia_http_service.request.side_effect = [
Mock(
links={"next": {"url": sentinel.next_url}},
json=Mock(return_value={"members": [sentinel.member_1]}),
),
Mock(
links={},
json=Mock(return_value={"members": [sentinel.member_2]}),
),
]

memberships = svc.get_context_memberships(
lti_registration, sentinel.service_url
)

ltia_http_service.request.assert_has_calls(
[
call(
lti_registration,
"GET",
sentinel.service_url,
scopes=LTINamesRolesService.LTIA_SCOPES,
headers={
"Accept": "application/vnd.ims.lti-nrps.v2.membershipcontainer+json"
},
params={"limit": 100},
),
call(
lti_registration,
"GET",
sentinel.next_url,
scopes=LTINamesRolesService.LTIA_SCOPES,
headers={
"Accept": "application/vnd.ims.lti-nrps.v2.membershipcontainer+json"
},
params={"limit": 100},
),
]
)
assert memberships == [sentinel.member_1, sentinel.member_2]

def test_get_context_memberships_with_resource_link_id(
self, svc, ltia_http_service, lti_registration
):
ltia_http_service.request.return_value.links = {}

memberships = svc.get_context_memberships(
lti_registration, sentinel.service_url, sentinel.resource_link_id
)
Expand All @@ -41,7 +89,7 @@ def test_get_context_memberships_with_resource_link_id(
headers={
"Accept": "application/vnd.ims.lti-nrps.v2.membershipcontainer+json"
},
params={"rlid": sentinel.resource_link_id},
params={"rlid": sentinel.resource_link_id, "limit": 100},
)
assert (
memberships
Expand Down

0 comments on commit fcc7f12

Please sign in to comment.