From a99c5f8b3c62f1af8722a195b3a04f74ffff1a9a Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 11:44:15 -0700 Subject: [PATCH 01/14] DB: fetch_assignment_questions returns chapter and subchapter with question --- .../rsptx/admin_server_api/routers/instructor.py | 2 +- components/rsptx/db/crud/question.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/bases/rsptx/admin_server_api/routers/instructor.py b/bases/rsptx/admin_server_api/routers/instructor.py index 07553ad6e..6226f7a28 100644 --- a/bases/rsptx/admin_server_api/routers/instructor.py +++ b/bases/rsptx/admin_server_api/routers/instructor.py @@ -1151,7 +1151,7 @@ async def _copy_one_assignment( assignment_questions = await fetch_assignment_questions(old_assignment_id) for question_data in assignment_questions: - question, assignment_question = question_data + question, assignment_question, chapter, subchapter = question_data new_assignment_question_data = AssignmentQuestionValidator( assignment_id=new_assignment.id, diff --git a/components/rsptx/db/crud/question.py b/components/rsptx/db/crud/question.py index e1875726f..61ce98bb7 100644 --- a/components/rsptx/db/crud/question.py +++ b/components/rsptx/db/crud/question.py @@ -346,17 +346,27 @@ async def fetch_assignment_question( async def fetch_assignment_questions( assignment_id: int, -) -> List[Tuple[Question, AssignmentQuestion]]: +) -> List[Tuple[Question, AssignmentQuestion, Chapter, SubChapter]]: """ Retrieve the AssignmentQuestion entry for the given assignment_name and question_name. :param assignment_name: str, the name of the assignment :param question_name: str, the name (div_id) of the question - :return: AssignmentQuestionValidator, the AssignmentQuestionValidator object + :return: AssignmentQuestionValidator, AssignmentQuestionValidator, ChapterValidator, SubChapterValidator """ query = ( - select(Question, AssignmentQuestion) + select(Question, AssignmentQuestion, Chapter, SubChapter) .join(Question, AssignmentQuestion.question_id == Question.id) + .join( + Chapter, + (Question.chapter == Chapter.chapter_label) + & (Question.base_course == Chapter.course_id), + ) + .join( + SubChapter, + (Question.subchapter == SubChapter.sub_chapter_label) + & (SubChapter.chapter_id == Chapter.id), + ) .where(AssignmentQuestion.assignment_id == assignment_id) .order_by(AssignmentQuestion.sorting_priority) ) From fbb803119bfb4d0342bd2765dd6092180381da18 Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Mon, 6 Jul 2026 14:12:12 -0700 Subject: [PATCH 02/14] Explicitly unpack fetch_assignment_questions results in existing code --- bases/rsptx/admin_server_api/routers/instructor.py | 2 +- bases/rsptx/assignment_server_api/routers/peer.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bases/rsptx/admin_server_api/routers/instructor.py b/bases/rsptx/admin_server_api/routers/instructor.py index 6226f7a28..ad4f6637f 100644 --- a/bases/rsptx/admin_server_api/routers/instructor.py +++ b/bases/rsptx/admin_server_api/routers/instructor.py @@ -1151,7 +1151,7 @@ async def _copy_one_assignment( assignment_questions = await fetch_assignment_questions(old_assignment_id) for question_data in assignment_questions: - question, assignment_question, chapter, subchapter = question_data + assignment_question = question_data.AssignmentQuestion new_assignment_question_data = AssignmentQuestionValidator( assignment_id=new_assignment.id, diff --git a/bases/rsptx/assignment_server_api/routers/peer.py b/bases/rsptx/assignment_server_api/routers/peer.py index c4ea76925..dd36e0bf9 100644 --- a/bases/rsptx/assignment_server_api/routers/peer.py +++ b/bases/rsptx/assignment_server_api/routers/peer.py @@ -161,7 +161,7 @@ async def get_peer_dashboard( questions_result = await fetch_assignment_questions(assignment_id) questions = [] for row in questions_result: - question, assignment_question = row + question = row.Question questions.append(question) # Get current question based on assignment's current_index @@ -287,7 +287,7 @@ async def get_peer_extra( assignment = await fetch_one_assignment(assignment_id) questions_result = await fetch_assignment_questions(assignment_id) - questions = [q for q, _aq in questions_result] + questions = [q.Question for q in questions_result] current_idx = ( assignment.current_index if assignment.current_index is not None else 0 @@ -400,7 +400,7 @@ async def get_peer_question( questions_result = await fetch_assignment_questions(assignment_id) questions = [] for row in questions_result: - question, assignment_question = row + question = row.Question questions.append(question) # Get the current question based on assignment's current_index @@ -529,7 +529,7 @@ async def get_peer_async( ) qa_pairs = list(await fetch_assignment_questions(assignment_id)) - questions = [q for q, _aq in qa_pairs] + questions = [q.Question for q in qa_pairs] total_questions = len(questions) idx = question_num - 1 From 3cafb1eb7f1dd0143f5cde977ce005bcebb586bf Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 12:26:43 -0700 Subject: [PATCH 03/14] Drop hardcoded old pretext css from static_assets --- components/rsptx/templates/common/static_assets.html | 2 -- components/rsptx/templates/common/static_assets_min.html | 2 -- 2 files changed, 4 deletions(-) diff --git a/components/rsptx/templates/common/static_assets.html b/components/rsptx/templates/common/static_assets.html index d23286885..3b4a24f2a 100644 --- a/components/rsptx/templates/common/static_assets.html +++ b/components/rsptx/templates/common/static_assets.html @@ -44,8 +44,6 @@ - - diff --git a/components/rsptx/templates/common/static_assets_min.html b/components/rsptx/templates/common/static_assets_min.html index 2a0f0e62c..74386a062 100644 --- a/components/rsptx/templates/common/static_assets_min.html +++ b/components/rsptx/templates/common/static_assets_min.html @@ -23,8 +23,6 @@ - - - -{% for css_import in wp_imports["css"] %} - -{% endfor %} -{% for js_import in wp_imports["js"] %} - -{% endfor %} - -{% if webwork_js_version %} - -{% else %} - -{% endif %} - - - - - - - - - - - @@ -48,57 +18,87 @@ - + {% endfor %} + + {% if webwork_js_version %} + + {% else %} + + {% endif %} + + + + + + + + + + + + startup: { + pageReady() { + return MathJax.startup.defaultPageReady().then(function () { + console.log("in ready function"); + rsMathReady(); + } + )} + }, + }; + + + + + + +{% endif %} - - - - From 72162a2fafc8b0cb4b41729df20011ae0cf5b504 Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 13:27:45 -0700 Subject: [PATCH 09/14] Update assignment styles --- .../staticAssets/assignment/assignment.css | 155 ++++++++++++++++++ .../assignment/assignment_block.css | 40 +++++ .../rsptx/templates/staticAssets/index.css | 31 ---- 3 files changed, 195 insertions(+), 31 deletions(-) create mode 100644 components/rsptx/templates/staticAssets/assignment/assignment.css create mode 100644 components/rsptx/templates/staticAssets/assignment/assignment_block.css diff --git a/components/rsptx/templates/staticAssets/assignment/assignment.css b/components/rsptx/templates/staticAssets/assignment/assignment.css new file mode 100644 index 000000000..4c272a085 --- /dev/null +++ b/components/rsptx/templates/staticAssets/assignment/assignment.css @@ -0,0 +1,155 @@ +/* Styles for choose/do assignment pages. */ +/* Styles at top are universal. Below are sections for RS or PTX based templates. */ + +:root { + --color-success: #11b70e; + --color-error: #ff0000; + --rs-assignment-meta: #fafafa; + --rs-assignment-meta-border: #aaa; +} + +:root.dark-mode { + --color-success: rgb(110, 183, 14); + --color-error: rgb(230, 50, 50); + --rs-assignment-meta: #2d2d2d; + --rs-assignment-meta-border: #555; +} + +.overdue { + color: var(--color-error); +} +.duesoon { + color: var(--color-success); +} + +.assignment-questions { + padding-left: 0; +} + +.assignment-questions > li { + list-style: none; +} + +.assignment-grade-notice { + margin-top: 1em; +} + +.assignment-meta { + display: flex; + column-gap: 40px; + row-gap: 20px; + flex-wrap: wrap; + background-color: var(--rs-assignment-meta, #ddd); + border: 1px solid var(--rs-assignment-meta-border, #aaa); + padding: 1em; +} + +.assignment-meta > * { + flex: 1; + flex-basis: 200px; + margin-top: 0; +} + +.questionblock-question-number { + font-size: 1.25em; + margin: 0; +} + +.questionblock-header, +.questionblock-score-section { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + column-gap: 40px; + row-gap: 0.25em; + margin-bottom: 0.5em; + align-items: baseline; +} + +.questionblock-header > *, +.questionblock-score-section > * { + flex: 1; + flex-basis: 300px; +} + +.question-contents { + padding-bottom: 3px; +} + +/* 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)) { + .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%; + } +} + +.question-status { + font-size: 1.2em; +} +.question-status::before { + translate: 0 3px; + display: inline-block; +} + +.question-error { + color: var(--color-error); +} + + +/* Everything in this construct should only be applied when + 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:is(.ptx-runestone-template)) { + .assignment-readings-chapters { + padding-left: 0; + } + .assignment-readings-chapters > li { + list-style: none; + } + .assignment-readings-subchapters { + margin-top: 0.75em; + } + + h2 { + margin-bottom: 0.5em; + } +} diff --git a/components/rsptx/templates/staticAssets/assignment/assignment_block.css b/components/rsptx/templates/staticAssets/assignment/assignment_block.css new file mode 100644 index 000000000..986f35213 --- /dev/null +++ b/components/rsptx/templates/staticAssets/assignment/assignment_block.css @@ -0,0 +1,40 @@ +/* +Styles for the assignment block table. +This is used in the chooseAssignment page and the current_course page. +*/ + +/* 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%; + 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; + } +} \ No newline at end of file diff --git a/components/rsptx/templates/staticAssets/index.css b/components/rsptx/templates/staticAssets/index.css index 8af3c49b1..a05fc6844 100644 --- a/components/rsptx/templates/staticAssets/index.css +++ b/components/rsptx/templates/staticAssets/index.css @@ -96,35 +96,4 @@ input { .library_entry { margin-bottom: 5pt; -} - - - -.metaquestion { - display: flex; - flex-direction: row; - flex-wrap: wrap; - width: 100%; -} - -.griditem { - display: flex; - flex-direction: column; - flex-basis: 100%; - flex: 1; -} - -.questionblock { - background-color: #ddd; - padding: 1rem; - margin-bottom: 1rem; - border-radius: 10px; -} - -.overdue { - color: #ff0000; -} - -.duesoon { - color: #11b70e; } \ No newline at end of file From b576465cd14a75400f521f21c100e8de2eaf976b Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 13:28:26 -0700 Subject: [PATCH 10/14] Move doAssignment and chooseAssignment to ptx-based _base --- .../assignment_server_api/routers/student.py | 148 ++++++--- .../assignment/student/assignment_block.html | 6 +- .../assignment/student/chooseAssignment.html | 30 +- .../assignment/student/doAssignment.html | 293 +++++++++--------- 4 files changed, 277 insertions(+), 200 deletions(-) diff --git a/bases/rsptx/assignment_server_api/routers/student.py b/bases/rsptx/assignment_server_api/routers/student.py index 1ae7efc0d..83b32f66f 100644 --- a/bases/rsptx/assignment_server_api/routers/student.py +++ b/bases/rsptx/assignment_server_api/routers/student.py @@ -65,7 +65,7 @@ from rsptx.db.models import GradeValidator, UseinfoValidation, CoursesValidator from rsptx.db.crud.assignment import is_assignment_visible_to_students from rsptx.auth.session import auth_manager, is_instructor -from rsptx.templates import template_folder +from rsptx.templates import template_folder, get_jinja_templates from rsptx.response_helpers import construct_course_url, safe_join from rsptx.response_helpers.core import ( make_json_response, @@ -104,8 +104,11 @@ async def get_assignments( sid = user.username course = await fetch_course(user.course_name) + course_attrs = await fetch_all_course_attributes(course.id) is_lti1p1_course = await fetch_lti_version(course.id) == "1.1" - templates = Jinja2Templates(directory=template_folder) + + course_markup_system = course_attrs.get("markup_system", "Runestone") + user_is_instructor = await is_instructor(request, user=user) if user_is_instructor: # if the user is an instructor, we need to show all assignments @@ -168,20 +171,45 @@ def sort_key(assignment): for a in assignments: visibility_map[a.id] = is_assignment_visible_to_students(a) + book_path = safe_join( + settings.book_path, + course.base_course, + "published", + course.base_course, + ) + if book_path: + templates = get_jinja_templates(book_path) + else: + rslogger.warning( + "Unsafe or invalid book_path computed for course %s; falling back to shared templates", + course.course_name, + ) + templates = Jinja2Templates(directory=template_folder) + + context = dict( + course=course, + course_name=user.course_name, + request=request, + assignment_list=assignments, + readings="null", # force bookfuncs to not render assignment tracker + stats=stats, + user=sid, + is_logged_in="true", + is_instructor="true" if user_is_instructor else "false", + student_page=True, + lti1p1=is_lti1p1_course, + now=now, + visibility_map=visibility_map, + settings=settings, + base_url=construct_course_url(course), + activity_info="{}", + downloads_enabled="true" if course.downloads_enabled else "false", + allow_pairs="true" if course.allow_pairs else "false", + origin=course_markup_system, + **course_attrs, + ) return templates.TemplateResponse( - "assignment/student/chooseAssignment.html", - { - "assignment_list": assignments, - "stats": stats, - "course": course, - "user": sid, - "request": request, - "is_instructor": user_is_instructor, - "student_page": True, - "lti1p1": is_lti1p1_course, - "now": now, - "visibility_map": visibility_map, - }, + "assignment/student/chooseAssignment.html", context ) @@ -666,6 +694,7 @@ async def doAssignment( questions_score = 0 readings = dict() readings_score = 0 + needs_manual_grading = False # For each question, accumulate information, and add it to either the readings or questions data structure # If scores have not been released for the question or if there are no scores yet available, the scoring information will be recorded as empty strings @@ -726,27 +755,52 @@ async def doAssignment( if score is None: score = 0 - chap_name = q.Question.chapter - subchap_name = q.Question.subchapter + is_incorrect = score == 0 and comment != "ungraded" + + chap_label = q.Question.chapter + subchap_label = q.Question.subchapter if q.Question.base_course not in preambles: preambles[q.Question.base_course] = await addPreamble( q.Question.base_course ) + # Temporarily prevent chapter numbers from being duplicated while we unwind + # number being added to chapter name in DB. Assume any leaning numbers are + # not part of the chapter name and should be stripped off. + chapter_name = q.Chapter.chapter_name + chapter_name = re.sub(r"^\d+\s*", "", chapter_name) + chapter_chapter_name = q.SubChapter.sub_chapter_name + chapter_chapter_name = re.sub(r"^[\d\.]+\s*", "", chapter_chapter_name) + info = dict( htmlsrc=htmlsrc, score=score, + is_incorrect=is_incorrect, points=q.AssignmentQuestion.points, comment=comment, - chapter=q.Question.chapter, - subchapter=q.Question.subchapter, - chapter_name=chap_name, - subchapter_name=subchap_name, name=q.Question.name, + qnumber=q.Question.qnumber, question_type=q.Question.question_type, activities_required=q.AssignmentQuestion.activities_required, + chapter_name=chapter_name, + chapter_label=chap_label, + chapter_num=q.Chapter.chapter_num, + chapter_chapter_name=chapter_chapter_name, + sub_chapter_label=subchap_label, + chapter_chapter_num=q.SubChapter.sub_chapter_num, + base_url=construct_course_url(course), + # for rendering with ptx template, we need these for the menu + is_logged_in="true", + is_instructor="true" if user_is_instructor else "false", + readings=[], + activity_info="{}", + downloads_enabled="true" if course.downloads_enabled else "false", + allow_pairs="true" if course.allow_pairs else "false", + **course_attrs, ) if q.AssignmentQuestion.autograde == "manual": + if comment == "ungraded": + needs_manual_grading = True info["how_graded"] = "Needs Manual Grading" elif q.AssignmentQuestion.which_to_grade == "first_answer": info["how_graded"] = "First Answer" @@ -757,9 +811,9 @@ async def doAssignment( if q.AssignmentQuestion.reading_assignment: # add to readings - if chap_name not in readings: + if chap_label not in readings: # add chapter info - completion = await fetch_user_chapter_progress(user, chap_name) + completion = await fetch_user_chapter_progress(user, chap_label) if not completion: status = "notstarted" elif completion.status == 1: @@ -768,12 +822,18 @@ async def doAssignment( status = "started" else: status = "notstarted" - readings[chap_name] = dict(status=status, subchapters=[]) + readings[chap_label] = dict( + status=status, + subchapters=[], + chapter_label=chap_label, + chapter_name=chapter_name, + chapter_num=q.Chapter.chapter_num, + ) # add subchapter info # add completion status to info subch_completion = await fetch_user_sub_chapter_progress( - user, chap_name, subchap_name + user, chap_label, subchap_label ) if not subch_completion: @@ -788,8 +848,8 @@ async def doAssignment( # Make sure we don't create duplicate entries for older courses. New style # courses only have the base course in the database, but old will have both - if info not in readings[chap_name]["subchapters"]: - readings[chap_name]["subchapters"].append(info) + if info not in readings[chap_label]["subchapters"]: + readings[chap_label]["subchapters"].append(info) readings_score += info["score"] else: @@ -824,7 +884,7 @@ async def doAssignment( readings_names = [] for chapname in readings: readings_names = readings_names + [ - "{}/{}.html".format(d["chapter"], d["subchapter"]) + "{}/{}.html".format(d["chapter_label"], d["sub_chapter_label"]) for d in readings[chapname]["subchapters"] ] @@ -839,8 +899,7 @@ async def doAssignment( parsed_js = {} parsed_js["readings"] = readings_names - c_origin = course_attrs.get("markup_system", "Runestone") - print("ORIGIN", c_origin) + course_markup_system = course_attrs.get("markup_system", "Runestone") # grabs the row for the current user and and assignment in the grades table grade = await fetch_grade(user.id, assignment_id) @@ -874,37 +933,54 @@ async def doAssignment( overdue = False if timestamp > deadline: overdue = True - templates = Jinja2Templates(directory=template_folder) + + book_path = safe_join( + settings.book_path, + course.base_course, + "published", + course.base_course, + ) + templates = get_jinja_templates(book_path) + + # templates = Jinja2Templates(directory=template_folder) # reverse the order of the keys in the preambles dictionary so that the first key I added is now the last # this will ensure that when multiple preamble definitions are used the last one is from the current course preambles = dict((k, v) for k, v in reversed(preambles.items())) context = dict( # This is all the variables that will be used in the doAssignment.html document course=course, + request=request, course_name=user.course_name, assignment=assignment, questioninfo=questionslist, course_id=user.course_name, - readings=readings, + readings="null", # force bookfuncs to not render assignment tracker + readings_full=readings, questions_score=questions_score, readings_score=readings_score, user=user, # gradeRecordingUrl=URL('assignments', 'record_grade'), # calcTotalsURL=URL('assignments', 'calculate_totals'), released=assignment.released, - is_instructor=user_is_instructor, - student_page=True, - origin=c_origin, + is_logged_in="true", + is_instructor="true" if user_is_instructor else "false", + needs_manual_grading=needs_manual_grading, + student_page="true", + origin=course_markup_system, is_submit=grade.is_submit, is_graded=is_graded, overdue=overdue, enforce_pastdue=enforce_pastdue, ptx_js_version=course_attrs.get("ptx_js_version", "0.2"), webwork_js_version=course_attrs.get("webwork_js_version", "2.20"), - request=request, latex_preamble_dict=preambles, wp_imports=get_webpack_static_imports(course), settings=settings, course_attrs=course_attrs, + base_url=construct_course_url(course), + activity_info="{}", + downloads_enabled="true" if course.downloads_enabled else "false", + allow_pairs="true" if course.allow_pairs else "false", + **course_attrs, ) response = templates.TemplateResponse( "assignment/student/doAssignment.html", context diff --git a/components/rsptx/templates/assignment/student/assignment_block.html b/components/rsptx/templates/assignment/student/assignment_block.html index c7ff55f76..d3167450c 100644 --- a/components/rsptx/templates/assignment/student/assignment_block.html +++ b/components/rsptx/templates/assignment/student/assignment_block.html @@ -1,13 +1,13 @@ -

Assignments

{% if is_instructor %} {% endif %} {% if assignment_list %} - +
+ diff --git a/components/rsptx/templates/assignment/student/chooseAssignment.html b/components/rsptx/templates/assignment/student/chooseAssignment.html index 1d55592ce..ce65e5cc1 100644 --- a/components/rsptx/templates/assignment/student/chooseAssignment.html +++ b/components/rsptx/templates/assignment/student/chooseAssignment.html @@ -1,25 +1,35 @@ -{% extends "_base.html" %} +{% extends "_base.html" %} + {% block title %} Choose Assignment {% endblock %} + + {% block css %} + + + - + {% if origin == "PreTeXt" and using_ptx_base != "true" %} + + {% endif %} {% endblock %} + {% block content %} -
+
+

Choose Assignment

+ +
{% if lti1p1 %} -

You must launch assignments through your LMS for this course

+

You must launch assignments through your LMS for this course

{% else %} - {% include 'assignment/student/assignment_block.html'%} + {% include 'assignment/student/assignment_block.html' %} {% endif %} -

Help

-

Hint: You can help yourself stay organized by tracking your progress on an assignment. Use the dropdown menu to quickly mark your assignment as Not Started, In Progress, or Complete. When your instructor grades the assignment it will be graded regardless of the value in the dropdown menu. You can also get a preliminary score by clicking on the "Compute Score for Assignment" button on the assignment page.

- -
- + + {% endblock %} + {% block js %} {% endblock %} diff --git a/components/rsptx/templates/assignment/student/doAssignment.html b/components/rsptx/templates/assignment/student/doAssignment.html index 844bb83d7..aeed2019c 100644 --- a/components/rsptx/templates/assignment/student/doAssignment.html +++ b/components/rsptx/templates/assignment/student/doAssignment.html @@ -3,148 +3,168 @@ {% block title %} Do Assignment {% endblock %} + + {% block css %} + {% include 'common/static_assets.html' %} + + + + {% if origin == "PreTeXt" and using_ptx_base != "true" %} + + {% endif %} +{% endblock %} -{% include 'common/static_assets.html' %} - -{% if origin == "PreTeXt" %} - -{% endif %} -{% endblock %} {% block content %} +{% if using_ptx_base != "true" %} + {% include 'common/ebook_config.html' %} +{% endif %} -{% include 'common/ebook_config.html' %} {% for base_course, latex_preamble in latex_preamble_dict.items() %} {% endfor %} -
-

{{ assignment['name'] }}

-

- - Due: {{ assignment['duedate'] }} - - {% if enforce_pastdue %} - Past due and no longer scoring submissions +

+

Assignment: {{ assignment['name'] }}

+ +
+
+
+ Due: {{ assignment['duedate'] }} +
+ {% if enforce_pastdue %} + (Past due and no longer scoring submissions) + {% endif %} +
+
+ {% if assignment['points'] > 0 %} +
+ Current Score: + {{questions_score+readings_score}} of + {{assignment['points']}} = + {{'%0.2f' | format(100*(questions_score + readings_score)/assignment['points']|float)}}% +
+ {% if released and not assignment.is_timed and needs_manual_grading %} +
+ Warning: This assignment includes manually graded problems that have not been scored yet. +
+ {% endif %} + + {% else %} + This assignment is ungraded + {% endif %} +
+
+ + {% if assignment['description'] %} +
+

Description

+

{{ assignment['description'] }}

+
{% endif %} -

- - {% if assignment['points'] > 0 %} - Current Score: - {{questions_score+readings_score}} of - {{assignment['points']}} = - {{'%0.2f' | format(100*(questions_score + readings_score)/assignment['points']|float)}}% - {% else %} - This assignment is ungraded - {% endif %} - -
-

Description: {{ assignment['description'] }}

-
- {% if readings|length > 0 %} -
-

Readings

-
    - {% for reading in readings %} -
  • {{ reading }}
  • -
      - {% for r in readings[reading]['subchapters'] %} + + {% if readings_full|length > 0 %} +
      +

      Readings

      + -
+ + {% endfor %} + +
{% endif %} {% if questioninfo|length > 0 %} -
- -

Questions

- {% if assignment.is_timed %} - + {% else %} +

Questions

+
    + {% for q in questioninfo %} +
  1. +
    +

    + Question: {{loop.index}} ({{ q['qnumber'] | trim }}) + {% if q['is_incorrect'] %} + not correct + + {% endif %} +

    + - -
    -

    Score: {{ q['score']}} / {{ q['points'] }}

    - {% if q['comment'] != 'autograded' and q['comment'] != 'ungraded' %} -

    Comment: {{ q['comment'] }}

    - {% else %} -

    Scoring Method: {{q['how_graded']}}

    - {% endif %}
    +
    +
    +
    + Score: {{ q['score']}} / {{ q['points'] }} +
    + {% if q['comment'] != 'autograded' and q['comment'] != 'ungraded' %} +
    Comment: {{ q['comment'] }}
    + {% else %} +
    Scoring Method: {{q['how_graded']}}
    + {% endif %} +
    {% if q['question_type'] == 'webwork' or origin == "PreTeXt" %}
    {% else %} @@ -152,30 +172,21 @@

    Questions

    {% endif %} {{ q.htmlsrc|safe }}
    +
    - {% endfor %} - {% endif %} - -
+ + {% endfor %} + + {% endif %} + {% endif %} -
- -{% if not released and not assignment.is_timed %} -
- -

Warning: 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.

-
- -{% endif %} - + + + From 3c4b3b12ce628c8825c1ec09b0c26b74738e783d Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 13:29:46 -0700 Subject: [PATCH 11/14] Move course homepage to ptx-based _base --- bases/rsptx/book_server_api/routers/course.py | 34 ++++-- .../templates/book/course/current_course.html | 112 ++++++++++-------- .../rsptx/templates/staticAssets/course.css | 31 +++-- 3 files changed, 102 insertions(+), 75 deletions(-) diff --git a/bases/rsptx/book_server_api/routers/course.py b/bases/rsptx/book_server_api/routers/course.py index 3ef3010fb..08262391b 100644 --- a/bases/rsptx/book_server_api/routers/course.py +++ b/bases/rsptx/book_server_api/routers/course.py @@ -15,13 +15,12 @@ # ------------------- from typing import Optional from fastapi import APIRouter, Cookie, Request, Depends -from fastapi.templating import Jinja2Templates # Local application imports # ------------------------- from rsptx.auth.session import auth_manager -from rsptx.templates import template_folder +from rsptx.templates import get_jinja_templates from rsptx.db.crud import ( fetch_assignments, fetch_all_assignment_stats, @@ -35,7 +34,9 @@ update_user, fetch_lti_version, ) +from rsptx.configuration import settings from rsptx.logging import rslogger +from rsptx.response_helpers import construct_course_url, safe_join from rsptx.response_helpers.core import canonical_utcnow, make_json_response from rsptx.auth.session import is_instructor from rsptx.db.crud.assignment import is_assignment_visible_to_students @@ -70,12 +71,10 @@ async def index( instructors = await fetch_course_instructors(course_name) rslogger.debug(f"{instructors=}") attrs = await fetch_all_course_attributes(course.id) - templates = Jinja2Templates(directory=template_folder) - books = await fetch_library_book(course.base_course) - if books is None: - books = [] - else: - books = [books] + book = await fetch_library_book(course.base_course) + + course_markup_system = attrs.get("markup_system", "Runestone") + row = await fetch_last_page(user, course_name) if row: last_page_url = row.last_page_url @@ -147,6 +146,17 @@ def sort_key(assignment): for a in assignments: visibility_map[a.id] = is_assignment_visible_to_students(a) + book_path = safe_join( + settings.book_path, + course.base_course, + "published", + course.base_course, + ) + if book_path: + templates = get_jinja_templates(book_path) + else: + templates = Jinja2Templates(directory=template_folder) + return templates.TemplateResponse( "book/course/current_course.html", { @@ -160,16 +170,18 @@ def sort_key(assignment): "institution": course.institution, "instructor_list": instructors, "base_course": course.base_course, - "book_list": books, + "origin": course_markup_system, + "book": book, "lastPageUrl": last_page_url, "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, "now": now, "visibility_map": visibility_map, "course_attrs": attrs, + "base_url": construct_course_url(course), }, ) diff --git a/components/rsptx/templates/book/course/current_course.html b/components/rsptx/templates/book/course/current_course.html index 4d8d99929..4357419a6 100644 --- a/components/rsptx/templates/book/course/current_course.html +++ b/components/rsptx/templates/book/course/current_course.html @@ -3,9 +3,13 @@ My Course {% endblock %} {% block css %} + + + - - + {% if origin == "PreTeXt" and using_ptx_base != "true" %} + + {% endif %} {% endblock %} {% set back_to = "textbook" %} @@ -28,21 +32,28 @@ {% endset %} {% block content %} -
-
-

{{course.course_name}}

- {% if course_description %} +
+

{{course.course_name}}

+ + {% if course_description %} + + {% endif %} + + + +
+

Instructors

    {% for instructor in instructor_list: %}
  • @@ -50,53 +61,50 @@

    Instructors

  • {% endfor %}
-
-
-

Textbooks

- + {% else %} +

No textbook for this course.

+ {% endif %} + -
-
+
+

Assignments

{% if lti1p1 %} -

You must launch assignments through your LMS for this course

+

You must launch assignments through your LMS for this course

{% else %} - {% include 'assignment/student/assignment_block.html'%} +
+ {% include 'assignment/student/assignment_block.html'%} +
{% endif %} -
+ {% if is_instructor %} -
-

Instructor Links

- -
+ {% if has_discussion_group: %} +

Get help, learn about updates, share resources!

+ {% if book and book.social_url: %} + Instructors Group for {{book.title}} + {% endif %} + {% endif %} + {% endif %} -
+ {% endblock %} diff --git a/components/rsptx/templates/staticAssets/course.css b/components/rsptx/templates/staticAssets/course.css index 08a8ad72c..e7cf08812 100644 --- a/components/rsptx/templates/staticAssets/course.css +++ b/components/rsptx/templates/staticAssets/course.css @@ -1,15 +1,22 @@ -.course_page { - width: 90%; - margin-left: auto; - margin-right: auto; +.assignment-chooser { + max-height: 36rem; + overflow-y: scroll; + margin-bottom: 1rem; + border: 1px solid #ccc; + padding: .5rem; + border-radius: 5px; + margin-top: 1.5em; } - -.assignment_chooser { - max-height: 24rem; - overflow-y: scroll; - margin-bottom: 1rem; - border: solid #ccc; - padding: .5rem; - border-radius: 5px; +/* 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; + } } \ No newline at end of file From e5805185de0b39e9d96dfe63338089c0fec22ee5 Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 13:40:05 -0700 Subject: [PATCH 12/14] Require opt in for ptx-based student pages --- .../admin_server_api/routers/instructor.py | 3 ++ .../assignment_server_api/routers/student.py | 53 +++++++++++++------ bases/rsptx/book_server_api/routers/course.py | 23 +++++--- .../admin/instructor/course_settings.html | 9 ++++ 4 files changed, 63 insertions(+), 25 deletions(-) diff --git a/bases/rsptx/admin_server_api/routers/instructor.py b/bases/rsptx/admin_server_api/routers/instructor.py index ad4f6637f..17d9540b2 100644 --- a/bases/rsptx/admin_server_api/routers/instructor.py +++ b/bases/rsptx/admin_server_api/routers/instructor.py @@ -423,6 +423,9 @@ async def get_course_settings( "show_points": course_attrs.get("show_points") == "true", "groupsize": course_attrs.get("groupsize", "3"), "enable_async_llm_modes": course_attrs.get("enable_async_llm_modes", "false"), + "use_pretext_student_pages": str( + course_attrs.get("use_pretext_student_pages", "false") + ).lower(), } return templates.TemplateResponse("admin/instructor/course_settings.html", context) diff --git a/bases/rsptx/assignment_server_api/routers/student.py b/bases/rsptx/assignment_server_api/routers/student.py index 83b32f66f..c52ad0f9e 100644 --- a/bases/rsptx/assignment_server_api/routers/student.py +++ b/bases/rsptx/assignment_server_api/routers/student.py @@ -171,19 +171,25 @@ def sort_key(assignment): for a in assignments: visibility_map[a.id] = is_assignment_visible_to_students(a) - book_path = safe_join( - settings.book_path, - course.base_course, - "published", - course.base_course, + use_pretext_student_pages = ( + str(course_attrs.get("use_pretext_student_pages", "false")).lower() == "true" ) - if book_path: - templates = get_jinja_templates(book_path) - else: - rslogger.warning( - "Unsafe or invalid book_path computed for course %s; falling back to shared templates", - course.course_name, + if use_pretext_student_pages: + book_path = safe_join( + settings.book_path, + course.base_course, + "published", + course.base_course, ) + if book_path: + templates = get_jinja_templates(book_path) + else: + rslogger.warning( + "Unsafe or invalid book_path computed for course %s; falling back to shared templates", + course.course_name, + ) + templates = Jinja2Templates(directory=template_folder) + else: templates = Jinja2Templates(directory=template_folder) context = dict( @@ -934,13 +940,26 @@ async def doAssignment( if timestamp > deadline: overdue = True - book_path = safe_join( - settings.book_path, - course.base_course, - "published", - course.base_course, + use_pretext_student_pages = ( + str(course_attrs.get("use_pretext_student_pages", "false")).lower() == "true" ) - templates = get_jinja_templates(book_path) + if use_pretext_student_pages: + book_path = safe_join( + settings.book_path, + course.base_course, + "published", + course.base_course, + ) + if book_path: + templates = get_jinja_templates(book_path) + else: + rslogger.warning( + "Unsafe or invalid book_path computed for course %s; falling back to shared templates", + course.course_name, + ) + templates = Jinja2Templates(directory=template_folder) + else: + templates = Jinja2Templates(directory=template_folder) # templates = Jinja2Templates(directory=template_folder) # reverse the order of the keys in the preambles dictionary so that the first key I added is now the last diff --git a/bases/rsptx/book_server_api/routers/course.py b/bases/rsptx/book_server_api/routers/course.py index 08262391b..5fe588440 100644 --- a/bases/rsptx/book_server_api/routers/course.py +++ b/bases/rsptx/book_server_api/routers/course.py @@ -15,12 +15,13 @@ # ------------------- from typing import Optional from fastapi import APIRouter, Cookie, Request, Depends +from fastapi.templating import Jinja2Templates # Local application imports # ------------------------- from rsptx.auth.session import auth_manager -from rsptx.templates import get_jinja_templates +from rsptx.templates import get_jinja_templates, template_folder from rsptx.db.crud import ( fetch_assignments, fetch_all_assignment_stats, @@ -146,14 +147,20 @@ def sort_key(assignment): for a in assignments: visibility_map[a.id] = is_assignment_visible_to_students(a) - book_path = safe_join( - settings.book_path, - course.base_course, - "published", - course.base_course, + use_pretext_student_pages = ( + str(attrs.get("use_pretext_student_pages", "false")).lower() == "true" ) - if book_path: - templates = get_jinja_templates(book_path) + if use_pretext_student_pages: + book_path = safe_join( + settings.book_path, + course.base_course, + "published", + course.base_course, + ) + if book_path: + templates = get_jinja_templates(book_path) + else: + templates = Jinja2Templates(directory=template_folder) else: templates = Jinja2Templates(directory=template_folder) diff --git a/components/rsptx/templates/admin/instructor/course_settings.html b/components/rsptx/templates/admin/instructor/course_settings.html index d3a8bd009..437e3eb70 100644 --- a/components/rsptx/templates/admin/instructor/course_settings.html +++ b/components/rsptx/templates/admin/instructor/course_settings.html @@ -72,6 +72,15 @@

{{ course.course_name }}

Allow per-question LLM chat mode selection in asynchronous peer instruction activities
+
+
+ + +
+
When enabled, student pages can use book-specific template overrides.
+
+
Student View (Hide Hidden Assignments) @@ -11,7 +11,7 @@
- {% if is_instructor %} + {% if is_instructor == True or is_instructor | lower == "true" %} {% endif %} @@ -36,7 +36,7 @@ {{assignment.name}} - {% if is_instructor %} + {% if is_instructor == True or is_instructor | lower == "true" %}
Assignment List
Name
NameActionDue diff --git a/components/rsptx/templates/book/course/current_course.html b/components/rsptx/templates/book/course/current_course.html index 4357419a6..784ee6802 100644 --- a/components/rsptx/templates/book/course/current_course.html +++ b/components/rsptx/templates/book/course/current_course.html @@ -43,7 +43,7 @@

{{course.course_name}}