import webapp2
import json
from settings import app_config
from webapp2 import Response, cached_property
from webapp2_extras import sessions
from google.appengine.api import users
from wtforms.ext.appengine.db import model_form
from ferris.core import inflector
from ferris.core.ndb import key_urlsafe_for, key_from_string
from ferris.core.uri import Uri
from ferris.core.event import NamedEvents
from ferris.core import events
from ferris.core.json_util import DatastoreEncoder, DatastoreDecoder
from scaffolding import Scaffolding, scaffold
import ferris.core.routing as routing
import ferris.core.template as templating
from bunch import Bunch
from webob.multidict import MultiDict
import logging
[docs]def route(f):
"""
Marks a class method to enable it to be automatically routed and accessible via HTTP. This
decorator should always be the outermost decorator.
"""
setattr(f, 'route', True)
return f
[docs]def route_with(*args, **kwargs):
"""
Marks a class method to be routed and passes and additional arguments to the webapp2.Route
constructor.
:param template: Sets the URL template for this action
"""
def inner(f):
setattr(f, 'route', (args, kwargs))
return f
return inner
[docs]class Handler(webapp2.RequestHandler, Uri):
"""
Handler allows grouping of common actions and provides them with
automatic routing, reusable components, and automatic template
discovery and rendering.
"""
#: List of components.
#: When declaring a handler, this must be a list of classes.
#: When the handler is constructed, this will be transformed into a Bunch of instances.
components = []
#: If set to true, the handler will attempt to render the template determined by :meth:`_get_template_name` if an action returns ``None``.
auto_render = True
#: If set, this will be used as the template to render instead of calling :meth:`_get_template_name`
template_name = None
#: The extension used by :meth:`_get_template_name` when finding templates.
template_ext = 'html'
#: Set to change the theme used by render_template
theme = None
#: Context that is passed on to the template, use :meth:`get` and and :meth:`set`.
template_vars = {}
# Prefixes are added in from of handlers (like admin_list) and will cause routing
# to produce a url such as '/admin/name/list' and a name such as 'admin-name-list'
prefixes = []
#: The current action
action = None
#: The current prefix
prefix = None
# The name of this class, lowercase (automatically determined)
name = 'handler'
#: The current user as determined by ``google.appengine.api.users.get_current_user()``.
user = None
def __init__(self, *args, **kwargs):
"""
* Constructs the events map
* Constructs all of the components
* Determines the prefix and action
* Populates default template arguments
"""
super(Handler, self).__init__(*args, **kwargs)
self.events = NamedEvents()
self._init_route_members()
self._init_template_vars()
self._build_components()
def _build_components(self):
self._delegate_event('before_build_components', handler=self)
if hasattr(self, 'components'):
components = self.components
self.components = Bunch()
for cls in components:
if hasattr(cls, 'name'):
name = cls.name
else:
name = inflector.underscore(cls.__name__)
self.components[name] = (cls(self))
else:
self.components = Bunch()
self._delegate_event('after_build_components', handler=self)
def _init_route_members(self):
self.action = self.request.route.handler_method
for prefix in self.prefixes:
if self.action.startswith(prefix):
self.prefix = prefix
self.action = self.action.replace(self.prefix + '_', '')
def _init_template_vars(self):
self.name = inflector.underscore(self.__class__.__name__)
self.proper_name = self.__class__.__name__
self.template_vars = {}
self.template_vars['handler'] = {
'name': self.name,
'uri': self.uri.__get__(self, Handler),
'prefix': self.prefix,
'action': self.action,
'uri_exists': self.uri_exists.__get__(self, Handler),
'on_uri': self.on_uri.__get__(self, Handler),
'request': self.request,
'self': self,
'url_id_for': self.url_id_for.__get__(self, Handler),
'url_key_for': self.url_id_for.__get__(self, Handler),
'user': self.user
}
self._delegate_event('template_vars', handler=self)
def _delegate_event(self, name, *args, **kwargs):
"""
Calls an event locally, globally, and invokes callback methods
"""
# callback events
if name == 'before_dispatch':
self.before_dispatch()
elif name == 'after_dispatch':
self.after_dispatch(kwargs['response'])
elif name == 'before_render':
self.before_render()
elif name == 'after_render':
self.after_render(kwargs['result'])
self.events[name].fire(*args, **kwargs) # Local events
events.fire('handler_' + name, *args, **kwargs) # Global Events
@classmethod
def build_routes(cls, router):
"""
Called in the main app router to get all of this handler's routes.
Override to add custom/additional routes.
"""
# Route the rest methods
router.add(routing.build_scaffold_routes_for_handler(cls))
for prefix in cls.prefixes:
router.add(routing.build_scaffold_routes_for_handler(cls, prefix))
# Auto route the remaining methods
for route in routing.build_routes_for_handler(cls):
router.add(route)
events.fire('handler_build_routes', cls=cls, router=router)
[docs] def startup(self):
"""Called when a new request is received before authorization and dispatching."""
pass
def is_authorized(self):
if self.prefix == 'admin' and not users.is_current_user_admin():
return Response("You must be an administrator.", status="401 Unauthorized")
if 'allowed_auth_domains' in app_config:
if not users.get_current_user().email().split('@').pop() in app_config['allowed_auth_domains']:
return Response("Your domain does not have access to this application.", status="401 Unauthorized")
try:
self._delegate_event('is_authorized', handler=self)
except Exception, e:
return Response(str(e), status='401 Unauthorized')
return True
[docs] def before_dispatch(self):
"""Called during dispatch before control is handed over to the action"""
pass
[docs] def after_dispatch(self, response):
"""Called during dispatch after control is handed back from the action"""
pass
[docs] def before_render(self):
"""Called during render_template before invoking the template engine"""
pass
[docs] def after_render(self, result):
"""Called during render_template after the template has been rendered by the template engine"""
pass
def dispatch(self):
"""
Calls startup and then the handler method. Will also make sure that the user
is an administrator is the current prefix is 'admin'.
If self.auto_render is True, then we will try to automatically render the template
at templates/{name}/{prefix}_{action}.{extension}. The automatic name can be overriden
by setting self.template_name.
If the handler method returns anything other than None, auto-rendering is skipped
and the result (return value) is returned to the dispatcher.
"""
self.user = users.get_current_user()
self._init_route_members()
self._init_template_vars()
self.session_store = sessions.get_store(request=self.request)
self.template_vars['handler']['session'] = self.session
self._delegate_event('before_startup', handler=self)
self.startup()
self._delegate_event('after_startup', handler=self)
auth_result = self.is_authorized()
if auth_result != True:
return auth_result
try:
self._delegate_event('before_dispatch', handler=self)
response = super(Handler, self).dispatch()
self._delegate_event('after_dispatch', response=response, handler=self)
if self.auto_render and response == None and not self.response.body:
response = self.render_template(self._get_template_name())
finally:
pass
if isinstance(response, basestring):
# Clear redirect
if self.response.status_int in [300, 301, 302]:
self.response.status = 200
del self.response.headers['Location']
if isinstance(response, unicode):
self.response.charset = 'utf8'
self.response.unicode_body = response
else:
self.response.body = response
elif isinstance(response, tuple):
self.response = Response(response)
elif isinstance(response, int):
self.response.status = response
elif response == None:
pass
self._delegate_event('dispatch_complete', handler=self)
self.session_store.save_sessions(self.response)
return self.response
@cached_property
[docs] def session(self):
"""
Session object backed by an encrypted cookie and memcache.
"""
return self.session_store.get_session(backend='memcache')
[docs] def json(self, data, *args, **kwargs):
"""Returns a json encoded string for the given object. Uses :mod:`ferris.core.json_util` so it is capable of handling Datastore types."""
return json.dumps(data, cls=DatastoreEncoder, *args, **kwargs)
[docs] def render_template(self, template):
"""
Render a given template with :attr:`template_vars` as the context.
This is called automatically during :meth:`dispatch` if :attr:`auto_render` is ``True`` and an action returns ``None``.
"""
self._delegate_event('before_render', handler=self)
result = templating.render_template(template, self.template_vars, theme=self.theme)
self._delegate_event('after_render', handler=self, result=result)
return result
[docs] def _get_template_name(self):
"""
Generates a list of template names.
The template engine will try each template in the list until it finds one.
For non-prefixed actions, the return value is simply: ``[ "[handler]/[action].[ext]" ]``.
For prefixed actions, another entry is added to the list : ``[ "[handler]/[prefix_][action].[ext]" ]``. This means that actions that are prefixed can fallback to using the non-prefixed template.
For example, the action ``Posts.json_list`` would try these templates::
posts/json_list.html
posts/list.html
"""
if not self.template_name == None:
return self.template_name
templates = []
if self.prefix:
template = self.name + '/' + self.prefix + '_' + self.action + '.' + self.template_ext
templates.append(template)
# non-prefixed
template = self.name + '/' + self.action + '.' + self.template_ext
templates.append(template)
self._delegate_event('template_names', handler=self, templates=templates)
return templates
[docs] def set(self, name=None, value=None, **kwargs):
""" Set a variable in the template context. You can specify name and value or specify multiple values using kwargs. """
if not name == None:
self.template_vars[name] = value
self.template_vars.update(kwargs)
[docs] def get(self, name, default=None):
""" Get a variable from the template context """
return self.template_vars.get(name, default)
def url_id_for(self, item):
"""
Returns a properly formatted urlsafe version of an ``ndb.Key``.
"""
return ':' + key_urlsafe_for(item)
url_key_for = url_id_for
[docs] def key_from_string(self, str, kind=None):
"""
Returns an ``ndb.Key`` object from a properly formatted urlsafe version.
"""
return key_from_string(str, kind)
events.register([
'handler_before_build_components',
'handler_after_build_components',
'handler_template_vars',
'handler_build_routes',
'handler_is_authorized',
'handler_before_startup',
'handler_after_startup',
'handler_before_dispatch',
'handler_after_dispatch',
'handler_dispatch_complete',
'handler_before_render',
'handler_after_render',
'handler_template_names',
'handler_before_process_form_data',
'handler_after_process_form_data',
])