diff options
author | Magnus Hagander | 2017-01-11 12:03:52 +0000 |
---|---|---|
committer | Magnus Hagander | 2017-01-20 19:57:36 +0000 |
commit | 742d65e5f348d2c45917333317a62645b462517c (patch) | |
tree | ddb1a9b6fe2ac7423f7fb87af88691803a405ae3 /tools/deploystatic/deploystatic.py | |
parent | 02738d0bda1d2cb9d8415bda2c308cc5b8256132 (diff) |
Implement jinja2 based templating for confreg
This implements the ability to render confreg templates (registration,
cfp, schedules etc etc) using jinja2 instead of django templates. The
important difference is that these templates are rendered in a complete
sandbox, so they cannot reach into other parts of the system by
exploiting connected objects or by including templates they are not
supposed to.
Jinja templates are used whenever the "jinjadir" variable is set on a
conference. When it is, the variables for basetemplate, templatemodule,
templateoverride and templatemediabase are all ignored, as their
functionality is either no longer needed or implemented in a different
way using the templates.
For the time being we support both the old (django based) templates and
the new (jinja based) templates. That means that any changes made to the
confreg templates must be done twice. At some point not too far in the
future we should decide to either desupport old conferences that have
the old style templates, or re-render those as static. (For closed
conferences most pages aren't reachable anyway, but things like
schedule and session descriptions are reachable way past the end of
a conference)
Along with the templates come a new command called "deploystatic.py",
which runs outside the django environment. This command can be used for
deployment of static sites based on the jinja templates, similar to how
some conference sites have done it before. Since the templates run in a
sandbox, this should be much more safe than what's been done before, and
therefor access can be granted to more people. This command is made to
run standalone so conference template developers can run it locally
without having to install full support for django.
Diffstat (limited to 'tools/deploystatic/deploystatic.py')
-rwxr-xr-x | tools/deploystatic/deploystatic.py | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/tools/deploystatic/deploystatic.py b/tools/deploystatic/deploystatic.py new file mode 100755 index 00000000..8a425ee3 --- /dev/null +++ b/tools/deploystatic/deploystatic.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Deploy a static site based on jinja2 templates (sanxboxed) + +import argparse +import sys +import os +import os.path +import filecmp +import shutil +import json +import random + +import jinja2 +import jinja2.sandbox + +# +# Some useful filters. We include them inline in this file to make it +# standalone useful. +# +# Like |groupby, except support grouping by objects and not just by values, and sort by +# attributes on the grouped objects. +def filter_groupby_sort(objects, keyfield, sortkey): + group = [(key, list(group)) for key, group in groupby(objects, lambda x: getattr(x, keyfield))] + return sorted(group, key=lambda y: y[0] and getattr(y[0], sortkey) or None) + +# Shuffle the order in a list, for example to randomize the order of sponsors +def filter_shuffle(l): + try: + r = list(l) + random.shuffle(r) + return r + except: + return l + +global_filters = { + 'groupby_sort': filter_groupby_sort, + 'shuffle': filter_shuffle, +} + + +# Optionally load a JSON context +def load_context(jsonfile): + if os.path.isfile(jsonfile): + with open(jsonfile) as f: + return json.load(f) + else: + return {} + +# Locaate which git revision we're on +def find_git_revision(path): + while path != '/': + if os.path.exists(os.path.join(path, ".git/HEAD")): + # Found it! + with open(os.path.join(path, '.git/HEAD')) as f: + ref = f.readline().strip() + if not ref.startswith('ref: refs/heads/'): + print "Invalid git reference {0}".format(ref) + return None + refname = os.path.join(path, ".git/", ref[5:]) + if not os.path.isfile(refname): + print "Could not find git ref {0}".format(refname) + return None + with open(refname) as f: + fullref = f.readline() + return fullref[:7] + # Else step up one level + path = os.path.dirname(path) + return None + +# Actual deployment function +def deploy_template(env, template, destfile, context): + t = env.get_template(template) + s = t.render(**context).encode('utf8') + + # Only write the file if it has actually changed + if os.path.isfile(destfile): + with open(destfile) as f: + if f.read() == s: + return + + with open(destfile, 'w') as f: + f.write(s) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Deploy jinja based static site') + parser.add_argument('sourcepath', type=str, help='Source path') + parser.add_argument('destpath', type=str, help='Destination path') + + args = parser.parse_args() + + if not os.path.isabs(args.sourcepath): + print "Source path is not absolute!" + sys.exit(1) + if not os.path.isabs(args.destpath): + print "Destination path isn ot absolute!" + sys.exit(1) + + if not os.path.isdir(args.sourcepath): + print "Source directory does not exist!" + sys.exit(1) + + if not os.path.isdir(args.destpath): + print "Destination directory does not exist!" + sys.exit(1) + + for d in ('templates', 'templates/pages', 'static'): + if not os.path.isdir(os.path.join(args.sourcepath, d)): + print "'{0}' subdirectory does not exist in source!".format(d) + sys.exit(1) + + staticroot = os.path.join(args.sourcepath, 'static/') + staticdest = os.path.join(args.destpath, 'static/') + + # Set up jinja environment + env = jinja2.sandbox.SandboxedEnvironment(loader=jinja2.FileSystemLoader([os.path.join(args.sourcepath, 'templates/'),])) + env.filters.update(global_filters) + + # If there is a context json, load it as well + context = load_context(os.path.join(args.sourcepath, 'templates', 'context.json')) + + # Fetch the current git revision if this is coming out of a git repository + context['githash'] = find_git_revision(args.sourcepath) + + # Load a context that can override everything, including static hashes + context.update(load_context(os.path.join(args.sourcepath, 'templates', 'context.override.json'))) + + + knownfiles = [] + # We could use copytree(), but we need to know which files are there so we can + # remove old files, so we might as well do the full processing this way. + for dn, subdirs, filenames in os.walk(staticroot): + relpath = os.path.relpath(dn, staticroot) + if not os.path.isdir(os.path.join(staticdest, relpath)): + os.makedirs(os.path.join(staticdest, relpath)) + + for fn in filenames: + fullsrc = os.path.join(staticroot, relpath, fn) + fulldest = os.path.join(staticdest, relpath, fn) + if (not os.path.exists(fulldest)) or (not filecmp.cmp(fullsrc, fulldest)): + shutil.copy2(fullsrc, fulldest) + knownfiles.append(os.path.join('static', relpath, fn)) + + pagesroot = os.path.join(args.sourcepath, 'templates/pages') + for fn in os.listdir(pagesroot): + # We don't use subdirectories yet, so don't bother even looking at that + if os.path.splitext(fn)[1] != '.html': + continue + + if fn == 'index.html': + destdir = '' + else: + destdir = os.path.splitext(fn)[0] + + if not os.path.isdir(os.path.join(args.destpath, destdir)): + os.mkdir(os.path.join(args.destpath, destdir)) + + context['page'] = destdir + + deploy_template(env, os.path.join('pages', fn), + os.path.join(args.destpath, destdir, 'index.html'), + context) + + knownfiles.append(os.path.join(destdir, 'index.html')) + + # Look for things to remove + for dn, subdirs, filenames in os.walk(args.destpath): + relpath = os.path.relpath(dn, args.destpath) + if relpath == '.': + relpath = '' + for fn in filenames: + f = os.path.join(relpath, fn) + if not f in knownfiles: + os.unlink(os.path.join(args.destpath, f)) |