Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions bases/rsptx/assignment_server_api/routers/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
get_jinja_templates,
get_shared_templates,
)
from rsptx.templates.ebook_config import ebook_config_override
from rsptx.response_helpers import construct_course_url, safe_join
from rsptx.response_helpers.core import (
make_json_response,
Expand Down Expand Up @@ -214,6 +215,15 @@ def sort_key(assignment):
origin=course_markup_system,
**course_attrs,
)
if use_pretext_student_pages:
context["ebook_config_override"] = await ebook_config_override(
course=course,
user=user,
user_is_instructor=user_is_instructor,
course_attrs=course_attrs,
readings=None, # force bookfuncs to not render assignment tracker
activities={},
)
return templates.TemplateResponse(
"assignment/student/chooseAssignment.html", context
)
Expand Down Expand Up @@ -1096,6 +1106,15 @@ async def doAssignment(
allow_pairs="true" if course.allow_pairs else "false",
**course_attrs,
)
if use_pretext_student_pages:
context["ebook_config_override"] = await ebook_config_override(
course=course,
user=user,
user_is_instructor=user_is_instructor,
course_attrs=course_attrs,
readings=None, # force bookfuncs to not render assignment tracker
activities={},
)
response = templates.TemplateResponse(
"assignment/student/doAssignment.html", context
)
Expand Down
10 changes: 10 additions & 0 deletions bases/rsptx/book_server_api/routers/books.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from rsptx.db.models import UseinfoValidation
from rsptx.auth.session import is_instructor
from rsptx.templates import template_folder
from rsptx.templates.ebook_config import ebook_config_override
from rsptx.response_helpers import safe_join
from rsptx.response_helpers.core import canonical_utcnow
from typing_extensions import Annotated
Expand Down Expand Up @@ -677,6 +678,15 @@ async def serve_page(
worker_name=worker_name,
appname="runestone", # for peer+ links
course_attrs=course_attrs,
ebook_config_override=await ebook_config_override(
course=course_row,
user=user,
user_is_instructor=user_is_instructor,
course_attrs=course_attrs,
readings=reading_list,
activities=activity_info,
use_services=use_services,
),
**course_attrs,
)
# See `templates <https://fastapi.tiangolo.com/advanced/templates/>`_.
Expand Down
66 changes: 37 additions & 29 deletions bases/rsptx/book_server_api/routers/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from rsptx.auth.session import auth_manager
from rsptx.templates import get_jinja_templates, get_shared_templates
from rsptx.templates.ebook_config import ebook_config_override
from rsptx.db.crud import (
fetch_assignments,
fetch_all_assignment_stats,
Expand Down Expand Up @@ -152,35 +153,42 @@ def sort_key(assignment):
else:
templates = get_shared_templates()

return templates.TemplateResponse(
"book/course/current_course.html",
{
"assignment_list": assignments,
"stats": stats,
"course": course,
"is_old_course": course.term_start_date
< (datetime.datetime.now() - datetime.timedelta(weeks=52)).date(),
"user": user,
"request": request,
"institution": course.institution,
"instructor_list": instructors,
"base_course": course.base_course,
"origin": course_markup_system,
"book": book,
"lastPageUrl": last_page_url,
"student_page": True,
"course_list": course_list,
"is_instructor": "true" if user_is_instructor else "false",
"has_discussion_group": book.social_url if book else None,
"lti1p1": is_lti1p1_course,
"now": now,
"visibility_map": visibility_map,
"course_attrs": attrs,
"course_name": course.course_name,
"base_url": construct_course_url(course),
"settings": settings,
},
)
context = {
"assignment_list": assignments,
"stats": stats,
"course": course,
"is_old_course": course.term_start_date
< (datetime.datetime.now() - datetime.timedelta(weeks=52)).date(),
"user": user,
"request": request,
"institution": course.institution,
"instructor_list": instructors,
"base_course": course.base_course,
"origin": course_markup_system,
"book": book,
"lastPageUrl": last_page_url,
"student_page": True,
"course_list": course_list,
"is_instructor": "true" if user_is_instructor else "false",
"has_discussion_group": book.social_url if book else None,
"lti1p1": is_lti1p1_course,
"now": now,
"visibility_map": visibility_map,
"course_attrs": attrs,
"course_name": course.course_name,
"base_url": construct_course_url(course),
"settings": settings,
}
if use_pretext_student_pages:
context["ebook_config_override"] = await ebook_config_override(
course=course,
user=user,
user_is_instructor=user_is_instructor,
course_attrs=attrs,
readings=[],
activities={},
)
return templates.TemplateResponse("book/course/current_course.html", context)


@router.api_route("/changeCourse/{course_name:str}", methods=["GET", "POST"])
Expand Down
22 changes: 21 additions & 1 deletion bases/rsptx/interactives/runestone/common/js/bookfuncs.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@
import RunestoneBase from "./runestonebase.js";
import { marked } from "marked";

// ---------------------------------------------------------------------------
// Server-supplied eBookConfig values (issue #1392)
//
// A PreTeXt page served by a Runestone server sets ``ebook_config_override``
// right after the book template's own eBookConfig assignments. The server is
// the authority for those values (course, user, activities, ...), so they win
// over whatever the template wrote. This runs when the module is evaluated,
// before anything else reads eBookConfig.
// ---------------------------------------------------------------------------
export function applyEbookConfigOverride() {
const override = window.ebook_config_override;
if (override && typeof override === "object") {
window.eBookConfig = Object.assign(window.eBookConfig || {}, override);
}
}
applyEbookConfigOverride();

var rb = new RunestoneBase();

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1701,14 +1718,17 @@ window.addEventListener("DOMContentLoaded", function (event) {
let authorUrl =
(eBookConfig.authorServerUrl ||
"https://author.runestone.academy/author") + "/";
let editorUrl =
eBookConfig.editorServerUrl || "/admin/editor/manage_exercises";

if (eBookConfig.isAuthor) {
menuContentArea.appendChild(
makeLink(authorUrl, "Author Dashboard"),
);
}
if (eBookConfig.isEditor) {
menuContentArea.appendChild(
makeLink(authorUrl, "Editor Dashboard"),
makeLink(editorUrl, "Editor Dashboard"),
);
}
menuContentArea.appendChild(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Tests for merging the server's ``ebook_config_override`` into eBookConfig
* (issue #1392). The book server injects the override after the PreTeXt
* template's own eBookConfig assignments; its values must win.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";

import { applyEbookConfigOverride } from "../js/bookfuncs.js";

describe("applyEbookConfigOverride", () => {
let saved;

beforeEach(() => {
saved = window.eBookConfig;
delete window.ebook_config_override;
});

afterEach(() => {
window.eBookConfig = saved;
delete window.ebook_config_override;
});

it("overrides template values and keeps the ones it doesn't name", () => {
window.eBookConfig = {
course: "from_template",
isLoggedIn: false,
enableScratchAC: true,
};
window.ebook_config_override = {
course: "from_server",
isLoggedIn: true,
activities: { page: 0 },
};
applyEbookConfigOverride();
expect(window.eBookConfig).toEqual({
course: "from_server",
isLoggedIn: true,
enableScratchAC: true,
activities: { page: 0 },
});
});

it("extends the existing object rather than replacing it", () => {
const config = { course: "c" };
window.eBookConfig = config;
window.ebook_config_override = { username: "u" };
applyEbookConfigOverride();
expect(window.eBookConfig).toBe(config);
expect(config.username).toBe("u");
});

it("leaves eBookConfig alone when there is no override", () => {
const config = { course: "c" };
window.eBookConfig = config;
applyEbookConfigOverride();
expect(window.eBookConfig).toBe(config);
expect(config).toEqual({ course: "c" });
});

it("creates eBookConfig if the page didn't define one", () => {
window.eBookConfig = undefined;
window.ebook_config_override = { course: "c" };
applyEbookConfigOverride();
expect(window.eBookConfig).toEqual({ course: "c" });
});
});
3 changes: 3 additions & 0 deletions components/rsptx/db/crud/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ async def is_author(userid: int) -> bool:
:rtype: bool
"""
ed = await fetch_group("author")
if ed is None:
# No author group has been created in this database, so nobody is one.
return False
row = await fetch_membership(ed.id, userid)

if row:
Expand Down
68 changes: 68 additions & 0 deletions components/rsptx/templates/ebook_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# *****************************************************
# |docname| - server-supplied eBookConfig values
# *****************************************************
# A PreTeXt book's ``_base.html`` sets up ``eBookConfig`` from template
# variables, then emits ``{{ ebook_config_override | safe }}``. ``bookfuncs.js``
# merges that object into ``eBookConfig`` before anything reads it, so the
# server, not the book's template, decides the values only a Runestone server
# can know: the green items in issue #1392.
#
# enableScratchAC and acDefaultLanguage are also green there, but they are
# publisher options with no server-side source, so the book keeps them.
from typing import Any, Optional

from jinja2.utils import htmlsafe_json_dumps

from rsptx.db.crud import is_author, is_editor


async def ebook_config_override(
*,
course: Any,
user: Optional[Any],
user_is_instructor: bool,
course_attrs: dict,
readings: Optional[list],
activities: dict,
use_services: bool = True,
) -> str:
"""Return the ``ebook_config_override`` script statement for a page.

:param course: the course row being viewed
:param user: the logged-in user, or None
:param user_is_instructor: whether ``user`` instructs ``course``
:param course_attrs: the course's attributes
:param readings: the page's reading list; None (JSON ``null``) keeps
bookfuncs from drawing the assignment reading tracker
:param activities: the activity counts for the progress bar
:param use_services: False for a book page viewed in browsing mode
"""
# The Author and Editor Dashboard links only appear in the instructor menu,
# so only instructors pay for the group lookups.
if user and user_is_instructor:
user_is_author = await is_author(user.id)
user_is_editor = await is_editor(user.id)
else:
user_is_author = user_is_editor = False

ebook_attrs = dict(
useRunestoneServices=use_services,
course=course.course_name,
termStartDate=course.term_start_date.isoformat(),
basecourse=course.base_course,
isLoggedIn=user is not None,
email=user.email if user else "",
isInstructor=user_is_instructor,
logLevel=10,
username=user.username if user else "",
readings=readings,
activities=activities,
downloadsEnabled=bool(course.downloads_enabled),
allow_pairs=bool(course.allow_pairs),
course_attrs=course_attrs,
isEditor=user_is_editor,
isAuthor=user_is_author,
)
# htmlsafe_json_dumps escapes <, >, & and ' so a course attribute can't
# close the <script> tag this lands in.
return "ebook_config_override = " + str(htmlsafe_json_dumps(ebook_attrs))
Loading
Loading