diff --git a/bases/rsptx/admin_server_api/routers/instructor.py b/bases/rsptx/admin_server_api/routers/instructor.py index 07553ad6e..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) @@ -1151,7 +1154,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/bases/rsptx/assignment_server_api/routers/instructor.py b/bases/rsptx/assignment_server_api/routers/instructor.py index 49d04ad0a..c6057f14e 100644 --- a/bases/rsptx/assignment_server_api/routers/instructor.py +++ b/bases/rsptx/assignment_server_api/routers/instructor.py @@ -84,6 +84,7 @@ from rsptx.auth.session import auth_manager, is_instructor from rsptx.templates import template_folder from rsptx.configuration import settings +from rsptx.response_helpers import construct_course_url from rsptx.response_helpers.core import ( make_json_response, get_webpack_static_imports, @@ -116,8 +117,6 @@ from rsptx.data_types.language import LanguageOptions from rsptx.data_types.question_type import QuestionType -from .student import get_course_url - # Routing # ======= # See `APIRouter config` for an explanation of this approach. @@ -184,9 +183,9 @@ async def review_peer_assignment( # This replacement is to render images bts = q.Question.htmlsrc htmlsrc = bts.replace( - 'src="../_static/', 'src="' + get_course_url(course, "_static/") + 'src="../_static/', 'src="' + construct_course_url(course, "_static/") ) - htmlsrc = htmlsrc.replace("../_images", get_course_url(course, "_images")) + htmlsrc = htmlsrc.replace("../_images", construct_course_url(course, "_images")) # Rewrite xref links and knowls in fillintheblank questions if "fillintheblank" in htmlsrc: diff --git a/bases/rsptx/assignment_server_api/routers/student.py b/bases/rsptx/assignment_server_api/routers/student.py index dbf06591c..f79148473 100644 --- a/bases/rsptx/assignment_server_api/routers/student.py +++ b/bases/rsptx/assignment_server_api/routers/student.py @@ -65,7 +65,8 @@ 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, get_webpack_static_imports, @@ -103,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 @@ -167,20 +171,45 @@ def sort_key(assignment): for a in assignments: visibility_map[a.id] = is_assignment_visible_to_students(a) + use_pretext_student_pages = ( + str(course_attrs.get("use_pretext_student_pages", "false")).lower() + == "true" + ) + 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, + 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 ) @@ -563,20 +592,6 @@ async def update_submit( await upsert_grade(grade) return make_json_response(detail=dict(success=True)) - - -def get_course_url(course: CoursesValidator, *args) -> str: - """Get the URL for the course. - - :param course: _description_ - :type course: CourseValidator - :return: _description_ - :rtype: str - """ - rest = "/".join(args) - return f"/ns/books/published/{course.course_name}/{rest}" - - @router.get("/doAssignment") async def doAssignment( request: Request, @@ -677,6 +692,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 @@ -688,11 +704,11 @@ async def doAssignment( # This replacement is to render images bts = q.Question.htmlsrc htmlsrc = bts.replace( - 'src="../_static/', 'src="' + get_course_url(course, "_static/") + 'src="../_static/', 'src="' + construct_course_url(course, "_static/") ) - htmlsrc = htmlsrc.replace("../_images", get_course_url(course, "_images")) + htmlsrc = htmlsrc.replace("../_images", construct_course_url(course, "_images")) htmlsrc = htmlsrc.replace( - 'src=\\"external', 'src=\\"' + get_course_url(course, "external") + 'src=\\"external', 'src=\\"' + construct_course_url(course, "external") ) # htmlsrc = htmlsrc.replace( # "generated/webwork", get_course_url(course, "generated/webwork") @@ -735,27 +751,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" @@ -766,9 +807,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: @@ -777,12 +818,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: @@ -797,8 +838,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: @@ -833,7 +874,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"] ] @@ -848,8 +889,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) @@ -883,37 +923,61 @@ async def doAssignment( overdue = False if timestamp > deadline: overdue = True - templates = Jinja2Templates(directory=template_folder) + + use_pretext_student_pages = ( + str(course_attrs.get("use_pretext_student_pages", "false")).lower() + == "true" + ) + 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 # 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/bases/rsptx/book_server_api/routers/books.py b/bases/rsptx/book_server_api/routers/books.py index e4083d039..69bdd7fe1 100644 --- a/bases/rsptx/book_server_api/routers/books.py +++ b/bases/rsptx/book_server_api/routers/books.py @@ -26,7 +26,6 @@ import json import os import os.path -import posixpath import random import socket from typing import Optional @@ -59,6 +58,7 @@ from rsptx.db.models import UseinfoValidation from rsptx.auth.session import is_instructor from rsptx.templates import template_folder +from rsptx.response_helpers import safe_join from rsptx.response_helpers.core import canonical_utcnow from typing_extensions import Annotated @@ -509,12 +509,6 @@ async def library(request: Request, response_class=HTMLResponse): # Utilities # ========= -# This is copied verbatim from https://github.com/pallets/werkzeug/blob/master/werkzeug/security.py#L30. -_os_alt_seps = list( - sep for sep in [os.path.sep, os.path.altsep] if sep not in (None, "/") -) - - def URL(*argv): return "/".join(argv) @@ -523,27 +517,6 @@ def XML(arg): return arg -# This is copied verbatim from https://github.com/pallets/werkzeug/blob/master/werkzeug/security.py#L216. -def safe_join(directory, *pathnames): - """Safely join ``directory`` and one or more untrusted ``pathnames``. If this - cannot be done, this function returns ``None``. The main thing this does is make sure that we do not allow relative pathnames going up the directory tree to be joined to the base directory. This is important because it prevents an attacker from using a pathname like ``../../../etc/passwd`` to read an arbitrary file on the server filesystem. - - :param directory: the base directory. - :param pathnames: the untrusted pathnames relative to that directory. - :return: the joined path or ``None`` if this cannot be done. - """ - parts = [directory] - for filename in pathnames: - if filename != "": - filename = posixpath.normpath(filename) - for sep in _os_alt_seps: - if sep in filename: - return None - if os.path.isabs(filename) or filename == ".." or filename.startswith("../"): - return None - parts.append(filename) - return posixpath.join(*parts) - async def fetch_subchaptoc(course: str, chap: str): res = await fetch_subchapters(course, chap) diff --git a/bases/rsptx/book_server_api/routers/course.py b/bases/rsptx/book_server_api/routers/course.py index 3ef3010fb..ace8de2fb 100644 --- a/bases/rsptx/book_server_api/routers/course.py +++ b/bases/rsptx/book_server_api/routers/course.py @@ -21,7 +21,7 @@ # ------------------------- from rsptx.auth.session import auth_manager -from rsptx.templates import template_folder +from rsptx.templates import get_jinja_templates, template_folder from rsptx.db.crud import ( fetch_assignments, fetch_all_assignment_stats, @@ -35,7 +35,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 +72,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 +147,20 @@ def sort_key(assignment): for a in assignments: visibility_map[a.id] = is_assignment_visible_to_students(a) + use_pretext_student_pages = ( + str(attrs.get("use_pretext_student_pages", "false")).lower() == "true" + ) + 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", { @@ -160,16 +174,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/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) ) diff --git a/components/rsptx/response_helpers/__init__.py b/components/rsptx/response_helpers/__init__.py index 413128d7e..bae87837b 100644 --- a/components/rsptx/response_helpers/__init__.py +++ b/components/rsptx/response_helpers/__init__.py @@ -1,3 +1,4 @@ from rsptx.response_helpers import core +from rsptx.response_helpers.core import construct_course_url, safe_join -__all__ = ["core"] +__all__ = ["core", "construct_course_url", "safe_join"] diff --git a/components/rsptx/response_helpers/core.py b/components/rsptx/response_helpers/core.py index 475b315eb..6a1dc7f37 100644 --- a/components/rsptx/response_helpers/core.py +++ b/components/rsptx/response_helpers/core.py @@ -14,9 +14,10 @@ import json import os from pathlib import Path +import posixpath import re import sys -from typing import Any, List +from typing import Any, List, Optional # Third-party imports # ------------------- @@ -153,3 +154,31 @@ def canonical_utcnow(): return datetime.datetime.now(datetime.UTC).replace(tzinfo=None) else: return datetime.datetime.utcnow() + + +def construct_course_url(course: Any, *args: str) -> str: + """Build the published book URL for a course with an optional suffix.""" + rest = "/".join(args) + return f"/ns/books/published/{course.course_name}/{rest}" + + +# This is copied verbatim from +# https://github.com/pallets/werkzeug/blob/master/werkzeug/security.py#L30. +_os_alt_seps = [sep for sep in [os.path.sep, os.path.altsep] if sep not in (None, "/")] + + +# This is copied verbatim from +# https://github.com/pallets/werkzeug/blob/master/werkzeug/security.py#L216. +def safe_join(directory: str, *pathnames: str) -> Optional[str]: + """Safely join a trusted base directory and untrusted relative paths.""" + parts = [directory] + for filename in pathnames: + if filename != "": + filename = posixpath.normpath(filename) + for sep in _os_alt_seps: + if sep in filename: + return None + if os.path.isabs(filename) or filename == ".." or filename.startswith("../"): + return None + parts.append(filename) + return posixpath.join(*parts) diff --git a/components/rsptx/templates/__init__.py b/components/rsptx/templates/__init__.py index 29d6c2711..70d31cf7b 100644 --- a/components/rsptx/templates/__init__.py +++ b/components/rsptx/templates/__init__.py @@ -1,6 +1,7 @@ from pathlib import Path from rsptx.templates import core +from rsptx.templates.core import get_jinja_templates -__all__ = ["core"] +__all__ = ["core", "get_jinja_templates", "template_folder"] template_folder = Path(__file__).parent.absolute() diff --git a/components/rsptx/templates/_base.html b/components/rsptx/templates/_base.html index daf68820e..6e1119ae5 100644 --- a/components/rsptx/templates/_base.html +++ b/components/rsptx/templates/_base.html @@ -1,20 +1,3 @@ -{% macro with_errors(field) %} -
- {% if field.errors %} - {% set css_class = 'has-error ' + kwargs.pop('class', '') %} - {{field.label}}: {{ field(class=css_class, **kwargs) }} - - {% else %} - {{field.label}}: {{ field(**kwargs) }} - {% endif %} -
-{% endmacro %} - - 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 + {% 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 %} - + + + diff --git a/components/rsptx/templates/author/editlibrary.html b/components/rsptx/templates/author/editlibrary.html index 8f651e6f2..3dccdf2d3 100644 --- a/components/rsptx/templates/author/editlibrary.html +++ b/components/rsptx/templates/author/editlibrary.html @@ -1,5 +1,20 @@ {% extends "_base.html" %} +{% macro with_errors(field) %} +
+ {% if field.errors %} + {% set css_class = 'has-error ' + kwargs.pop('class', '') %} + {{field.label}}: {{ field(class=css_class, **kwargs) }} + + {% else %} + {{field.label}}: {{ field(**kwargs) }} + {% endif %} +
+{% endmacro %} {% block content %}
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/common/static_assets.html b/components/rsptx/templates/common/static_assets.html index d23286885..766e4ee81 100644 --- a/components/rsptx/templates/common/static_assets.html +++ b/components/rsptx/templates/common/static_assets.html @@ -6,101 +6,99 @@ --> - - - -{% for css_import in wp_imports["css"] %} - -{% endfor %} -{% for js_import in wp_imports["js"] %} - -{% endfor %} - -{% if webwork_js_version %} - -{% else %} - -{% endif %} - - - - - - - - - - - - - - + {% endfor %} + + {% if webwork_js_version %} + + {% else %} + + {% endif %} + + + + + + + + + + + + loader: { + load: ['input/asciimath', '[tex]/extpfeil', '[tex]/amscd', '[tex]/newcommand', '[pretext]/mathjaxknowl3.js'], + paths: {pretext: "https://pretextbook.org/js/lib"}, + }, + startup: { + pageReady() { + return MathJax.startup.defaultPageReady().then(function () { + console.log("in ready function"); + rsMathReady(); + } + )} + }, + }; + + + + + + +{% endif %} - - - - 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 @@ - -
Assignment List
Name