books.py - route to a textbook¶
This controller provides routes to a specific textbook page.
The expected URL is:
Imports¶
These are listed in the order prescribed by PEP 8.
Standard library¶
Third-party imports¶
None.
Local application imports¶
None.
Supporting functions¶
THIS FUNCTION IS DEPRECATED
Get the base course passed in request.args[0]
, or return a 404 if that argument is missing.
Does this book originate as a Runestone book or a PreTeXt book. if it is pretext book we use different delimiters for the templates as LaTeX is full of {{ These values are set by the runestone process-manifest command
See caching selects.
Find the course to access.
Given a logged-in user, use auth.user.course_id
.
Get course info.
Ensure the base course in the URL agrees with the base course in course
.
If not, ask the user to select a course.
set defaults for various attrs
Determine if we should ask for support Trying to do banner ads during the 2nd and 3rd weeks of the term but not to high school students or if the instructor has donated for the course
now = datetime.datetime.utcnow().date()
week2 = datetime.timedelta(weeks=2)
week4 = datetime.timedelta(weeks=4)
if (
now >= (course.term_start_date + week2)
and now <= (course.term_start_date + week4)
and course.base_course != "csawesome"
and course.courselevel != "high"
and getCourseAttribute(course.id, "supporter") is None
):
settings.show_rs_banner = True
elif course.course_name == course.base_course and random.random() <= 0.2:
Show banners to base course users 20% of the time.
Ensure the user has access to this book.
if (
is_published
and not db(
(db.user_courses.user_id == auth.user.id)
& (db.user_courses.course_id == auth.user.course_id)
)
.select(db.user_courses.id, **cache_kwargs)
.first()
):
session.flash = "Sorry you are not registered for this course. You can view most Open courses if you log out"
redirect(URL(c="default", f="courses"))
else:
Get the base course from the URL.
The user is trying to access the base course for the last course they logged in to there is a 99% chance this is an error and we should make them log in.
session.flash = "You most likely want to log in to access your course"
redirect(URL(c="default", f="courses"))
response.serve_ad = True
course = (
db(db.courses.course_name == base_course)
.select(
db.courses.id,
db.courses.course_name,
db.courses.base_course,
db.courses.login_required,
db.courses.allow_pairs,
db.courses.downloads_enabled,
**cache_kwargs,
)
.first()
)
if not course:
This course doesn’t exist.
set defaults for various attrs
Require a login if necessary.
Ask for a login by invoking the auth decorator.
This code should never run!
Make this an absolute path.
See if this is static content. By default, the Sphinx static directory names are _static
and _images
.
See the response. Warning: this is slow. Configure a production server to serve this statically.
It’s HTML – use the file as a template.
Make sure the file exists. Otherwise, the rendered “page” will look goofy.
if not os.path.isfile(book_path):
logger.error("Bad Path for {} given {}".format(book_path, request.args[1:]))
raise HTTP(404)
response.view = book_path
chapter = os.path.split(os.path.split(book_path)[0])[1]
subchapter = os.path.basename(os.path.splitext(book_path)[0])
div_counts = {}
if auth.user:
user_id = auth.user.username
email = auth.user.email
is_logged_in = "true"
Get the necessary information to update subchapter progress on the page
page_divids = db(
(db.questions.subchapter == subchapter)
& (db.questions.chapter == chapter)
& (db.questions.from_source == True) # noqa: E712
& ((db.questions.optional == False) | (db.questions.optional == None))
& (db.questions.base_course == base_course)
).select(db.questions.name)
div_counts = {q.name: 0 for q in page_divids}
sid_counts = db(
(db.questions.subchapter == subchapter)
& (db.questions.chapter == chapter)
& (db.questions.base_course == base_course)
& (db.questions.from_source == True) # noqa: E712
& ((db.questions.optional == False) | (db.questions.optional == None))
& (db.questions.name == db.useinfo.div_id)
& (db.useinfo.course_id == auth.user.course_name)
& (db.useinfo.sid == auth.user.username)
).select(db.useinfo.div_id, distinct=True)
for row in sid_counts:
div_counts[row.div_id] = 1
else:
user_id = "Anonymous"
email = ""
is_logged_in = "false"
if session.readings:
reading_list = session.readings
else:
reading_list = "null"
try:
db.useinfo.insert(
sid=user_id,
act="view",
div_id=book_path,
event="page",
timestamp=datetime.datetime.utcnow(),
course_id=course.course_name,
)
except Exception as e:
logger.error(
"failed to insert log record for {} in {} : {} {} {}".format(
user_id, course.course_name, book_path, "page", "view"
)
)
logger.error("Database Error Detail: {}".format(e))
user_is_instructor = (
"true"
if auth.user and verifyInstructorStatus(auth.user.course_name, auth.user)
else "false"
)
Support Runestone Campaign
settings.show_rs_banner = True # debug only
banner_num = None
if donated:
banner_num = 0 # Thank You Banner
else:
if settings.num_banners > 0:
banner_num = random.randrange(
1, settings.num_banners + 1
) # select a random banner
else:
settings.show_rs_banner = False
questions = None
if subchapter == "Exercises":
questions = _exercises(base_course, chapter)
logger.debug("QUESTIONS = {} {}".format(subchapter, questions))
return dict(
course_name=course.course_name,
base_course=base_course,
is_logged_in=is_logged_in,
user_id=user_id,
user_email=email,
is_instructor=user_is_instructor,
allow_pairs=allow_pairs,
readings=XML(reading_list),
activity_info=json.dumps(div_counts),
downloads_enabled=downloads_enabled,
subchapter_list=_subchaptoc(base_course, chapter),
questions=questions,
motd=motd,
banner_num=banner_num,
**attrdict,
)
def _subchaptoc(course, chap):
res = db(
(db.chapters.id == db.sub_chapters.chapter_id)
& (db.chapters.course_id == course)
& (db.chapters.chapter_label == chap)
).select(
db.sub_chapters.sub_chapter_label,
db.sub_chapters.sub_chapter_name,
orderby=db.sub_chapters.sub_chapter_num,
cache=(cache.ram, 3600),
cacheable=True,
)
toclist = []
for row in res:
sc_url = "{}.html".format(row.sub_chapter_label)
title = row.sub_chapter_name
toclist.append(dict(subchap_uri=sc_url, title=title))
return toclist
def _exercises(basecourse, chapter):
Given a base course and a chapter return the instructor generated questions for the Exercises subchapter.
print("{} {}".format(chapter, basecourse))
questions = db(
(db.questions.chapter == chapter)
& (db.questions.subchapter == "Exercises")
& (db.questions.base_course == basecourse)
& (db.questions.is_private == "F")
& (db.questions.from_source == "F")
& (
(db.questions.review_flag != "T") | (db.questions.review_flag == None)
) # noqa: E711
).select(
db.questions.htmlsrc,
db.questions.author,
db.questions.difficulty,
db.questions.qnumber,
orderby=db.questions.timestamp,
)
return questions
This is copied verbatim from https://github.com/pallets/werkzeug/blob/master/werkzeug/security.py#L30.
This is copied verbatim from https://github.com/pallets/werkzeug/blob/master/werkzeug/security.py#L216.
Safely join directory
and one or more untrusted pathnames
. If this
cannot be done, this function returns None
.
:param directory: the base directory.
:param pathnames: the untrusted pathnames relative to that directory.
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)
Endpoints¶
This serves pages directly from the book’s build directory. Therefore, restrict access.
Serve from the published
directory, instead of the build
directory.
Called by default (and by published if no args)
Produce a list of books based on the directory structure of runestone/books
TODO: Port this to books in bookserver
PreTeXt books are set up with an index.thml that meta refreshes to the real start page. This is great for flexibility but not good for ?mode=browsing or for SEO scores. We can parse the “real” home page for the book from index.html