Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

prepare for opensource

  • Loading branch information...
commit 931a8022744666189150dcd2753e6ff1046c7f6d 0 parents
@JoeGermuska JoeGermuska authored
Showing with 31,511 additions and 0 deletions.
  1. +2 −0  .gitignore
  2. +52 −0 LICENSE
  3. +5 −0 README
  4. +44 −0 apache/production/apache
  5. +34 −0 apache/production/apache_maintenance
  6. +38 −0 apache/staging/apache
  7. +37 −0 apache/staging/apache_maintenance
  8. +592 −0 app/app.py
  9. +25 −0 app/config.py
  10. +20 −0 app/decorators.py
  11. +29 −0 app/production.wsgi
  12. +30 −0 app/s3deploy.py
  13. +29 −0 app/staging.wsgi
  14. +1 −0  app/static/.gitignore
  15. +28 −0 app/static/bookmarklet/iframe.css
  16. +92 −0 app/static/bookmarklet/load.js
  17. +686 −0 app/static/bootstrap/css/bootstrap-responsive.css
  18. +12 −0 app/static/bootstrap/css/bootstrap-responsive.min.css
  19. +3,990 −0 app/static/bootstrap/css/bootstrap.css
  20. +689 −0 app/static/bootstrap/css/bootstrap.min.css
  21. BIN  app/static/bootstrap/img/glyphicons-halflings-white.png
  22. BIN  app/static/bootstrap/img/glyphicons-halflings.png
  23. +210 −0 app/static/bootstrap/js/bootstrap-modal.js
  24. +130 −0 app/static/bootstrap/js/bootstrap-tab.js
  25. +271 −0 app/static/bootstrap/js/bootstrap-typeahead.js
  26. +1,726 −0 app/static/bootstrap/js/bootstrap.js
  27. +6 −0 app/static/bootstrap/js/bootstrap.min.js
  28. +4 −0 app/static/css/admin.css
  29. +47 −0 app/static/css/editor.css
  30. +23 −0 app/static/css/layercake-edit.css
  31. +12 −0 app/static/css/layercake.css
  32. BIN  app/static/img/logo-med-white.png
  33. BIN  app/static/img/play-button.png
  34. +24 −0 app/static/js/ckeditor/.htaccess
  35. +1,503 −0 app/static/js/ckeditor/CHANGES.html
  36. +92 −0 app/static/js/ckeditor/INSTALL.html
  37. +1,327 −0 app/static/js/ckeditor/LICENSE.html
  38. +32 −0 app/static/js/ckeditor/_samples/adobeair/application.xml
  39. +9 −0 app/static/js/ckeditor/_samples/adobeair/run.bat
  40. +8 −0 app/static/js/ckeditor/_samples/adobeair/run.sh
  41. +45 −0 app/static/js/ckeditor/_samples/adobeair/sample.html
  42. +98 −0 app/static/js/ckeditor/_samples/ajax.html
  43. +192 −0 app/static/js/ckeditor/_samples/api.html
  44. +198 −0 app/static/js/ckeditor/_samples/api_dialog.html
  45. +28 −0 app/static/js/ckeditor/_samples/api_dialog/my_dialog.js
  46. +105 −0 app/static/js/ckeditor/_samples/asp/advanced.asp
  47. +136 −0 app/static/js/ckeditor/_samples/asp/events.asp
  48. +103 −0 app/static/js/ckeditor/_samples/asp/index.html
  49. +72 −0 app/static/js/ckeditor/_samples/asp/replace.asp
  50. +77 −0 app/static/js/ckeditor/_samples/asp/replaceall.asp
  51. +46 −0 app/static/js/ckeditor/_samples/asp/sample_posteddata.asp
  52. +72 −0 app/static/js/ckeditor/_samples/asp/standalone.asp
  53. +59 −0 app/static/js/ckeditor/_samples/assets/_posteddata.php
  54. BIN  app/static/js/ckeditor/_samples/assets/output_for_flash.fla
  55. BIN  app/static/js/ckeditor/_samples/assets/output_for_flash.swf
  56. +204 −0 app/static/js/ckeditor/_samples/assets/output_xhtml.css
  57. +70 −0 app/static/js/ckeditor/_samples/assets/parsesample.css
  58. +18 −0 app/static/js/ckeditor/_samples/assets/swfobject.js
  59. +108 −0 app/static/js/ckeditor/_samples/autogrow.html
  60. +125 −0 app/static/js/ckeditor/_samples/bbcode.html
  61. +94 −0 app/static/js/ckeditor/_samples/devtools.html
  62. +154 −0 app/static/js/ckeditor/_samples/divreplace.html
  63. +115 −0 app/static/js/ckeditor/_samples/enterkey.html
  64. +82 −0 app/static/js/ckeditor/_samples/fullpage.html
  65. +116 −0 app/static/js/ckeditor/_samples/index.html
  66. +99 −0 app/static/js/ckeditor/_samples/jqueryadapter.html
  67. +275 −0 app/static/js/ckeditor/_samples/output_for_flash.html
  68. +285 −0 app/static/js/ckeditor/_samples/output_html.html
  69. +181 −0 app/static/js/ckeditor/_samples/output_xhtml.html
  70. +120 −0 app/static/js/ckeditor/_samples/php/advanced.php
  71. +153 −0 app/static/js/ckeditor/_samples/php/events.php
  72. +47 −0 app/static/js/ckeditor/_samples/php/index.html
  73. +87 −0 app/static/js/ckeditor/_samples/php/replace.php
  74. +88 −0 app/static/js/ckeditor/_samples/php/replaceall.php
  75. +83 −0 app/static/js/ckeditor/_samples/php/standalone.php
  76. +81 −0 app/static/js/ckeditor/_samples/placeholder.html
  77. +91 −0 app/static/js/ckeditor/_samples/readonly.html
  78. +64 −0 app/static/js/ckeditor/_samples/replacebyclass.html
  79. +97 −0 app/static/js/ckeditor/_samples/replacebycode.html
  80. +163 −0 app/static/js/ckeditor/_samples/sample.css
  81. +65 −0 app/static/js/ckeditor/_samples/sample.js
  82. +21 −0 app/static/js/ckeditor/_samples/sample_posteddata.php
  83. +153 −0 app/static/js/ckeditor/_samples/sharedspaces.html
  84. +110 −0 app/static/js/ckeditor/_samples/skins.html
  85. +93 −0 app/static/js/ckeditor/_samples/stylesheetparser.html
  86. +115 −0 app/static/js/ckeditor/_samples/tableresize.html
  87. +129 −0 app/static/js/ckeditor/_samples/ui_color.html
  88. +134 −0 app/static/js/ckeditor/_samples/ui_languages.html
  89. +306 −0 app/static/js/ckeditor/_source/adapters/jquery.js
  90. +87 −0 app/static/js/ckeditor/_source/core/_bootstrap.js
  91. +141 −0 app/static/js/ckeditor/_source/core/ckeditor.js
  92. +235 −0 app/static/js/ckeditor/_source/core/ckeditor_base.js
  93. +238 −0 app/static/js/ckeditor/_source/core/ckeditor_basic.js
  94. +225 −0 app/static/js/ckeditor/_source/core/command.js
  95. +129 −0 app/static/js/ckeditor/_source/core/commanddefinition.js
  96. +447 −0 app/static/js/ckeditor/_source/core/config.js
  97. +65 −0 app/static/js/ckeditor/_source/core/dataprocessor.js
  98. +20 −0 app/static/js/ckeditor/_source/core/dom.js
  99. +44 −0 app/static/js/ckeditor/_source/core/dom/comment.js
  100. +251 −0 app/static/js/ckeditor/_source/core/dom/document.js
  101. +49 −0 app/static/js/ckeditor/_source/core/dom/documentfragment.js
  102. +258 −0 app/static/js/ckeditor/_source/core/dom/domobject.js
  103. +1,796 −0 app/static/js/ckeditor/_source/core/dom/element.js
  104. +117 −0 app/static/js/ckeditor/_source/core/dom/elementpath.js
  105. +145 −0 app/static/js/ckeditor/_source/core/dom/event.js
  106. +691 −0 app/static/js/ckeditor/_source/core/dom/node.js
  107. +26 −0 app/static/js/ckeditor/_source/core/dom/nodelist.js
  108. +2,068 −0 app/static/js/ckeditor/_source/core/dom/range.js
  109. +213 −0 app/static/js/ckeditor/_source/core/dom/rangelist.js
  110. +128 −0 app/static/js/ckeditor/_source/core/dom/text.js
  111. +496 −0 app/static/js/ckeditor/_source/core/dom/walker.js
  112. +96 −0 app/static/js/ckeditor/_source/core/dom/window.js
  113. +266 −0 app/static/js/ckeditor/_source/core/dtd.js
  114. +1,059 −0 app/static/js/ckeditor/_source/core/editor.js
  115. +186 −0 app/static/js/ckeditor/_source/core/editor_basic.js
  116. +305 −0 app/static/js/ckeditor/_source/core/env.js
  117. +342 −0 app/static/js/ckeditor/_source/core/event.js
  118. +120 −0 app/static/js/ckeditor/_source/core/eventInfo.js
  119. +152 −0 app/static/js/ckeditor/_source/core/focusmanager.js
  120. +224 −0 app/static/js/ckeditor/_source/core/htmlparser.js
  121. +145 −0 app/static/js/ckeditor/_source/core/htmlparser/basicwriter.js
  122. +43 −0 app/static/js/ckeditor/_source/core/htmlparser/cdata.js
  123. +60 −0 app/static/js/ckeditor/_source/core/htmlparser/comment.js
  124. +308 −0 app/static/js/ckeditor/_source/core/htmlparser/element.js
  125. +288 −0 app/static/js/ckeditor/_source/core/htmlparser/filter.js
  126. +537 −0 app/static/js/ckeditor/_source/core/htmlparser/fragment.js
  127. +53 −0 app/static/js/ckeditor/_source/core/htmlparser/text.js
  128. +157 −0 app/static/js/ckeditor/_source/core/lang.js
  129. +240 −0 app/static/js/ckeditor/_source/core/loader.js
  130. +83 −0 app/static/js/ckeditor/_source/core/plugindefinition.js
  131. +103 −0 app/static/js/ckeditor/_source/core/plugins.js
  132. +238 −0 app/static/js/ckeditor/_source/core/resourcemanager.js
  133. +180 −0 app/static/js/ckeditor/_source/core/scriptloader.js
  134. +184 −0 app/static/js/ckeditor/_source/core/skins.js
  135. +19 −0 app/static/js/ckeditor/_source/core/themes.js
  136. +763 −0 app/static/js/ckeditor/_source/core/tools.js
  137. +128 −0 app/static/js/ckeditor/_source/core/ui.js
  138. +84 −0 app/static/js/ckeditor/_source/lang/_languages.js
  139. +63 −0 app/static/js/ckeditor/_source/lang/_translationstatus.txt
Sorry, we could not display the entire diff because too many files (934) changed.
2  .gitignore
@@ -0,0 +1,2 @@
+*.pyc
+gzip
52 LICENSE
@@ -0,0 +1,52 @@
+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.
+
+--------
+This software uses portions of the Django project:
+
+Copyright (c) Django Software Foundation and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of Django nor the names of its contributors may be used
+ to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
5 README
@@ -0,0 +1,5 @@
+This is a python-flask application which manages live-blog style stories 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.
44 apache/production/apache
@@ -0,0 +1,44 @@
+<VirtualHost *:80>
+ServerName layercake.tribapps.com
+ServerAlias www.layercake.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/layercake/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/layercake/repository/app/production.wsgi
+
+ Redirect permanent /favicon.ico http://media.apps.chicagotribune.com/favicon.ico
+
+ ErrorLog /home/newsapps/logs/layercake.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/layercake.access.log combinedio env=is-forwarder
+ CustomLog /home/newsapps/logs/layercake.access.log combinedio2 env=!is-forwarder
+
+ ServerSignature Off
+
+ RewriteEngine on
+ # canonical hostname
+ RewriteCond %{HTTP_HOST} ^www.layercake.chicagotribune.com [NC]
+ RewriteRule ^/(.*) http://layercake.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 layercake.tribapps.com
+ServerAlias www.layercake.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/layercake.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/layercake.access.log combinedio env=is-forwarder
+ CustomLog /home/newsapps/logs/layercake.access.log combinedio2 env=!is-forwarder
+
+ ServerSignature Off
+
+ RewriteEngine on
+ # canonical hostname
+ RewriteCond %{HTTP_HOST} ^layercake.tribapps.com [NC]
+ RewriteRule ^/(.*) http://layercake.tribapps.com/$1 [L,R]
+
+ DocumentRoot /home/newsapps/sites/layercake/repository/app/static
+
+ RewriteCond %{REQUEST_URI} !/maintenance.html$
+ RewriteRule $ /maintenance.html [R=302,L]
+</VirtualHost>
38 apache/staging/apache
@@ -0,0 +1,38 @@
+<VirtualHost *:80>
+ ServerName layercake.beta.tribapps.com
+ ServerAlias www.layercake.beta.tribapps.com
+
+ SetEnv DEPLOYMENT_TARGET staging
+
+ <Location />
+ Order Deny,Allow
+ Allow from all
+ </Location>
+
+ <Directory /home/newsapps/sites/layercake/repository/app>
+ AuthType Basic
+ AuthName "Authorized Access Only"
+ AuthUserFile /etc/apache2/passwords
+ Require valid-user
+ </Directory>
+
+ WSGIScriptAlias / /home/newsapps/sites/layercake/repository/app/staging.wsgi
+
+ Redirect permanent /favicon.ico http://media-beta.tribapps.com/favicon.ico
+
+ ErrorLog /home/newsapps/logs/layercake.error.log
+ LogLevel warn
+
+ CustomLog /home/newsapps/logs/layercake.access.log combined
+
+ ServerSignature Off
+
+ RewriteEngine on
+
+ # canonical hostname
+ RewriteCond %{HTTP_HOST} ^www.layercake.beta.tribapps.com [NC]
+ RewriteRule ^/(.*) http://layercake.beta.tribapps.com/$1 [L,R]
+
+ RewriteCond %{REQUEST_URI} /maintenance.html$
+ RewriteRule $ / [R=302,L]
+</VirtualHost>
37 apache/staging/apache_maintenance
@@ -0,0 +1,37 @@
+<VirtualHost *:80>
+ServerName layercake.beta.tribapps.com
+ServerAlias www.layercake.beta.tribapps.com
+
+ <Location />
+ Order Deny,Allow
+ Allow from all
+ </Location>
+
+ <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/schools/repository/schools/assets/robots.txt
+
+ ErrorLog /home/newsapps/logs/layercake.error.log
+ LogLevel warn
+
+ CustomLog /home/newsapps/logs/layercake.access.log combined
+
+ ServerSignature Off
+
+ RewriteEngine on
+ # canonical hostname
+ RewriteCond %{HTTP_HOST} ^www.layercake.beta.tribapps.com [NC]
+ RewriteRule ^/(.*) http://layercake.beta.tribapps.com/$1 [L,R]
+
+ DocumentRoot /home/newsapps/sites/layercake/repository/assets/templates/
+
+ RewriteCond %{REQUEST_URI} !/maintenance.html$
+ RewriteRule $ /maintenance.html [R=302,L]
+</VirtualHost>
592 app/app.py
@@ -0,0 +1,592 @@
+from flask import Flask, request, redirect, flash, get_flashed_messages, render_template, session
+from decorators import templated
+import sys
+import json
+from lxml.html import fromstring,tostring
+from lxml.cssselect import CSSSelector
+from datetime import datetime
+from time import time
+import dateutil.tz
+import requests
+import os, os.path
+import s3deploy
+import re
+
+from optparse import OptionParser
+
+CURRENT_DIR = os.path.dirname(__file__)
+LOCAL_CONTENT_DIR = os.path.join(CURRENT_DIR,'static/out')
+
+def ap_formatted(dt):
+ return dt.strftime("%l:%M %p").strip().replace('AM','a.m.').replace('PM','p.m.')
+
+# From Django, via http://stackoverflow.com/questions/4970426/html-truncating-in-python
+def truncate_html_words(s, num, ellipsis = ' ...'):
+ """
+ Truncates html to a certain number of words (not counting tags and comments).
+ Closes opened tags if they were correctly closed in the given html.
+ """
+ length = int(num)
+ if length <= 0:
+ return ''
+ html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input')
+ # Set up regular expressions
+ re_words = re.compile(r'&.*?;|<.*?>|([A-Za-z0-9][\w-]*)')
+ re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
+ # Count non-HTML words and keep note of open tags
+ pos = 0
+ ellipsis_pos = 0
+ words = 0
+ open_tags = []
+ while words <= length:
+ m = re_words.search(s, pos)
+ if not m:
+ # Checked through whole string
+ break
+ pos = m.end(0)
+ if m.group(1):
+ # It's an actual non-HTML word
+ words += 1
+ if words == length:
+ ellipsis_pos = pos
+ continue
+ # Check for tag
+ tag = re_tag.match(m.group(0))
+ if not tag or ellipsis_pos:
+ # Don't worry about non tags or tags after our truncate point
+ continue
+ closing_tag, tagname, self_closing = tag.groups()
+ tagname = tagname.lower() # Element names are always case-insensitive
+ if self_closing or tagname in html4_singlets:
+ pass
+ elif closing_tag:
+ # Check for match in open tags list
+ try:
+ i = open_tags.index(tagname)
+ except ValueError:
+ pass
+ else:
+ # SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags
+ open_tags = open_tags[i+1:]
+ else:
+ # Add it to the start of the open tags list
+ open_tags.insert(0, tagname)
+ if words <= length:
+ # Don't try to close tags if we don't need to truncate
+ return s
+ out = s[:ellipsis_pos] + ellipsis
+ # Close any tags still open
+ for tag in open_tags:
+ out += '</%s>' % tag
+ # Return string
+ return out
+
+
+class NotLayerCake(Exception):
+ pass
+
+class LayerCakeApp(Flask):
+ """docstring for LayerCakeApp"""
+ def __init__(self, module,config_class="Config"):
+ super(LayerCakeApp, self).__init__(module)
+ self.config.from_object('config.%s' % config_class)
+
+ def http_headers(self, content_type=None):
+ h = {
+ 'Authorization': 'Bearer %(P2P_AUTH_TOKEN)s' % self.config,
+ }
+ if content_type is not None:
+ h['content-type'] = content_type
+ return h
+
+ def p2p_get(self,url):
+ resp = requests.get(url,headers=self.http_headers())
+ if not resp.ok: resp.raise_for_status()
+ return json.loads(resp.content)
+
+ def p2p_get_content_item(self,slug,related_items=False):
+ url = "%s/content_items/%s.json" % (self.config['P2P_API_ROOT'],slug)
+ if related_items:
+ url += "?include[]=related_items"
+ j = self.p2p_get(url)
+ return j['content_item']
+
+ def p2p_id_for_slug(self, slug):
+ ci = self.p2p_get_content_item(slug)
+ return ci['id']
+
+ def p2p_update(self, slug, content,update_timestamp=True):
+ if update_timestamp:
+ content = dict(content)
+ content['display_time'] = datetime.now(dateutil.tz.gettz('America/Chicago')).isoformat()
+
+ d = { 'content_item': content}
+ url = "%s/content_items/%s.json" % (self.config['P2P_API_ROOT'],slug)
+ resp = requests.put(url,data=json.dumps(d),headers=self.http_headers('application/json'))
+ if not resp.ok:
+ resp.raise_for_status()
+ return resp
+
+ def p2p_create_content_item(self, headline, body='',content_item_type="htmlstory"):
+ # slug seems to get ignored...
+ data = {'content_item': {
+ "content_item_type_code": content_item_type,
+ "product_affiliate_code": "chinews",
+ "source_code": "chicagotribune",
+ "content_item_state_code": "live",
+ "title": headline,
+ "body": body,
+ }}
+ url = '%(P2P_API_ROOT)s/content_items.json' % self.config
+ resp = requests.post(url,data=json.dumps(data),headers=self.http_headers('application/json'))
+ if not resp.ok:
+ resp.raise_for_status()
+ j = json.loads(resp.content)
+ return j['story']
+
+ def p2p_junk(self, slug):
+ self.p2p_update(slug, {'content_item_state_code': 'junk'} )
+
+ def p2p_search(self,params):
+ """This doesn't work!"""
+ url = "%(P2P_API_ROOT)s/content_items/search.json" % self.config
+ resp = requests.post(url,data=params,headers=self.http_headers())
+ if not resp.ok: resp.raise_for_status()
+ j = json.loads(resp.content)
+ return j['content_items']
+
+ def p2p_fetch_gallery(self, gallery_slug):
+ """For photo AND story galleries, retrieve the root content item and full records for all related items"""
+ item = self.p2p_get_content_item(gallery_slug,related_items=True)
+ items = self.fetch_multiple_items(live_related_item_ids(item['related_items']))
+ return {'gallery': item, 'related': items }
+
+ 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 = "%(P2P_API_ROOT)s/content_items/multi.json" % self.config
+ 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 update_meta_markup(self,slug,topper=None, story_url=None):
+ content_item = self.p2p_get_content_item(slug)
+ current_content = content_item['body']
+ parsed = fromstring(current_content)
+ if parsed.attrib.get('id','') == 'layercake':
+ wrapper = parsed
+ else:
+ wrapper = parsed.find('.//div[@id="layercake"]')
+ if wrapper is None:
+ raise NotLayerCake
+ if story_url:
+ wrapper.attrib['data-url'] = story_url
+ if topper:
+ topper_elem = wrapper.find('.//div[@id="layercake-topper"]')
+ if topper_elem is None:
+ raise NotLayerCake
+
+ # support markup in topper without requiring it be submitted wrapped in an element
+ new_topper = fromstring("<div id='layercake-topper'>%s</div>" % topper)
+ topper_elem.addnext(new_topper)
+ topper_elem.getparent().remove(topper_elem)
+
+ return tostring(parsed)
+
+ def prepare_updated_content(self,slug,rendered_content):
+ """Given a slug, get its current content. Either insert the HTML string 'rendered_content' as the first child
+ of the current content for the slug, or, if an existing element with the same tag and id exists, replace that
+ element. Return the title and current content as a tuple of strings."""
+ content_item = self.p2p_get_content_item(slug)
+ current_content = content_item['body']
+ title = content_item['title']
+ parsed = fromstring(current_content)
+ container = parsed.find('.//div[@id="layercake-items"]')
+ if container is None:
+ container = parsed.makeelement('div',{'id': 'layercake-items'})
+ made_container = True
+ else:
+ made_container = False
+ new_parsed = fromstring(rendered_content)
+ try:
+ existing = container.find("%s[@id='%s']" % (new_parsed.tag,new_parsed.attrib['id']))
+ except KeyError:
+ existing = None
+
+ if existing is not None:
+ existing.addnext(new_parsed)
+ container.remove(existing)
+ else:
+ container.insert(0,new_parsed)
+
+ # TODO: consider timestamping the CSS URL
+ if made_container:
+ new_content = tostring(container)
+ return title,current_content + new_content
+
+ return title,tostring(parsed)
+
+ def delete_update(self, slug, edit_id):
+ content_item = self.p2p_get_content_item(slug)
+ current_content = content_item['body']
+ parsed = fromstring(current_content)
+
+ existing = parsed.find( ".//div[@id='%s']" % (edit_id,) )
+ existing.getparent().remove(existing)
+
+ return content_item['title'], tostring(parsed)
+
+app = LayerCakeApp(__name__)
+app.secret_key = 'MAKE_THIS_UNIQUE_AND_SECRET'
+
+from flaskext.embedly import Embedly
+e = Embedly(app)
+
+@app.route('/')
+@templated()
+def index():
+ return {}
+
+@app.route('/create',methods=['post','get']) # remove 'get' when we have a real form
+def create():
+ """Given a headline (and any other strictly required values), create a new HTML Story content item prepped
+ with basic layercake formatting.
+ """
+ d = {
+ 'css_url': app.config['CSS_URL'],
+ 'topper': request.values.get('topper',''),
+ }
+ body = render_template('_new_cake.html',**d)
+ created = app.p2p_create_content_item(request.values.get('headline'),body=body)
+ created.update(app.config)
+ session['brand_new'] = True
+ return redirect('/edit/%s' % (created['slug'],))
+
+@app.route('/edit/<slug>',methods=['get'])
+@templated()
+def edit(slug):
+ context = { 'headline': '', 'update': '', 'topper': '', 'title': '', 'source': '', 'brand_new': session.pop('brand_new', False) }
+ context['slug'] = slug
+ context['active_tab'] = 'update'
+ context['edit_id'] = request.values.get('edit_id','')
+ context['timestamp'] = ''
+ context['edit_title'] = ''
+
+ if context['slug']:
+ content_item = app.p2p_get_content_item(context['slug'])
+ current_content = content_item['body']
+ context['title'] = content_item['title']
+ context['id'] = content_item['id']
+ populate_context_with_meta(current_content, context)
+
+ if context['edit_id']:
+ existing = extract_item(current_content, context['edit_id'])
+ try:
+ if existing is not None:
+ data_type = existing.attrib.get('data-type')
+ context['active_tab'] = data_type
+ CONTENT_HANDLERS[data_type].populate_form_context(context,existing)
+ except KeyError:
+ pass
+ else:
+ # TODO Redirect to create form when it exits
+ flash("This form must be loaded with a slug.","error")
+
+ return context
+
+@app.route('/preview/<slug>',methods=['get'])
+@templated()
+def preview(slug):
+ context = {}
+ if slug:
+ context['content'] = app.p2p_get_content_item(slug)
+ return context
+
+def extract_item(layercake,edit_id):
+ """Given the body of a layercake content item, return the specific edit id requested as a parsed lxml element"""
+ parsed = fromstring(layercake)
+ return parsed.find(".//div[@id='%s']" % (edit_id,))
+
+def populate_context_with_meta(content, context):
+ parsed = fromstring(content)
+ if parsed.attrib.get('id','') == 'layercake':
+ wrapper = parsed
+ else:
+ wrapper = parsed.find('.//div[@id="layercake"]')
+ if wrapper is None:
+ raise NotLayerCake
+ context['story_url'] = wrapper.attrib.get('data-url','')
+ topper = parsed.find('.//div[@id="layercake-topper"]')
+ if topper is None:
+ context['topper'] = ''
+ else:
+ parts = [topper.text]
+ for kid in topper.iterchildren():
+ parts.append(tostring(kid))
+ context['topper'] = ''.join(filter(None,parts))
+
+def extract_text(elem_list):
+ parts = []
+ for x in elem_list:
+ parts.extend(x.xpath('./text()'))
+ return ' '.join(parts).strip()
+
+def extract_markup(elem_list):
+ parts = []
+ for el in elem_list:
+ parts.extend(''.join([tostring(child) for child in el.iterchildren()]))
+ return ''.join(parts).strip()
+
+@app.route('/bookmarklet')
+@templated()
+def bookmarklet():
+ return { 'host' : request.environ['HTTP_HOST'], }
+
+@app.route('/update',methods=['post'])
+def update():
+ try:
+ if request.json:
+ values = request.json
+ else:
+ values = request.form
+ slug = values['slug']
+ if values['template'] == 'meta':
+ title = values['title']
+ new_content = app.update_meta_markup(slug,topper=values.get('topper',None),story_url=values.get('story_url',None))
+ app.p2p_update(slug, {'title': title, 'body': new_content })
+ else:
+ if values.has_key('intention') and values['intention'].lower().startswith('delete'):
+ edit_id = values.get('edit_id')
+ title, new_content = app.delete_update(slug, edit_id)
+ else:
+ handler = CONTENT_HANDLERS[values['template']]
+ context = handler.prepare_render_context(slug, request, values)
+ rendered_content = handler.render(context)
+ title, new_content = app.prepare_updated_content(slug,rendered_content)
+ app.p2p_update(slug, {'body': new_content })
+ update_blurb(slug, title, new_content)
+ flash("updated!")
+ except NotLayerCake:
+ flash("This is not a layercake story.", 'error')
+ return redirect('/edit/%s' % (slug,))
+
+
+class ContentItemHandler(object):
+ """All content handlers should have a template name. Most will not change the 'render' method
+ but will update process_context"""
+ text_extract_classes = ['headline', 'timestamp']
+ markup_extract_classes = []
+
+ def prepare_render_context(self, slug, request, values):
+ context = {'content_type': self.content_type}
+ for k in ['headline', 'timestamp', 'edit_id']:
+ context[k] = values.get(k,'')
+ if not (context.has_key('edit_id') and context['edit_id']):
+ context['edit_id'] = "%s_%s" % (slug,time())
+ if not context['timestamp'] or context['timestamp'] == 'auto':
+ context['timestamp'] = ap_formatted(datetime.now(dateutil.tz.gettz('America/Chicago')))
+ return context
+
+ def render(self, context):
+ "Context should come from 'prepare_render_context' to be sure everything is set up, esp. edit_id"
+ return render_template(self.template_name,**context)
+
+ def populate_form_context(self, context, existing_elem):
+ for k in self.text_extract_classes:
+ context[k] = extract_text(existing_elem.xpath(CSSSelector('.%s' % k).path))
+ for k in self.markup_extract_classes:
+ context[k] = extract_markup(existing_elem.xpath(CSSSelector('.%s' % k).path))
+ context['edit_title'] = 'Editing <em>%s</em>' % context['headline']
+ context['edit_id'] = existing_elem.attrib.get('id','')
+ context['content_type'] = existing_elem.attrib.get('data-type','')
+
+ def render_blurb(self, existing_elem):
+ context = {}
+ self.populate_form_context(context, existing_elem)
+
+ # @TODO maybe this is a weird location, should be in different handler?
+ if 'update' in context:
+ context['update'] = truncate_html_words(context['update'], 20)
+
+ try:
+ return render_template(self.blurb_template_name,**context)
+ except:
+ return ''
+
+ @property
+ def blurb_template_name(self):
+ return self.template_name.replace("_","_blurb_",1)
+
+class UpdateHandler(ContentItemHandler):
+ """Handle basic updates"""
+ content_type = 'update'
+ template_name = '_update.html'
+ text_extract_classes = ['headline', 'timestamp', 'source']
+ markup_extract_classes = ['update', 'caption']
+ def prepare_render_context(self, slug, request, values):
+ context = super(UpdateHandler, self).prepare_render_context(slug, request, values)
+ for k in ['body', 'source', 'caption',]:
+ context[k] = values.get(k,'')
+
+ try:
+ upload = request.files['photo']
+ if upload.filename:
+ key_name = 'layercake/uploads/%s/%s' % (slug, upload.filename)
+ context['photo_url'] = s3deploy.s3_upload_flo(upload.stream, app.config['S3_BUCKET'], key_name, upload.mimetype)
+ else:
+ context['photo_url'] = values['photo_url']
+ except KeyError, e:
+ context['photo_url'] = values['photo_url']
+
+ return context
+
+ def populate_form_context(self, context, existing_elem):
+ super(UpdateHandler, self).populate_form_context(context, existing_elem)
+ img = existing_elem.find('.//img[@class="photo"]')
+ if img is not None:
+ context['photo_url'] = img.attrib['src']
+
+class VideoHandler(ContentItemHandler):
+ """Handle video updates"""
+ content_type = 'video'
+ template_name = '_video.html'
+ text_extract_classes = ['headline', 'timestamp', 'source']
+ markup_extract_classes = ['update',]
+ def prepare_render_context(self, slug, request, values):
+ context = super(VideoHandler, self).prepare_render_context(slug, request, values)
+ for k in ['body', 'source', ]:
+ context[k] = values.get(k,'')
+
+ video_id = values.get('video_id','')
+ context['video'] = app.p2p_get_content_item(video_id)
+ return context
+
+ def populate_form_context(self, context, existing_elem):
+ super(VideoHandler, self).populate_form_context(context, existing_elem)
+ vid = existing_elem.find('.//div[@class="video"]')
+ if vid is not None:
+ context['video_id'] = vid.attrib['data-video-id']
+
+class TweetHandler(ContentItemHandler):
+ content_type = 'tweet'
+ template_name = '_tweet.html'
+ text_extract_classes = ['headline', 'timestamp',]
+ markup_extract_classes = ['update',]
+ def prepare_render_context(self, slug, request, values):
+ context = super(TweetHandler, self).prepare_render_context(slug, request, values)
+ for k in ['body', ]:
+ context[k] = values.get(k,'')
+ try:
+ twitter_response = get_twitter_embed(values['tweet_url'])
+ context['embed'] = twitter_response['html']
+ context['tweet_url'] = twitter_response['url']
+ except Exception, e:
+ app.logger.error("Error fetching tweet")
+ app.logger.error(e)
+ context['embed'] = 'Error!'
+ context['tweet_url'] = values.get('tweet_url','')
+ return context
+
+ def populate_form_context(self, context, existing_elem):
+ super(TweetHandler, self).populate_form_context(context, existing_elem)
+ wrapper = existing_elem.find('.//div[@class="tweet"]')
+ if wrapper is not None:
+ context['tweet_url'] = wrapper.attrib['data-url']
+ context['embed'] = extract_markup(wrapper) # this seems to miss the outer blockquote...
+
+
+class OEmbedHandler(ContentItemHandler):
+ content_type = 'oembed'
+ template_name = '_oembed.html'
+ text_extract_classes = ['headline', 'timestamp',]
+ markup_extract_classes = ['update',]
+ def prepare_render_context(self, slug, request, values):
+ context = super(OEmbedHandler, self).prepare_render_context(slug, request, values)
+ for k in ['body', 'oembed_url']:
+ context[k] = values.get(k,'')
+
+ return context
+
+ def populate_form_context(self, context, existing_elem):
+ super(OEmbedHandler, self).populate_form_context(context, existing_elem)
+ wrapper = existing_elem.find('.//div[@class="oembed"]')
+ if wrapper is not None:
+ context['oembed_url'] = wrapper.attrib['data-url']
+
+
+CONTENT_HANDLERS = {
+ 'update': UpdateHandler(),
+ 'video': VideoHandler(),
+ 'tweet': TweetHandler(),
+ 'oembed': OEmbedHandler(),
+}
+
+def get_twitter_embed(url_or_id):
+ if url_or_id.find('/') != -1:
+ id = url_or_id.split('/')[-1]
+ else:
+ id = url_or_id
+ resp = requests.get('https://api.twitter.com/1/statuses/oembed.json?id=%s' % id)
+ j = json.loads(resp.content)
+ if 'error' in j:
+ raise Exception(j['error'])
+ else:
+ return j
+
+def update_blurb(slug, title, new_content):
+ context = { 'title': title, 'blurbs': [] }
+ populate_context_with_meta(new_content, context)
+
+ for item in fromstring(new_content).xpath('.//div[@class="layercake-item"]')[:5]:
+ handler = CONTENT_HANDLERS[item.attrib['data-type']]
+ context['blurbs'].append(handler.render_blurb(item))
+ from StringIO import StringIO
+ key_name = 'layercake/uploads/%s/blurb.html' % (slug, )
+ blurb_url = s3deploy.s3_upload_flo(StringIO(render_template("_blurb.html", **context).encode('utf-8')), app.config['S3_BUCKET'], key_name, "text/html")
+
+def parse_args():
+ parser = OptionParser()
+ parser.add_option("-o", "--host", dest="host", default='localhost',
+ 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.")
+ (options, args) = parser.parse_args()
+ return options
+
+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 (which doesn't apply to story galleries...)"""
+ 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
+
+
+if __name__ == '__main__':
+ opts = parse_args()
+ if opts.target:
+ app.config.from_object('config.%s' % opts.target)
+
+ kwargs = { 'debug': True, 'host': opts.host }
+
+ app.run(**kwargs)
25 app/config.py
@@ -0,0 +1,25 @@
+class Config(object):
+ DEBUG = True
+ 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_TRIBTECH'
+ CSS_URL = 'http://localhost:5000/static/css/layercake.css'
+ S3_BUCKET = 'media-beta.tribapps.com'
+
+class StagingConfig(Config):
+ CSS_URL = 'http://media-beta.tribapps.com/layercake/css/layercake.css'
+ # staging p2p is just busted...
+ P2P_API_ROOT = 'https://content-api.p2p.tribuneinteractive.com'
+ P2P_ROOT = 'http://content.p2p.tila.trb'
+ P2P_AUTH_TOKEN = 'GET_THIS_FROM_TRIBTECH'
+
+class ProductionConfig(Config):
+ DEBUG = False
+ P2P_API_ROOT = 'https://content-api.p2p.tribuneinteractive.com'
+ P2P_ROOT = 'http://content.p2p.tila.trb'
+ P2P_AUTH_TOKEN = 'GET_THIS_FROM_TRIBTECH'
+ CSS_URL = 'http://media.apps.chicagotribune.com/layercake/css/layercake.css'
+ S3_BUCKET = 'media.apps.chicagotribune.com'
+
+class LocalProdConfig(ProductionConfig):
+ CSS_URL = 'http://localhost:5000/static/css/layercake.css'
20 app/decorators.py
@@ -0,0 +1,20 @@
+from flask import request, render_template
+from functools import wraps
+
+def templated(template=None):
+ """lifted from http://flask.pocoo.org/docs/patterns/viewdecorators/#templating-decorator"""
+ def decorator(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ template_name = template
+ if template_name is None:
+ template_name = request.endpoint.replace('.', '/') + '.html'
+ ctx = f(*args, **kwargs)
+ if ctx is None:
+ ctx = {}
+ elif not isinstance(ctx, dict):
+ return ctx
+ return render_template(template_name, **ctx)
+ return decorated_function
+ return decorator
+
29 app/production.wsgi
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+import sys, os, os.path, site
+import logging
+sys.stdout = sys.stderr # protect against spurious printing...
+
+# 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
+
+application.config.from_object('config.ProductionConfig')
30 app/s3deploy.py
@@ -0,0 +1,30 @@
+import sys, os.path
+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
+
+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 s3_upload_flo(flo, bucket_name, key_name, mimetype):
+ conn = _s3conn()
+ bucket = conn.get_bucket(bucket_name)
+
+ options = { 'Content-Type' : mimetype }
+
+ k = Key(bucket)
+ k.key = key_name
+ k.set_contents_from_file(flo, options, policy='public-read')
+ return 'http://%s/%s' % (bucket_name,key_name)
+
29 app/staging.wsgi
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+import sys, os, os.path, site
+import logging
+sys.stdout = sys.stderr # protect against spurious printing...
+
+# 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
+
+application.config.from_object('config.StagingConfig')
1  app/static/.gitignore
@@ -0,0 +1 @@
+out
28 app/static/bookmarklet/iframe.css
@@ -0,0 +1,28 @@
+iframe#layercake-editor {
+ position: fixed;
+ right: 0;
+ top: 0;
+ z-index: 9999999999; /* Highest z-index on CT.com is 2147483605 */
+ height: 100%;
+ width: 400px;
+ border-left: 1px solid #888;
+ -moz-box-shadow: -3px 0 5px #aaa;
+ -webkit-box-shadow: -3px 0 5px #aaa;
+ box-shadow: -3px 0 5px #aaa;
+}
+
+.item-edit {
+ float: left;
+ margin-left: -100px;
+ margin-top: -2px;
+ padding: 4px;
+ background-color: #faa732;
+ border: 1px solid #999;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ font-weight: bold;
+ font-size: 14px;
+ font-family: Helvetica, Arial, sans-serif;
+}
+
92 app/static/bookmarklet/load.js
@@ -0,0 +1,92 @@
+var iframe = document.getElementById('layercake-editor'),
+ start = document.location.href.lastIndexOf('/') + 1,
+ end = document.location.href.indexOf(','),
+ slug = (start > 0 && end > start) ? document.location.href.slice(start, end) : '',
+ baseURL = window.layercake.baseURL;
+
+if (!iframe) {
+ // Load CSS
+ var link = document.createElement('link');
+ link.setAttribute('type', 'text/css');
+ link.setAttribute('href', window.layercake.baseURL + '/static/bookmarklet/iframe.css');
+ link.setAttribute('rel', 'stylesheet');
+ link.setAttribute('media', 'screen');
+ document.body.appendChild(link);
+
+ var iframe = document.createElement('iframe');
+ iframe.setAttribute('id', 'layercake-editor');
+ iframe.setAttribute('allowTransparency', true);
+ iframe.setAttribute('frameBorder', 0);
+ iframe.setAttribute('data-open', 0);
+ iframe.setAttribute('src', baseURL + '/form?slug=' + encodeURIComponent(slug) );
+ document.body.appendChild(iframe);
+
+ add_edit_links();
+}
+
+function add_edit_links() {
+ var layercake = document.getElementById('layercake-items');
+
+ var items = document.getElementsByClassName('layercake-item');
+ for (i = 0; i < items.length; i++) {
+ var item = items[i];
+
+ var edit_link = document.createElement("a");
+ edit_link.setAttribute('data-item-id', item.id);
+ edit_link.setAttribute('class', 'item-edit');
+
+ var edit_text = document.createTextNode("Edit update");
+
+ edit_link.appendChild(edit_text);
+
+ layercake.insertBefore(edit_link, item);
+
+ edit_link.onclick = update_iframe;
+ edit_link.onkeydown = update_iframe;
+ }
+}
+
+function update_iframe() {
+ var id = this.getAttribute('data-item-id');
+ iframe.setAttribute('src', baseURL + '/form?slug=' + encodeURIComponent(slug) + '&edit_id=' + id);
+ return false;
+};
+
+
+function toggle_iframe() {
+ var iframe = document.getElementById('layercake-editor');
+ if (iframe.getAttribute('data-open') == 0) {
+ iframe.style.display = 'block';
+ iframe.setAttribute('data-open', 1);
+ }
+ else {
+ iframe.style.display = 'none';
+ iframe.setAttribute('data-open', 0);
+ }
+}
+
+// Create reload button which doesn't work on IE
+jQuery('<a id="reload-layercake">Reload story</a>')
+.attr('href', window.location.href + '?' + new Date().getTime())
+.insertBefore('#layercake');
+
+/*.click(function() {
+ jQuery('#layercake').load(window.location.href + ' #layercake', function() {
+ add_edit_links();
+ });
+ return false;
+});*/
+
+// Remove?
+/*if (window.addEventListener && window.postMessage) {
+ window.addEventListener("message", reloadContentItem, false);
+ function reloadContentItem(event) {
+ if (event.origin == baseURL && event.data == 'reload') {
+ jQuery('#story-body').load(window.location.href + ' #story-body', function() {
+ add_edit_links();
+ });
+ }
+ }
+} else {
+
+}*/
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;
+ }
+ .nav-collapse {
+ overflow: hidden;
+ height: 0;
+ }
+}
+@media (min-width: 980px) {
+ .nav-collapse.collapse {
+ height: auto !important;
+ overflow: visible !important;
+ }
+}
+@media (min-width: 1200px) {
+ .row {
+ margin-left: -30px;
+ *zoom: 1;
+ }
+ .row:before,
+ .row:after {
+ display: table;
+ content: "";
+ }
+ .row:after {
+ clear: both;
+ }
+ [class*="span"] {
+ float: left;
+ margin-left: 30px;
+ }
+ .container,
+ .navbar-fixed-top .container,
+ .navbar-fixed-bottom .container {
+ width: 1170px;
+ }
+ .span12 {
+ width: 1170px;
+ }
+ .span11 {
+ width: 1070px;
+ }
+ .span10 {
+ width: 970px;
+ }
+ .span9 {
+ width: 870px;
+ }
+ .span8 {
+ width: 770px;
+ }
+ .span7 {
+ width: 670px;
+ }
+ .span6 {
+ width: 570px;
+ }
+ .span5 {
+ width: 470px;
+ }
+ .span4 {
+ width: 370px;
+ }
+ .span3 {
+ width: 270px;
+ }
+ .span2 {
+ width: 170px;
+ }
+ .span1 {
+ width: 70px;
+ }
+ .offset12 {
+ margin-left: 1230px;
+ }
+ .offset11 {
+ margin-left: 1130px;
+ }
+ .offset10 {
+ margin-left: 1030px;
+ }
+ .offset9 {
+ margin-left: 930px;
+ }
+ .offset8 {
+ margin-left: 830px;
+ }
+ .offset7 {
+ margin-left: 730px;
+ }
+ .offset6 {
+ margin-left: 630px;
+ }
+ .offset5 {
+ margin-left: 530px;
+ }
+ .offset4 {
+ margin-left: 430px;
+ }
+ .offset3 {
+ margin-left: 330px;
+ }
+ .offset2 {
+ margin-left: 230px;
+ }
+ .offset1 {
+ margin-left: 130px;
+ }
+ .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.564102564%;
+ }
+ .row-fluid > [class*="span"]:first-child {
+ margin-left: 0;
+ }
+ .row-fluid > .span12 {
+ width: 100%;
+ }
+ .row-fluid > .span11 {
+ width: 91.45299145300001%;
+ }
+ .row-fluid > .span10 {
+ width: 82.905982906%;
+ }
+ .row-fluid > .span9 {
+ width: 74.358974359%;
+ }
+ .row-fluid > .span8 {
+ width: 65.81196581200001%;
+ }
+ .row-fluid > .span7 {
+ width: 57.264957265%;
+ }
+ .row-fluid > .span6 {
+ width: 48.717948718%;
+ }
+ .row-fluid > .span5 {
+ width: 40.170940171000005%;
+ }
+ .row-fluid > .span4 {
+ width: 31.623931624%;
+ }
+ .row-fluid > .span3 {
+ width: 23.076923077%;
+ }
+ .row-fluid > .span2 {
+ width: 14.529914530000001%;
+ }
+ .row-fluid > .span1 {
+ width: 5.982905983%;
+ }
+ input,
+ textarea,
+ .uneditable-input {
+ margin-left: 0;
+ }
+ input.span12, textarea.span12, .uneditable-input.span12 {
+ width: 1160px;
+ }
+ input.span11, textarea.span11, .uneditable-input.span11 {
+ width: 1060px;
+ }
+ input.span10, textarea.span10, .uneditable-input.span10 {
+ width: 960px;
+ }
+ input.span9, textarea.span9, .uneditable-input.span9 {
+ width: 860px;
+ }
+ input.span8, textarea.span8, .uneditable-input.span8 {
+ width: 760px;
+ }
+ input.span7, textarea.span7, .uneditable-input.span7 {
+ width: 660px;
+ }
+ input.span6, textarea.span6, .uneditable-input.span6 {
+ width: 560px;
+ }
+ input.span5, textarea.span5, .uneditable-input.span5 {
+ width: 460px;
+ }
+ input.span4, textarea.span4, .uneditable-input.span4 {
+ width: 360px;
+ }
+ input.span3, textarea.span3, .uneditable-input.span3 {
+ width: 260px;
+ }
+ input.span2, textarea.span2, .uneditable-input.span2 {
+ width: 160px;
+ }
+ input.span1, textarea.span1, .uneditable-input.span1 {
+ width: 60px;
+ }
+ .thumbnails {
+ margin-left: -30px;
+ }
+ .thumbnails > li {
+ margin-left: 30px;
+ }
+}
12 app/static/bootstrap/css/bootstrap-responsive.min.css
@@ -0,0 +1,12 @@
+.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;-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;-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;} .nav-collapse{overflow:hidden;height:0;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:30px;} .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .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.564102564%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid > .span12{width:100%;} .row-fluid > .span11{width:91.45299145300001%;} .row-fluid > .span10{width:82.905982906%;} .row-fluid > .span9{width:74.358974359%;} .row-fluid > .span8{width:65.81196581200001%;} .row-fluid > .span7{width:57.264957265%;} .row-fluid > .span6{width:48.717948718%;} .row-fluid > .span5{width:40.170940171000005%;} .row-fluid > .span4{width:31.623931624%;} .row-fluid > .span3{width:23.076923077%;} .row-fluid > .span2{width:14.529914530000001%;} .row-fluid > .span1{width:5.982905983%;} input,textarea,.uneditable-input{margin-left:0;} input.span12, textarea.span12, .uneditable-input.span12{width:1160px;} input.span11, textarea.span11, .uneditable-input.span11{width:1060px;} input.span10, textarea.span10, .uneditable-input.span10{width:960px;} input.span9, textarea.span9, .uneditable-input.span9{width:860px;} input.span8, textarea.span8, .uneditable-input.span8{width:760px;} input.span7, textarea.span7, .uneditable-input.span7{width:660px;} input.span6, textarea.span6, .uneditable-input.span6{width:560px;} input.span5, textarea.span5, .uneditable-input.span5{width:460px;} input.span4, textarea.span4, .uneditable-input.span4{width:360px;} input.span3, textarea.span3, .uneditable-input.span3{width:260px;} input.span2, textarea.span2, .uneditable-input.span2{width:160px;} input.span1, textarea.span1, .uneditable-input.span1{width:60px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;}}
3,990 app/static/bootstrap/css/bootstrap.css
@@ -0,0 +1,3990 @@
+/*!
+ * Bootstrap 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.
+ */
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section {
+ display: block;
+}
+audio,
+canvas,
+video {
+ display: inline-block;
+ *display: inline;
+ *zoom: 1;
+}
+audio:not([controls]) {
+ display: none;
+}
+html {
+ font-size: 100%;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+}
+a:focus {
+ outline: thin dotted #333;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px;
+}
+a:hover,
+a:active {
+ outline: 0;
+}
+sub,
+sup {
+ position: relative;
+ font-size: 75%;
+ line-height: 0;
+ vertical-align: baseline;
+}
+sup {
+ top: -0.5em;
+}
+sub {
+ bottom: -0.25em;
+}
+img {
+ height: auto;
+ border: 0;
+ -ms-interpolation-mode: bicubic;
+ vertical-align: middle;
+}
+button,
+input,
+select,
+textarea {
+ margin: 0;
+ font-size: 100%;
+ vertical-align: middle;
+}
+button,
+input {
+ *overflow: visible;
+ line-height: normal;
+}
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+}
+button,
+input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+ cursor: pointer;
+ -webkit-appearance: button;
+}
+input[type="search"] {
+ -webkit-appearance: textfield;
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+}
+input[type="search"]::-webkit-search-decoration,
+input[type="search"]::-webkit-search-cancel-button {
+ -webkit-appearance: none;
+}
+textarea {
+ overflow: auto;
+ vertical-align: top;
+}
+.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;
+}
+body {
+ margin: 0;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ color: #333333;
+ background-color: #ffffff;
+}
+a {
+ color: #0088cc;
+ text-decoration: none;
+}
+a:hover {
+ color: #005580;
+ text-decoration: underline;
+}
+.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: 940px;
+}
+.span12 {
+ width: 940px;
+}
+.span11 {
+ width: 860px;
+}
+.span10 {
+ width: 780px;
+}
+.span9 {
+ width: 700px;
+}
+.span8 {
+ width: 620px;
+}
+.span7 {
+ width: 540px;
+}
+.span6 {
+ width: 460px;
+}
+.span5 {
+ width: 380px;
+}
+.span4 {
+ width: 300px;
+}
+.span3 {
+ width: 220px;
+}
+.span2 {
+ width: 140px;
+}
+.span1 {
+ width: 60px;
+}
+.offset12 {
+ margin-left: 980px;
+}
+.offset11 {
+ margin-left: 900px;
+}
+.offset10 {
+ margin-left: 820px;
+}
+.offset9 {
+ margin-left: 740px;
+}
+.offset8 {
+ margin-left: 660px;
+}
+.offset7 {
+ margin-left: 580px;
+}
+.offset6 {
+ margin-left: 500px;
+}
+.offset5 {
+ margin-left: 420px;
+}
+.offset4 {
+ margin-left: 340px;
+}
+.offset3 {
+ margin-left: 260px;
+}
+.offset2 {
+ margin-left: 180px;
+}
+.offset1 {
+ margin-left: 100px;
+}
+.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.127659574%;
+}
+.row-fluid > [class*="span"]:first-child {
+ margin-left: 0;
+}
+.row-fluid > .span12 {
+ width: 99.99999998999999%;
+}
+.row-fluid > .span11 {
+ width: 91.489361693%;
+}
+.row-fluid > .span10 {
+ width: 82.97872339599999%;
+}
+.row-fluid > .span9 {
+ width: 74.468085099%;
+}
+.row-fluid > .span8 {
+ width: 65.95744680199999%;
+}
+.row-fluid > .span7 {
+ width: 57.446808505%;
+}
+.row-fluid > .span6 {
+ width: 48.93617020799999%;
+}
+.row-fluid > .span5 {
+ width: 40.425531911%;
+}
+.row-fluid > .span4 {
+ width: 31.914893614%;
+}
+.row-fluid > .span3 {
+ width: 23.404255317%;
+}
+.row-fluid > .span2 {
+ width: 14.89361702%;
+}
+.row-fluid > .span1 {
+ width: 6.382978723%;
+}
+.container {
+ margin-left: auto;
+ margin-right: auto;
+ *zoom: 1;
+}
+.container:before,
+.container:after {
+ display: table;
+ content: "";
+}
+.container:after {
+ clear: both;
+}
+.container-fluid {
+ padding-left: 20px;
+ padding-right: 20px;
+ *zoom: 1;
+}
+.container-fluid:before,
+.container-fluid:after {
+ display: table;
+ content: "";
+}
+.container-fluid:after {
+ clear: both;
+}
+p {
+ margin: 0 0 9px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+}
+p small {
+ font-size: 11px;
+ color: #999999;
+}
+.lead {
+ margin-bottom: 18px;
+ font-size: 20px;
+ font-weight: 200;
+ line-height: 27px;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin: 0;
+ font-family: inherit;
+ font-weight: bold;
+ color: inherit;
+ text-rendering: optimizelegibility;
+}
+h1 small,
+h2 small,
+h3 small,
+h4 small,
+h5 small,
+h6 small {
+ font-weight: normal;
+ color: #999999;
+}
+h1 {
+ font-size: 30px;
+ line-height: 36px;
+}
+h1 small {
+ font-size: 18px;
+}
+h2 {
+ font-size: 24px;
+ line-height: 36px;
+}
+h2 small {
+ font-size: 18px;
+}
+h3 {
+ line-height: 27px;
+ font-size: 18px;
+}
+h3 small {
+ font-size: 14px;
+}
+h4,
+h5,
+h6 {
+ line-height: 18px;
+}
+h4 {
+ font-size: 14px;
+}
+h4 small {
+ font-size: 12px;
+}
+h5 {
+ font-size: 12px;
+}
+h6 {
+ font-size: 11px;
+ color: #999999;
+ text-transform: uppercase;
+}
+.page-header {
+ padding-bottom: 17px;
+ margin: 18px 0;
+ border-bottom: 1px solid #eeeeee;
+}
+.page-header h1 {
+ line-height: 1;
+}
+ul,
+ol {
+ padding: 0;
+ margin: 0 0 9px 25px;
+}
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+ margin-bottom: 0;
+}
+ul {
+ list-style: disc;
+}
+ol {
+ list-style: decimal;
+}
+li {
+ line-height: 18px;
+}
+ul.unstyled,
+ol.unstyled {
+ margin-left: 0;
+ list-style: none;
+}
+dl {
+ margin-bottom: 18px;
+}
+dt,
+dd {
+ line-height: 18px;
+}
+dt {
+ font-weight: bold;
+ line-height: 17px;
+}
+dd {
+ margin-left: 9px;
+}
+.dl-horizontal dt {
+ float: left;
+ clear: left;
+ width: 120px;
+ text-align: right;
+}
+.dl-horizontal dd {
+ margin-left: 130px;
+}
+hr {
+ margin: 18px 0;
+ border: 0;
+ border-top: 1px solid #eeeeee;
+ border-bottom: 1px solid #ffffff;
+}
+strong {
+ font-weight: bold;
+}
+em {
+ font-style: italic;
+}
+.muted {
+ color: #999999;
+}
+abbr[title] {
+ border-bottom: 1px dotted #ddd;
+ cursor: help;
+}
+abbr.initialism {
+ font-size: 90%;
+ text-transform: uppercase;
+}
+blockquote {
+ padding: 0 0 0 15px;
+ margin: 0 0 18px;
+ border-left: 5px solid #eeeeee;
+}
+blockquote p {
+ margin-bottom: 0;
+ font-size: 16px;
+ font-weight: 300;
+ line-height: 22.5px;
+}
+blockquote small {
+ display: block;
+ line-height: 18px;
+ color: #999999;
+}
+blockquote small:before {
+ content: '\2014 \00A0';
+}
+blockquote.pull-right {
+ float: right;
+ padding-left: 0;
+ padding-right: 15px;
+ border-left: 0;