aboutsummaryrefslogtreecommitdiffstats
path: root/tipboard/app.py
diff options
context:
space:
mode:
Diffstat (limited to 'tipboard/app.py')
-rw-r--r--tipboard/app.py299
1 files changed, 299 insertions, 0 deletions
diff --git a/tipboard/app.py b/tipboard/app.py
new file mode 100644
index 0000000..59f93b4
--- /dev/null
+++ b/tipboard/app.py
@@ -0,0 +1,299 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import os
+import json
+import logging
+
+from raven.contrib.tornado import SentryMixin, AsyncSentryClient
+import tornado.gen
+import tornado.ioloop
+import tornado.options
+import tornado.web
+import tornado.websocket
+import tornadoredis
+
+from tipboard import settings
+from tipboard.api import api_version, MetaProperty, TileData, ProjectInfo, Push
+from tipboard.parser import process_layout_config, get_config_files_names
+from tipboard.redis_utils import db_events_path
+
+log = logging.getLogger(__name__)
+
+
+class Flipboard(object):
+
+ def __init__(self):
+ self.last_found_configs_number = -1
+ self.paths = []
+
+ def get_paths(self):
+ """
+ Returns url paths to dashboards (created from .yaml config from
+ userspace).
+ """
+ config_names = self._get_config_names()
+ if len(config_names) != self.last_found_configs_number:
+ self.paths = self._config_names2paths(config_names)
+ self.last_found_configs_number = len(config_names)
+ return self.paths
+
+ def get_flipboard_title(self):
+ """
+ Returns title to display as a html title.
+ """
+ title = ''
+ config_names = self._get_config_names()
+ if len(config_names) == 1:
+ config = process_layout_config(config_names[0])
+ try:
+ title = config['details']['page_title']
+ except KeyError:
+ msg = 'config {} has no key: details/page_title'.format(
+ config_names[0]
+ )
+ log.error(msg)
+ elif len(config_names) > 1:
+ # TODO: put here more suitable title?
+ title = 'Flipboard Mode'
+ return title
+
+ def _get_config_names(self):
+ config_names = settings.FLIPBOARD_SEQUENCE
+ if not any(config_names):
+ config_names = get_config_files_names()
+ if not any(config_names):
+ raise Exception('No config (.yaml) file found in ~/.tipboard/')
+ return config_names
+
+ def _config_names2paths(self, config_names):
+ paths = []
+ for name in config_names:
+ path = '/' + name
+ paths.append(path)
+ return paths
+
+
+class RedisMixin(object):
+ """Trivial connection mixin."""
+
+ def setup_redis(self):
+ # We don't have to manage reconnections, tornadoredis does that
+ # automatically.
+ client = tornadoredis.Client(**settings.REDIS_ASYNC)
+ client.connect()
+ return client
+
+
+class DashboardSocketHandler(tornado.websocket.WebSocketHandler, RedisMixin,
+ SentryMixin):
+ """Handles client connections on web sockets and listens on a Redis
+ subscription."""
+
+ cache = set() # Only cache keys. Values will be retrieved from Redis
+ # every time. This ensures users don't get stale data.
+ tipboard_helpers = {
+ 'color': settings.COLORS,
+ 'log_level': settings.JS_LOG_LEVEL,
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(DashboardSocketHandler, self).__init__(*args, **kwargs)
+ self.listen()
+ self.getter = self.setup_redis()
+
+ def allow_draft76(self):
+ # for iOS 5.0 Safari
+ return True
+
+ def on_close(self):
+ log.info('Web socket closed.')
+ if self.pubsub.subscribed:
+ self.pubsub.unsubscribe(db_events_path())
+ self.pubsub.disconnect()
+ self.getter.disconnect()
+
+ @tornado.gen.engine
+ def on_message(self, message):
+ log.info('Message received: %s.', message)
+ if message != 'update':
+ return
+ for tile_id in self.cache:
+ log.debug('Putting data for tile: {}'.format(tile_id))
+ raw = yield tornado.gen.Task(self.getter.get, tile_id)
+ if not raw:
+ log.warn('No data in key %s on Redis.', tile_id)
+ del self.cache[tile_id]
+ continue
+ data = json.loads(raw)
+ data['tipboard'] = self.tipboard_helpers
+ self.write_message(data)
+
+ @tornado.gen.engine
+ def on_publish(self, msg):
+ if msg.kind == 'disconnect':
+ log.warn('Redis disconnected, closing Web socket.')
+ self.write_message(
+ 'The connection terminated due to a Redis server error.'
+ )
+ self.close()
+ elif msg.kind == 'message':
+ tile_id = str(msg.body)
+ log.info('Updating %s...', tile_id)
+ raw = yield tornado.gen.Task(self.getter.get, tile_id)
+ if not raw:
+ log.warn('No data in key %s on Redis.', tile_id)
+ return
+ data = json.loads(raw)
+ data['tipboard'] = self.tipboard_helpers
+ self.cache.add(tile_id)
+ self.write_message(data)
+ log.info('Sent new data for %s through the Web socket.', tile_id)
+
+ @tornado.gen.engine
+ def listen(self):
+ log.info('Web socket opened.')
+ self.pubsub = self.setup_redis()
+ yield tornado.gen.Task(
+ self.pubsub.subscribe, db_events_path()
+ )
+ self.pubsub.listen(self.on_publish)
+ log.info('Subscribed to %s on Redis.' % db_events_path())
+
+
+class DashboardRendererHandler(tornado.web.RequestHandler, SentryMixin):
+ def get(self, layout_name):
+ def _verify_statics(static_file):
+ user_tiles_path = os.path.join(
+ os.path.expanduser('~'), '.tipboard/custom_tiles'
+ )
+ tipboard_tiles_path = os.path.join(settings.TIPBOARD_PATH, 'tiles')
+ found = False
+ for path in user_tiles_path, tipboard_tiles_path:
+ if os.path.exists(os.path.join(path, static_file)):
+ found = True
+ break
+ return found
+
+ def _tile_path(tile_name):
+ """
+ Searches for tile's html file (in user's 'custom_tiles' folder,
+ and then in app's 'tiles' folder) and returns full path of
+ the tile, or raises exception if html file is not present in none
+ of those locations.
+ """
+ user_tiles_path = os.path.join(
+ os.path.expanduser('~'), '.tipboard/custom_tiles'
+ )
+ tipboard_tiles_path = os.path.join(settings.TIPBOARD_PATH, 'tiles')
+ tile_html = '.'.join((tile_name, 'html'))
+ for path in user_tiles_path, tipboard_tiles_path:
+ tile_path = os.path.join(path, tile_html)
+ if os.path.exists(tile_path):
+ return tile_path
+ raise UserWarning('No such tile: %s' % tile_name)
+
+ try:
+ config = process_layout_config(layout_name or 'layout_config')
+ except IOError as e:
+ msg = '<br>'.join([
+ '<div style="color: red">',
+ 'No config file found for dashboard: {}'.format(layout_name),
+ 'Make sure that file: "{}" exists.'.format(e.filename),
+ '</div>',
+ ])
+ self.write(msg)
+ return
+
+ tiles_js = ['.'.join((name, 'js')) for name in config['tiles_names']]
+ tiles_js = filter(_verify_statics, tiles_js)
+ tiles_css = ['.'.join((name, 'css')) for name in config['tiles_names']]
+ tiles_css = filter(_verify_statics, tiles_css)
+ self.render(
+ 'layout.html',
+ details=config['details'],
+ layout=config['layout'],
+ tipboard_css=settings.TIPBOARD_CSS_STYLES,
+ tipboard_js=settings.TIPBOARD_JAVASCRIPTS,
+ tiles_css=tiles_css,
+ tiles_js=tiles_js,
+ tile_path=_tile_path,
+ )
+
+ def head(self):
+ # muted
+ pass
+
+
+class GetDashboardsPaths(tornado.web.RequestHandler):
+ def post(self):
+ paths = flipboard.get_paths()
+ jsoned = json.dumps({'paths': paths})
+ self.set_header("Content-Type", 'application/json')
+ self.write(jsoned)
+
+
+class FlipboardHandler(tornado.web.RequestHandler, SentryMixin):
+ def get(self):
+ self.render(
+ 'flipboard.html',
+ page_title=flipboard.get_flipboard_title(),
+ tipboard_css=settings.TIPBOARD_CSS_STYLES,
+ tipboard_js=['js/lib/jquery.js', 'js/flipboard.js'],
+ flipboard_interval=settings.FLIPBOARD_INTERVAL,
+ )
+
+
+class MultiStaticFileHandler(tornado.web.StaticFileHandler, SentryMixin):
+ def initialize(self, path):
+ self.static_paths = []
+ self.static_paths.extend(self.settings.get('tiles_paths'))
+ self.static_paths.append(path)
+
+ def get(self, path):
+ # static paths are examined in following order:
+ # custom_tiles --> tipboard/tiles --> tipboard/static
+ for p in self.static_paths:
+ try:
+ super(MultiStaticFileHandler, self).initialize(p)
+ return super(MultiStaticFileHandler, self).get(path)
+ except tornado.web.HTTPError as exc:
+ if exc.status_code == 404:
+ continue
+ raise
+ raise tornado.web.HTTPError(404)
+
+ @classmethod
+ def get_version(cls, settings, path):
+ # temporarily muted
+ return None
+
+
+urls = [
+ (r"/", FlipboardHandler),
+ (r"/flipboard/getDashboardsPaths", GetDashboardsPaths),
+ (r"/communication/websocket", DashboardSocketHandler),
+ (r"/([a-zA-Z0-9_-]*)", DashboardRendererHandler),
+ (r"/api/{}/{}/tileconfig/([a-zA-Z0-9_-]+)".format(
+ api_version, settings.API_KEY), MetaProperty),
+ (r"/api/{}/{}/tiledata/([a-zA-Z0-9_-]+)".format(
+ api_version, settings.API_KEY), TileData),
+ (r"/api/{}/{}/info".format(api_version, settings.API_KEY), ProjectInfo),
+ (r"/api/{}/{}/push".format(api_version, settings.API_KEY), Push),
+]
+
+flipboard = Flipboard()
+app = tornado.web.Application(
+ urls,
+ template_path=os.path.join(settings.TIPBOARD_PATH, 'templates'),
+ static_path=os.path.join(settings.TIPBOARD_PATH, "static"),
+ static_handler_class=MultiStaticFileHandler,
+ debug=settings.DEBUG,
+ tiles_paths=settings.TILES_PATHS,
+ sentry_client=AsyncSentryClient(settings.SENTRY_DSN),
+)