Skip to content
Snippets Groups Projects
Commit 6e8a62aa authored by Greg Wilson's avatar Greg Wilson
Browse files

Validation tool for lesson sites

parent 84b45ebe
Branches
Tags
No related merge requests found
......@@ -2,57 +2,59 @@
Display syllabus in tabular form.
Days are displayed if at least one episode has 'start = true'.
{% endcomment %}
<h2>Schedule</h2>
<div class="syllabus">
<h2>Schedule</h2>
{% assign day = 0 %}
{% assign multiday = false %}
{% for episode in site.episodes %}
{% if episode.start %}{% assign multiday = true %}{% break %}{% endif %}
{% endfor %}
{% assign current = site.start_time %}
{% assign day = 0 %}
{% assign multiday = false %}
{% for episode in site.episodes %}
{% if episode.start %}{% assign multiday = true %}{% break %}{% endif %}
{% endfor %}
{% assign current = site.start_time %}
<table class="table table-striped">
{% for episode in site.episodes %}
{% if episode.start %} {% comment %} Starting a new day? {% endcomment %}
{% assign day = day | plus: 1 %}
{% if day > 1 %} {% comment %} If about to start day 2 or later, show finishing time for previous day {% endcomment %}
{% assign hours = current | divided_by: 60 %}
{% assign minutes = current | modulo: 60 %}
<tr>
{% if multiday %}<td></td>{% endif %}
<td class="col-md-1">{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}</td>
<td class="col-md-3">Finish</td>
<td class="col-md-7"></td>
</tr>
<table class="table table-striped">
{% for episode in site.episodes %}
{% if episode.start %} {% comment %} Starting a new day? {% endcomment %}
{% assign day = day | plus: 1 %}
{% if day > 1 %} {% comment %} If about to start day 2 or later, show finishing time for previous day {% endcomment %}
{% assign hours = current | divided_by: 60 %}
{% assign minutes = current | modulo: 60 %}
<tr>
{% if multiday %}<td></td>{% endif %}
<td class="col-md-1">{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}</td>
<td class="col-md-3">Finish</td>
<td class="col-md-7"></td>
</tr>
{% endif %}
{% assign current = site.start_time %} {% comment %}Re-set start time of this episode to general daily start time {% endcomment %}
{% endif %}
{% assign current = site.start_time %} {% comment %}Re-set start time of this episode to general daily start time {% endcomment %}
{% endif %}
{% assign hours = current | divided_by: 60 %}
{% assign minutes = current | modulo: 60 %}
<tr>
{% if multiday %}<td class="col-md-1">{% if episode.start %}Day {{ day }}{% endif %}</td>{% endif %}
<td class="col-md-1">{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}</td>
<td class="col-md-3">
<a href="{{ site.root }}/{{ episode.url }}">{{ episode.title }}</a>
</td>
<td class="col-md-7">
{% if episode.break %}
Break
{% else %}
{% if episode.questions %}
{{ episode.questions | join: '<br/>' }}
{% endif %}
{% endif %}
</td>
</tr>
{% assign current = current | plus: episode.teaching | plus: episode.exercises | plus: episode.break %}
{% endfor %}
{% assign hours = current | divided_by: 60 %}
{% assign minutes = current | modulo: 60 %}
<tr>
{% if multiday %}<td class="col-md-1">{% if episode.start %}Day {{ day }}{% endif %}</td>{% endif %}
{% if multiday %}<td></td>{% endif %}
<td class="col-md-1">{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}</td>
<td class="col-md-3">
<a href="{{ site.root }}/{{ episode.url }}">{{ episode.title }}</a>
</td>
<td class="col-md-7">
{% if episode.break %}
Break
{% else %}
{% if episode.questions %}
{{ episode.questions | join: '<br/>' }}
{% endif %}
{% endif %}
</td>
<td class="col-md-3">Finish</td>
<td class="col-md-7"></td>
</tr>
{% assign current = current | plus: episode.teaching | plus: episode.exercises | plus: episode.break %}
{% endfor %}
{% assign hours = current | divided_by: 60 %}
{% assign minutes = current | modulo: 60 %}
<tr>
{% if multiday %}<td></td>{% endif %}
<td class="col-md-1">{% if hours < 10 %}0{% endif %}{{ hours }}:{% if minutes < 10 %}0{% endif %}{{ minutes }}</td>
<td class="col-md-3">Finish</td>
<td class="col-md-7"></td>
</tr>
</table>
</table>
</div>
#!/usr/bin/env python
'''
Validate layout of generated HTML pages.
(We validate the HTML because source may not be in Markdown.)
'''
import sys
import os
import glob
import fnmatch
import yaml
from optparse import OptionParser
from bs4 import BeautifulSoup
from lxml import etree
# Default configuration.
DEFAULT_CONFIG = '''\
patterns:
'*.html':
- has_title_in_head
- has_navbar
- has_title_in_body
- has_footer
index.html:
- has_prereq
- has_syllabus
'*-*/index.html':
- has_objectives
'''
# Record all the rules.
RULES = {}
def rule(fn):
RULES[fn.__name__] = fn
return fn
def main():
'''Main driver: check all files with all rules that apply.'''
args = parse_args()
read_config(args)
docs = read_all_docs(args.source_dir)
_require(docs, 'No source files found in {0}'.format(args.source_dir))
all_filenames = docs.keys()
for filename in all_filenames:
if args.verbose > 0:
print(filename, '...', file=sys.stderr)
for pattern in args.patterns:
full_pattern = os.path.join(args.source_dir, pattern)
if fnmatch.fnmatch(filename, full_pattern):
for rule in args.patterns[pattern]:
if args.verbose > 1:
print('...', rule, file=sys.stderr)
RULES[rule](filename, docs[filename])
def parse_args():
'''Parse command-line arguments.'''
parser = OptionParser()
parser.add_option('-c', '--config',
default=None,
dest='config_file',
help='configuration file')
parser.add_option('-s', '--source',
default='_site',
dest='source_dir',
help='source directory')
parser.add_option('-v', '--verbose',
default=0,
action='count',
dest='verbose',
help='report actions')
args, extras = parser.parse_args()
_require(not extras, 'Unexpected trailing command-line arguments "{0}"'.format(extras))
return args
def read_config(args):
'''
Read configuration file.
'''
if args.config_file:
with open(args.config_file, 'r') as reader:
args.config = yaml.load(reader)
else:
args.config = yaml.load(DEFAULT_CONFIG)
args.patterns = args.config['patterns']
def read_all_docs(source_dir):
'''
Read all HTML pages under the source directory.
Returns a dictionary of (path, doc).
'''
pattern = os.path.join(source_dir, '**/*.html')
result = {}
for path in glob.iglob(pattern, recursive=True):
try:
with open(path, 'r') as reader:
raw = reader.read().replace('<!doctype html>\n', '')
soup = BeautifulSoup(raw, 'html.parser').prettify()
doc = etree.fromstring(soup)
result[path] = doc
except IOError as e:
print('Unable to open {0}: {1}'.format(path, e), file=sys.stderr)
sys.exit(1)
return result
@rule
def has_footer(filename, doc):
'''Document has footer element.'''
_check_1(filename, doc, 'footers', '//footer')
@rule
def has_navbar(filename, doc):
'''Document has header element.'''
_check_1(filename, doc, 'div navbar', '//div[@class="navbar-header"]')
@rule
def has_objectives(filename, doc):
'''Episode has objectives.'''
_check_1(filename, doc, 'objectives div', '//blockquote[@class="objectives"]')
@rule
def has_prereq(filename, doc):
'''Index page has prerequisites block.'''
_check_1(filename, doc, 'prerequisites blockquote', '//blockquote[@class="prereq"]')
@rule
def has_syllabus(filename, doc):
'''Index page has syllabus.'''
_check_1(filename, doc, 'syllabus', '//div[@class="syllabus"]')
_check_1(filename, doc, 'syllabus title', '//div[@class="syllabus"]/h2')
_check_1(filename, doc, 'syllabus table', '//div[@class="syllabus"]/table')
@rule
def has_title_in_head(filename, doc):
'''Document has a title in the head.'''
_check_1(filename, doc, 'title in head', '//head//title')
@rule
def has_title_in_body(filename, doc):
'''Document has a title in the body.'''
_check_1(filename, doc, 'title in body', '//body//h1[@class="maintitle"]')
def _check_1(filename, doc, rulename, xpath):
'''Check that an equality holds.'''
actual = doc.xpath(xpath)
if len(actual) != 1:
print('In {0}, checking {1}: expected 1 match, got {2}'.format(filename, rulename, len(actual)))
def _require(condition, message):
'''Fail if condition not met.'''
if not condition:
print(message, file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment