diff --git a/lms/routes.py b/lms/routes.py index 27b0a97fc1..0d3e549774 100644 --- a/lms/routes.py +++ b/lms/routes.py @@ -233,11 +233,17 @@ def includeme(config): # pylint:disable=too-many-statements config.add_route( "dashboard.launch.assignment", "/dashboard/launch/assignment/{assignment_id}" ) + config.add_route( "dashboard.assignment", "/dashboard/organization/{public_id}/assignment/{assignment_id}", factory="lms.resources.dashboard.DashboardResource", ) + config.add_route( + "dashboard.course", + "/dashboard/organization/{public_id}/course/{course_id}", + factory="lms.resources.dashboard.DashboardResource", + ) config.add_route( "dashboard.api.assignment", "/dashboard/api/assignment/{assignment_id}" diff --git a/lms/security.py b/lms/security.py index 8d5bdfb178..39444a399d 100644 --- a/lms/security.py +++ b/lms/security.py @@ -147,7 +147,7 @@ def get_policy(request: Request): return HeadersBearerTokenLTIUserPolicy() if path in {"/assignment", "/assignment/edit"} or path.startswith( - "/dashboard/launch/assignment/" + "/dashboard/launch/" ): # LTUser serialized in a from for non deep-linked assignment configuration return FormBearerTokenLTIUserPolicy() diff --git a/lms/templates/dashboard/index.html.jinja2 b/lms/templates/dashboard/index.html.jinja2 index 319b39027a..39c500d4c0 100644 --- a/lms/templates/dashboard/index.html.jinja2 +++ b/lms/templates/dashboard/index.html.jinja2 @@ -2,7 +2,7 @@ {% block title %} - {{ assignment.title }} - Hypothesis + {{ title }} - Hypothesis {% endblock %} {% block content %} diff --git a/lms/views/dashboard/views.py b/lms/views/dashboard/views.py index 2fe6141c0f..fec310074c 100644 --- a/lms/views/dashboard/views.py +++ b/lms/views/dashboard/views.py @@ -3,7 +3,7 @@ from lms.security import Permissions from lms.validation.authentication import BearerTokenSchema -from lms.views.dashboard.base import get_request_assignment +from lms.views.dashboard.base import get_request_assignment, get_request_course @forbidden_view_config( @@ -16,6 +16,11 @@ request_method="GET", renderer="lms:templates/dashboard/forbidden.html.jinja2", ) +@forbidden_view_config( + route_name="dashboard.course", + request_method="GET", + renderer="lms:templates/dashboard/forbidden.html.jinja2", +) def forbidden(_request): # pragma: no cover return {} @@ -24,6 +29,7 @@ class DashboardViews: def __init__(self, request) -> None: self.request = request self.assignment_service = request.find_service(name="assignment") + self.course_service = request.find_service(name="course") @view_config( route_name="dashboard.launch.assignment", @@ -32,7 +38,7 @@ def __init__(self, request) -> None: ) def assignment_redirect_from_launch(self): """ - Entry point to the dashboards from an LTI launch. + Entry point to the single assignment view from an LTI launch. Here we "promote" the LTILaunch token present as a form parameter to a cookie. """ @@ -62,7 +68,23 @@ def assignment_show(self): assignment = get_request_assignment(self.request, self.assignment_service) self.request.context.js_config.enable_dashboard_mode() self._set_lti_user_cookie(self.request.response) - return {"assignment": assignment} + return {"title": assignment.title} + + @view_config( + route_name="dashboard.course", + permission=Permissions.DASHBOARD_VIEW, + request_method="GET", + renderer="lms:templates/dashboard/index.html.jinja2", + ) + def course_show(self): + """Start the dashboard miniapp in the frontend. + + Authenticated via the LTIUser present in a cookie making this endpoint accessible directly in the browser. + """ + course = get_request_course(self.request, self.course_service) + self.request.context.js_config.enable_dashboard_mode() + self._set_lti_user_cookie(self.request.response) + return {"title": course.lms_name} def _set_lti_user_cookie(self, response): lti_user = self.request.lti_user diff --git a/tests/unit/lms/views/dashboard/views_test.py b/tests/unit/lms/views/dashboard/views_test.py index cf5b88d622..d7002678a8 100644 --- a/tests/unit/lms/views/dashboard/views_test.py +++ b/tests/unit/lms/views/dashboard/views_test.py @@ -9,7 +9,7 @@ from lms.resources.dashboard import DashboardResource from lms.views.dashboard.views import DashboardViews -pytestmark = pytest.mark.usefixtures("h_api", "assignment_service") +pytestmark = pytest.mark.usefixtures("h_api", "assignment_service", "course_service") # pylint:disable=protected-access @@ -54,6 +54,23 @@ def test_assignment_show( == f"authorization=TOKEN; Max-Age=86400; Path=/dashboard/organization/{organization._public_id}; expires=Tue, 02-Apr-2024 12:00:00 GMT; secure; HttpOnly" ) + @freeze_time("2024-04-01 12:00:00") + @pytest.mark.usefixtures("BearerTokenSchema") + def test_course_show(self, views, pyramid_request, course_service, organization): + context = DashboardResource(pyramid_request) + context.js_config = create_autospec(JSConfig, spec_set=True, instance=True) + pyramid_request.context = context + pyramid_request.matchdict["course_id"] = sentinel.id + + views.course_show() + + course_service.get_by_id.assert_called_once_with(sentinel.id) + pyramid_request.context.js_config.enable_dashboard_mode.assert_called_once() + assert ( + pyramid_request.response.headers["Set-Cookie"] + == f"authorization=TOKEN; Max-Age=86400; Path=/dashboard/organization/{organization._public_id}; expires=Tue, 02-Apr-2024 12:00:00 GMT; secure; HttpOnly" + ) + def test_assignment_show_with_no_lti_user( self, views, pyramid_request, assignment_service ):