From 6c75a851d861667bbfad6b15779ce724d9bbc3a6 Mon Sep 17 00:00:00 2001 From: Tan Jia Qing Date: Wed, 27 Nov 2024 15:55:30 +0000 Subject: [PATCH] Support flow tabbed view for displaying external resources --- course/content.py | 25 +++++++++ course/exam.py | 2 + course/flow.py | 39 +++++++++++++- course/templates/course/tabbed-page.html | 66 ++++++++++++++++++++++++ course/validation.py | 40 +++++++------- doc/flow.rst | 33 ++++++++++++ relate/urls.py | 9 ++++ 7 files changed, 193 insertions(+), 21 deletions(-) create mode 100644 course/templates/course/tabbed-page.html diff --git a/course/content.py b/course/content.py index ee5397d59..1ff9890ac 100644 --- a/course/content.py +++ b/course/content.py @@ -527,6 +527,25 @@ class FlowRulesDesc(Struct): # {{{ mypy: flow +class TabDesc(Struct): + """ + .. attribute:: title + + (Required) Title to be displayed on the tab. + + .. attribute:: url + + (Required) The URL of the external web page. + """ + + def __init__(self, title: str, url: str) -> None: + self.title = title + self.url = url + + title: str + url: str + + class FlowPageDesc(Struct): id: str type: str @@ -594,6 +613,11 @@ class FlowDesc(Struct): A list of :ref:`pages `. If you specify this, a single :class:`FlowPageGroupDesc` will be implicitly created. Exactly one of :attr:`groups` or :class:`pages` must be given. + + .. attribute:: external_resources + + A list of :class:`TabDesc`. These are links to external + resources that are displayed as tabs on the flow tabbed page. """ title: str @@ -601,6 +625,7 @@ class FlowDesc(Struct): rules: FlowRulesDesc pages: list[FlowPageDesc] groups: list[FlowPageGroupDesc] + external_resources: list[TabDesc] notify_on_submit: list[str] | None # }}} diff --git a/course/exam.py b/course/exam.py index e4452292f..5295bb4cb 100644 --- a/course/exam.py +++ b/course/exam.py @@ -837,6 +837,7 @@ def __call__(self, request): update_expiration_mode, update_page_bookmark_state, view_flow_page, + view_flow_page_with_ext_resource_tabs, view_resume_flow, view_start_flow, ) @@ -868,6 +869,7 @@ def __call__(self, request): resolver_match.func in [ view_resume_flow, view_flow_page, + view_flow_page_with_ext_resource_tabs, update_expiration_mode, update_page_bookmark_state, finish_flow_session_view] diff --git a/course/flow.py b/course/flow.py index cbc3a8eeb..39e65d0da 100644 --- a/course/flow.py +++ b/course/flow.py @@ -56,7 +56,7 @@ is_expiration_mode_allowed, participation_permission as pperm, ) -from course.content import FlowPageDesc +from course.content import FlowPageDesc, TabDesc from course.exam import get_login_exam_ticket from course.models import ( Course, @@ -2107,6 +2107,43 @@ def view_flow_page( # }}} +@course_view +def view_flow_page_with_ext_resource_tabs( + pctx: CoursePageContext, + flow_session_id: int, + page_ordinal: int) -> http.HttpResponse: + request = pctx.request + + flow_session_id = int(flow_session_id) + flow_session = get_and_check_flow_session(pctx, flow_session_id) + + assert flow_session is not None + + flow_id = flow_session.flow_id + + adjust_flow_session_page_data(pctx.repo, flow_session, pctx.course.identifier, + respect_preview=True) + + fctx = FlowContext(pctx.repo, pctx.course, flow_id, + participation=pctx.participation) + + assert fctx.flow_desc.external_resources is not None + + target_url = reverse( + "relate-view_flow_page", + args=[pctx.course.identifier, flow_session_id, page_ordinal], + ) + + return render( + request, + "course/tabbed-page.html", + {"tabs": [ + TabDesc(str(_("Relate")), target_url), + *fctx.flow_desc.external_resources + ]}, + ) + + @course_view def get_prev_answer_visits_dropdown_content( pctx, flow_session_id, page_ordinal, prev_visit_id): diff --git a/course/templates/course/tabbed-page.html b/course/templates/course/tabbed-page.html new file mode 100644 index 000000000..2c2bd87e7 --- /dev/null +++ b/course/templates/course/tabbed-page.html @@ -0,0 +1,66 @@ + +{% load i18n %} +{% load static %} + + + + + + + + {% block favicon %}{% endblock %} + + {% block title %}{{ relate_site_name }}{% endblock %} + + {% block bundle_loads %} + + + {% endblock %} + + + + + + + +
+ {% for tab in tabs %} +
+ +
+ {% endfor %} +
+ + \ No newline at end of file diff --git a/course/validation.py b/course/validation.py index 39f659f77..87554bedf 100644 --- a/course/validation.py +++ b/course/validation.py @@ -1009,26 +1009,26 @@ def validate_flow_permission( def validate_flow_desc(vctx, location, flow_desc): validate_struct( - vctx, - location, - flow_desc, - required_attrs=[ - ("title", str), - ("description", "markup"), - ], - allowed_attrs=[ - ("completion_text", "markup"), - ("rules", Struct), - ("groups", list), - ("pages", list), - ("notify_on_submit", list), - - # deprecated (moved to grading rule) - ("max_points", (int, float)), - ("max_points_enforced_cap", (int, float)), - ("bonus_points", (int, float)), - ] - ) + vctx, + location, + flow_desc, + required_attrs=[ + ("title", str), + ("description", "markup"), + ], + allowed_attrs=[ + ("completion_text", "markup"), + ("rules", Struct), + ("groups", list), + ("pages", list), + ("notify_on_submit", list), + ("external_resources", list), + # deprecated (moved to grading rule) + ("max_points", (int, float)), + ("max_points_enforced_cap", (int, float)), + ("bonus_points", (int, float)), + ], + ) if hasattr(flow_desc, "rules"): validate_flow_rules(vctx, location, flow_desc.rules) diff --git a/doc/flow.rst b/doc/flow.rst index e87eed740..7c460d4d4 100644 --- a/doc/flow.rst +++ b/doc/flow.rst @@ -110,6 +110,12 @@ An Example - Green - ~CORRECT~ Yellow + external_resources: + + - + title: Numpy + url: https://numpy.org/doc/ + completion_text: | # See you in class! @@ -295,6 +301,33 @@ For example, to grant permission to revise an answer on a - change_answer value: 1 +.. _tabbed-page-view: + +Tabbed page view +^^^^^^^^^^^^^^^^^^^^ + +A flow page can be displayed in a tabbed view, where the first tab is the +flow page itself, and the subsequent tabs are additional external websites. + +An example use case is when the participant does not have access to +browser-native tab functionality. This is the case when using the +"Guardian" browser with the "ProctorU" proctoring service. + +To access the tabbed page for a flow, append `/ext-resource-tabs` to the URL. +Alternatively, you can create a link to allow users to navigate to the tabbed +page directly. For example, `[Open tabs](ext-resource-tabs)`. + +You might need to set `X_FRAME_OPTIONS` in your Django settings to allow embedding +the flow page and external websites in iframes, depending on your site's configuration. +For example, you can add the following to your `local_settings.py`: + +.. code-block:: python + + X_FRAME_OPTIONS = 'ALLOWALL' # or specify a domain like 'ALLOW-FROM https://www.yourwebsite.com' + + +.. autoclass:: TabDesc + .. _flow-life-cycle: Life cycle diff --git a/relate/urls.py b/relate/urls.py index 4d51c2b58..fe59c169d 100644 --- a/relate/urls.py +++ b/relate/urls.py @@ -395,6 +395,15 @@ "/$", course.flow.view_flow_page, name="relate-view_flow_page"), + re_path(r"^course" + "/" + COURSE_ID_REGEX + + "/flow-session" + "/(?P[0-9]+)" + "/(?P[0-9]+)" + "/ext-resource-tabs" + "/$", + course.flow.view_flow_page_with_ext_resource_tabs, + name="relate-view_flow_page_with_ext_resource_tabs"), re_path(r"^course" "/" + COURSE_ID_REGEX + "/prev_answers"