From 2999e90d452277ad69949c769e6cdcf054f54518 Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 11:44:15 -0700 Subject: [PATCH 01/11] DB: fetch_assignment_questions returns chapter and subchapter with question --- bases/rsptx/admin_server_api/routers/instructor.py | 2 +- components/rsptx/db/crud/question.py | 8 +++++--- 2 files changed, 6 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..87d6dc19e 100644 --- a/components/rsptx/db/crud/question.py +++ b/components/rsptx/db/crud/question.py @@ -346,17 +346,19 @@ 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 f3054752707ca875deac506aa8ee9e6939c4fa8f Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 12:26:43 -0700 Subject: [PATCH 02/11] 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 e5efae27abe84e6562ce73c7ce41f48c4df0a07a Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 13:27:45 -0700 Subject: [PATCH 08/11] 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..5ff218c5a --- /dev/null +++ b/components/rsptx/templates/staticAssets/assignment/assignment.css @@ -0,0 +1,155 @@ +/* Styles for choose/do assignment pages. */ +/* Styles at top are univeral. 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 3fbb40cf4b3495634b924b2ea9c962f7423b8bd8 Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 13:28:26 -0700 Subject: [PATCH 09/11] Move doAssignment and chooseAssignment to ptx-based _base --- .../assignment_server_api/routers/student.py | 135 ++++++-- .../assignment/student/assignment_block.html | 6 +- .../assignment/student/chooseAssignment.html | 30 +- .../assignment/student/doAssignment.html | 307 +++++++++--------- 4 files changed, 271 insertions(+), 207 deletions(-) diff --git a/bases/rsptx/assignment_server_api/routers/student.py b/bases/rsptx/assignment_server_api/routers/student.py index 46773ad2d..bab6609c7 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,38 @@ 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, + ) + templates = get_jinja_templates(book_path) + + 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 ) @@ -664,6 +685,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 @@ -722,27 +744,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" @@ -753,9 +800,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: @@ -764,12 +811,12 @@ 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: @@ -784,8 +831,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: @@ -820,7 +867,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"] ] @@ -835,8 +882,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) @@ -870,37 +916,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 bc9577cd1..2c1d3d5d9 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..482f260e5 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 origin == "Runestone" %} -
    • {{ r['name'] }} - {% else: %} -
    • {{ r['name'] }} - {% endif %} - {% if r['points'] > 0 %} - {% if r['comment'] != 'ungraded' %} -
      {{ r['score']}} of {{ r['points']}} points earned; minimum {{ r['activities_required'] }} activities required - {% else %} -
      not graded yet: {{ r['points']}} points; minimum {{ r['activities_required'] }} activities required - {% endif %} + + {% 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 c2830bab5e6c54d9a1557acfeba3277dab7f4351 Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 13:29:46 -0700 Subject: [PATCH 10/11] Move course homepage to ptx-based _base --- bases/rsptx/book_server_api/routers/course.py | 32 +++-- .../templates/book/course/current_course.html | 114 +++++++++--------- .../rsptx/templates/staticAssets/course.css | 31 +++-- 3 files changed, 98 insertions(+), 79 deletions(-) diff --git a/bases/rsptx/book_server_api/routers/course.py b/bases/rsptx/book_server_api/routers/course.py index 3ef3010fb..92246c790 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,13 @@ 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, + ) + templates = get_jinja_templates(book_path) return templates.TemplateResponse( "book/course/current_course.html", { @@ -160,16 +166,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, + "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": "true" if book.social_url else "false", "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 f39a0298b..b74c2a181 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,54 +61,47 @@

    Instructors

  • {% endfor %}
-
-
-

Textbooks

-
    - {% for book in book_list: %} -
  • {{book.title}} - {% if lastPageUrl %} - (Last Page) - {% endif %} -
  • - {% endfor %} -
+ -
-
+
+

Textbook

+ {{book.title}} + {% if lastPageUrl %} + (Last Page) + {% 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 True or 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 91bf11548143ed91c18ac6179e6847b1fe66a049 Mon Sep 17 00:00:00 2001 From: Andrew Scholer Date: Fri, 3 Jul 2026 13:40:05 -0700 Subject: [PATCH 11/11] Require opt in for ptx-based student pages --- .../admin_server_api/routers/instructor.py | 3 ++ .../assignment_server_api/routers/student.py | 38 +++++++++++++------ bases/rsptx/book_server_api/routers/course.py | 22 +++++++---- .../admin/instructor/course_settings.html | 9 +++++ 4 files changed, 53 insertions(+), 19 deletions(-) diff --git a/bases/rsptx/admin_server_api/routers/instructor.py b/bases/rsptx/admin_server_api/routers/instructor.py index 6226f7a28..70ab98824 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 bab6609c7..f79148473 100644 --- a/bases/rsptx/assignment_server_api/routers/student.py +++ b/bases/rsptx/assignment_server_api/routers/student.py @@ -171,13 +171,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(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, + ) + templates = get_jinja_templates(book_path) + else: + templates = Jinja2Templates(directory=template_folder) context = dict( course=course, @@ -917,13 +924,20 @@ 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, + ) + templates = get_jinja_templates(book_path) + 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 92246c790..ace8de2fb 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,13 +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" ) - 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, + ) + templates = get_jinja_templates(book_path) + else: + templates = Jinja2Templates(directory=template_folder) + return templates.TemplateResponse( "book/course/current_course.html", { 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.
+
+
Assignment List
Name