Skip to content

Commit

Permalink
WIP Courses endpoint with pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
marcospri committed Jun 24, 2024
1 parent 9b4feee commit 3311ae6
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 10 deletions.
2 changes: 2 additions & 0 deletions lms/js_config_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class APIStudent(TypedDict):
class APICourses(TypedDict):
courses: list[APICourse]

next: NotRequired[str]


class APIAssignment(TypedDict):
id: int
Expand Down
2 changes: 2 additions & 0 deletions lms/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ def includeme(config): # noqa: PLR0915
factory="lms.resources.dashboard.DashboardResource",
)

config.add_route("api.dashboard.courses", "/api/dashboard/courses")

config.add_route(
"api.dashboard.organizations.courses",
"/api/dashboard/organizations/{organization_public_id}/courses",
Expand Down
26 changes: 20 additions & 6 deletions lms/services/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,19 +140,33 @@ def search( # noqa: PLR0913, PLR0917
h_userid=h_userid,
).all()

def get_organization_courses(
self, organization: Organization, h_userid: str | None
def get_courses(
self,
h_userid: str | None,
organization: Organization | None = None,
):
"""Get a list of unique courses.
:param organization: organization the courses belong to.
:param h_userid: only courses this user has access to.
"""
courses_query = self._search_query(
organization_ids=[organization.id],
organization_ids=[organization.id] if organization else None,
h_userid=h_userid,
limit=None,
)
return (
# Deduplicate courses by authority_provided_id, take the last updated one

# Deduplicate courses by authority_provided_id, take the last updated one
deduplicated_courses = select(Course.id).select_from(
courses_query.distinct(Course.authority_provided_id)
.order_by(Course.authority_provided_id, Course.updated.desc())
.all()
.subquery()
)

return (
self._db.query(Course)
.filter(Course.id.in_(deduplicated_courses))
.order_by(Course.lms_name, Course.id)
)

def _deduplicated_course_assigments_query(self, courses: list[Course]):
Expand Down
77 changes: 73 additions & 4 deletions lms/views/dashboard/api/course.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import base64
import json

from pyramid.view import view_config
from webargs import fields

from lms.js_config_types import (
AnnotationMetrics,
Expand All @@ -8,12 +12,49 @@
APICourses,
CourseMetrics,
)
from lms.models import RoleScope, RoleType
from lms.models import Course, RoleScope, RoleType
from lms.security import Permissions
from lms.services.h_api import HAPI
from lms.services.organization import OrganizationService
from lms.validation._base import PyramidRequestSchema
from lms.views.dashboard.base import get_request_course, get_request_organization

PAGINATION_LIMIT = 100
"""Maximum numbr of items to return in paginated endpoints"""


class ListCoursesSchema(PyramidRequestSchema):
location = "query"

organization = fields.Str()
"""Only list courses that belong to this organization"""

limit = fields.Integer(required=True)
"""Maximum number of items to return."""

cursor = fields.Str()
"""Position to return the elements from. This correponds to the `next` value in the repsonse."""


def _next_parameter(courses: list[Course]) -> str:
"""Build the `next` parameter for a list of courses.
We'll sort courses by both name and id, expose both as a tuple.
"""
last_element = courses[-1]

cursor_data = (last_element.lms_name, last_element.id)
return (
base64.urlsafe_b64encode(json.dumps(cursor_data).encode("utf-8"))
.decode("utf-8")
.replace("=", "")
)


def _get_cursor_values(coursor: str) -> tuple[str, int]:
"""Decode the values from a next cursor."""
return json.loads(base64.urlsafe_b64decode(coursor + "==="))


class CourseViews:
def __init__(self, request) -> None:
Expand All @@ -22,16 +63,44 @@ def __init__(self, request) -> None:
self.h_api = request.find_service(HAPI)
self.organization_service = request.find_service(OrganizationService)

@view_config(
route_name="api.dashboard.courses",
request_method="GET",
renderer="json",
permission=Permissions.DASHBOARD_VIEW,
schema=ListCoursesSchema,
)
def courses(self) -> APICourses:
courses = self.course_service.get_courses(
h_userid=self.request.user.h_userid if self.request.user else None,
)

limit = min(PAGINATION_LIMIT, self.request.parsed_params["limit"])
if cursor := self.request.parsed_params.get("cursor"):
cursor_course_name, cursor_course_id = _get_cursor_values(cursor)
courses = courses.filter(
(Course.lms_name, Course.id) > (cursor_course_name, cursor_course_id)
)

courses = courses.limit(limit).all()
return {
"courses": [
APICourse(id=course.id, title=course.lms_name) for course in courses
],
"next": _next_parameter(courses),
}

@view_config(
route_name="api.dashboard.organizations.courses",
request_method="GET",
renderer="json",
permission=Permissions.DASHBOARD_VIEW,
)
def organization_courses(self) -> APICourses:
def organization_courses_metrics(self) -> APICourses:
org = get_request_organization(self.request, self.organization_service)
courses = self.course_service.get_organization_courses(
org, h_userid=self.request.user.h_userid if self.request.user else None
courses = self.course_service.get_courses(
organization=org,
h_userid=self.request.user.h_userid if self.request.user else None,
)
courses_assignment_counts = self.course_service.get_courses_assignments_count(
courses
Expand Down

0 comments on commit 3311ae6

Please sign in to comment.