summaryrefslogtreecommitdiff
path: root/tools/deploystatic/deploystatic.py
diff options
context:
space:
mode:
authorMagnus Hagander2017-01-11 12:03:52 +0000
committerMagnus Hagander2017-01-20 19:57:36 +0000
commit742d65e5f348d2c45917333317a62645b462517c (patch)
treeddb1a9b6fe2ac7423f7fb87af88691803a405ae3 /tools/deploystatic/deploystatic.py
parent02738d0bda1d2cb9d8415bda2c308cc5b8256132 (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-xtools/deploystatic/deploystatic.py176
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))