Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

create open source repo

  • Loading branch information...
commit 5c7528878f27fa8632e0cfcff33c46392f427270 0 parents
@JoeGermuska JoeGermuska authored
Showing with 18,082 additions and 0 deletions.
  1. +3 −0  .gitignore
  2. +21 −0 LICENSE
  3. +5 −0 README
  4. +8 −0 apache/error.html
  5. +13 −0 apache/index.html
  6. +46 −0 apache/production/apache
  7. +34 −0 apache/production/apache_maintenance
  8. +23 −0 apache/production/logging.txt
  9. +39 −0 apache/staging/apache
  10. +32 −0 apache/staging/apache_maintenance
  11. +190 −0 app/app.py
  12. +47 −0 app/config.py
  13. +18 −0 app/distill_sections.py
  14. +34 −0 app/production.wsgi
  15. +460 −0 app/render_gallery.py
  16. +83 −0 app/s3deploy.py
  17. +1 −0  app/sections.json
  18. +756 −0 app/sections.txt
  19. +34 −0 app/staging.wsgi
  20. +1 −0  app/static/.gitignore
  21. +686 −0 app/static/bootstrap/css/bootstrap-responsive.css
  22. +12 −0 app/static/bootstrap/css/bootstrap-responsive.min.css
  23. +3,990 −0 app/static/bootstrap/css/bootstrap.css
  24. +689 −0 app/static/bootstrap/css/bootstrap.min.css
  25. BIN  app/static/bootstrap/img/glyphicons-halflings-white.png
  26. BIN  app/static/bootstrap/img/glyphicons-halflings.png
  27. +210 −0 app/static/bootstrap/js/bootstrap-modal.js
  28. +271 −0 app/static/bootstrap/js/bootstrap-typeahead.js
  29. +1,726 −0 app/static/bootstrap/js/bootstrap.js
  30. +6 −0 app/static/bootstrap/js/bootstrap.min.js
  31. +36 −0 app/static/css/admin.css
  32. BIN  app/static/img/logo-med-white.png
  33. +47 −0 app/static/js/jquery.spin.js
  34. +51 −0 app/static/js/jquery.validate.min.js
  35. +2 −0  app/static/js/spin.min.js
  36. +31 −0 app/static/js/underscore-min.js
  37. +8 −0 app/static/maintenance.html
  38. +4 −0 app/templates/_section_options.html
  39. +25 −0 app/templates/base.html
  40. +20 −0 app/templates/done.html
  41. +18 −0 app/templates/error.html
  42. +71 −0 app/templates/front.html
  43. +111 −0 app/templates/review.html
  44. +89 −0 app/test.wsgi
  45. +686 −0 assets/bootstrap/css/bootstrap-responsive.css
  46. +12 −0 assets/bootstrap/css/bootstrap-responsive.min.css
  47. +3,990 −0 assets/bootstrap/css/bootstrap.css
  48. +689 −0 assets/bootstrap/css/bootstrap.min.css
  49. BIN  assets/bootstrap/img/glyphicons-halflings-white.png
  50. BIN  assets/bootstrap/img/glyphicons-halflings.png
  51. +210 −0 assets/bootstrap/js/bootstrap-modal.js
  52. +1,726 −0 assets/bootstrap/js/bootstrap.js
  53. +6 −0 assets/bootstrap/js/bootstrap.min.js
  54. +79 −0 assets/css/gallery.css
  55. BIN  assets/img/logo-med-white.png
  56. BIN  assets/img/next.png
  57. BIN  assets/img/previous.png
  58. +142 −0 assets/js/gallery.js
  59. +20 −0 assets/js/jquery.smooth-scroll.min.js
  60. +31 −0 assets/js/underscore-min.js
  61. +203 −0 assets/templates/default.html
  62. +17 −0 assets/templates/default_opengraph.html
  63. +8 −0 assets/templates/maintenance.html
  64. +265 −0 fabfile.py
  65. +18 −0 requirements.txt
  66. +29 −0 tests/test_render_gallery.py
3  .gitignore
@@ -0,0 +1,3 @@
+.DS_Store
+*.pyc
+out
21 LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2012 The Chicago Tribune
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
5 README
@@ -0,0 +1,5 @@
+This is a python-flask application which generates big photo galleries from photo gallery content items in Tribune Co's content management system. If you don't have access to a Tribune Co CMS, you are not likely to get much use out of this, but feel free to peek at the code. This should be seen as a snapshot of work-in-progress, and not necessarily a perfect example of best-practices for development.
+
+Read http://blog.apps.chicagotribune.com/2012/05/29/covering-the-nato-summit/ for some more about this and a related tool.
+
+See LICENSE for license information.
8 apache/error.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Not found</title>
+ </head>
+ <body>
+ The page you requested was not found.
+ </body>
+</html>
13 apache/index.html
@@ -0,0 +1,13 @@
+<html>
+ <head>
+ <title>Chicago Tribune Photo Galleries</title>
+ <script type="text/javascript" charset="utf-8">
+ function redir() {
+ window.location.href = 'http://www.chicagotribune.com/news/photo/';
+ }
+ </script>
+ </head>
+ <body onload='redir();'>
+
+ </body>
+</html>
46 apache/production/apache
@@ -0,0 +1,46 @@
+<VirtualHost *:80>
+ServerName gallery.tribapps.com
+ServerAlias www.gallery.tribapps.com
+ServerAlias gallery.beta.tribapps.com
+ServerAlias www.gallery.beta.tribapps.com
+
+ SetEnv DEPLOYMENT_TARGET production
+
+ SetEnvIf X-Forwarded-For "^163\.192\..*\..*" trib
+ <Location />
+ Order Deny,Allow
+ Allow from all
+ </Location>
+
+ <Directory /home/newsapps/sites/gallery/repository/app>
+ AuthType Basic
+ AuthName "Authorized Access Only"
+ AuthUserFile /mnt/apps/passwords
+ Require valid-user
+ </Directory>
+
+ Redirect permanent /favicon.ico http://media.apps.chicagotribune.com/favicon.ico
+
+ WSGIScriptAlias / /home/newsapps/sites/gallery/repository/app/production.wsgi
+
+ Redirect permanent /favicon.ico http://media.apps.chicagotribune.com/favicon.ico
+
+ ErrorLog /home/newsapps/logs/gallery.error.log
+ LogLevel warn
+
+ SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" is-forwarder
+ LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
+ LogFormat "[%h] %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio2
+ CustomLog /home/newsapps/logs/gallery.access.log combinedio env=is-forwarder
+ CustomLog /home/newsapps/logs/gallery.access.log combinedio2 env=!is-forwarder
+
+ ServerSignature Off
+
+ RewriteEngine on
+ # canonical hostname
+ # RewriteCond %{HTTP_HOST} ^www.gallery.chicagotribune.com [NC]
+ # RewriteRule ^/(.*) http://gallery.tribapps.com/$1 [L,R]
+
+ RewriteCond %{REQUEST_URI} /maintenance.html$
+ RewriteRule $ / [R=302,L]
+</VirtualHost>
34 apache/production/apache_maintenance
@@ -0,0 +1,34 @@
+<VirtualHost *:80>
+ServerName gallery.beta.tribapps.com
+ServerAlias www.gallery.beta.tribapps.com
+
+ SetEnvIf X-Forwarded-For "^163\.192\..*\..*" trib
+ <Location /> # until launch
+ Order Deny,Allow
+ # Allow from all
+ Allow from env=trib
+ </Location>
+
+ Redirect permanent /favicon.ico http://media.apps.chicagotribune.com/favicon.ico
+
+ ErrorLog /home/newsapps/logs/gallery.error.log
+ LogLevel warn
+
+ SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" is-forwarder
+ LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
+ LogFormat "[%h] %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio2
+ CustomLog /home/newsapps/logs/gallery.access.log combinedio env=is-forwarder
+ CustomLog /home/newsapps/logs/gallery.access.log combinedio2 env=!is-forwarder
+
+ ServerSignature Off
+
+ RewriteEngine on
+ # canonical hostname
+ RewriteCond %{HTTP_HOST} ^gallery.tribapps.com [NC]
+ RewriteRule ^/(.*) http://gallery.tribapps.com/$1 [L,R]
+
+ DocumentRoot /home/newsapps/sites/gallery/repository/app/static
+
+ RewriteCond %{REQUEST_URI} !/maintenance.html$
+ RewriteRule $ /maintenance.html [R=302,L]
+</VirtualHost>
23 apache/production/logging.txt
@@ -0,0 +1,23 @@
+[loggers]
+keys=root
+
+[handlers]
+keys=handler
+
+[formatters]
+keys=formatter
+
+[logger_root]
+level=INFO
+handlers=handler
+
+[handler_handler]
+class=FileHandler
+level=NOTSET
+formatter=formatter
+args=('/home/newsapps/logs/gallery.wsgi.log', 'w')
+
+[formatter_formatter]
+format=%(asctime)s %(levelname)s %(message)s
+datefmt=
+class=logging.Formatter
39 apache/staging/apache
@@ -0,0 +1,39 @@
+<VirtualHost *:80>
+ ServerName gallery.beta.tribapps.com
+ ServerAlias www.gallery.beta.tribapps.com
+
+ SetEnv DEPLOYMENT_TARGET staging
+
+ SetEnvIf X-Forwarded-For "^163\.192\..*\..*" trib
+ <Location />
+ Order Deny,Allow
+ Allow from all
+ </Location>
+
+ <Directory /home/newsapps/sites/gallery/repository/app>
+ AuthType Basic
+ AuthName "Authorized Access Only"
+ AuthUserFile /mnt/apps/passwords
+ Require valid-user
+ </Directory>
+
+ WSGIScriptAlias / /home/newsapps/sites/gallery/repository/app/staging.wsgi
+
+ Redirect permanent /favicon.ico http://media-beta.tribapps.com/favicon.ico
+
+ ErrorLog /home/newsapps/logs/gallery.error.log
+ LogLevel warn
+
+ CustomLog /home/newsapps/logs/gallery.access.log combined
+
+ ServerSignature Off
+
+ RewriteEngine on
+
+ # canonical hostname
+ RewriteCond %{HTTP_HOST} ^www.gallery.beta.tribapps.com [NC]
+ RewriteRule ^/(.*) http://gallery.beta.tribapps.com/$1 [L,R]
+
+ RewriteCond %{REQUEST_URI} /maintenance.html$
+ RewriteRule $ / [R=302,L]
+</VirtualHost>
32 apache/staging/apache_maintenance
@@ -0,0 +1,32 @@
+<VirtualHost *:80>
+ServerName gallery.beta.tribapps.com
+ServerAlias www.gallery.beta.tribapps.com
+
+ <Directory /home/newsapps/sites/gallery/repository/app>
+ Order allow,deny
+ Allow from 163.192.0.0/16
+ Allow from 163.193.0.0/16
+ Allow from 163.194.0.0/16
+ </Directory>
+
+ Redirect permanent /favicon.ico http://media-beta.tribapps.com/favicon.ico
+
+ Alias /robots.txt /home/newsapps/sites/gallery/repository/gallery/assets/robots.txt
+
+ ErrorLog /home/newsapps/logs/gallery.error.log
+ LogLevel warn
+
+ CustomLog /home/newsapps/logs/gallery.access.log combined
+
+ ServerSignature Off
+
+ RewriteEngine on
+ # canonical hostname
+ RewriteCond %{HTTP_HOST} ^www.gallery.beta.tribapps.com [NC]
+ RewriteRule ^/(.*) http://gallery.beta.tribapps.com/$1 [L,R]
+
+ DocumentRoot /home/newsapps/sites/gallery/repository/app/static
+
+ RewriteCond %{REQUEST_URI} !/maintenance.html$
+ RewriteRule $ /maintenance.html [R=302,L]
+</VirtualHost>
190 app/app.py
@@ -0,0 +1,190 @@
+import sys, os.path
+CURRENT_DIR = os.path.dirname(__file__)
+sys.path.append(CURRENT_DIR)
+import config
+import logging
+import traceback
+from flask import Flask, request, session, redirect, render_template, g, flash
+import render_gallery
+import requests
+import json
+import s3deploy
+import time
+from optparse import OptionParser
+
+# create our little application :)
+app = Flask(__name__)
+app.secret_key = 'MAKE_THIS_SOMETHING_UNIQUE'
+CURRENT_DIR,filename = os.path.split(__file__)
+SECTIONS = json.load(open(os.path.join(CURRENT_DIR,'sections.json')))
+
+@app.before_request
+def global_settings():
+ try:
+ g.settings = app.config['SETTINGS']
+ except KeyError:
+ try:
+ g.settings = config.get_settings(request.environ['DEPLOYMENT_TARGET'])
+ except:
+ g.settings = config.get_settings()
+
+ g.renderer = render_gallery.GalleryRenderer(g.settings)
+
+@app.template_filter('propose_storylink_slug')
+def propose_storylink_slug(pg_slug):
+ if pg_slug.endswith('-pg'):
+ pg_slug = pg_slug[:-3]
+ return pg_slug + "-gallery-link"
+
+@app.route("/")
+def hello():
+ env = app.create_jinja_environment()
+ photo_gallery_slug = session.pop('photo_gallery_slug','')
+ section = session.pop('section','')
+ return render_template('front.html',photo_gallery_slug=photo_gallery_slug,section=section,sections=SECTIONS)
+
+@app.route("/render_gallery", methods=['POST'])
+def render():
+ slug = request.form['photo_gallery_slug']
+ section = request.form['section']
+ context = dict(g.settings)
+ context['section'] = section
+ context['slug'] = slug
+ try:
+ gallery = g.renderer.fetch_and_render_gallery(slug,context=context)
+ return redirect("/review/%s" % slug, 302)
+ except Exception, e:
+ (type, value, tb) = sys.exc_info()
+ app.logger.error("%s Error rendering %s: %s" % (type,slug,value))
+ for line in traceback.format_exception(type, value, tb):
+ app.logger.error(line)
+ flash('<span class="err-label">Error encountered:</span> %s' % str(e),"error")
+ session['photo_gallery_slug'] = slug
+ session['section'] = section
+ return redirect('/', 302)
+
+@app.route("/publish", methods=['post'])
+def publish():
+ try:
+ slug = request.form['slug']
+ rendered_dir = g.renderer.build_gallery_filesystem_root(slug)
+ try:
+ context = dict((str(k),v) for k,v in json.load(open(os.path.join(rendered_dir,"context.json"))).items())
+ except:
+ return "Error retrieving gallery %s from %s" % (slug, rendered_dir), 404
+
+ s3deploy.deploy_to_s3(rendered_dir,g.settings['S3_BUCKET_NAME'])
+
+ published_url = "http://%s/%s" % (context['S3_BUCKET_NAME'], slug)
+ if request.form.has_key('create_storylink'):
+ resp = g.renderer.create_storylink(context['gallery']['title'], published_url)
+ context['storylink'] = json.loads(resp.content)['url']
+ else:
+ context['storylink'] = None
+
+ for p in context['photos']:
+ render_gallery.ping_fb(p['og_url'])
+ render_gallery.ping_fb(published_url)
+ render_gallery.ping_fb(published_url + "index.html")
+ json.dump(context,open(os.path.join(rendered_dir,'context.json'),"w"),indent=2)
+
+ return redirect("/done/%s" % slug, 302)
+ except Exception, e:
+ flash('<span class="err-label">Error encountered:</span> %s' % str(e),"error")
+ return redirect('/review/%s' % slug,302)
+
+@app.route("/done/<slug>", methods=['get'])
+def done(slug):
+ rendered_dir = g.renderer.build_gallery_filesystem_root(slug)
+ try:
+ context = dict((str(k),v) for k,v in json.load(open(os.path.join(rendered_dir,"context.json"))).items())
+ except Exception, e:
+ return "Error retrieving gallery %s from %s (%s)" % (slug, rendered_dir, e), 404
+ return render_template("done.html",**context)
+
+@app.route("/review/<slug>", methods=['get'])
+def review(slug):
+ """For all servers: load the rendered template in an iframe, under a 'does this look right' form """
+ if slug.endswith('/'):
+ slug = slug[:-1]
+ rendered_dir = g.renderer.build_gallery_filesystem_root(slug)
+ try:
+ context = dict((str(k),v) for k,v in json.load(open(os.path.join(rendered_dir,"context.json"))).items())
+ except:
+ return "Error retrieving gallery %s from %s" % (slug, rendered_dir), 404
+
+ base_url = g.renderer.build_review_url(slug)
+ rendered_url='/'.join([base_url,"index.html"])
+ if not slug in context:
+ context['slug'] = slug
+ context['timestamp'] = time.time()
+ return render_template("review.html",url=rendered_url,base_url=base_url,sections=SECTIONS,**context)
+
+@app.route("/preview/<slug>", methods=['get'])
+def preview(slug):
+ """For LOCAL SERVERS: regenerate the template, recopy the assets, render as if its full screen."""
+ if slug.endswith('/'):
+ slug = slug[:-1]
+ rendered_dir = g.renderer.build_gallery_filesystem_root(slug)
+ path = os.path.join(rendered_dir,'index.html')
+ if request.args.get('reload') == 'true' or not os.path.exists(path):
+ g.renderer.fetch_and_render_gallery(slug)
+
+ render_gallery.copy_assets_to(rendered_dir) # pick up CSS changes
+
+ context = json.load(open(os.path.join(rendered_dir,'context.json')))
+
+ context['FLASK_BASE'] = 'http://%s/static/out/%s/index.html' % (g.settings['FLASK_HOST_NAME'],slug)
+ render_gallery.render_template('default',path,context)
+ return open(path).read()
+
+@app.route("/dump_settings")
+def dump_settings():
+ lines = []
+ lines.append("<h1>Current settings</h1>")
+ for k,v in g.settings.items():
+ lines.append('<b>%s</b>: %s<br>' % (k,v))
+ return '\n'.join(lines)
+
+
+
+@app.errorhandler(500)
+def handle_exception(error):
+ logging.error(error)
+ return render_template('error.html',exception=error)
+
+@app.errorhandler(404)
+def page_not_found(error):
+ return 'This page does not exist', 404
+
+def parse_args():
+ parser = OptionParser()
+ parser.add_option("-o", "--host", dest="host",
+ help="override the 'host' value so that one can access the local server from other machines.")
+ parser.add_option("-t", "--deployment-target",
+ action="store", dest="target",
+ help="If specified, use the config settings for the given deployment target.")
+ parser.add_option("-l", "--logging_level",
+ action="store", dest="logging_level",
+ help="If specified, use for the logging level.")
+ (options, args) = parser.parse_args()
+ return options
+
+if __name__ == "__main__":
+ opts = parse_args()
+ if opts.target:
+ _settings = config.get_settings(opts.target)
+ else:
+ _settings = config.get_settings()
+ if opts.host:
+ _settings['HOST'] = opts.host
+ _settings['FLASK_HOST_NAME'] = "%s:5000" % opts.host
+ if opts.logging_level:
+ logging_level = opts.logging_level
+ else:
+ logging_level = _settings['LOGGING_LEVEL']
+ logging.basicConfig(stream=sys.stderr,level=logging_level)
+ logging.info("App.py loaded for deployment target %(DEPLOYMENT_TARGET)s" % _settings)
+ app.config['SETTINGS'] = _settings
+ app.run(debug=True, host=_settings['HOST'])
+
47 app/config.py
@@ -0,0 +1,47 @@
+import os
+
+BASE_SETTINGS = {
+ 'FLASK_HOST_NAME': 'localhost:5000',
+ 'REVIEW_GALLERY_ROOT': '/static/out',
+ 'S3_BUCKET_NAME': 'galleries.beta.tribapps.com',
+ 'P2P_API_ROOT': 'http://content-api.p2p.tribuneinteractive.com.stage.tribdev.com',
+ 'P2P_ROOT': 'http://content.p2p.tribuneinteractive.com.stage.tribdev.com',
+ 'P2P_AUTH_TOKEN': 'GET_THIS_FROM_TRIB_TECH',
+ 'HOST' : '0.0.0.0',
+ 'DEBUG': True,
+ 'LOGGING_LEVEL': 'DEBUG',
+}
+
+OVERRIDES = {
+ 'staging': {
+ 'REVIEW_GALLERY_ROOT': 'http://gallery.beta.tribapps.com/static/out',
+ },
+ 'localprod': {
+ 'S3_BUCKET_NAME': 'galleries.apps.chicagotribune.com',
+ 'P2P_AUTH_TOKEN': 'GET_THIS_FROM_TRIB_TECH',
+ 'P2P_API_ROOT': 'https://content-api.p2p.tribuneinteractive.com',
+ 'P2P_ROOT': 'http://content.p2p.tila.trb',
+ 'DEBUG': False,
+ },
+ 'production': {
+ 'REVIEW_GALLERY_ROOT': 'http://gallery.tribapps.com/static/out',
+ 'S3_BUCKET_NAME': 'galleries.apps.chicagotribune.com',
+ 'P2P_AUTH_TOKEN': 'GET_THIS_FROM_TRIB_TECH',
+ 'P2P_API_ROOT': 'https://content-api.p2p.tribuneinteractive.com',
+ 'P2P_ROOT': 'http://content.p2p.tila.trb',
+ 'DEBUG': False,
+ },
+}
+
+def get_settings(deployment_target=None):
+ if deployment_target is None:
+ deployment_target = os.environ.get('DEPLOYMENT_TARGET')
+ settings = dict(BASE_SETTINGS)
+ try:
+ settings['DEPLOYMENT_TARGET'] = deployment_target
+ deployment_overrides = OVERRIDES[deployment_target]
+ settings.update(deployment_overrides)
+ except KeyError:
+ pass
+ return settings
+
18 app/distill_sections.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+# Read a file in "sections.txt" and produce "sections.json" which is a distillation of ad sections
+# "sections.txt" can be copy/pasted out of
+# http://content.p2p.edge.tribuneinteractive.com/product_affiliate_sections?id=366
+# and this will ignore the junk and produce a json file which is just an array of section paths
+import json, os.path
+CURRENT_DIR = os.path.dirname(__file__)
+PREFIXES = ['/news', '/sports', '/business', '/entertainment', '/features', '/health', '/travel', '/classified']
+sections = []
+for line in map(str.rstrip,open(os.path.join(CURRENT_DIR,"sections.txt"))):
+ for start in PREFIXES:
+ if line.startswith(start):
+ sections.append(line)
+
+sections.sort()
+json.dump(sections,open(os.path.join(CURRENT_DIR,'sections.json'),"w"))
+print "Done: %i sections" % (len(sections))
+
34 app/production.wsgi
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+DEPLOYMENT_TARGET = 'production'
+import sys, os, os.path, site
+import logging, logging.config
+sys.stdout = sys.stderr # protect against spurious printing...
+
+try:
+ logging.config.fileConfig(os.path.join(os.path.dirname(__file__),'../apache/%s/logging.txt' % DEPLOYMENT_TARGET))
+except Exception, e:
+ print e
+
+# Reordering the path code from http://code.google.com/p/modwsgi/wiki/VirtualEnvironments
+
+# Remember original sys.path.
+prev_sys_path = list(sys.path)
+
+site.addsitedir(os.path.abspath(os.path.dirname(__file__)))
+
+site.addsitedir(os.path.join(
+ os.path.abspath(os.path.join(os.path.dirname(__file__), "../../env")),
+ "lib/python2.6/site-packages"
+))
+
+# Reorder sys.path so new directories at the front.
+new_sys_path = []
+for item in list(sys.path):
+ if item not in prev_sys_path:
+ new_sys_path.append(item)
+ sys.path.remove(item)
+sys.path[:0] = new_sys_path
+
+from app import app as application
+from config import get_settings
+application.config['SETTINGS'] = get_settings(DEPLOYMENT_TARGET)
460 app/render_gallery.py
@@ -0,0 +1,460 @@
+import sys, os.path
+CURRENT_DIR = os.path.dirname(__file__)
+sys.path.append(CURRENT_DIR)
+from config import get_settings
+# keep the above together -- the sys.path hack is key to locating config.settings
+import requests
+import json
+import jinja2
+import codecs
+from urllib import urlencode, quote_plus
+import time
+import logging
+import shutil
+from operator import itemgetter
+from dateutil.parser import parse
+from dateutil.tz import gettz
+from PIL import Image
+
+import eventlet
+eventlet.monkey_patch()
+
+ASSETS_DIR=os.path.join(CURRENT_DIR,"../assets")
+OUTPUT_DIR=os.path.join(CURRENT_DIR,"static/out")
+
+EXTENSION_FOR_CONTENT_TYPE = {
+ 'image/jpeg': 'jpg',
+}
+
+ENV = jinja2.Environment(loader=jinja2.FileSystemLoader(ASSETS_DIR), extensions=['jinja2.ext.i18n'])
+ENV.filters['quote_plus'] = quote_plus
+
+def format_iso(val,format_string='%b %d, %Y'):
+ parsed = parse(val)
+ try:
+ parsed = parsed.astimezone(gettz("America/Chicago"))
+ except ValueError: pass
+ formatted = parsed.strftime(format_string)
+ formatted = formatted.replace("May.","May") # attend to fact that May doesn't get abbreviated
+ return formatted
+
+ENV.filters['format_iso'] = format_iso
+
+P2P_API_DOCUMENTATION="""
+http://content-api.p2p.tribuneinteractive.com.stage.tribdev.com/docs/content_items
+"""
+"""
+curl -H 'Authorization: Bearer pn_540o3o65fprmx400nw8e0vu1x_64rnrtqalfd40j50xtjtqxjm5' 'https://content-api.p2p.tribuneinteractive.com.stage.tribdev.com/current_collections/chi_news_watchdog_college_1_headlines_trb.json?include[]=items' -g
+curl -H 'Authorization: Bearer 6l4tyk87sc7qjfcskxburnp9pji2bm1dn85' 'https://content-api.p2p.tribuneinteractive.com/current_collections/chi_news_watchdog_college_1_headlines_trb.json?include[]=items' -g
+"""
+COLLECTION_URL = "%(P2P_API_ROOT)s/current_collections/%(slug)s.json?include[]=items"
+CONTENT_ITEM_URL = "%(P2P_API_ROOT)s/content_items/%(slug)s.json?include[]=related_items"
+MULTI_CONTENT_URL = "%(P2P_API_ROOT)s/content_items/multi.json" # USE POST
+CREATE_CONTENT_ITEM_URL = '%(P2P_API_ROOT)s/content_items.json'
+FILE_UPLOAD_URL = '%(P2P_API_ROOT)s/file_uploads/upload_simple'
+
+THUMB_WIDTH = 187
+THUMB_HEIGHT = 105
+
+IMAGE_SIZES = {
+ 'phone': 480,
+ 'tablet': 768,
+ 'desktop': 1024,
+ 'full': 1280,
+}
+
+CONCURRENCY = 10
+
+MAX_HEIGHT_SCALE = 0.6
+
+class GalleryRenderer(object):
+ """docstring for GalleryRenderer"""
+ def __init__(self, settings):
+ super(GalleryRenderer, self).__init__()
+ self.settings = settings
+
+ def http_headers(self, content_type=None):
+ h = {
+ 'Authorization': 'Bearer %(P2P_AUTH_TOKEN)s' % self.settings,
+ }
+ if content_type is not None:
+ h['content-type'] = content_type
+ return h
+
+ def fetch_collection(self,slug):
+ d = dict(self.settings)
+ d['slug'] = slug
+ url = COLLECTION_URL % d
+ resp = requests.get(url,headers=self.http_headers())
+ if not resp.ok: resp.raise_for_status()
+ j = json.loads(resp.content)
+ return j['collection_layout']
+
+ def content_item_url(self, slug):
+ d = dict(self.settings)
+ d['slug'] = slug
+ url = CONTENT_ITEM_URL % d
+ return url
+
+ def fetch_single_item(self, slug):
+ url = self.content_item_url(slug)
+ resp = requests.get(url,headers=self.http_headers())
+ if not resp.ok: resp.raise_for_status()
+ try:
+ j = json.loads(resp.content)
+ except ValueError:
+ if resp.status_code == 404:
+ raise NotFoundException(slug, url)
+ raise APIException(resp)
+ if j.has_key('errors'):
+ logging.warn("error fetching %s" % url)
+ raise Exception(j['errors'])
+ return j['content_item']
+
+ def fetch_gallery(self, gallery_slug):
+ item = self.fetch_single_item(gallery_slug)
+ photos = self.fetch_multiple_items(live_related_item_ids(item['related_items']))
+ for photo in photos:
+ try:
+ if photo['height'] and photo['width']:
+ if not photo['thumbnail_url']:
+ photo['thumbnail_url'] = thumbnail_url(photo)
+ # we probably never want to use the auto resizer now that we're using PIL but
+ # only turning off alt since we don't use that often
+ # if not photo['alt_thumbnail_url']:
+ # photo['alt_thumbnail_url'] = thumbnail_url(photo,max_dimension=400)
+ except: pass
+
+ return {'gallery': item, 'photos': photos }
+
+ def fetch_multiple_items(self, ids):
+ ids = list(ids)
+ if len(ids) > 25:
+ agg = []
+ id_groups = segment_list(ids, 25)
+ for group in id_groups:
+ agg.extend(self.fetch_multiple_items(group))
+ return agg
+
+ url = MULTI_CONTENT_URL % self.settings
+ items = [{'id': id} for id in ids]
+ data = json.dumps({ "content_items": items })
+ h = dict(self.http_headers('application/json'))
+ resp = requests.post(url,data=data,headers=h)
+ if not resp.ok: resp.raise_for_status()
+ j = json.loads(resp.content)
+ return [x['body']['content_item'] for x in j]
+
+ def fetch_and_render_gallery(self, slug,context=None):
+ pg = self.fetch_gallery(slug)
+ self.render_gallery(slug,pg,context)
+ return pg
+
+ def render_gallery(self, slug,gallery,context=None,template='default',fetch_images=True):
+ logging.info("Begin rendering gallery [%s]" % slug)
+ start_time = time.time()
+ if context is None:
+ context = dict(self.settings)
+
+ gallery_dir = self.build_gallery_filesystem_root(slug)
+ gallery['gallery']['url_root'] = self.build_s3_url(slug)
+
+
+ try:
+ shutil.rmtree(gallery_dir)
+ except OSError: pass
+ if not os.path.exists(OUTPUT_DIR):
+ os.makedirs(OUTPUT_DIR)
+ logging.info("Copying assets to %s" % gallery_dir)
+ copy_assets_to(gallery_dir)
+
+ if fetch_images:
+ logging.info("Fetching images to %s" % gallery_dir)
+ fetch_gallery_images(gallery,gallery_dir)
+
+ context.update(gallery)
+
+ logging.info("Rendering template %s" % os.path.join(gallery_dir,'index.html'))
+ render_template(template,os.path.join(gallery_dir,'index.html'),context)
+ logging.info("Rendering opengraph pages")
+ render_opengraph_pages(slug,gallery,gallery_dir,template)
+ context['rendering_time'] = time.time() - start_time
+ json.dump(context,open(os.path.join(gallery_dir,'context.json'),"w"),indent=2)
+ logging.info("Finished rendering %s in %.2f seconds" % (slug, context['rendering_time']))
+
+ def build_review_url(self, slug):
+ return '%s/%s' % (self.settings['REVIEW_GALLERY_ROOT'],slug)
+
+ def build_s3_url(self, slug):
+ return "http://%s/%s" % (self.settings['S3_BUCKET_NAME'],slug)
+
+ def build_gallery_filesystem_root(self, slug):
+ return os.path.join(OUTPUT_DIR,slug)
+
+ def create_storylink(self,title,url):
+ # slug seems to get ignored...
+ h = self.http_headers('application/json')
+ data = { 'content_item':
+ {"content_item_type_code": "storylink",
+ "product_affiliate_code": "chinews",
+ "source_code": "chicagotribune",
+ "content_item_state_code": "live",
+ "title": title,
+ "url": url,
+ }
+ }
+ resp = requests.post(CREATE_CONTENT_ITEM_URL % self.settings,data=json.dumps(data),headers=h)
+ if not resp.ok:
+ resp.raise_for_status()
+ return resp
+
+
+ def upload_image(self,flo):
+ resp = requests.post(FILE_UPLOAD_URL % self.settings,files={'Filedata': flo},headers=self.http_headers())
+ return resp
+
+
+ def ping_fb(self,url):
+
+ param = "%s?fbrefresh=CANBEANYTHING" % our_url
+ fb_url = "http://developers.facebook.com/tools/debug/og/object?q=%s" % quote_plus(param)
+ urlopen(fb_url)
+
+
+
+class APIException(Exception):
+ """docstring for APIException"""
+ def __init__(self, response):
+ super(APIException, self).__init__(response)
+ self.response = response
+
+class NotFoundException(Exception):
+ """docstring for NotFoundException"""
+ def __init__(self, slug, url):
+ super(NotFoundException, self).__init__("Slug %s/url %s not found" % (slug,url))
+ self.slug = slug
+ self.url = url
+
+class PhotoNotRetrieved(Exception):
+ pass
+
+def thumbnail_url(photo,max_dimension=187,ratio='16x9'):
+ # url="http://image.p2p.tribuneinteractive.com.stage.tribdev.com/photos/preview/turbine/%(slug)s" % photo
+ url="http://image.p2p.tribuneinteractive.com/photos/preview/turbine/%(slug)s" % photo
+ params = {
+ 'namespace':'turbine',
+ 'size':'101279',
+ 'slug':photo['slug'],
+ 'max_dimension':str(max_dimension),
+ 'ratio':'16x9',
+ 'bust':str(time.time()),
+ }
+ return '?'.join([url,urlencode(params)])
+
+def grab_and_save(url,output_dir, basename,try_harder=False):
+ # get it, check its mimetype and write it to basename.extension
+ if not url:
+ logging.warn("no url provided to fetch %s" % basename)
+ return None
+ resp = requests.get(url)
+ if resp.status_code == 408 and try_harder:
+ logging.warn("Recieved 408 timeout for %s" % url)
+ resp = requests.get(url)
+ if not resp.ok: resp.raise_for_status()
+ content_type = resp.headers['content-type']
+ extension = None
+ if content_type:
+ content_type = content_type.split(';')[0]
+ if not content_type.startswith('image/'):
+ raise NotFoundException(basename,url)
+ try:
+ extension = EXTENSION_FOR_CONTENT_TYPE[content_type]
+ except KeyError:
+ raise Exception("Unknown content type %s for %s" % (content_type,url))
+ if not extension:
+ raise Exception("Content type was not specified in response.")
+ basename += '.%s' % extension
+ open(os.path.join(output_dir,basename),"w").write(resp.content)
+ return basename
+
+def _fetch_gallery_image(photo, gallery_dir, photos, try_harder=False):
+ file_name = grab_and_save(photo['photo_services_url'],gallery_dir,photo['slug'],try_harder=try_harder)
+ if file_name:
+ photo['orig_name'] = file_name
+
+ # Cut responsive images
+ image = Image.open(os.path.join(gallery_dir, file_name))
+ for label, max_width in IMAGE_SIZES.items():
+ max_height = max_width * MAX_HEIGHT_SCALE
+ if image.size[0] > max_width or image.size[1] > max_height:
+ resized = image.copy()
+
+ if image.size[0] > image.size[1]:
+ width_scale = max_width / float(resized.size[0])
+ height = int(float(resized.size[1]) * float(width_scale))
+ width = int(max_width)
+ else:
+ height_scale = max_height / float(resized.size[1])
+ width = int(float(resized.size[0]) * float(height_scale))
+ height = int(max_height)
+
+ resized = resized.resize((width, height), Image.ANTIALIAS)
+ resized_name = "%s-%s" % (label, file_name)
+ resized.save(os.path.join(gallery_dir,resized_name))
+ else:
+ resized_name = file_name
+
+ photo["%s_url" % label] = resized_name
+
+ # We need a 187x105 thumbnail. Ideally we can use photo['thumbnail_url']. Practically, we've seen that
+ # be too small. If it's too small, we need to grab alt_thumbnail_url and scale it down.
+ basename = '%s_thumb' % photo['slug']
+ file_name = grab_and_save(photo['thumbnail_url'],gallery_dir,basename,try_harder=try_harder)
+ if file_name:
+ thumb = Image.open(os.path.join(gallery_dir, file_name))
+ if thumb.size[1] == THUMB_HEIGHT:
+ photo['gallery_thumbnail_url'] = file_name
+ else:
+ logging.warn("Given thumbnail [%s] too small for gallery thumbnail [%s x %s]" % (file_name, thumb.size[0], thumb.size[1]))
+ # if we don't yet have a thumb, grab the alt and scale it down...
+ if not photo.has_key('gallery_thumbnail_url') or not photo['gallery_thumbnail_url']:
+ if photo['alt_thumbnail_url']:
+ logging.info("No gallery thumbnail yet, grabbing alt_thumbnail_url")
+ basename = '%s_alt' % photo['slug']
+ file_name = grab_and_save(photo['alt_thumbnail_url'],gallery_dir,basename,try_harder=try_harder)
+ else:
+ logging.info("No gallery thumbnail, no alt_thumbnail, resizing main image")
+ file_name = photo['orig_name']
+ if file_name:
+ # Resize
+ thumb = Image.open(os.path.join(gallery_dir, file_name))
+ resized = image.copy()
+ #
+ # width_scale = THUMB_WIDTH / float(resized.size[0])
+ # height = int(float(resized.size[1]) * float(width_scale))
+ # resized = resized.resize((THUMB_WIDTH, height), Image.ANTIALIAS)
+ #
+ height_scale = THUMB_HEIGHT / float(resized.size[1])
+ width = int(float(resized.size[0]) * float(height_scale))
+ resized = resized.resize((width, THUMB_HEIGHT), Image.ANTIALIAS)
+
+ resized_name = "thumb-%s" % file_name
+ resized.save(os.path.join(gallery_dir,resized_name))
+ photo['gallery_thumbnail_url'] = resized_name
+
+
+ photos[photo['id']] = photo
+
+def fetch_gallery_images(gal_and_photo_dict, gallery_dir):
+ """For each photo dict in the given gallery's photos, attempt to retrieve and store locally all
+ relevant image files/sizes. When images have been retrieved and stored, the photo dict will be updated
+ to have the filename under which the retrieved image was stored, relative to 'gallery_dir'."""
+
+ pool = eventlet.GreenPool(CONCURRENCY)
+
+ photos = {}
+ errors = []
+ def handle_error(gt,photo,errors):
+ try:
+ gt.wait()
+ except Exception, e:
+ errors.append((photo['id'],e))
+
+ for i, photo in enumerate(gal_and_photo_dict['photos']):
+ t = pool.spawn(_fetch_gallery_image, photo, gallery_dir, photos,try_harder=True)
+ t.link(handle_error,photo,errors)
+ pool.waitall()
+
+ if errors:
+ msgs = ["Problems with %i photos." % len(errors)]
+ for tup in errors:
+ msgs.append("Photo ID %s: %s" % tup)
+ raise PhotoNotRetrieved(';'.join(msgs))
+
+ i = 0
+ for photo in gal_and_photo_dict['photos']:
+ i += 1
+ try:
+ photo = photos[photo['id']]
+ except KeyError, e:
+ raise PhotoNotRetrieved("Photo %s was not retrieved." % photo['id'])
+ photo['s3_photo_url'] = "%s/%s" % (gal_and_photo_dict['gallery']['url_root'],photo['orig_name'])
+ photo['og_url'] = "%s/og_%s.html" % (gal_and_photo_dict['gallery']['url_root'],photo['slug'])
+
+def copy_assets_to(gallery_dir):
+ # can't simply use shutil.copytree because we don't want to clobber an existing directory
+ if not os.path.exists(gallery_dir):
+ os.makedirs(gallery_dir)
+ for path,dirnames,filenames in os.walk(ASSETS_DIR):
+ destpath = path.replace(ASSETS_DIR,gallery_dir)
+ try: dirnames.remove('templates')
+ except ValueError: pass
+ for dn in dirnames:
+ try: os.makedirs(os.path.join(destpath,dn))
+ except OSError: pass
+ for fn in filenames:
+ shutil.copy(os.path.join(path,fn),os.path.join(destpath,fn))
+
+def make_opengraph_title(photo, gallery):
+ parts = [gallery['source_name'], gallery['title']]
+ if photo.get('title'):
+ parts.append(photo['title'])
+ elif photo.get('seo_title'):
+ parts.append(photo['seo_title'])
+ return ' - '.join(parts)
+
+def render_opengraph_pages(slug, gallery,gallery_dir,template='default'):
+ for i,photo in enumerate(gallery['photos']):
+ count = i+1
+ path = os.path.join(gallery_dir,'og_%s.html' % photo['slug'])
+ print path
+ if photo['thumbnail_url'].startswith('http'):
+ thumbnail_url = photo['thumbnail_url']
+ else:
+ thumbnail_url = '%s/%s' % (gallery['gallery']['url_root'], photo['thumbnail_url'])
+ context = {
+ 'photo': photo,
+ 'gallery': gallery,
+ 'count': count,
+ 'title': make_opengraph_title(photo, gallery['gallery']),
+ 'thumbnail_url': thumbnail_url,
+ }
+ render_template('%s_opengraph' % template,path, context)
+
+def render_template(template,path,context):
+ kwargs = dict((str(k),v) for k,v in context.items()) # un-unicode keys
+ kwargs['timestamp'] = str(time.time())
+ output = ENV.get_template("templates/%s.html" % template).render(**kwargs)
+ w = codecs.getwriter('utf-8')(open(path, 'w'))
+ w.write(output)
+ w.close()
+
+def segment_list(l,max_len):
+ segments = []
+ for x in range(0,(len(l) // max_len)):
+ segments.append(l[x*max_len:(x*max_len)+max_len])
+ segments.append(l[(x+1)*max_len:])
+ return segments
+
+def live_related_item_ids(item_dicts):
+ """Only return the ids for live items, and not the one which is not a real photo"""
+ ids = []
+ for ri in item_dicts:
+ if ri.get('slug') != 'chi-end-photo' and ri.get(u'content_item_state_code') == 'live':
+ ids.append(ri['relatedcontentitem_id'])
+ return ids
+
+def ping_fb(url,tenacious=True):
+ param = "%s?fbrefresh=CANBEANYTHING" % url
+ fb_url = "http://developers.facebook.com/tools/debug/og/object?q=%s" % quote_plus(param)
+ resp = requests.get(fb_url)
+ if not resp.ok and tenacious:
+ time.sleep(2)
+ ping_fb(url,False)
+
+if __name__ == '__main__':
+ settings = get_settings()
+ renderer = GalleryRenderer(settings)
+ for arg in sys.argv[1:]:
+ renderer.fetch_and_render_gallery(arg)
+
83 app/s3deploy.py
@@ -0,0 +1,83 @@
+import sys, os.path
+import eventlet
+from config import get_settings
+from boto.s3.connection import S3Connection
+#s3conn = eventlet.import_patched("boto.s3.connection")
+#S3Connection = s3conn.S3Connection
+from boto.s3.key import Key
+import os
+import mimetypes
+import gzip
+import tempfile
+import logging
+import shutil
+eventlet.monkey_patch()
+
+AWS_ACCESS_KEY_ID = "PUT_YOURS_HERE"
+AWS_SECRET_ACCESS_KEY = "PUT_YOURS_HERE"
+CONCURRENCY = 32
+
+def _s3conn(aws_access_key_id=AWS_ACCESS_KEY_ID, aws_secret_access_key=AWS_SECRET_ACCESS_KEY):
+ return S3Connection(aws_access_key_id, aws_secret_access_key)
+
+def deploy_to_s3(directory, bucket):
+ """
+ Deploy a directory to an s3 bucket using parallel uploads.
+ """
+ directory = directory.rstrip('/')
+ slug = directory.split('/')[-1]
+
+ tempdir = tempfile.mkdtemp('gallery')
+ pool = eventlet.GreenPool(CONCURRENCY)
+ for keyname, absolute_path in find_file_paths(directory):
+ pool.spawn(s3_upload, slug, keyname, absolute_path, bucket, tempdir)
+
+ pool.waitall()
+ shutil.rmtree(tempdir,True)
+ return True
+
+def s3_upload(slug, keyname, absolute_path, bucket, tempdir):
+ """
+ Upload a file to s3
+ """
+ conn = _s3conn()
+ bucket = conn.get_bucket(bucket)
+
+ mimetype = mimetypes.guess_type(absolute_path)
+ options = { 'Content-Type' : mimetype[0] }
+
+ # There's a possible race condition if files have the same name
+ key_parts = keyname.split('/')
+ filename = key_parts.pop()
+
+ if mimetype[0] is not None and mimetype[0].startswith('text/') and not filename.startswith('og_'):
+ upload = open(absolute_path);
+ options['Content-Encoding'] = 'gzip'
+ temp_path = os.path.join(tempdir, filename)
+ gzfile = gzip.open(temp_path, 'wb')
+ gzfile.write(upload.read())
+ gzfile.close()
+ absolute_path = temp_path
+
+ k = Key(bucket)
+ k.key = '%s/%s' % (slug, keyname)
+ k.set_contents_from_filename(absolute_path, options, policy='public-read')
+
+def find_file_paths(directory):
+ """
+ A generator function that recursively finds all files in the upload directory.
+ """
+ for root, dirs, files in os.walk(directory):
+ rel_path = os.path.relpath(root, directory)
+
+ for f in files:
+ if rel_path == '.':
+ yield (f, os.path.join(root, f))
+ else:
+ yield (os.path.join(rel_path, f), os.path.join(root, f))
+
+if __name__ == '__main__':
+ settings = get_settings()
+ for arg in sys.argv[1:]:
+ deploy_to_s3(arg,settings['S3_BUCKET_NAME'])
+
1  app/sections.json
@@ -0,0 +1 @@
+["/business", "/business/alert", "/business/alert/text", "/business/bizwrap", "/business/bizwrap/text", "/business/breaking", "/business/breaking/blackfriday", "/business/careers", "/business/careers/topworkplaces", "/business/careers/topworkplaces2011", "/business/columnists", "/business/problemsolver", "/business/problemsolver/all", "/business/problemsolver/blog", "/business/problemsolver/death", "/business/problemsolver/employment", "/business/problemsolver/government", "/business/problemsolver/healthcare", "/business/problemsolver/money", "/business/problemsolver/parking", "/business/problemsolver/transportation", "/business/problemsolver/utilities", "/business/promo", "/business/technology", "/business/yourmoney", "/classified", "/classified/automotive", "/classified/automotive/autos", "/classified/automotive/autoshow", "/classified/automotive/dealers", "/classified/automotive/new", "/classified/automotive/recalls", "/classified/automotive/traffic", "/classified/automotive/used", "/classified/jobs", "/classified/jobs/topjobs", "/classified/jobs/whos-hiring", "/classified/merchandise", "/classified/merchandise/in-memoriam", "/classified/merchandise/in-memoriam/faq", "/classified/realestate", "/classified/realestate/Chicago_IL", "/classified/realestate/Cook_County_IL", "/classified/realestate/DuPage_County_IL", "/classified/realestate/Kane_County_IL", "/classified/realestate/Kendall_County_IL", "/classified/realestate/Lake_County_IL", "/classified/realestate/McHenry_County_IL", "/classified/realestate/Will_County_IL", "/classified/realestate/advice", "/classified/realestate/apartments", "/classified/realestate/buy", "/classified/realestate/communities", "/classified/realestate/foreclosure", "/classified/realestate/fsbo", "/classified/realestate/home", "/classified/realestate/home/newsletter", "/classified/realestate/home/newsletter/text", "/classified/realestate/luxury", "/classified/realestate/newhomes", "/classified/realestate/openhouse", "/classified/realestate/sell", "/classified/realestate/transactions", "/entertainment", "/entertainment/art", "/entertainment/books", "/entertainment/books/makeyourmark", "/entertainment/books/printersrow", "/entertainment/books/printersrowlitfest", "/entertainment/books/printersrownewsletter", "/entertainment/books/printersrownewsletter/text", "/entertainment/books/printersrownewslettertext", "/entertainment/books/printersrowpage", "/entertainment/books/resources", "/entertainment/breaking", "/entertainment/celebrity", "/entertainment/celebrity/aboutlastnight", "/entertainment/celebrity/aboutlastnight/blog", "/entertainment/columnists", "/entertainment/comics", "/entertainment/dining", "/entertainment/events", "/entertainment/events/familynewsletter", "/entertainment/events/familynewsletter/text", "/entertainment/funstuff", "/entertainment/funstuff/comics", "/entertainment/gamereviews", "/entertainment/games", "/entertainment/movies", "/entertainment/movies/newsletter", "/entertainment/movies/newsletter/text", "/entertainment/movies/oscars", "/entertainment/movies/talkingpictures", "/entertainment/music", "/entertainment/music/newsletter", "/entertainment/music/newsletter/text", "/entertainment/music/turnitup", "/entertainment/oprah", "/entertainment/promo", "/entertainment/theater", "/entertainment/theater/newsletter", "/entertainment/theater/newsletter/text", "/entertainment/theater/theaterloop", "/entertainment/tv", "/features", "/features/blackhistory", "/features/books", "/features/books/newsletter", "/features/books/newsletter/text", "/features/columnists", "/features/food", "/features/food/newsletter", "/features/food/newsletter/text", "/features/food/stew", "/features/gaychicago", "/features/green", "/features/holidaily", "/features/horoscopes", "/features/horoscopes/celebrity", "/features/horoscopes/celebrity/results", "/features/horoscopes/forecast", "/features/horoscopes/forecast/results", "/features/lottery", "/features/obituaries", "/features/peeps", "/features/printersrow", "/features/promo", "/features/style", "/features/tribu", "/features/tribu/askamy", "/features/tribu/chicagoscene", "/features/tribu/events", "/features/tribu/ijustworkhere", "/features/tribu/julieshealth", "/features/tribu/newsletter", "/features/tribu/newsletter/text", "/features/tribu/onmoney", "/features/tribu/weigel", "/features/yearinreview", "/health", "/health/agentorange", "/health/boostershots", "/health/breastcancer", "/health/cancercentral", "/health/docdollars", "/health/neglect", "/health/newsletter", "/health/newsletter/text", "/news", "/news/alert", "/news/alert/rss", "/news/alert/text", "/news/chicagonow", "/news/chicagonow/newsletter", "/news/chicagonow/newsletter/text", "/news/columnists", "/news/columnists/all", "/news/columnists/today", "/news/corrections", "/news/data", "/news/daywatch", "/news/daywatch/text", "/news/education", "/news/education/chicagoschools", "/news/elections", "/news/local", "/news/local/alden", "/news/local/blagojevich", "/news/local/breaking", "/news/local/breaking/newsletteram", "/news/local/breaking/newsletteram/text", "/news/local/breaking/newsletterpm", "/news/local/breaking/newsletterpm/text", "/news/local/daley", "/news/local/elections", "/news/local/emanuel", "/news/local/jenniferhudson", "/news/local/notredameaccident", "/news/local/suburbs", "/news/local/suburbs/arlington_heights", "/news/local/suburbs/barrington", "/news/local/suburbs/bolingbrook", "/news/local/suburbs/crystal_lake", "/news/local/suburbs/deerfield", "/news/local/suburbs/des_plaines", "/news/local/suburbs/downers_grove", "/news/local/suburbs/elgin", "/news/local/suburbs/elmhurst", "/news/local/suburbs/evanston", "/news/local/suburbs/feeds", "/news/local/suburbs/glen_ellyn", "/news/local/suburbs/glenview", "/news/local/suburbs/grayslake", "/news/local/suburbs/gurnee", "/news/local/suburbs/highland_park-highwood", "/news/local/suburbs/hinsdale", "/news/local/suburbs/joliet", "/news/local/suburbs/libertyville", "/news/local/suburbs/naperville", "/news/local/suburbs/northbrook", "/news/local/suburbs/oak_park-river_forest", "/news/local/suburbs/orland_park", "/news/local/suburbs/plainfield", "/news/local/suburbs/promos", "/news/local/suburbs/schaumburg", "/news/local/suburbs/tinley_park", "/news/local/suburbs/wheaton", "/news/local/suburbs/wilmette-kenilworth", "/news/local/suburbs/winnetka-northfield", "/news/lottery", "/news/nationworld", "/news/nationworld/911-anniversary", "/news/obituaries", "/news/opinion", "/news/opinion/action", "/news/opinion/blogs", "/news/opinion/chapman", "/news/opinion/columnists", "/news/opinion/commentary", "/news/opinion/editorials", "/news/opinion/letters", "/news/opinion/newsletter", "/news/opinion/newsletter/text", "/news/opinion/page", "/news/opinion/prickly", "/news/opinion/share", "/news/photo", "/news/podcasts", "/news/politics", "/news/politics/clout", "/news/politics/localelections", "/news/politics/obama", "/news/politics/obamapictures", "/news/politicsnow", "/news/religion", "/news/rss", "/news/talk", "/news/tribnation", "/news/tribnation/blog", "/news/tribnation/chicagoforwardevents", "/news/tribnation/eventlist", "/news/tribnation/events", "/news/tribnation/events/past", "/news/tribnation/events/podcasts", "/news/tribnation/events/test", "/news/tribnation/literaryevents", "/news/tribnation/newsletter", "/news/tribnation/newsletter/text", "/news/tribnation/talkers", "/news/video", "/news/watchdog", "/news/watchdog/childabduct", "/news/watchdog/children", "/news/watchdog/college", "/news/watchdog/consumer", "/news/watchdog/corruption", "/news/watchdog/fugitives", "/news/watchdog/nursinghomes", "/news/watchdog/secrecy", "/news/weather", "/sports", "/sports/alert", "/sports/alert/text", "/sports/baseball", "/sports/baseball/cubs", "/sports/baseball/cubs/newsletter", "/sports/baseball/cubs/newsletter/text", "/sports/baseball/cubs/santo", "/sports/baseball/whitesox", "/sports/baseball/whitesox/newsletter", "/sports/baseball/whitesox/newsletter/text", "/sports/basketball", "/sports/basketball/bulls", "/sports/basketball/bulls/michaeljordan", "/sports/basketball/bulls/newsletter", "/sports/basketball/bulls/newsletter/text", "/sports/basketball/sky", "/sports/breaking", "/sports/breaking/halftime", "/sports/breaking/halftime/text", "/sports/chicagomarathon", "/sports/college", "/sports/college/illinois", "/sports/college/northwestern", "/sports/college/notredame", "/sports/college/silverfootball", "/sports/columnists", "/sports/columnists/greenstein", "/sports/football", "/sports/football/bears", "/sports/football/bears/newsletter", "/sports/football/bears/newsletter/text", "/sports/football/bears/nfldraft", "/sports/football/fantasyfootball", "/sports/football/superbowlads", "/sports/globetrotting", "/sports/globetrotting/archive", "/sports/globetrotting/skating", "/sports/golf", "/sports/halftimenewsletter", "/sports/halftimenewslettertext", "/sports/highschool", "/sports/highschool/baseball", "/sports/highschool/boysbasketball", "/sports/highschool/crosscountry", "/sports/highschool/football", "/sports/highschool/girlsbasketball", "/sports/highschool/golf", "/sports/highschool/gymnastics", "/sports/highschool/insidescoop", "/sports/highschool/soccer", "/sports/highschool/softball", "/sports/highschool/swimming", "/sports/highschool/tennis", "/sports/highschool/trackandfield", "/sports/highschool/volleyball", "/sports/highschool/waterpolo", "/sports/highschool/wgn", "/sports/highschool/wrestling", "/sports/hockey", "/sports/hockey/blackhawks", "/sports/hockey/blackhawks/newsletter", "/sports/hockey/blackhawks/newsletter/text", "/sports/hockey/wolves", "/sports/horseracing", "/sports/international", "/sports/motorracing", "/sports/ncaatournament", "/sports/olympics", "/sports/poll", "/sports/promo", "/sports/rosenblog", "/sports/scoresstats", "/sports/smack", "/sports/soccer", "/sports/tennis", "/travel", "/travel/chicago", "/travel/deals", "/travel/escapes", "/travel/family", "/travel/midwest", "/travel/midwest/illinois", "/travel/midwest/indiana", "/travel/midwest/iowa", "/travel/midwest/michigan", "/travel/midwest/minnesota", "/travel/midwest/missouri", "/travel/midwest/ohio", "/travel/midwest/wisconsin", "/travel/newsletter", "/travel/newsletter/text", "/travel/other", "/travel/promo", "/travel/takingoff", "/travel/unitedstates", "/travel/vacation-starter/hotels", "/travel/virtualvacation"]
756 app/sections.txt
@@ -0,0 +1,756 @@
+Welcome, Joe!Settings ProfileUser ManagementSite Configuration
+LogoutTIVID Assembler Knowledgebase
+Home
+Content Items
+Collections
+Sections
+Push
+UGC
+Registration
+Find Local
+Edge
+Chicago Tribune
+vSearch Sections...
+
++
+Section Index
+Add Section
+Section Index
+Choose an affiliate, then select a section from the list, or add a new one
+View: All | Configured Sections | Recent Sections
+Collapse All / Expand All
+/ (root section)
++/-
+/about
+/about/chicagolive
+/about/chicagolive/about
+/about/chicagolive/almanac
+/about/chicagolive/almanac/past
+/about/chicagolive/blog
+/about/chicagolive/episodes
+/about/chicagolive/film
+/about/chicagolive/food
+/about/chicagolive/guests
+/about/chicagolive/kogan
+/about/chicagolive/kogancarlson
+/about/chicagolive/listen
+/about/chicagolive/media
+/about/chicagolive/music
+/about/chicagolive/newsletter
+/about/chicagolive/newsletter/text
+/about/chicagolive/photos
+/about/chicagolive/podcast
+/about/chicagolive/podcast/almanac
+/about/chicagolive/podcast/full
+/about/chicagolive/podcast/uncut
+/about/chicagolive/politics
+/about/chicagolive/sports
+/about/chicagolive/testfront
+/about/chicagolive/theater
+/about/chicagolive/tickets
+/about/chicagolive/videos
+/about/chicagolive/videos/bts
+/about/chicagolive/videos/other
+/about/chicagolive/videos/recap
+/about/chicagolive/videos/segment
+/about/community
+/about/communitygiving
+/about/contact
+/about/dwayne
+/about/events
+/about/events/chicagomagazine
+/about/events/pinktiegala
+/about/events/redeyemetromix
+/about/events/signature
+/about/events/teg
+/about/facebook
+/about/frontrails
+/about/joe
+/about/joinus
+/about/membership
+/about/mobile
+/about/onlinecommunity
+/about/pressrelease
+/about/sean
+/about/sectionrails
+/about/test2
+/about/themash
+/about/themash/blog
+/about/themash/contests
+/about/themash/entertainment
+/about/themash/events
+/about/themash/news
+/about/themash/photos
+/about/themash/sports
+/about/tribeffect
+/about/tribunecareers
++/-
+/advertiser
+/advertiser/2012testADSS
+/advertiser/2012testADSS/all-display-options
+/advertiser/2012testADSS/all-online-options
+/advertiser/2012testADSS/Manage-Your-Orders
+/advertiser/ad-types-sizes
+/advertiser/advertising-reach
+/advertiser/business
+/advertiser/category
+/advertiser/category/auto
+/advertiser/category/business-and-financial
+/advertiser/category/celebrations
+/advertiser/category/chicagotribune
+/advertiser/category/entertainment
+/advertiser/category/estate-and-garage-sales-merchandise
+/advertiser/category/events
+/advertiser/category/health-services
+/advertiser/category/healthcare
+/advertiser/category/hoy
+/advertiser/category/jobs
+/advertiser/category/mash
+/advertiser/category/obituaries
+/advertiser/category/pets
+/advertiser/category/professional-services
+/advertiser/category/real-estate
+/advertiser/category/real-estate-apartments
+/advertiser/category/real-estate-commercial-rentals
+/advertiser/category/real-estate-rentals
+/advertiser/category/real-estate-residential-rentals
+/advertiser/category/redeye
+/advertiser/category/retail
+/advertiser/category/special
+/advertiser/category/triblocal
+/advertiser/contact-us
+/advertiser/create
+/advertiser/faq
+/advertiser/how-it-works
+/advertiser/privacy
+/advertiser/terms
++/-
+/business
+/business/alert
+/business/alert/text
+/business/bizwrap
+/business/bizwrap/text
+/business/breaking
+/business/breaking/blackfriday
+/business/careers
+/business/careers/topworkplaces
+/business/careers/topworkplaces2011
+/business/columnists
+/business/problemsolver
+/business/problemsolver/all
+/business/problemsolver/blog
+/business/problemsolver/death
+/business/problemsolver/employment
+/business/problemsolver/government
+/business/problemsolver/healthcare
+/business/problemsolver/money
+/business/problemsolver/parking
+/business/problemsolver/transportation
+/business/problemsolver/utilities
+/business/promo
+/business/technology
+/business/yourmoney
++/-
+/classified
+/classified/automotive
+/classified/automotive/autos
+/classified/automotive/autoshow
+/classified/automotive/dealers
+/classified/automotive/new
+/classified/automotive/recalls
+/classified/automotive/traffic
+/classified/automotive/used
+/classified/jobs
+/classified/jobs/topjobs
+/classified/jobs/whos-hiring
+/classified/merchandise
+/classified/merchandise/in-memoriam
+/classified/merchandise/in-memoriam/faq
+/classified/realestate
+/classified/realestate/advice
+/classified/realestate/apartments
+/classified/realestate/buy
+/classified/realestate/Chicago_IL
+/classified/realestate/communities
+/classified/realestate/Cook_County_IL
+/classified/realestate/DuPage_County_IL
+/classified/realestate/foreclosure
+/classified/realestate/fsbo
+/classified/realestate/home
+/classified/realestate/home/newsletter
+/classified/realestate/home/newsletter/text
+/classified/realestate/Kane_County_IL
+/classified/realestate/Kendall_County_IL
+/classified/realestate/Lake_County_IL
+/classified/realestate/luxury
+/classified/realestate/McHenry_County_IL
+/classified/realestate/newhomes
+/classified/realestate/openhouse
+/classified/realestate/sell
+/classified/realestate/transactions
+/classified/realestate/Will_County_IL
+/community
++/-
+/email
+/email/photo_publish_email
++/-
+/entertainment
+/entertainment/art
+/entertainment/books
+/entertainment/books/makeyourmark
+/entertainment/books/printersrow
+/entertainment/books/printersrowlitfest
+/entertainment/books/printersrownewsletter
+/entertainment/books/printersrownewsletter/text
+/entertainment/books/printersrownewslettertext
+/entertainment/books/printersrowpage
+/entertainment/books/resources
+/entertainment/breaking
+/entertainment/celebrity
+/entertainment/celebrity/aboutlastnight
+/entertainment/celebrity/aboutlastnight/blog
+/entertainment/columnists
+/entertainment/comics
+/entertainment/dining
+/entertainment/events
+/entertainment/events/familynewsletter
+/entertainment/events/familynewsletter/text
+/entertainment/funstuff
+/entertainment/funstuff/comics
+/entertainment/gamereviews
+/entertainment/games
+/entertainment/movies
+/entertainment/movies/newsletter
+/entertainment/movies/newsletter/text
+/entertainment/movies/oscars
+/entertainment/movies/talkingpictures
+/entertainment/music
+/entertainment/music/newsletter
+/entertainment/music/newsletter/text
+/entertainment/music/turnitup
+/entertainment/oprah
+/entertainment/promo
+/entertainment/theater
+/entertainment/theater/newsletter
+/entertainment/theater/newsletter/text
+/entertainment/theater/theaterloop
+/entertainment/tv
++/-
+/features
+/features/blackhistory
+/features/books
+/features/books/newsletter
+/features/books/newsletter/text
+/features/columnists
+/features/food
+/features/food/newsletter
+/features/food/newsletter/text
+/features/food/stew
+/features/gaychicago
+/features/green
+/features/holidaily
+/features/horoscopes
+/features/horoscopes/celebrity
+/features/horoscopes/celebrity/results
+/features/horoscopes/forecast
+/features/horoscopes/forecast/results
+/features/lottery
+/features/obituaries
+/features/peeps
+/features/printersrow
+/features/promo
+/features/style
+/features/tribu
+/features/tribu/askamy
+/features/tribu/chicagoscene
+/features/tribu/events
+/features/tribu/ijustworkhere
+/features/tribu/julieshealth
+/features/tribu/newsletter
+/features/tribu/newsletter/text
+/features/tribu/onmoney
+/features/tribu/weigel
+/features/yearinreview
++/-
+/health
+/health/agentorange
+/health/boostershots
+/health/breastcancer
+/health/cancercentral
+/health/docdollars
+/health/neglect
+/health/newsletter
+/health/newsletter/text
+/iphone
++/-
+/mobile
+/mobile/alerts
+/mobile/business
+/mobile/entertainment
+/mobile/features
+/mobile/health
+/mobile/localnews
+/mobile/newsnationworld
+/mobile/opinion
+/mobile/shopping
+/mobile/sports
+/mobile/test
+/mobile/topnews
+/mobile/travel
++/-
+/news
+/news/alert
+/news/alert/rss
+/news/alert/text
+/news/chicagonow
+/news/chicagonow/newsletter
+/news/chicagonow/newsletter/text
+/news/columnists
+/news/columnists/all
+/news/columnists/today
+/news/corrections
+/news/data
+/news/daywatch
+/news/daywatch/text
+/news/education
+/news/education/chicagoschools
+/news/elections
+/news/local
+/news/local/alden
+/news/local/blagojevich
+/news/local/breaking
+/news/local/breaking/newsletteram
+/news/local/breaking/newsletteram/text
+/news/local/breaking/newsletterpm
+/news/local/breaking/newsletterpm/text
+/news/local/daley
+/news/local/elections
+/news/local/emanuel
+/news/local/jenniferhudson
+/news/local/notredameaccident
+/news/local/suburbs
+/news/local/suburbs/arlington_heights
+/news/local/suburbs/barrington
+/news/local/suburbs/bolingbrook
+/news/local/suburbs/crystal_lake
+/news/local/suburbs/deerfield
+/news/local/suburbs/des_plaines
+/news/local/suburbs/downers_grove
+/news/local/suburbs/elgin
+/news/local/suburbs/elmhurst
+/news/local/suburbs/evanston
+/news/local/suburbs/feeds
+/news/local/suburbs/glenview
+/news/local/suburbs/glen_ellyn
+/news/local/suburbs/grayslake
+/news/local/suburbs/gurnee
+/news/local/suburbs/highland_park-highwood
+/news/local/suburbs/hinsdale
+/news/local/suburbs/joliet
+/news/local/suburbs/libertyville
+/news/local/suburbs/naperville
+/news/local/suburbs/northbrook
+/news/local/suburbs/oak_park-river_forest
+/news/local/suburbs/orland_park
+/news/local/suburbs/plainfield
+/news/local/suburbs/promos
+/news/local/suburbs/schaumburg
+/news/local/suburbs/tinley_park
+/news/local/suburbs/wheaton
+/news/local/suburbs/wilmette-kenilworth
+/news/local/suburbs/winnetka-northfield
+/news/lottery
+/news/nationworld
+/news/nationworld/911-anniversary
+/news/obituaries
+/news/opinion
+/news/opinion/action
+/news/opinion/blogs
+/news/opinion/chapman
+/news/opinion/columnists
+/news/opinion/commentary
+/news/opinion/editorials
+/news/opinion/letters
+/news/opinion/newsletter
+/news/opinion/newsletter/text
+/news/opinion/page
+/news/opinion/prickly
+/news/opinion/share
+/news/photo
+/news/podcasts
+/news/politics
+/news/politics/clout
+/news/politics/localelections
+/news/politics/obama
+/news/politics/obamapictures
+/news/politicsnow
+/news/religion
+/news/rss
+/news/talk
+/news/tribnation
+/news/tribnation/blog
+/news/tribnation/chicagoforwardevents
+/news/tribnation/eventlist
+/news/tribnation/events
+/news/tribnation/events/past
+/news/tribnation/events/podcasts
+/news/tribnation/events/test
+/news/tribnation/literaryevents
+/news/tribnation/newsletter
+/news/tribnation/newsletter/text
+/news/tribnation/talkers
+/news/video
+/news/watchdog
+/news/watchdog/childabduct
+/news/watchdog/children
+/news/watchdog/college
+/news/watchdog/consumer
+/news/watchdog/corruption
+/news/watchdog/fugitives
+/news/watchdog/nursinghomes
+/news/watchdog/secrecy
+/news/weather
++/-
+/rss
+/rss/home
+/rss/topic
++/-
+/search
+/search/advancedsearch
+/search/bearstoday
+/search/trb
+/search_results
++/-
+/services
+/services/newsletters
+/services/newspaper/eedition
+/services/nie
+/services/ooh
+/services/rss
+/services/site
+/services/site/my-account
+/services/site/newspaper
+/services/site/newspaper/community
+/services/site/newspaper/extras
+/services/site/newspaper/premiumcontent
+/services/site/registration
+/services/site/self-service
++/-
+/shopping
+/shopping/circular
+/shopping/circular/target
++/-
+/site
+/site/newspaper
+/site/newspaper/business
+/site/newspaper/entertainment
+/site/newspaper/features
+/site/newspaper/local
+/site/newspaper/magazine
+/site/newspaper/news
+/site/newspaper/obituaries
+/site/newspaper/opinion
+/site/newspaper/sports
++/-
+/special
+/special/adsections
+/special/adsections/bestlawyers2012
+/special/adsections/cashdash
+/special/adsections/midwestcasinoguide2012
+/special/adsections/personalinjurylaw
+/special/advance
+/special/educationtoday
+/special/holidays
+/special/primetime
++/-
+/sports
+/sports/alert
+/sports/alert/text
+/sports/baseball
+/sports/baseball/cubs
+/sports/baseball/cubs/newsletter
+/sports/baseball/cubs/newsletter/text
+/sports/baseball/cubs/santo
+/sports/baseball/whitesox
+/sports/baseball/whitesox/newsletter
+/sports/baseball/whitesox/newsletter/text
+/sports/basketball
+/sports/basketball/bulls
+/sports/basketball/bulls/michaeljordan
+/sports/basketball/bulls/newsletter
+/sports/basketball/bulls/newsletter/text
+/sports/basketball/sky
+/sports/breaking
+/sports/breaking/halftime
+/sports/breaking/halftime/text
+/sports/chicagomarathon
+/sports/college
+/sports/college/illinois
+/sports/college/northwestern
+/sports/college/notredame
+/sports/college/silverfootball
+/sports/columnists
+/sports/columnists/greenstein
+/sports/football
+/sports/football/bears
+/sports/football/bears/newsletter
+/sports/football/bears/newsletter/text
+/sports/football/bears/nfldraft
+/sports/football/fantasyfootball
+/sports/football/superbowlads
+/sports/globetrotting
+/sports/globetrotting/archive
+/sports/globetrotting/skating
+/sports/golf
+/sports/halftimenewsletter
+/sports/halftimenewslettertext
+/sports/highschool
+/sports/highschool/baseball
+/sports/highschool/boysbasketball
+/sports/highschool/crosscountry
+/sports/highschool/football
+/sports/highschool/girlsbasketball
+/sports/highschool/golf
+/sports/highschool/gymnastics
+/sports/highschool/insidescoop
+/sports/highschool/soccer
+/sports/highschool/softball
+/sports/highschool/swimming
+/sports/highschool/tennis
+/sports/highschool/trackandfield
+/sports/highschool/volleyball
+/sports/highschool/waterpolo
+/sports/highschool/wgn
+/sports/highschool/wrestling
+/sports/hockey
+/sports/hockey/blackhawks
+/sports/hockey/blackhawks/newsletter
+/sports/hockey/blackhawks/newsletter/text
+/sports/hockey/wolves
+/sports/horseracing
+/sports/international
+/sports/motorracing
+/sports/ncaatournament
+/sports/olympics
+/sports/poll
+/sports/promo
+/sports/rosenblog
+/sports/scoresstats
+/sports/smack
+/sports/soccer
+/sports/tennis
++/-
+/test
+/test/adops
+/test/adops/hp
+/test/ams
+/test/bindu
+/test/bindu/cloned
+/test/blago
+/test/blog
+/test/buddy
+/test/cheryl
+/test/chrisk
+/test/chrissp
+/test/classified
+/test/classified1
+/test/classified2
+/test/clout
+/test/columns
+/test/commclass
+/test/commclass/templateOnly
+/test/contest
+/test/cp
+/test/cp/blog
+/test/cp/themash
+/test/data
+/test/daywatch
+/test/demo
+/test/destiny
+/test/destiny/2
+/test/dg
+/test/dg/sectionhealth
+/test/dg2
+/test/dg2/business
+/test/dg2/clone
+/test/dg2/entertainment
+/test/dg2/news
+/test/dg2/news2
+/test/dg2/section
+/test/dg2/sectionbusiness
+/test/dg2/sectionentertainment
+/test/dg2/sectionsports
+/test/dg2/sports
+/test/dg2/sports/bulls
+/test/dg2/sports1
+/test/dg2/sports2
+/test/dg2/sports3
+/test/dg2/sports4
+/test/dg2/sports5
+/test/dg2/sports6
+/test/dg2/verdana
+/test/dog
+/test/feed
+/test/food
+/test/front
+/test/g
+/test/golf
+/test/health
+/test/jgoode
+/test/joe
+/test/joe/cloned
+/test/joe/jobs
+/test/john
+/test/kot
+/test/kyle
+/test/kyle/child
+/test/kyle/realestate
+/test/luis
+/test/makeyourmark
+/test/marks
+/test/matt
+/test/Michelle
+/test/michelle
+/test/Michelle2
+/test/Michelle4
+/test/Michelle5
+/test/Michelle6
+/test/Michelle7
+/test/michelle8
+/test/mike
+/test/mike2
+/test/mobile
+/test/modules
+/test/newfront
+/test/onmoney
+/test/page
+/test/pat
+/test/photo
+/test/QA
+/test/QA/PR1
+/test/QA/PR2
+/test/QA2
+/test/QA3
+/test/rivers
+/test/root
+/test/rtm
+/test/rtm-dev
+/test/ryan
+/test/ryan/blog
+/test/ryan/results
+/test/style
+/test/styleguide
+/test/styleguide/images
+/test/sudha
+/test/sudha/BC
+/test/sudha/clone1
+/test/sudha/clone2
+/test/terry
+/test/test2
+/test/theater
+/test/tina
+/test/tribnation
+/test/tribu
+/test/ux
+/test/vast-ad
+/test/video
++/-
+/thirdparty
+/thirdparty/adss
+/thirdparty/apartments
+/thirdparty/bankrate
+/thirdparty/bizbuysell
+/thirdparty/calendars
+/thirdparty/cancercentral
+/thirdparty/careerbuilder
+/thirdparty/celebrations
+/thirdparty/cityfeet
+/thirdparty/coupons
+/thirdparty/ctmg
+/thirdparty/dba
+/thirdparty/formstack
+/thirdparty/garagesales
+/thirdparty/homefinder
+/thirdparty/homes
+/thirdparty/interest
+/thirdparty/legacy
+/thirdparty/listings
+/thirdparty/marketwatch
+/thirdparty/metro
+/thirdparty/metro/allergen
+/thirdparty/metro/contributions
+/thirdparty/metro/crime
+/thirdparty/metro/inspection
+/thirdparty/metro/rsei
+/thirdparty/metro/schoolreportcard
+/thirdparty/metro/watchdog-contributions
+/thirdparty/myteam
+/thirdparty/newsapps
+/thirdparty/newsapps/citycouncilexpenses
+/thirdparty/newsapps/elections
+/thirdparty/nie
+/thirdparty/p2i
+/thirdparty/perfectmarket
+/thirdparty/pets
+/thirdparty/pictopia
+/thirdparty/proquest
+/thirdparty/ris
+/thirdparty/ris/consumer
+/thirdparty/ris/foreclosure
+/thirdparty/sports
+/thirdparty/sports/upickem
+/thirdparty/sports/upickem/ncaatournament
+/thirdparty/sports/video
+/thirdparty/sportsnetwork
+/thirdparty/subscription
+/thirdparty/traffic
+/thirdparty/tribunecareers
+/thirdparty/workplacedynamics
+/thirdparty/zap2it
++/-
+/topic
+/topic/crime-law-justice
+/topic/custom
+/topic/custom/cars
+/topic/custom/health
+/topic/entertainment
+/topic/entertainment/music
+/topic/services-shopping
+/topic/services-shopping/vehicles
+/topic/services-shopping/vehicles/makes-models
+/topic/top
++/-
+/travel
+/travel/chicago
+/travel/deals
+/travel/escapes
+/travel/family
+/travel/midwest
+/travel/midwest/illinois
+/travel/midwest/indiana
+/travel/midwest/iowa
+/travel/midwest/michigan
+/travel/midwest/minnesota
+/travel/midwest/missouri
+/travel/midwest/ohio
+/travel/midwest/wisconsin
+/travel/newsletter
+/travel/newsletter/text
+/travel/other
+/travel/promo
+Configuration | Theme | Layout | Clone
+/travel/takingoff
+/travel/unitedstates
+/travel/vacation-starter/hotels
+/travel/virtualvacation
++/-
+/video
+/video/all
+/video/breaking
+/videogallery
+Add Section
+Work Palette
+Clipboard
+Content Item Search
+Favorites
+Recent History
34 app/staging.wsgi
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+DEPLOYMENT_TARGET = 'staging'
+import sys, os, os.path, site
+import logging, logging.config
+sys.stdout = sys.stderr # protect against spurious printing...
+
+try:
+ logging.config.fileConfig(os.path.join(os.path.dirname(__file__),'../%s/logging.txt' % DEPLOYMENT_TARGET))
+except Exception, e:
+ print e
+
+# Reordering the path code from http://code.google.com/p/modwsgi/wiki/VirtualEnvironments
+
+# Remember original sys.path.
+prev_sys_path = list(sys.path)
+
+site.addsitedir(os.path.abspath(os.path.dirname(__file__)))
+
+site.addsitedir(os.path.join(
+ os.path.abspath(os.path.join(os.path.dirname(__file__), "../../env")),
+ "lib/python2.6/site-packages"
+))
+
+# Reorder sys.path so new directories at the front.
+new_sys_path = []
+for item in list(sys.path):
+ if item not in prev_sys_path:
+ new_sys_path.append(item)
+ sys.path.remove(item)
+sys.path[:0] = new_sys_path
+
+from app import app as application
+from config import get_settings
+application.config['SETTINGS'] = get_settings(DEPLOYMENT_TARGET)
1  app/static/.gitignore
@@ -0,0 +1 @@
+out
686 app/static/bootstrap/css/bootstrap-responsive.css
@@ -0,0 +1,686 @@
+/*!
+ * Bootstrap Responsive v2.0.2
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ */
+.clearfix {
+ *zoom: 1;
+}
+.clearfix:before,
+.clearfix:after {
+ display: table;
+ content: "";
+}
+.clearfix:after {
+ clear: both;
+}
+.hide-text {
+ overflow: hidden;
+ text-indent: 100%;
+ white-space: nowrap;
+}
+.input-block-level {
+ display: block;
+ width: 100%;
+ min-height: 28px;
+ /* Make inputs at least the height of their button counterpart */
+
+ /* Makes inputs behave like true block-level elements */
+
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.hidden {
+ display: none;
+ visibility: hidden;
+}
+.visible-phone {
+ display: none;
+}
+.visible-tablet {
+ display: none;
+}
+.visible-desktop {
+ display: block;
+}
+.hidden-phone {
+ display: block;
+}
+.hidden-tablet {
+ display: block;
+}
+.hidden-desktop {
+ display: none;
+}
+@media (max-width: 767px) {
+ .visible-phone {
+ display: block;
+ }
+ .hidden-phone {
+ display: none;
+ }
+ .hidden-desktop {
+ display: block;
+ }
+ .visible-desktop {
+ display: none;
+ }
+}
+@media (min-width: 768px) and (max-width: 979px) {
+ .visible-tablet {
+ display: block;
+ }
+ .hidden-tablet {
+ display: none;
+ }
+ .hidden-desktop {
+ display: block;
+ }
+ .visible-desktop {
+ display: none;
+ }
+}
+@media (max-width: 480px) {
+ .nav-collapse {
+ -webkit-transform: translate3d(0, 0, 0);
+ }
+ .page-header h1 small {
+ display: block;
+ line-height: 18px;
+ }
+ input[type="checkbox"],
+ input[type="radio"] {
+ border: 1px solid #ccc;
+ }
+ .form-horizontal .control-group > label {
+ float: none;
+ width: auto;
+ padding-top: 0;
+ text-align: left;
+ }
+ .form-horizontal .controls {
+ margin-left: 0;
+ }
+ .form-horizontal .control-list {
+ padding-top: 0;
+ }
+ .form-horizontal .form-actions {
+ padding-left: 10px;
+ padding-right: 10px;
+ }
+ .modal {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ right: 10px;
+ width: auto;
+ margin: 0;
+ }
+ .modal.fade.in {
+ top: auto;
+ }
+ .modal-header .close {
+ padding: 10px;
+ margin: -10px;
+ }
+ .carousel-caption {
+ position: static;
+ }
+}
+@media (max-width: 767px) {
+ body {
+ padding-left: 20px;
+ padding-right: 20px;
+ }
+ .navbar-fixed-top {
+ margin-left: -20px;
+ margin-right: -20px;
+ }
+ .container {
+ width: auto;
+ }
+ .row-fluid {
+ width: 100%;
+ }
+ .row {
+ margin-left: 0;
+ }
+ .row > [class*="span"],
+ .row-fluid > [class*="span"] {
+ float: none;
+ display: block;
+ width: auto;
+ margin: 0;
+ }
+ .thumbnails [class*="span"] {
+ width: auto;
+ }
+ input[class*="span"],
+ select[class*="span"],
+ textarea[class*="span"],
+ .uneditable-input {
+ display: block;
+ width: 100%;
+ min-height: 28px;
+ /* Make inputs at least the height of their button counterpart */
+
+ /* Makes inputs behave like true block-level elements */
+
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+ .input-prepend input[class*="span"],
+ .input-append input[class*="span"] {
+ width: auto;
+ }
+}
+@media (min-width: 768px) and (max-width: 979px) {
+ .row {
+ margin-left: -20px;
+ *zoom: 1;
+ }
+ .row:before,
+ .row:after {
+ display: table;
+ content: "";
+ }
+ .row:after {
+ clear: both;
+ }
+ [class*="span"] {
+ float: left;
+ margin-left: 20px;
+ }
+ .container,
+ .navbar-fixed-top .container,
+ .navbar-fixed-bottom .container {
+ width: 724px;
+ }
+ .span12 {
+ width: 724px;
+ }
+ .span11 {
+ width: 662px;
+ }
+ .span10 {
+ width: 600px;
+ }
+ .span9 {
+ width: 538px;
+ }
+ .span8 {
+ width: 476px;
+ }
+ .span7 {
+ width: 414px;
+ }
+ .span6 {
+ width: 352px;
+ }
+ .span5 {
+ width: 290px;
+ }
+ .span4 {
+ width: 228px;
+ }
+ .span3 {
+ width: 166px;
+ }
+ .span2 {
+ width: 104px;
+ }
+ .span1 {
+ width: 42px;
+ }
+ .offset12 {
+ margin-left: 764px;
+ }
+ .offset11 {
+ margin-left: 702px;
+ }
+ .offset10 {
+ margin-left: 640px;
+ }
+ .offset9 {
+ margin-left: 578px;
+ }
+ .offset8 {
+ margin-left: 516px;
+ }
+ .offset7 {
+ margin-left: 454px;
+ }
+ .offset6 {
+ margin-left: 392px;
+ }
+ .offset5 {
+ margin-left: 330px;
+ }
+ .offset4 {
+ margin-left: 268px;
+ }
+ .offset3 {
+ margin-left: 206px;
+ }
+ .offset2 {
+ margin-left: 144px;
+ }
+ .offset1 {
+ margin-left: 82px;
+ }
+ .row-fluid {
+ width: 100%;
+ *zoom: 1;
+ }
+ .row-fluid:before,
+ .row-fluid:after {
+ display: table;
+ content: "";
+ }
+ .row-fluid:after {
+ clear: both;
+ }
+ .row-fluid > [class*="span"] {
+ float: left;
+ margin-left: 2.762430939%;
+ }
+ .row-fluid > [class*="span"]:first-child {
+ margin-left: 0;
+ }
+ .row-fluid > .span12 {
+ width: 99.999999993%;
+ }
+ .row-fluid > .span11 {
+ width: 91.436464082%;
+ }
+ .row-fluid > .span10 {
+ width: 82.87292817100001%;
+ }
+ .row-fluid > .span9 {
+ width: 74.30939226%;
+ }
+ .row-fluid > .span8 {
+ width: 65.74585634900001%;
+ }
+ .row-fluid > .span7 {
+ width: 57.182320438000005%;
+ }
+ .row-fluid > .span6 {
+ width: 48.618784527%;
+ }
+ .row-fluid > .span5 {
+ width: 40.055248616%;
+ }
+ .row-fluid > .span4 {
+ width: 31.491712705%;
+ }
+ .row-fluid > .span3 {
+ width: 22.928176794%;
+ }
+ .row-fluid > .span2 {
+ width: 14.364640883%;
+ }
+ .row-fluid > .span1 {
+ width: 5.801104972%;
+ }
+ input,
+ textarea,
+ .uneditable-input {
+ margin-left: 0;
+ }
+ input.span12, textarea.span12, .uneditable-input.span12 {
+ width: 714px;
+ }
+ input.span11, textarea.span11, .uneditable-input.span11 {
+ width: 652px;
+ }
+ input.span10, textarea.span10, .uneditable-input.span10 {
+ width: 590px;
+ }
+ input.span9, textarea.span9, .uneditable-input.span9 {
+ width: 528px;
+ }
+ input.span8, textarea.span8, .uneditable-input.span8 {
+ width: 466px;
+ }
+ input.span7, textarea.span7, .uneditable-input.span7 {
+ width: 404px;
+ }
+ input.span6, textarea.span6, .uneditable-input.span6 {
+ width: 342px;
+ }
+ input.span5, textarea.span5, .uneditable-input.span5 {
+ width: 280px;
+ }
+ input.span4, textarea.span4, .uneditable-input.span4 {
+ width: 218px;
+ }
+ input.span3, textarea.span3, .uneditable-input.span3 {
+ width: 156px;
+ }
+ input.span2, textarea.span2, .uneditable-input.span2 {
+ width: 94px;
+ }
+ input.span1, textarea.span1, .uneditable-input.span1 {
+ width: 32px;
+ }
+}
+@media (max-width: 979px) {
+ body {
+ padding-top: 0;
+ }
+ .navbar-fixed-top {
+ position: static;
+ margin-bottom: 18px;
+ }
+ .navbar-fixed-top .navbar-inner {
+ padding: 5px;
+ }
+ .navbar .container {
+ width: auto;
+ padding: 0;
+ }
+ .navbar .brand {
+ padding-left: 10px;
+ padding-right: 10px;
+ margin: 0 0 0 -5px;
+ }
+ .navbar .nav-collapse {
+ clear: left;
+ }
+ .navbar .nav {
+ float: none;
+ margin: 0 0 9px;
+ }
+ .navbar .nav > li {
+ float: none;
+ }
+ .navbar .nav > li > a {
+ margin-bottom: 2px;
+ }
+ .navbar .nav > .divider-vertical {
+ display: none;
+ }
+ .navbar .nav .nav-header {
+ color: #999999;
+ text-shadow: none;
+ }
+ .navbar .nav > li > a,
+ .navbar .dropdown-menu a {
+ padding: 6px 15px;
+ font-weight: bold;
+ color: #999999;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ }
+ .navbar .dropdown-menu li + li a {
+ margin-bottom: 2px;
+ }
+ .navbar .nav > li > a:hover,
+ .navbar .dropdown-menu a:hover {
+ background-color: #222222;
+ }
+ .navbar .dropdown-menu {
+ position: static;
+ top: auto;
+ left: auto;
+ float: none;
+ display: block;
+ max-width: none;
+ margin: 0 15px;
+ padding: 0;
+ background-color: transparent;
+ border: none;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ }
+ .navbar .dropdown-menu:before,
+ .navbar .dropdown-menu:after {
+ display: none;
+ }
+ .navbar .dropdown-menu .divider {
+ display: none;
+ }
+ .navbar-form,
+ .navbar-search {
+ float: none;
+ padding: 9px 15px;
+ margin: 9px 0;
+ border-top: 1px solid #222222;
+ border-bottom: 1px solid #222222;
+ -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+ -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+ }
+ .navbar .nav.pull-right {
+ float: none;
+ margin-left: 0;
+ }
+ .navbar-static .navbar-inner {
+ padding-left: 10px;
+ padding-right: 10px;
+ }
+ .btn-navbar {
+ display: block;
+ }