发布于 2015-09-01 14:09:55 | 204 次阅读 | 评论: 0 | 来源: PHPERZ
# -*- coding: utf-8 -*-
from werkzeug import OrderedMultiDict
from flask import Blueprint, url_for, request, abort
from views import ObjectListView, ObjectFormView
from views import ObjectDeleteView, secure
def recursive_getattr(obj, attr):
"""Returns object related attributes, as it's a template filter None
is return when attribute doesn't exists.
eg::
a = object()
a.b = object()
a.b.c = 1
recursive_getattr(a, 'b.c') => 1
recursive_getattr(a, 'b.d') => None
"""
try:
if "." not in attr:
return getattr(obj, attr)
else:
l = attr.split('.')
return recursive_getattr(getattr(obj, l[0]), '.'.join(l[1:]))
except AttributeError:
return None
class AdminNode(object):
"""An AdminNode just act as navigation container, it doesn't provide any
rules.
:param admin: The parent admin object
:param url_prefix: The url prefix
:param enpoint: The endpoint
:param short_title: The short module title use on navigation
& breadcrumbs
:param title: The long title
:param parent: The parent node
"""
def __init__(self, admin, url_prefix, endpoint, short_title, title=None,
parent=None):
self.admin = admin
self.parent = parent
self.url_prefix = url_prefix
self.endpoint = endpoint
self.short_title = short_title
self.title = title
self.children = []
@property
def url_path(self):
"""Returns the url path relative to admin one.
"""
if self.parent:
return self.parent.url_path + self.url_prefix
else:
return self.url_prefix
@property
def parents(self):
"""Returns all parent hierarchy as list. Usefull for breadcrumbs.
"""
if self.parent:
parents = list(self.parent.parents)
parents.append(self.parent)
return parents
else:
return []
def secure(self, http_code=403):
"""Gives a way to secure specific url path.
:param http_code: The response http code when False
"""
def decorator(f):
self.admin.add_path_security(self.url_path, f, http_code)
return f
return decorator
class Admin(object):
"""Class that provides a way to add admin interface to Flask applications.
:param app: The Flask application
:param url_prefix: The url prefix
:param main_dashboard: The main dashboard object
:param endpoint: The endpoint
"""
def __init__(self, app, url_prefix="/admin", title="flask-dashed",
main_dashboard=None, endpoint='admin'):
if not main_dashboard:
from dashboard import DefaultDashboard
main_dashboard = DefaultDashboard
self.blueprint = Blueprint(endpoint, __name__,
static_folder='static', template_folder='templates')
self.app = app
self.url_prefix = url_prefix
self.endpoint = endpoint
self.title = title
self.secure_functions = OrderedMultiDict()
# Checks security for current path
self.blueprint.before_request(
lambda: self.check_path_security(request.path))
self.app.register_blueprint(self.blueprint, url_prefix=url_prefix)
self.root_nodes = []
self._add_node(main_dashboard, '/', 'main-dashboard', 'dashboard')
# Registers recursive_getattr filter
self.app.jinja_env.filters['recursive_getattr'] = recursive_getattr
def register_node(self, url_prefix, endpoint, short_title, title=None,
parent=None, node_class=AdminNode):
"""Registers admin node.
:param url_prefix: The url prefix
:param endpoint: The endpoint
:param short_title: The short title
:param title: The long title
:param parent: The parent node path
:param node_class: The class for node objects
"""
return self._add_node(node_class, url_prefix, endpoint, short_title,
title=title, parent=parent)
def register_module(self, module_class, url_prefix, endpoint, short_title,
title=None, parent=None):
"""Registers new module to current admin.
"""
return self._add_node(module_class, url_prefix, endpoint, short_title,
title=title, parent=parent)
def _add_node(self, node_class, url_prefix, endpoint, short_title,
title=None, parent=None):
"""Registers new node object to current admin object.
"""
title = short_title if not title else title
if parent and not issubclass(parent.__class__, AdminNode):
raise Exception('`parent` class must be AdminNode subclass')
new_node = node_class(self, url_prefix, endpoint, short_title,
title=title, parent=parent)
if parent:
parent.children.append(new_node)
else:
self.root_nodes.append(new_node)
return new_node
@property
def main_dashboard(self):
return self.root_nodes[0]
def add_path_security(self, path, function, http_code=403):
"""Registers security function for given path.
:param path: The endpoint to secure
:param function: The security function
:param http_code: The response http code
"""
self.secure_functions.add(path, (function, http_code))
def check_path_security(self, path):
"""Checks security for specific and path.
:param path: The path to check
"""
for key in self.secure_functions.iterkeys():
if path.startswith("%s%s" % (self.url_prefix, key)):
for function, http_code in self.secure_functions.getlist(key):
if not function():
return abort(http_code)
class AdminModule(AdminNode):
"""Class that provides a way to create simple admin module.
:param admin: The parent admin object
:param url_prefix: The url prefix
:param enpoint: The endpoint
:param short_title: the short module title use on navigation
& breadcrumbs
:param title: The long title
:param parent: The parent node
"""
def __init__(self, *args, **kwargs):
super(AdminModule, self).__init__(*args, **kwargs)
self.rules = OrderedMultiDict()
self._register_rules()
def add_url_rule(self, rule, endpoint, view_func, **options):
"""Adds a routing rule to the application from relative endpoint.
`view_class` is copied as we need to dynamically apply decorators.
:param rule: The rule
:param endpoint: The endpoint
:param view_func: The view
"""
class ViewClass(view_func.view_class):
pass
ViewClass.__name__ = "%s_%s" % (self.endpoint, endpoint)
ViewClass.__module__ = view_func.__module__
view_func.view_class = ViewClass
full_endpoint = "%s.%s_%s" % (self.admin.endpoint,
self.endpoint, endpoint)
self.admin.app.add_url_rule("%s%s%s" % (self.admin.url_prefix,
self.url_path, rule), full_endpoint, view_func, **options)
self.rules.setlist(endpoint, [(rule, endpoint, view_func)])
def _register_rules(self):
"""Registers all module rules after initialization.
"""
if not hasattr(self, 'default_rules'):
raise NotImplementedError('Admin module class must provide'
+ ' default_rules')
for rule, endpoint, view_func in self.default_rules:
self.add_url_rule(rule, endpoint, view_func)
@property
def url(self):
"""Returns first registered (main) rule as url.
"""
try:
return url_for("%s.%s_%s" % (self.admin.endpoint,
self.endpoint, self.rules.lists()[0][0]))
# Cause OrderedMultiDict.keys() doesn't preserve order...
except IndexError:
raise Exception('`AdminModule` must provide at list one rule.')
def secure_endpoint(self, endpoint, http_code=403):
"""Gives a way to secure specific url path.
:param endpoint: The endpoint to protect
:param http_code: The response http code when False
"""
def decorator(f):
self._secure_enpoint(endpoint, f, http_code)
return f
return decorator
def _secure_enpoint(self, endpoint, secure_function, http_code):
"""Secure enpoint view function via `secure` decorator.
:param enpoint: The endpoint to secure
:param secure_function: The function to check
:param http_code: The response http code when False.
"""
rule, endpoint, view_func = self.rules.get(endpoint)
view_func.view_class.dispatch_request =\
secure(endpoint, secure_function, http_code)(
view_func.view_class.dispatch_request)
class ObjectAdminModule(AdminModule):
"""Base class for object admin modules backends.
Provides all required methods to retrieve, create, update and delete
objects.
"""
# List relateds
list_view = ObjectListView
list_template = 'flask_dashed/list.html'
list_fields = None
list_title = 'list'
list_per_page = 10
searchable_fields = None
order_by = None
# Edit relateds
edit_template = 'flask_dashed/edit.html'
form_view = ObjectFormView
form_class = None
edit_title = 'edit object'
# New relateds
new_title = 'new object'
# Delete relateds
delete_view = ObjectDeleteView
def __new__(cls, *args, **kwargs):
if not cls.list_fields:
raise NotImplementedError()
return super(ObjectAdminModule, cls).__new__(cls, *args, **kwargs)
@property
def default_rules(self):
"""Adds object list rule to current app.
"""
return [
('/', 'list', self.list_view.as_view('short_title', self)),
('/page/<page>', 'list', self.list_view.as_view('short_title',
self)),
('/new', 'new', self.form_view.as_view('short_title', self)),
('/<pk>/edit', 'edit', self.form_view.as_view('short_title',
self)),
('/<pk>/delete', 'delete', self.delete_view.as_view('short_title',
self)),
]
def get_object_list(self, search=None, order_by_field=None,
order_by_direction=None, offset=None, limit=None):
"""Returns objects list ordered and filtered.
:param search: The search string for quick filtering
:param order_by_field: The ordering field
:param order_by_direction: The ordering direction
:param offset: The pagintation offset
:param limit: The pagination limit
"""
raise NotImplementedError()
def count_list(self, search=None):
"""Counts filtered object list.
:param search: The search string for quick filtering.
"""
raise NotImplementedError()
def get_action_for_field(self, field, obj):
"""Returns title and link for given list field and object.
:param field: The field path.
:param object: The line object.
"""
title, url = None, None
field = self.list_fields[field]
if 'action' in field:
title = field['action'].get('title', None)
if callable(title):
title = title(obj)
url = field['action'].get('url', None)
if callable(url):
url = url(obj)
return title, url
def get_actions_for_object(self, object):
"""Returns action available for each object.
:param object: The raw object
"""
raise NotImplementedError()
def get_form(self, obj):
"""Returns form initialy populate from object instance.
:param obj: The object
"""
return self.form_class(obj=obj)
def get_object(self, pk=None):
"""Returns object retrieve by primary key.
:param pk: The object primary key
"""
raise NotImplementedError()
def create_object(self):
"""Returns new object instance."""
raise NotImplementedError()
def save_object(self, object):
"""Persits object.
:param object: The object to persist
"""
raise NotImplementedError()
def delete_object(self, object):
"""Deletes object.
:param object: The object to delete
"""
raise NotImplementedError()