Exemplo n.º 1
0
class WallApp(Application, EventTarget):
    def __init__(self, config={}, config_path=None):
        Application.__init__(self, template_path=template_path, autoescape=None)
        EventTarget.__init__(self)

        self.logger = getLogger('wall')
        self.bricks = {}
        self.post_types = {}
        self.clients = []
        self.current_post = None
        self._init = True

        self._setup_logger()

        config_paths = [os.path.join(res_path, 'default.cfg')]
        if config_path:
            config_paths.append(config_path)
        try:
            parser = SafeConfigParser()
            parser.read(config_paths)
        except ConfigParserError as e:
            self.logger.error('failed to parse configuration file')
            self._init = False
            return

        self.config = {}
        for section in parser.sections():
            prefix = section + '.' if section != 'wall' else ''
            for key, value in parser.items(section):
                self.config[prefix + key] = value
        self.config.update(config)

        self.db = ObjectRedis(StrictRedis(db=int(self.config['db'])),
            self._decode_redis_hash)
        self.posts = RedisContainer(self.db, 'posts')

        self.add_post_type(TextPost)
        self.add_post_type(ImagePost)
        self.msg_handlers = {
            'post': self.post_msg,
            'post_new': self.post_new_msg,
            'get_history': self.get_history_msg
        }
        self.add_event_listener('posted', self._posted)

        # initialize bricks
        bricks = self.config['bricks'].split()
        for name in bricks:
            module = __import__(name, globals(), locals(), [b'foo'])
            brick = module.Brick(self)
            self.bricks[brick.id] = brick

        self.do_post_handlers = []
        for handler in self.config['do_post_handlers'].split():
            if handler not in ['note', 'history']:
                self.logger.warning('configuration: invalid item in do_post_handlers: "{}" unknown'.format(handler));
                continue
            if handler in self.do_post_handlers:
                self.logger.warning('configuration: invalid item in do_post_handlers: "{}" non-unique'.format(handler))
                continue
            self.do_post_handlers.append(handler)

        if self.config['debug'] == 'True':
            self.settings['debug'] = True
            tornado.autoreload.watch(os.path.join(res_path, 'default.cfg'))
            tornado.autoreload.start()

        # setup URL handlers
        urls = [
            ('/$', ClientPage),
            ('/display$', DisplayPage),
            ('/display/post$', DisplayPostPage),
            ('/api/socket$', Socket),
        ]
        for brick in self.bricks.values():
            urls.append(('/static/{0}/(.+)$'.format(brick.id),
                StaticFileHandler, {'path': brick.static_path}))
        urls.append(('/static/(.+)$', StaticFileHandler, {'path': static_path}))
        self.add_handlers('.*$', urls)

    @property
    def js_modules(self):
        return [b.js_module for b in self.bricks.values()]

    @property
    def scripts(self):
        scripts = []
        for brick in self.bricks.values():
            scripts.extend(brick.id + '/' + s for s in brick.scripts)
        return scripts

    @property
    def stylesheets(self):
        stylesheets = []
        for brick in self.bricks.values():
            stylesheets.extend(brick.id + '/' + s for s in brick.stylesheets)
        return stylesheets

    def run(self):
        if not self._init:
            return
        self.listen(8080)
        self.logger.info('server started')
        IOLoop.instance().start()

    def add_message_handler(self, type, handler):
        """
        Extension API: register a new message `handler` for messages of the
        given `type`.

        A message handler is a function `handle(msg)` that processes a received
        message. It may return a `Message`, which is sent back to the sender as
        response. If a (subclass of) `Error` is raised, it is converted to a
        `Message` and sent back to the sender as error response.
        """
        self.msg_handlers[type] = handler

    def sendall(self, msg):
        for client in self.clients:
            client.send(msg)

    def post_msg(self, msg):
        # TODO: error handling
        post = self.post(msg.data['id'])
        return Message('post', post.json())

    def post_new_msg(self, msg):
        # wake display
        Popen('DISPLAY=:0.0 xset dpms force on', shell=True)

        post_type = msg.data.pop('type')
        post = self.post_new(post_type, **msg.data)
        return Message('post_new', post.json())

    def get_history_msg(self, msg):
        return Message('get_history',
            [p.json('common') for p in self.get_history()])

    def post(self, id):
        try:
            post = self.posts[id]
        except KeyError:
            raise ValueError('id_nonexistent')

        if self.current_post:
            self.current_post.deactivate()

        self.current_post = post
        post.posted = datetime.utcnow().isoformat()
        self.db.hset(post.id, 'posted', post.posted)
        post.activate()

        self.dispatch_event(Event('posted', post=post))
        return post

    def post_new(self, type, **args):
        try:
            post_type = self.post_types[type]
        except KeyError:
            raise ValueError('type_nonexistent')

        post = post_type.create(self, **args)
        self.db.sadd('posts', post.id)
        return self.post(post.id)

    def get_history(self):
        return sorted(self.posts.values(), key=lambda p: p.posted, reverse=True)

    def add_post_type(self, post_type):
        """
        Extension API: register a new post type. `post_type` is a class (type)
        that extends `Post`.
        """
        self.post_types[post_type.__name__] = post_type

    def _decode_redis_hash(self, hash):
        post_type = self.post_types[hash['__type__']]
        return post_type(self, **hash)

    def _setup_logger(self):
        logger = getLogger()
        logger.setLevel(DEBUG)
        handler = StreamHandler()
        handler.setFormatter(
            Formatter('%(asctime)s %(levelname)s %(name)s: %(message)s'))
        logger.addHandler(handler)

    def _posted(self, event):
        self.sendall(Message('posted', {'post': event.args['post'].json()}))
Exemplo n.º 2
0
class WallApp(Object, EventTarget, Collection, Application):
    """
    Wall application.

    Events:

     * `connected`
     * `disconnected`
    """

    def __init__(self, config={}, config_path=None):
        super(WallApp, self).__init__('wall', self)
        EventTarget.__init__(self)
        Collection.__init__(self)
        Application.__init__(self, template_path=template_path, autoescape=None)

        self.logger = getLogger('wall')
        self.bricks = {}
        self.post_types = {}
        self.clients = []
        self.current_post = None
        self._init = True

        self._setup_logger()

        config_paths = [os.path.join(res_path, 'default.cfg')]
        if config_path:
            config_paths.append(config_path)
        try:
            parser = SafeConfigParser()
            parser.read(config_paths)
        except ConfigParserError as e:
            self.logger.error('failed to parse configuration file')
            self._init = False
            return

        self.config = {}
        for section in parser.sections():
            prefix = section + '.' if section != 'wall' else ''
            for key, value in parser.items(section):
                self.config[prefix + key] = value
        self.config.update(config)

        self.db = ObjectRedis(StrictRedis(db=int(self.config['db'])),
            self._decode_redis_hash)
        self.posts = RedisContainer(self.db, 'posts')

        self.add_post_type(TextPost)
        self.add_post_type(ImagePost)
        self.add_post_type(GridPost)

        self.msg_handlers = {
            'get_history': self.get_history_msg,
            'collection_get_items': self.collection_get_items_msg,
            'collection_post': self.collection_post_msg,
            'collection_post_new': self.collection_post_new_msg,
            'collection_remove_item': self.collection_remove_item_msg
        }

        self.add_event_listener('collection_posted', self._collection_posted)
        self.add_event_listener('collection_item_removed',
            self._collection_item_removed)
        self.add_event_listener('collection_item_activated',
            self._collection_item_activated)
        self.add_event_listener('collection_item_deactivated',
            self._collection_item_deactivated)

        # initialize bricks
        bricks = self.config['bricks'].split()
        for name in bricks:
            module = __import__(name, globals(), locals(), [b'foo'])
            brick = module.Brick(self)
            self.bricks[brick.id] = brick

        if self.config['debug'] == 'True':
            self.settings['debug'] = True
            self.settings['autoreload'] = True
            self.settings['compiled_template_cache'] = False
            self.settings['static_hash_cache'] = False
            self.settings['serve_traceback'] = True
            tornado.autoreload.watch(os.path.join(res_path, 'default.cfg'))
            tornado.autoreload.start()

        # setup URL handlers
        urls = [
            ('/$', ClientPage),
            ('/display$', DisplayPage),
            ('/display/post$', DisplayPostPage),
            ('/api/socket$', Socket),
        ]
        for brick in self.bricks.values():
            urls.append(('/static/bricks/{0}/(.+)$'.format(brick.id),
                StaticFileHandler, {'path': brick.static_path}))
        urls.append(('/static/(.+)$', StaticFileHandler, {'path': static_path}))
        self.add_handlers('.*$', urls)

    @property
    def items(self):
        return [self.current_post] if self.current_post else []

    def run(self):
        if not self._init:
            return
        self.listen(8080)
        self.logger.info('server started')
        IOLoop.instance().start()

    def add_message_handler(self, type, handler):
        """
        Extension API: register a new message `handler` for messages of the
        given `type`.

        A message handler is a function `handle(msg)` that processes a received
        message. It may return a `Message`, which is sent back to the sender as
        response. If a (subclass of) `Error` is raised, it is converted to a
        `Message` and sent back to the sender as error response.
        """
        self.msg_handlers[type] = handler

    def sendall(self, msg):
        for client in self.clients:
            client.send(msg)

    def get_item(self, index):
        if self.current_post and index == 0:
            return self.current_post
        else:
            raise ValueError('index_out_of_range')

    def do_post(self, post):
        if self.current_post:
            self.remove_item(0)
        self.current_post = post
        self.activate_item(0)

    def do_remove_item(self, index):
        self.deactivate_item(index)
        post = self.current_post
        self.current_post = None
        return post

    def get_collection(self, id):
        if id == 'wall':
            return self
        else:
            post = self.posts.get(id)
            # also captures None
            if not isinstance(post, Collection):
                raise KeyError()
            return post

    # TODO: validate input in message handlers
    def get_history_msg(self, msg):
        return Message('get_history',
            [p.json('common') for p in self.get_history()])

    def collection_get_items_msg(self, msg):
        collection = self.get_collection(msg.data['collection_id'])
        return Message('collection_get_items',
            [p.json() for p in collection.items])

    def collection_post_msg(self, msg):
        collection = self.get_collection(msg.data['collection_id'])
        post = self.posts[msg.data['post_id']]
        collection.post(post)
        return Message('collection_post')

    def collection_post_new_msg(self, msg):
        # wake display
        Popen('DISPLAY=:0.0 xset dpms force on', shell=True)

        collection = self.get_collection(msg.data.pop('collection_id'))
        post_type = msg.data.pop('type')
        post = collection.post_new(post_type, **msg.data)
        return Message('collection_post_new', post.json())

    def collection_remove_item_msg(self, msg):
        collection = self.get_collection(msg.data['collection_id'])
        index = int(msg.data['index'])
        post = collection.remove_item(index)
        return Message('collection_remove_item', post.json())

    def get_history(self):
        return sorted(self.posts.values(), key=lambda p: p.posted, reverse=True)

    def add_post_type(self, post_type):
        """
        Extension API: register a new post type. `post_type` is a class (type)
        that extends `Post`.
        """
        self.post_types[post_type.__name__] = post_type

    def _decode_redis_hash(self, hash):
        post_type = self.post_types[hash['__type__']]
        return post_type(self, **hash)

    def _setup_logger(self):
        logger = getLogger()
        logger.setLevel(DEBUG)
        handler = StreamHandler()
        handler.setFormatter(
            Formatter('%(asctime)s %(levelname)s %(name)s: %(message)s'))
        logger.addHandler(handler)

    def _collection_posted(self, event):
        self.sendall(Message('collection_posted', {
            'collection_id': event.args['collection'].id,
            'post': event.args['post'].json()
        }))

    def _collection_item_removed(self, event):
        self.sendall(Message('collection_item_removed', {
            'collection_id': event.args['collection'].id,
            'index': event.args['index'],
            'post': event.args['post'].json()
        }))

    def _collection_item_activated(self, event):
        self.sendall(Message('collection_item_activated', {
            'collection_id': event.args['collection'].id,
            'index': event.args['index'],
            'post': event.args['post'].json()
        }))

    def _collection_item_deactivated(self, event):
        self.sendall(Message('collection_item_deactivated', {
            'collection_id': event.args['collection'].id,
            'index': event.args['index'],
            'post': event.args['post'].json()
        }))

    def __str__(self):
        return '<{}>'.format(self.__class__.__name__)
    __repr__ = __str__