Skip to content

Commit

Permalink
Extend staff access to organizations to all endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
marcospri committed Jul 31, 2024
1 parent 57efd80 commit a9a78b3
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 161 deletions.
43 changes: 24 additions & 19 deletions lms/services/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(
self._course_service = course_service
self._organization_service = organization_service

def get_request_assignment(self, request, admin_organizations: list[Organization]):
def get_request_assignment(self, request):
"""Get and authorize an assignment for the given request."""
assigment_id = request.matchdict.get(
"assignment_id"
Expand All @@ -30,6 +30,7 @@ def get_request_assignment(self, request, admin_organizations: list[Organization
# STAFF members in our admin pages can access all assignments
return assignment

admin_organizations = self.get_request_admin_organizations(request)
if (
admin_organizations
and assignment.course.application_instance.organization
Expand All @@ -43,7 +44,7 @@ def get_request_assignment(self, request, admin_organizations: list[Organization

return assignment

def get_request_course(self, request, admin_organizations: list[Organization]):
def get_request_course(self, request):
"""Get and authorize a course for the given request."""
course = self._course_service.get_by_id(request.matchdict["course_id"])
if not course:
Expand All @@ -53,6 +54,7 @@ def get_request_course(self, request, admin_organizations: list[Organization]):
# STAFF members in our admin pages can access all courses
return course

admin_organizations = self.get_request_admin_organizations(request)
if (
admin_organizations
and course.application_instance.organization in admin_organizations
Expand All @@ -65,23 +67,6 @@ def get_request_course(self, request, admin_organizations: list[Organization]):

return course

def get_request_organization(self, request) -> Organization | None:
"""Get and authorize an organization."""
if not request.has_permission(Permissions.STAFF):
# For now only staff request are scoped to an organization
return None

request_public_id = request.parsed_params.get("public_id")
if not request_public_id:
# Only relevant for request that include the query parameter
return None

organization = self._organization_service.get_by_public_id(request_public_id)
if not organization:
raise HTTPNotFound()

return organization

def get_organizations_by_admin_email(self, email: str) -> list[Organization]:
"""Get a list of organizations where the user with email `email` is an admin in."""
return self._db.scalars(
Expand All @@ -105,6 +90,26 @@ def delete_dashboard_admin(self, dashboard_admin_id: int) -> None:
"""Delete an existing dashboard admin."""
self._db.query(DashboardAdmin).filter_by(id=dashboard_admin_id).delete()

def get_request_admin_organizations(self, request) -> list[Organization]:
"""Get the organization the current user is an admin in."""
if request.has_permission(Permissions.STAFF) and (
request_public_id := request.params.get("public_id")
):
# We handle permissions and filtering specially for staff members
# If the request contains a filter for one organization, we will proceed as if the staff member
# is an admin in that organization. That will provide access to its data and filter by it
organization = self._organization_service.get_by_public_id(
request_public_id
)
if not organization:
raise HTTPNotFound()

return [organization]

return self.get_organizations_by_admin_email(
request.lti_user.email if request.lti_user else request.identity.userid
)


def factory(_context, request):
return DashboardService(
Expand Down
33 changes: 19 additions & 14 deletions lms/views/dashboard/api/assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class ListAssignmentsSchema(PaginationParametersMixin):
h_userids = fields.List(fields.Str(), data_key="h_userid")
"""Return metrics for these users only."""

public_id = fields.Str()
"""Return only the assignments which belong to this organization. For staff member only."""


class AssignmentsMetricsSchema(PyramidRequestSchema):
"""Query parameters to fetch metrics for assignments."""
Expand All @@ -38,6 +41,9 @@ class AssignmentsMetricsSchema(PyramidRequestSchema):
assignment_ids = fields.List(fields.Integer(), data_key="assignment_id")
"""Return metrics for these assignments only."""

public_id = fields.Str()
"""Return only the assignments which belong to this organization. For staff member only."""


class AssignmentViews:
def __init__(self, request) -> None:
Expand All @@ -48,12 +54,6 @@ def __init__(self, request) -> None:
self.course_service = request.find_service(name="course")
self.user_service: UserService = request.find_service(UserService)

self.admin_organizations = (
self.dashboard_service.get_organizations_by_admin_email(
request.lti_user.email if request.lti_user else request.identity.userid
)
)

@view_config(
route_name="api.dashboard.assignments",
request_method="GET",
Expand All @@ -63,8 +63,12 @@ def __init__(self, request) -> None:
)
def assignments(self) -> APIAssignments:
filter_by_h_userids = self.request.parsed_params.get("h_userids")
admin_organizations = self.dashboard_service.get_request_admin_organizations(
self.request
)

assignments = self.assignment_service.get_assignments(
admin_organization_ids=[org.id for org in self.admin_organizations],
admin_organization_ids=[org.id for org in admin_organizations],
instructor_h_userid=self.request.user.h_userid
if self.request.user
else None,
Expand All @@ -89,9 +93,7 @@ def assignments(self) -> APIAssignments:
permission=Permissions.DASHBOARD_VIEW,
)
def assignment(self) -> APIAssignment:
assignment = self.dashboard_service.get_request_assignment(
self.request, self.admin_organizations
)
assignment = self.dashboard_service.get_request_assignment(self.request)
return APIAssignment(
id=assignment.id,
title=assignment.title,
Expand All @@ -106,24 +108,27 @@ def assignment(self) -> APIAssignment:
schema=AssignmentsMetricsSchema,
)
def course_assignments_metrics(self) -> APIAssignments:
# pylint:disable=too-many-locals
current_h_userid = self.request.user.h_userid if self.request.user else None
filter_by_h_userids = self.request.parsed_params.get("h_userids")
filter_by_assignment_ids = self.request.parsed_params.get("assignment_ids")
course = self.dashboard_service.get_request_course(
self.request, self.admin_organizations
admin_organizations = self.dashboard_service.get_request_admin_organizations(
self.request
)

course = self.dashboard_service.get_request_course(self.request)
course_students = self.request.db.scalars(
self.user_service.get_users(
course_ids=[course.id],
role_scope=RoleScope.COURSE,
role_type=RoleType.LEARNER,
instructor_h_userid=current_h_userid,
admin_organization_ids=[org.id for org in self.admin_organizations],
admin_organization_ids=[org.id for org in admin_organizations],
h_userids=filter_by_h_userids,
)
).all()
assignments_query = self.assignment_service.get_assignments(
admin_organization_ids=[org.id for org in self.admin_organizations],
admin_organization_ids=[org.id for org in admin_organizations],
course_ids=[course.id],
instructor_h_userid=current_h_userid,
h_userids=filter_by_h_userids,
Expand Down
35 changes: 11 additions & 24 deletions lms/views/dashboard/api/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ListCoursesSchema(PaginationParametersMixin):
"""Return only the courses to which these assigments belong."""

public_id = fields.Str()
"""Return only the courses which belong to this organization."""
"""Return only the courses which belong to this organization. For staff member only."""


class CoursesMetricsSchema(PyramidRequestSchema):
Expand All @@ -38,7 +38,7 @@ class CoursesMetricsSchema(PyramidRequestSchema):
"""Return metrics for these courses only."""

public_id = fields.Str()
"""Return only the courses which belong to this organization."""
"""Return only the courses which belong to this organization. For staff member only."""


class CourseViews:
Expand All @@ -60,9 +60,12 @@ def __init__(self, request) -> None:
def courses(self) -> APICourses:
filter_by_h_userids = self.request.parsed_params.get("h_userids")
filter_by_assignment_ids = self.request.parsed_params.get("assignment_ids")
admin_organizations = self.dashboard_service.get_request_admin_organizations(
self.request
)

courses = self.course_service.get_courses(
admin_organization_ids=[org.id for org in self._admin_organizations],
admin_organization_ids=[org.id for org in admin_organizations],
instructor_h_userid=self.request.user.h_userid
if self.request.user
else None,
Expand Down Expand Up @@ -90,9 +93,12 @@ def courses_metrics(self) -> APICourses:
filter_by_h_userids = self.request.parsed_params.get("h_userids")
filter_by_assignment_ids = self.request.parsed_params.get("assignment_ids")
filter_by_course_ids = self.request.parsed_params.get("course_ids")
admin_organizations = self.dashboard_service.get_request_admin_organizations(
self.request
)

courses_query = self.course_service.get_courses(
admin_organization_ids=[org.id for org in self._admin_organizations],
admin_organization_ids=[org.id for org in admin_organizations],
instructor_h_userid=self.request.user.h_userid
if self.request.user
else None,
Expand Down Expand Up @@ -130,27 +136,8 @@ def courses_metrics(self) -> APICourses:
permission=Permissions.DASHBOARD_VIEW,
)
def course(self) -> APICourse:
course = self.dashboard_service.get_request_course(
self.request, self._admin_organizations
)
course = self.dashboard_service.get_request_course(self.request)
return {
"id": course.id,
"title": course.lms_name,
}

@property
def _admin_organizations(self):
request_organization = self.dashboard_service.get_request_organization(
self.request
)
if request_organization and self.request.has_permission(Permissions.STAFF):
# We special case permissions/filtering for staff members
# If the requests contains a filter for one organization will act as if the staff member
# is an admin in that organization. That will give access to its data and filter by it
return [request_organization]

return self.dashboard_service.get_organizations_by_admin_email(
self.request.lti_user.email
if self.request.lti_user
else self.request.identity.userid
)
28 changes: 17 additions & 11 deletions lms/views/dashboard/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class ListUsersSchema(PaginationParametersMixin):
)
"""Return users that belong to the assignment with these IDs."""

public_id = fields.Str()
"""Return only the users which belong to this organization. For staff member only."""


class UsersMetricsSchema(PyramidRequestSchema):
"""Query parameters to fetch metrics for users."""
Expand All @@ -39,6 +42,9 @@ class UsersMetricsSchema(PyramidRequestSchema):
h_userids = fields.List(fields.Str(), data_key="h_userid")
"""Return metrics for these users only."""

public_id = fields.Str()
"""Return only the users which belong to this organization. For staff member only."""


class UserViews:
def __init__(self, request) -> None:
Expand All @@ -48,12 +54,6 @@ def __init__(self, request) -> None:
self.h_api = request.find_service(HAPI)
self.user_service: UserService = request.find_service(UserService)

self.admin_organizations = (
self.dashboard_service.get_organizations_by_admin_email(
request.lti_user.email if request.lti_user else request.identity.userid
)
)

@view_config(
route_name="api.dashboard.students",
request_method="GET",
Expand All @@ -62,13 +62,17 @@ def __init__(self, request) -> None:
schema=ListUsersSchema,
)
def students(self) -> APIStudents:
admin_organizations = self.dashboard_service.get_request_admin_organizations(
self.request
)

students_query = self.user_service.get_users(
role_scope=RoleScope.COURSE,
role_type=RoleType.LEARNER,
instructor_h_userid=self.request.user.h_userid
if self.request.user
else None,
admin_organization_ids=[org.id for org in self.admin_organizations],
admin_organization_ids=[org.id for org in admin_organizations],
course_ids=self.request.parsed_params.get("course_ids"),
assignment_ids=self.request.parsed_params.get("assignment_ids"),
)
Expand Down Expand Up @@ -96,9 +100,7 @@ def students(self) -> APIStudents:
def students_metrics(self) -> APIStudents:
"""Fetch the stats for one particular assignment."""
request_h_userids = self.request.parsed_params.get("h_userids")
assignment = self.dashboard_service.get_request_assignment(
self.request, self.admin_organizations
)
assignment = self.dashboard_service.get_request_assignment(self.request)
stats = self.h_api.get_annotation_counts(
[g.authority_provided_id for g in assignment.groupings],
group_by="user",
Expand All @@ -109,6 +111,10 @@ def students_metrics(self) -> APIStudents:
stats_by_user = {s["userid"]: s for s in stats}
students: list[APIStudent] = []

admin_organizations = self.dashboard_service.get_request_admin_organizations(
self.request
)

users_query = self.user_service.get_users(
role_scope=RoleScope.COURSE,
role_type=RoleType.LEARNER,
Expand All @@ -117,7 +123,7 @@ def students_metrics(self) -> APIStudents:
instructor_h_userid=self.request.user.h_userid
if self.request.user
else None,
admin_organization_ids=[org.id for org in self.admin_organizations],
admin_organization_ids=[org.id for org in admin_organizations],
# Users the current user requested
h_userids=request_h_userids,
)
Expand Down
8 changes: 2 additions & 6 deletions lms/views/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,7 @@ def assignment_show(self):
Authenticated via the LTIUser present in a cookie making this endpoint accessible directly in the browser.
"""
assignment = self.dashboard_service.get_request_assignment(
self.request, self.admin_organizations
)
assignment = self.dashboard_service.get_request_assignment(self.request)
self.request.context.js_config.enable_dashboard_mode()
self._set_lti_user_cookie(self.request.response)
return {"title": assignment.title}
Expand All @@ -95,9 +93,7 @@ def course_show(self):
Authenticated via the LTIUser present in a cookie making this endpoint accessible directly in the browser.
"""
course = self.dashboard_service.get_request_course(
self.request, self.admin_organizations
)
course = self.dashboard_service.get_request_course(self.request)
self.request.context.js_config.enable_dashboard_mode()
self._set_lti_user_cookie(self.request.response)
return {"title": course.lms_name}
Expand Down
Loading

0 comments on commit a9a78b3

Please sign in to comment.