privly/privly-applications

View on GitHub
build.py

Summary

Maintainability
A
3 hrs
Test Coverage
# This python script builds Privly applications so they
# can share a common user interface and (eventually)
# support localization. If you are building
# a new application, you will probably want to copy
# an existing application folder, e.g. PlainPost,
# to a new directory and work there. When you are
# wanting to ship the application, we will work to
# integrate it with this build system.
#
# This build script looks for applications to build
# by looking at all the subfolders for manifest.json
# files. These files are expected to have the following format
#
# [
#  {
#    "release_status": "alpha", // Required values: redirect, experimental, deprecated, alpha, beta, release
#    "platforms": ["chrome"], // Optional values: web, chrome, firefox
#    "subtemplate_path": "Pages/ChromeFirstRun.html.subtemplate", // Required path to the subtemplate
#    "outfile_path": "Pages/ChromeFirstRun.html", // Required path the othe output file
#    "subtemplate_dict": {"name": "FirstRun", "action": "nav"} // Template dictionary values
#  },
#  {...}
# ]
#
# Prerequisites for running this script include
# html5lib, BeautifulSoup and Jinja2. You can install
# them all with:
#
# `pip install -r requirements.txt`
#
# This assumes you have python-pip installed:
# `sudo apt-get install python-pip`
#
# Alternatively, these can be installed using `easy_install`:
#
# `sudo easy_install html5lib beautifulsoup4 jinja2`
#
# This assumes you have python-setuptools:
# `sudo apt-get install python-setuptools`
#
# This script uses the jinja2 templating system. For
# information on Jinja2, see:
# http://jinja.pocoo.org/docs/
#
# We prefer readability over minified apps. BeautifulSoup
# properly formats the HTML so it is nested and readable.
# http://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-beautiful-soup
#
# You can run the script from the privly-applications directory:
# `python build.py`
#
# When opening pull requests to the project, you should not include the
# built HTML files. A useful command to permanently ignore all changes to
# HTML is:
# `find . -name \*.html -type f -exec git update-index --assume-unchanged '{}' \;`
# You can undo this with:
# find . -name \*.html -type f -exec git update-index --no-assume-unchanged '{}' \;
# These commands find all HTML files and ignore or show changes.

from jinja2 import Environment, FileSystemLoader
from bs4 import BeautifulSoup as bs
import os
import json
import re
import argparse # Parsing arguments

def make_readable(html):
  """
  Make the rendered HTML formatting readable
  @param {string} html The HTML that we need to make readable.
  """
  soup = bs(html, "html5lib")
  prettyHTML = soup.prettify().encode("utf8")
  
  # Beautiful soup breaks textarea formatting
  # since it adds extra whitespace. If you use "pre"
  # tags, you should be warry of the same problem
  return re.sub(r'[\ \n]{2,}</textarea>',
               "</textarea>",
               prettyHTML)

def render(outfile_path, subtemplate_path, subtemplate_dict):
  """
  Render the templates to html.
  @param {string} outfile The relative path to the file which we are rendering
    to.
  @param {string} subtemplate_path The relative path to the file of the subtemplate
    to be rendered.
  @param {dictionary} subtemplate_dict The variables required by the subtemplate.
  """
  f = open(outfile_path, 'w')
  subtemplate = env.get_template(subtemplate_path)
  html = subtemplate.render(subtemplate_dict)
  prettyHTML = make_readable(html)
  f.write(prettyHTML)
  f.close()

def is_build_target(template):
  """
  Determines whether the build target is currently active.
  @param {dictionary} template The dictionary of the object to build.
  """

  is_targeted_platform = "platforms" not in template or\
    args.platform in template["platforms"]
  is_targeted_release_type = release_titles.index(args.release) <=\
    release_titles.index(template["release_status"])

  return is_targeted_platform and is_targeted_release_type

def get_link_creation_apps():
  """
  Gets a list of the apps that will be included in the top navigation
  for generating new links
  """
  creation_apps = []
  for dirname, dirnames, filenames in os.walk('.'):
    if "manifest.json" in filenames:
      f = open(dirname + "/manifest.json", 'r')
      template_list = json.load(f)
      f.close()
      for template in template_list:
        if is_build_target(template):
          if "nav" in template.keys() and template["nav"] == "new":
            creation_apps.append(template["subtemplate_dict"]["name"])

  # Hack to maintain current app order
  creation_apps.sort()
  return creation_apps

release_titles = ["redirect", "experimental", "deprecated", "alpha", "beta", "release"]

if __name__ == "__main__":
  
  # Change the current working directory to the directory of the build script
  abspath = os.path.abspath(__file__)
  dname = os.path.dirname(abspath)
  os.chdir(dname)

  # Parse Arguments
  # Specify the potential build targets
  platforms = ['web', 'chrome', 'firefox']
  parser = argparse.ArgumentParser(description='Declare platform build target.')
  parser.add_argument('-p', '--platform', metavar='p', type=str,
                     help='The platform you are building for',
                     required=False,
                     default='web',
                     choices=platforms)
  parser.add_argument('-r', '--release', metavar='r', type=str,
                     help="""Which apps to include in the navigation:
                             experimental, deprecated, alpha, beta, release
                             building 'experimental' will build all apps,
                             whereas 'release' will only build apps marked
                             for release""",
                     required=False,
                     default='deprecated',
                     choices=release_titles)
  args = parser.parse_args()
  
  # Templates are all referenced relative to the current
  # working directory
  env = Environment(loader=FileSystemLoader('.'))
  
  # Listing of other apps so they can be added to the common nav
  packages = {"new": get_link_creation_apps()}
  
  print("################################################")
  print("Targeting the *{0}* platform".format(args.platform))
  print("To build for another platform, add the option --platform=NAME_HERE")
  print("Current platform options include {0}".format(platforms))
  print("################################################")

  # Build the templates.
  print("Building...")

  # Find all the manifest files
  for dirname, dirnames, filenames in os.walk('.'):
    if "manifest.json" in filenames:
      f = open(dirname + "/manifest.json", 'r')
      template_list = json.load(f)
      f.close()

      for template in template_list:

        # Don't build the app if a platform is specified and it is not the
        # currently targeted platform
        if not is_build_target(template):
          continue

        template["subtemplate_dict"].update({"args": args, "packages": packages})
        print("{0}'s {1} action to {2}".format(
          template["subtemplate_dict"]["name"],
          template["subtemplate_dict"]["action"],
          template["outfile_path"]))
        render(template["outfile_path"], template["subtemplate_path"],
               template["subtemplate_dict"])

print("################################################")
print("Build complete.  You can now view the generated applications in their folders.")