Ptx-based template for student pages #1280
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in PreTeXt (PTX)-based templating path for key student pages (course home, choose assignment, do assignment) and refactors shared utilities so student/instructor/book servers can consistently and securely build paths and published-book URLs.
Changes:
- Add PTX-based template selection (via
use_pretext_student_pages) with book-specific template override support and updated HTML/CSS for student pages. - Introduce shared helpers (
construct_course_url, sharedsafe_join) and remove duplicated implementations/usages. - Refactor assignment/course template contexts and UI structure (including accessibility-oriented tweaks like screen-reader-only text and table captioning).
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| components/rsptx/templates/staticAssets/main.css | Adds shared utility styles and adjusts heading margin overrides for non-PTX templates. |
| components/rsptx/templates/staticAssets/main-ptx-based.css | Introduces PTX-template-specific CSS variables and layout/style resets. |
| components/rsptx/templates/staticAssets/index.css | Removes assignment-related styles (moved into assignment-specific stylesheet). |
| components/rsptx/templates/staticAssets/course.css | Updates course page layout styles and scopes non-PTX styling. |
| components/rsptx/templates/staticAssets/assignment/assignment.css | New consolidated CSS for choose/do assignment pages with PTX vs non-PTX scoping. |
| components/rsptx/templates/staticAssets/assignment/assignment_block.css | New PTX-scoped styling for assignment tables on course/chooser pages. |
| components/rsptx/templates/core.py | Adds get_jinja_templates with a ChoiceLoader for book override + shared templates. |
| components/rsptx/templates/common/static_assets.html | Makes static asset inclusion conditional for PTX-based templates to avoid duplication. |
| components/rsptx/templates/common/static_assets_min.html | Removes PTX add-on CSS include from the “min” asset bundle. |
| components/rsptx/templates/book/course/current_course.html | Restructures course home page markup and hooks in new CSS/assets. |
| components/rsptx/templates/author/editlibrary.html | Moves with_errors macro usage locally into this template. |
| components/rsptx/templates/assignment/student/doAssignment.html | Refactors do-assignment page layout, readings display, and question UI. |
| components/rsptx/templates/assignment/student/chooseAssignment.html | Refactors chooser page markup and switches to assignment-specific CSS. |
| components/rsptx/templates/assignment/student/assignment_block.html | Updates assignment table markup to improve semantics/accessibility and styling hooks. |
| components/rsptx/templates/admin/instructor/course_settings.html | Exposes the use_pretext_student_pages toggle in instructor settings UI. |
| components/rsptx/templates/_base.html | Removes global with_errors macro from base template. |
| components/rsptx/templates/init.py | Exports get_jinja_templates from the templates package. |
| components/rsptx/response_helpers/core.py | Adds construct_course_url and centralizes safe_join implementation. |
| components/rsptx/response_helpers/init.py | Re-exports construct_course_url and safe_join. |
| components/rsptx/db/crud/question.py | Expands fetch_assignment_questions query to include Chapter/SubChapter metadata. |
| bases/rsptx/book_server_api/routers/course.py | Selects template loader based on course attribute and updates template context fields. |
| bases/rsptx/book_server_api/routers/books.py | Removes local safe_join and uses shared helper. |
| bases/rsptx/assignment_server_api/routers/student.py | Adds PTX template selection and refactors template context for chooser/doAssignment. |
| bases/rsptx/assignment_server_api/routers/instructor.py | Switches to construct_course_url for consistent asset URL rewriting. |
| bases/rsptx/admin_server_api/routers/instructor.py | Exposes use_pretext_student_pages in settings payload and updates assignment copying unpacking. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I'll check these out and make updates. |
|
The merge conflicts are easy to resolve, I had updated the links on the course home page for instructors so they were all pointing at the new servers. And I had removed the link to the practice tool. |
91bf115 to
9c9de28
Compare
9c9de28 to
39045a9
Compare
| "student_page": True, | ||
| "course_list": course_list, | ||
| "is_instructor": user_is_instructor, | ||
| "has_discussion_group": any([book.social_url for book in books]), | ||
| "is_instructor": "true" if user_is_instructor else "false", | ||
| "has_discussion_group": book.social_url, | ||
| "lti1p1": is_lti1p1_course, |
| {% if is_instructor %} | ||
| <label style="margin-bottom:10px; display:block;"> | ||
| <input type="checkbox" id="hide_hidden_assignments" onchange="toggleHiddenAssignments(this.checked)"> | ||
| Student View: Hide Hidden Assignments | ||
| <span for="hide_hidden_assignments"><strong>Student View</strong> (Hide Hidden Assignments)</span> | ||
| </label> |
| /* Everything in this construct should only be applied when not | ||
| using the pretext template. | ||
| :where forces the selector to have zero specificity, so it doesn't | ||
| make nested selectors more specific than they need to be | ||
| */ | ||
| :where(body:not(.ptx-runestone-template)) { | ||
| .course_page { | ||
| max-width: 90%; | ||
| margin-left: auto; | ||
| margin-right: auto; | ||
| } | ||
| } |
| :where(body:not(.ptx-runestone-template)) { | ||
| .assignment-title { | ||
| text-align: center; | ||
| } | ||
|
|
| /* This should only be applied when using the pretext template. | ||
| :where forces low specificity */ | ||
| :where(body:is(.ptx-runestone-template)) { | ||
| .rs-assignment-table { | ||
| width: 100%; |
39045a9 to
1d495cd
Compare
|
Rebased to current main/HEAD with revisions to address all of CoPilot's issues. |
|
Do you want to run black on components/rsptx to clean up the lint errors? I think there are 4 files. |
1d495cd to
b790e99
Compare
|
Sorry, looks like Black was busted in my VSCode. |
| "is_instructor": "true" if user_is_instructor else "false", | ||
| "has_discussion_group": book.social_url, |
| {% if is_instructor %} | ||
| <label style="margin-bottom:10px; display:block;"> | ||
| <input type="checkbox" id="hide_hidden_assignments" onchange="toggleHiddenAssignments(this.checked)"> | ||
| Student View: Hide Hidden Assignments | ||
| <span for="hide_hidden_assignments"><strong>Student View</strong> (Hide Hidden Assignments)</span> | ||
| </label> |
| is_logged_in="true", | ||
| is_instructor="true" if user_is_instructor else "false", | ||
| student_page=True, |
| is_logged_in="true", | ||
| is_instructor="true" if user_is_instructor else "false", | ||
| needs_manual_grading=needs_manual_grading, | ||
| student_page="true", |
| <li class="{{ readings_full[reading]['status']}}"><strong> Chapter {{ readings_full[reading]['chapter_num'] }} {{ readings_full[reading]['chapter_name'] }}:</strong></li> | ||
| {% else: %} | ||
| <li class="{{ readings_full[reading]['status']}}"><a href="/ns/books/published/{{course.course_name}}/{{readings_full[reading]['chapter_label']}}.html">Chapter {{ readings_full[reading]['chapter_num'] }} {{ readings_full[reading]['chapter_name'] }}:</a></li> | ||
| {% endif %} | ||
| <ul class="assignment-readings-subchapters"> |
| <h2 class="heading">Assignments</h2> | ||
| {% if lti1p1 %} | ||
| <h3>You must launch assignments through your LMS for this course</h3> | ||
| <p class="heading">You must launch assignments through your LMS for this course</p> |
| :where(body:not(.ptx-runestone-template)) { | ||
| .course_page { | ||
| max-width: 90%; | ||
| margin-left: auto; | ||
| margin-right: auto; | ||
| } | ||
| } |
| :where(body:not(.ptx-runestone-template)) { | ||
| .assignment-title { | ||
| text-align: center; | ||
| } | ||
|
|
||
| /*id for specificity in title*/ | ||
| #assignment_name.assignment-title, | ||
| #questions, | ||
| .assignment-banner, | ||
| .assignment-page, | ||
| .assignment-meta, | ||
| .assignment-readings { | ||
| max-width: 90%; | ||
| margin-left: auto; | ||
| margin-right: auto; | ||
| } | ||
|
|
||
| #questions { | ||
| .ac_section > * { | ||
| max-width: 100%; | ||
| } | ||
| } | ||
|
|
b790e99 to
6f6f8e6
Compare
|
Fixed two real issues (one new, one missed). The is_instructor type is inherently messy but hopefully can go away with eBookConfig refactor |
| "student_page": True, | ||
| "course_list": course_list, | ||
| "is_instructor": user_is_instructor, | ||
| "has_discussion_group": any([book.social_url for book in books]), | ||
| "is_instructor": "true" if user_is_instructor else "false", | ||
| "has_discussion_group": book.social_url, | ||
| "lti1p1": is_lti1p1_course, |
| select(Question, AssignmentQuestion, Chapter, SubChapter) | ||
| .join(Question, AssignmentQuestion.question_id == Question.id) | ||
| .join( | ||
| Chapter, | ||
| (Question.chapter == Chapter.chapter_label) |
| <!-- ptx-based templates include these already --> | ||
| {% if using_ptx_base != "true" %} | ||
| {% for css_import in wp_imports["css"] %} | ||
| <link rel="stylesheet" type="text/css" href="/ns/books/published/{{course.course_name}}/_static/{{css_import}}" /> | ||
| {% endfor %} | ||
| {% for js_import in wp_imports["js"] %} | ||
| <script src='/ns/books/published/{{course.course_name}}/_static/{{js_import}}'></script> | ||
| {% endfor %} |
| </section> | ||
| {% endif %} | ||
| </div> | ||
|
|
||
| {% if not released and not assignment.is_timed %} | ||
| <div style="text-align:center"> | ||
| <button class="btn btn-lg btn-warning buttonAskCompletion" id="gradeMeButton">Compute Score for Assignment</button> | ||
| <p><strong>Warning:</strong> Scores for problems that you self grade are unofficial. | ||
| Some problems will need to be manually graded, and your instructor may have requirements that cannot be autograded. | ||
| No deadlines are enforced when self grading, but your instructor may penalize you | ||
| for late work.</p> | ||
| </div> | ||
|
|
||
| {% endif %} | ||
| </div> | ||
| </div> | ||
| </section> |
| <label style="margin-bottom:10px; display:block;"> | ||
| <input type="checkbox" id="hide_hidden_assignments" onchange="toggleHiddenAssignments(this.checked)"> | ||
| Student View: Hide Hidden Assignments | ||
| <span for="hide_hidden_assignments"><strong>Student View</strong> (Hide Hidden Assignments)</span> | ||
| </label> |
| :where(body:not(.ptx-runestone-template)) { | ||
| .assignment-title { | ||
| text-align: center; | ||
| } | ||
|
|
||
| /*id for specificity in title*/ | ||
| #assignment_name.assignment-title, | ||
| #questions, | ||
| .assignment-banner, | ||
| .assignment-page, | ||
| .assignment-meta, | ||
| .assignment-readings { | ||
| max-width: 90%; | ||
| margin-left: auto; | ||
| margin-right: auto; | ||
| } |
| #questions { | ||
| .ac_section > * { | ||
| max-width: 100%; | ||
| } | ||
| } | ||
|
|
| .questionblock { | ||
| background-color: #ddd; | ||
| padding: 1rem; | ||
| margin-bottom: 1rem; | ||
| border-radius: 10px; | ||
| } | ||
|
|
||
| .metaquestion { | ||
| display: flex; | ||
| flex-direction: row; | ||
| flex-wrap: wrap; | ||
| width: 100%; | ||
| } | ||
| } |
| :where(body:is(.ptx-runestone-template)) { | ||
| .rs-assignment-table { | ||
| width: 100%; | ||
| border-collapse: collapse; | ||
| margin-bottom: 1rem; | ||
| } | ||
|
|
||
| .rs-assignment-table thead { | ||
| border-bottom: 2px solid var(--rs-table-border, #888); | ||
| } | ||
|
|
||
| .rs-assignment-table th { | ||
| border-bottom: 2px solid var(--rs-table-border, #888); | ||
| padding-top: 0.5rem; | ||
| padding-bottom: 0.5rem; | ||
| text-align: start; | ||
| font-size: 100%; | ||
| } |
| .rs-assignment-table td { | ||
| border-bottom: 1px solid var(--rs-table-border, #888); | ||
| padding-top: 0.5rem; | ||
| padding-bottom: 0.5rem; | ||
| font-size: 100%; | ||
| } | ||
|
|
||
| .rs-assignment-table .ti { | ||
| font-size: 125%; | ||
| } | ||
| .rs-assignment-table a { | ||
| border: 0; | ||
| } | ||
| } |
Depends on PTX PR: PreTeXtBook/pretext#2978
It is merged but may not be in CLI yet. But, this PR is backwards safe - old books should continue to function just fine. Any book that lacks a
_base.htmlfile will fall back to the normal RS _base.html.This is a good checkpoint on this project. I have not tackled peer pages, but the three main ones (course home, doAssignment, chooseAssignment) are set up.
Updated HTML is used for these always - there are some accessibility and structure updates that are good no matter what. Some PTX styles (section headings) affect pages regardless if full template is in use.
Right now it is opt in via course setting to use the PTX based template that is built for a course. I imagine we leave at as opt in until you and others get a chance to play. Then we make it opt out.
This pull request introduces several improvements and refactorings across the instructor, student, and book server APIs, focusing on enhancing template selection based on course settings, improving path handling, and unifying URL construction. The changes also bring more consistency to the handling of course and assignment attributes in context objects passed to templates, and clean up some legacy or duplicated code.
Template and Path Handling Improvements:
use_pretext_student_pagescourse attribute, allowing for PreTeXt-based student pages when enabled. Thesafe_joinhelper is used for secure path construction. ([[1]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072R107-R111),[[2]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072R174-R212),[[3]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072R926-R980))safe_joinimplementation is removed frombook_server_api/routers/books.py, and the shared helper fromrsptx.response_helpersis used instead, reducing code duplication and improving maintainability. ([[1]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dfad0b60c2f19f7a29315ea3c1a4440a2d4e18e3f75a6a2072032c99192b39e8R61),[[2]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dfad0b60c2f19f7a29315ea3c1a4440a2d4e18e3f75a6a2072032c99192b39e8L526-L546))Context and Attribute Consistency:
[[1]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072R174-R212),[[2]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072L738-R799),[[3]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072R926-R980))[bases/rsptx/assignment_server_api/routers/student.pyL738-R799](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072L738-R799))URL Construction and Static Asset Handling:
get_course_urlfunction are replaced with the newconstruct_course_urlhelper, ensuring consistent URL generation for static assets and external resources. ([[1]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dd60b6ed774f13b19ba90f1d48fa6b7197872eadd20abe4376603c86c782aa44R87),[[2]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dd60b6ed774f13b19ba90f1d48fa6b7197872eadd20abe4376603c86c782aa44L119-L120),[[3]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dd60b6ed774f13b19ba90f1d48fa6b7197872eadd20abe4376603c86c782aa44L187-R188),[[4]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072L691-R711))Instructor and Assignment Copying Updates:
[bases/rsptx/admin_server_api/routers/instructor.pyL1154-R1157](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-94fac4ad0473ba1f2c291e699c8e8f45914f7394132cb664da4fcdda3f6f2d20L1154-R1157))use_pretext_student_pagesattribute, allowing the admin UI to reflect and control this feature. ([bases/rsptx/admin_server_api/routers/instructor.pyR426-R428](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-94fac4ad0473ba1f2c291e699c8e8f45914f7394132cb664da4fcdda3f6f2d20R426-R428))General Refactoring and Cleanup:
get_course_urland customsafe_join) are removed for clarity and maintainability. ([[1]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dfad0b60c2f19f7a29315ea3c1a4440a2d4e18e3f75a6a2072032c99192b39e8L29),[[2]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072L566-L579))These changes set the stage for improved support of PreTeXt-based course content, more secure and consistent path handling, and a more maintainable codebase.
References:
[[1]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072R107-R111),[[2]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072R174-R212),[[3]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072R926-R980),[[4]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dfad0b60c2f19f7a29315ea3c1a4440a2d4e18e3f75a6a2072032c99192b39e8R61),[[5]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dfad0b60c2f19f7a29315ea3c1a4440a2d4e18e3f75a6a2072032c99192b39e8L526-L546))[[1]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072R174-R212),[[2]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072L738-R799),[[3]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072R926-R980))[[1]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dd60b6ed774f13b19ba90f1d48fa6b7197872eadd20abe4376603c86c782aa44R87),[[2]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dd60b6ed774f13b19ba90f1d48fa6b7197872eadd20abe4376603c86c782aa44L119-L120),[[3]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dd60b6ed774f13b19ba90f1d48fa6b7197872eadd20abe4376603c86c782aa44L187-R188),[[4]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072L691-R711))[[1]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-94fac4ad0473ba1f2c291e699c8e8f45914f7394132cb664da4fcdda3f6f2d20L1154-R1157),[[2]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-94fac4ad0473ba1f2c291e699c8e8f45914f7394132cb664da4fcdda3f6f2d20R426-R428))[[1]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-dfad0b60c2f19f7a29315ea3c1a4440a2d4e18e3f75a6a2072032c99192b39e8L29),[[2]](https://github.com/RunestoneInteractive/rs/pull/1280/files#diff-43e4ec0d538ce2326f320b85c5feba3b8bb005d8b803f44146c02f45dab57072L566-L579))