Skip to content

Commit

Permalink
Support flow tabbed view for displaying external resources
Browse files Browse the repository at this point in the history
  • Loading branch information
jiaqing23 authored and inducer committed Dec 4, 2024
1 parent 542e986 commit 6c75a85
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 21 deletions.
25 changes: 25 additions & 0 deletions course/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -594,13 +613,19 @@ class FlowDesc(Struct):
A list of :ref:`pages <flow-page>`. 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
description: str
rules: FlowRulesDesc
pages: list[FlowPageDesc]
groups: list[FlowPageGroupDesc]
external_resources: list[TabDesc]
notify_on_submit: list[str] | None

# }}}
Expand Down
2 changes: 2 additions & 0 deletions course/exam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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]
Expand Down
39 changes: 38 additions & 1 deletion course/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
66 changes: 66 additions & 0 deletions course/templates/course/tabbed-page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!DOCTYPE html>
{% load i18n %}
{% load static %}

<html lang="en">

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% block favicon %}{% endblock %}

<title>{% block title %}{{ relate_site_name }}{% endblock %}</title>

{% block bundle_loads %}
<script src="{% static 'bundle-base.js' %}"></script>
<script src="{% static 'bundle-base-with-markup.js' %}"></script>
{% endblock %}

<style>
html,
body {
height: 100%;
margin: 0;
}

.tab-content {
height: 100%;
display: flex;
flex-direction: column;
}

.tab-pane {
height: 100%;
}

iframe {
width: 100%;
height: 100%;
}
</style>

</head>

<ul class="nav nav-tabs" id="tab-bar" role="tablist">
{% for tab in tabs %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if forloop.first %}active{% endif %}" id="{{ tab.title }}-tab" data-bs-toggle="tab"
data-bs-target="#{{ tab.title }}-tab-pane" type="button" role="tab" aria-controls="{{ tab.title }}-tab-pane"
aria-selected="true">
{{ tab.title }}
</button>
</li>
{% endfor %}
</ul>

<div class="tab-content" id="tab-content">
{% for tab in tabs %}
<div class="tab-pane {% if forloop.first %}show active{% endif %}" id="{{ tab.title }}-tab-pane" role="tabpanel"
aria-labelledby="{{ tab.title }}-tab" tabindex="0">
<iframe src="{{ tab.url }}" frameborder="0" allowfullscreen></iframe>
</div>
{% endfor %}
</div>

</html>
40 changes: 20 additions & 20 deletions course/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions doc/flow.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ An Example
- Green
- ~CORRECT~ Yellow
external_resources:
-
title: Numpy
url: https://numpy.org/doc/
completion_text: |
# See you in class!
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions relate/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,15 @@
"/$",
course.flow.view_flow_page,
name="relate-view_flow_page"),
re_path(r"^course"
"/" + COURSE_ID_REGEX
+ "/flow-session"
"/(?P<flow_session_id>[0-9]+)"
"/(?P<page_ordinal>[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"
Expand Down

0 comments on commit 6c75a85

Please sign in to comment.