"""
Generates an HTML schedule for CSSE 120 from the list of
learning objectives and relevant information about the term dates.
Requires (via pip): gfm bs4 htmllib
Author: David Mutchler and his colleagues.
"""
import datetime
import string
import re
import abc
import gfm
import bs4 # BeautifulSoup
import enum
COMMENT_BEGIN_INDICATOR = "!!! BEGIN COMMENT"
COMMEND_END_INDICATOR = "!!! END COMMENT"
COMMENT_INDICATOR = "!!!"
SESSION_INDICATOR = r'^.*Session[ ]*[0-9]*:'
EXAM_INDICATOR = r'Exam '
EVENING_EXAM_TEMPLATE = string.Template("""
The regular in-class session on $REGULAR_DAY
is an OPTIONAL review session.
The test itself is
$EXAM_DAY
evening from 7:30 p.m. to 9:30 p.m.
(with an additional 30-minute grace period)
in rooms TBA.
""")
SHOW_TOPICS = False
@enum.unique
class SessionType(enum.Enum):
REGULAR = 1
NO_CLASS = 2
EVENING_EXAM = 3
IN_CLASS_EXAM = 4
PROJECT = 5
OTHER = 6
# The following really are defined, but PyDev does not think so, apparently.
RE_MULTILINE = re.MULTILINE # @UndefinedVariable
RE_DOTALL = re.DOTALL # @UndefinedVariable
class TermInfo(object):
"""
Everything the ScheduleMaker needs to know about the term,
except for the learning objectives.
"""
def __init__(self, term):
""" term is a string that identifies the term, e.g. 201720 """
# FIXME: Some of this does not belong in TERM information.
# TODO: Get the following from a file(s) instead of like this:
self.term = term
self.start_date = datetime.date(2017, 8, 28)
self.end_date = datetime.date(2017, 11, 9)
self.days_of_week = [1, 3, 4] # isoweekday: Monday is 1, Sunday is 7
self.weekdays_of_week = ['Monday', 'Wednesday', 'Thursday']
self.dates_to_skip = [ScheduleDate(2017, 8, 28, "No class"),
ScheduleDate(2017, 8, 30, "No class"),
ScheduleDate(2017, 10, 12, "Fall break")
]
exam1_msg = EVENING_EXAM_TEMPLATE.substitute(REGULAR_DAY='Thursday',
EXAM_DAY='Thursday')
exam2_msg = exam1_msg
self.evening_exams = [ScheduleDate(2017, 9, 14, exam1_msg),
ScheduleDate(2017, 10, 5, exam2_msg)]
self.start_session_number = 1
csse120_folder = "/Users/david/classes/120/"
term_folder = csse120_folder + 'Public/' + self.term + '/'
le_folder = term_folder + 'LearningObjectives/'
le_file = 'LearningObjectives.txt'
self.learning_objectives_filename = le_folder + le_file
st_folder = term_folder
st_file = 'index.html' # EVENTUALLY RETURN to: schedule_table.html
self.schedule_table_filename = st_folder + st_file
self.number_of_sessions = 30
class ScheduleDate(object):
def __init__(self, year, day, month, message=None):
self.datetime_date = datetime.date(year, day, month)
self.message = message
def __repr__(self):
return '{} {}'.format(self.datetime_date, self.message)
def __eq__(self, date):
# Date may be a ScheduleDate or it may be a datetime.date.
try:
return (self.datetime_date == date.datetime_date
and self.message == date.message)
except AttributeError:
return self.datetime_date == date
class ScheduleMaker(object):
"""
Generates an HTML schedule for CSSE 120 from the list of
learning objectives and relevant information about the term dates.
"""
def __init__(self, term_info):
"""
Given a file of Learning Objectives and information about the
current term, makes an HTML file for the Schedule Page.
:type term_info: TermInfo
"""
self.term_info = term_info
self.raw_data = None
self.topics_by_session = None
def make_schedule(self):
# Get the data from the Learning Objectives file:
self.get_raw_data()
# Parse the Learning Objectives file to get a list of ClassSession
# objects, one per session, with each ClassSession object having
# a title and GFM topics:
self.split_into_sessions()
# Go through the list of sessions, adding dates, session numbers,
# and (where needed) NoClassSession objects:
self.add_dates_numbers_and_NoClassSessions()
# Make the HTML for each session:
for session in self.sessions:
session.make_html()
# Make and prettify the entire table of sessions.
self.make_html_table()
# Prettify here?? Or just per session???
self.write_html(self.html)
def get_raw_data(self):
with open(self.term_info.learning_objectives_filename, 'r') as file:
self.raw_data = file.read()
def split_into_sessions(self):
"""
Parses the raw data to make a list of ClassSession objects.
The object will NOT yet have a correct Session Number or Date.
"""
# FIXME: The above comment is no longer completely accurate.
# Strip comments from the raw data:
data = self.strip_comments()
# Split the data by sessions:
self.topics_by_session = re.split(SESSION_INDICATOR, data,
flags=RE_MULTILINE)
self.topics_by_session = self.topics_by_session[1:] # Ignore 1st item
# Confirm that the number of topics matches the number of sessions.
try:
assert(len(self.topics_by_session)
== self.term_info.number_of_sessions)
except AssertionError:
raise AssertionError("Number of topics != number of sessions")
# Make the ClassSession objects. No dates or session numbers yet.
self.sessions = []
for k in range(len(self.topics_by_session)):
lines = self.topics_by_session[k].split('\n')
# Title is first line (which was on the same line as SessionXX.
title = lines[0]
# Rest are the topics for that session, in GFM.
topics = '\n'.join(lines[1:])
self.topics_by_session[k] = topics
session = ClassSession(title=title, topics=topics)
self.sessions.append(session)
def strip_comments(self):
# Parse by lines.
lines = self.raw_data.split('\n')
# Remove lines between a COMMENT_BEGIN_INDICATOR
# and the next COMMENT_END_INDICATOR, then any with COMMENT_INDICATOR.
i_am_inside_a_comment = False
lines_to_keep = []
for line in lines:
if i_am_inside_a_comment:
if COMMEND_END_INDICATOR in line:
i_am_inside_a_comment = False
else:
if COMMENT_BEGIN_INDICATOR in line:
i_am_inside_a_comment = True
elif COMMENT_INDICATOR not in line:
lines_to_keep.append(line)
return '\n'.join(lines_to_keep)
def add_dates_numbers_and_NoClassSessions(self):
date = self.term_info.start_date
session_number = self.term_info.start_session_number
session_index = 0
while True:
# Go through each date from the start of term to end of term.
# Done when we get to the last date of the term.
if date > self.term_info.end_date:
break
# Deal only with dates that are on class days-of-week.
if date.isoweekday() in self.term_info.days_of_week:
if date in self.term_info.dates_to_skip:
# Make a NoClassSession
index = self.term_info.dates_to_skip.index(date)
schedule_date = self.term_info.dates_to_skip[index]
session = NoClassSession(schedule_date)
self.sessions.insert(session_index, session)
else:
# Add date and session number to this session.
session = self.sessions[session_index]
session.datetime_date = date
session.session_number = session_number
session_number = session_number + 1
if date in self.term_info.evening_exams:
evening_exams = self.term_info.evening_exams
session.session_type = SessionType.EVENING_EXAM
index = evening_exams.index(date)
session.message = evening_exams[index].message
session_index = session_index + 1
date = date + datetime.timedelta(1)
def make_html_table(self):
"""
Sets self.html to the HTML for the schedule page,
based on the (previously computed) HTML for the self.sessions
"""
days_in_week = len(self.term_info.days_of_week)
sessions_html = ''
for k in range(len(self.sessions)):
if k % days_in_week == 0:
sessions_html += ScheduleTable.ROW_START
sessions_html += (ScheduleTable.ITEM_START
+ self.sessions[k].html
+ ScheduleTable.ITEM_END)
if k % days_in_week == days_in_week - 1:
sessions_html += ScheduleTable.ROW_END
self.html = (ScheduleHeader(self.term_info.weekdays_of_week).html
+ sessions_html
+ ScheduleTrailer().html)
def write_html(self, html):
with open(self.term_info.schedule_table_filename, 'w') as file:
file.write(html)
# def add_topics_and_html_to_sessions(self, topics_by_session, sessions):
# k = 0
# for session in sessions:
# if isinstance(session, NoClassSession):
# continue
# session.topics = topics_by_session[k]
# session.topics_html = self.make_html_from_topics(session.topics)
# k = k + 1
def make_html_from_topics(self, topics_for_session):
# Make the session title become a top-level list item,
# and increase the indentation of all subsequent lines.
topics_for_session = topics_for_session.replace('\n',
'\n ')
topics_for_session = '+ ' + topics_for_session
# print(topics_for_session)
# Convert each topic to HTML using Github Flavored Markdown.
html = gfm.markdown(topics_for_session)
# print(html)
# Add class info to the HTML for topics, tests, and sprints.
# The following is brittle.
ul_class = 'topics collapsibleList'
li_class = 'topic'
if re.match(r'.*Test [0-9]\..*', html, RE_DOTALL):
ul_class += ' exam'
li_class += ' exam'
elif re.match(r'.*Sprint [0-9].*', html, RE_DOTALL):
ul_class += ' sprint'
li_class += ' sprint'
# CONSIDER: the following adds the class to the FIRST
# but to ALL - 's. Is that what we want for sub-lists?
html = html.replace('