base.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. import os.path as op
  2. import warnings
  3. from functools import wraps
  4. from flask import Blueprint, current_app, render_template, abort, g, url_for
  5. from flask_admin import babel
  6. from flask_admin._compat import with_metaclass, as_unicode
  7. from flask_admin import helpers as h
  8. # For compatibility reasons import MenuLink
  9. from flask_admin.menu import MenuCategory, MenuView, MenuLink # noqa: F401
  10. def expose(url='/', methods=('GET',)):
  11. """
  12. Use this decorator to expose views in your view classes.
  13. :param url:
  14. Relative URL for the view
  15. :param methods:
  16. Allowed HTTP methods. By default only GET is allowed.
  17. """
  18. def wrap(f):
  19. if not hasattr(f, '_urls'):
  20. f._urls = []
  21. f._urls.append((url, methods))
  22. return f
  23. return wrap
  24. def expose_plugview(url='/'):
  25. """
  26. Decorator to expose Flask's pluggable view classes
  27. (``flask.views.View`` or ``flask.views.MethodView``).
  28. :param url:
  29. Relative URL for the view
  30. .. versionadded:: 1.0.4
  31. """
  32. def wrap(v):
  33. handler = expose(url, v.methods)
  34. if hasattr(v, 'as_view'):
  35. return handler(v.as_view(v.__name__))
  36. else:
  37. return handler(v)
  38. return wrap
  39. # Base views
  40. def _wrap_view(f):
  41. # Avoid wrapping view method twice
  42. if hasattr(f, '_wrapped'):
  43. return f
  44. @wraps(f)
  45. def inner(self, *args, **kwargs):
  46. # Store current admin view
  47. h.set_current_view(self)
  48. # Check if administrative piece is accessible
  49. abort = self._handle_view(f.__name__, **kwargs)
  50. if abort is not None:
  51. return abort
  52. return self._run_view(f, *args, **kwargs)
  53. inner._wrapped = True
  54. return inner
  55. class AdminViewMeta(type):
  56. """
  57. View metaclass.
  58. Does some precalculations (like getting list of view methods from the class) to avoid
  59. calculating them for each view class instance.
  60. """
  61. def __init__(cls, classname, bases, fields):
  62. type.__init__(cls, classname, bases, fields)
  63. # Gather exposed views
  64. cls._urls = []
  65. cls._default_view = None
  66. for p in dir(cls):
  67. attr = getattr(cls, p)
  68. if hasattr(attr, '_urls'):
  69. # Collect methods
  70. for url, methods in attr._urls:
  71. cls._urls.append((url, p, methods))
  72. if url == '/':
  73. cls._default_view = p
  74. # Wrap views
  75. setattr(cls, p, _wrap_view(attr))
  76. class BaseViewClass(object):
  77. pass
  78. class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
  79. """
  80. Base administrative view.
  81. Derive from this class to implement your administrative interface piece. For example::
  82. from flask_admin import BaseView, expose
  83. class MyView(BaseView):
  84. @expose('/')
  85. def index(self):
  86. return 'Hello World!'
  87. Icons can be added to the menu by using `menu_icon_type` and `menu_icon_value`. For example::
  88. admin.add_view(MyView(name='My View', menu_icon_type='glyph', menu_icon_value='glyphicon-home'))
  89. """
  90. @property
  91. def _template_args(self):
  92. """
  93. Extra template arguments.
  94. If you need to pass some extra parameters to the template,
  95. you can override particular view function, contribute
  96. arguments you want to pass to the template and call parent view.
  97. These arguments are local for this request and will be discarded
  98. in the next request.
  99. Any value passed through ``_template_args`` will override whatever
  100. parent view function passed to the template.
  101. For example::
  102. class MyAdmin(ModelView):
  103. @expose('/')
  104. def index(self):
  105. self._template_args['name'] = 'foobar'
  106. self._template_args['code'] = '12345'
  107. super(MyAdmin, self).index()
  108. """
  109. args = getattr(g, '_admin_template_args', None)
  110. if args is None:
  111. args = g._admin_template_args = dict()
  112. return args
  113. def __init__(self, name=None, category=None, endpoint=None, url=None,
  114. static_folder=None, static_url_path=None,
  115. menu_class_name=None, menu_icon_type=None, menu_icon_value=None):
  116. """
  117. Constructor.
  118. :param name:
  119. Name of this view. If not provided, will default to the class name.
  120. :param category:
  121. View category. If not provided, this view will be shown as a top-level menu item. Otherwise, it will
  122. be in a submenu.
  123. :param endpoint:
  124. Base endpoint name for the view. For example, if there's a view method called "index" and
  125. endpoint is set to "myadmin", you can use `url_for('myadmin.index')` to get the URL to the
  126. view method. Defaults to the class name in lower case.
  127. :param url:
  128. Base URL. If provided, affects how URLs are generated. For example, if the url parameter
  129. is "test", the resulting URL will look like "/admin/test/". If not provided, will
  130. use endpoint as a base url. However, if URL starts with '/', absolute path is assumed
  131. and '/admin/' prefix won't be applied.
  132. :param static_url_path:
  133. Static URL Path. If provided, this specifies the path to the static url directory.
  134. :param menu_class_name:
  135. Optional class name for the menu item.
  136. :param menu_icon_type:
  137. Optional icon. Possible icon types:
  138. - `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
  139. - `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
  140. - `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory
  141. - `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL
  142. :param menu_icon_value:
  143. Icon glyph name or URL, depending on `menu_icon_type` setting
  144. """
  145. self.name = name
  146. self.category = category
  147. self.endpoint = self._get_endpoint(endpoint)
  148. self.url = url
  149. self.static_folder = static_folder
  150. self.static_url_path = static_url_path
  151. self.menu = None
  152. self.menu_class_name = menu_class_name
  153. self.menu_icon_type = menu_icon_type
  154. self.menu_icon_value = menu_icon_value
  155. # Initialized from create_blueprint
  156. self.admin = None
  157. self.blueprint = None
  158. # Default view
  159. if self._default_view is None:
  160. raise Exception(u'Attempted to instantiate admin view %s without default view' % self.__class__.__name__)
  161. def _get_endpoint(self, endpoint):
  162. """
  163. Generate Flask endpoint name. By default converts class name to lower case if endpoint is
  164. not explicitly provided.
  165. """
  166. if endpoint:
  167. return endpoint
  168. return self.__class__.__name__.lower()
  169. def _get_view_url(self, admin, url):
  170. """
  171. Generate URL for the view. Override to change default behavior.
  172. """
  173. if url is None:
  174. if admin.url != '/':
  175. url = '%s/%s' % (admin.url, self.endpoint)
  176. else:
  177. if self == admin.index_view:
  178. url = '/'
  179. else:
  180. url = '/%s' % self.endpoint
  181. else:
  182. if not url.startswith('/'):
  183. url = '%s/%s' % (admin.url, url)
  184. return url
  185. def create_blueprint(self, admin):
  186. """
  187. Create Flask blueprint.
  188. """
  189. # Store admin instance
  190. self.admin = admin
  191. # If the static_url_path is not provided, use the admin's
  192. if not self.static_url_path:
  193. self.static_url_path = admin.static_url_path
  194. # Generate URL
  195. self.url = self._get_view_url(admin, self.url)
  196. # If we're working from the root of the site, set prefix to None
  197. if self.url == '/':
  198. self.url = None
  199. # prevent admin static files from conflicting with flask static files
  200. if not self.static_url_path:
  201. self.static_folder = 'static'
  202. self.static_url_path = '/static/admin'
  203. # If name is not povided, use capitalized endpoint name
  204. if self.name is None:
  205. self.name = self._prettify_class_name(self.__class__.__name__)
  206. # Create blueprint and register rules
  207. self.blueprint = Blueprint(self.endpoint, __name__,
  208. url_prefix=self.url,
  209. subdomain=self.admin.subdomain,
  210. template_folder=op.join('templates', self.admin.template_mode),
  211. static_folder=self.static_folder,
  212. static_url_path=self.static_url_path)
  213. for url, name, methods in self._urls:
  214. self.blueprint.add_url_rule(url,
  215. name,
  216. getattr(self, name),
  217. methods=methods)
  218. return self.blueprint
  219. def render(self, template, **kwargs):
  220. """
  221. Render template
  222. :param template:
  223. Template path to render
  224. :param kwargs:
  225. Template arguments
  226. """
  227. # Store self as admin_view
  228. kwargs['admin_view'] = self
  229. kwargs['admin_base_template'] = self.admin.base_template
  230. # Provide i18n support even if flask-babel is not installed
  231. # or enabled.
  232. kwargs['_gettext'] = babel.gettext
  233. kwargs['_ngettext'] = babel.ngettext
  234. kwargs['h'] = h
  235. # Expose get_url helper
  236. kwargs['get_url'] = self.get_url
  237. # Expose config info
  238. kwargs['config'] = current_app.config
  239. # Contribute extra arguments
  240. kwargs.update(self._template_args)
  241. return render_template(template, **kwargs)
  242. def _prettify_class_name(self, name):
  243. """
  244. Split words in PascalCase string into separate words.
  245. :param name:
  246. String to prettify
  247. """
  248. return h.prettify_class_name(name)
  249. def is_visible(self):
  250. """
  251. Override this method if you want dynamically hide or show administrative views
  252. from Flask-Admin menu structure
  253. By default, item is visible in menu.
  254. Please note that item should be both visible and accessible to be displayed in menu.
  255. """
  256. return True
  257. def is_accessible(self):
  258. """
  259. Override this method to add permission checks.
  260. Flask-Admin does not make any assumptions about the authentication system used in your application, so it is
  261. up to you to implement it.
  262. By default, it will allow access for everyone.
  263. """
  264. return True
  265. def _handle_view(self, name, **kwargs):
  266. """
  267. This method will be executed before calling any view method.
  268. It will execute the ``inaccessible_callback`` if the view is not
  269. accessible.
  270. :param name:
  271. View function name
  272. :param kwargs:
  273. View function arguments
  274. """
  275. if not self.is_accessible():
  276. return self.inaccessible_callback(name, **kwargs)
  277. def _run_view(self, fn, *args, **kwargs):
  278. """
  279. This method will run actual view function.
  280. While it is similar to _handle_view, can be used to change
  281. arguments that are passed to the view.
  282. :param fn:
  283. View function
  284. :param kwargs:
  285. Arguments
  286. """
  287. return fn(self, *args, **kwargs)
  288. def inaccessible_callback(self, name, **kwargs):
  289. """
  290. Handle the response to inaccessible views.
  291. By default, it throw HTTP 403 error. Override this method to
  292. customize the behaviour.
  293. """
  294. return abort(403)
  295. def get_url(self, endpoint, **kwargs):
  296. """
  297. Generate URL for the endpoint. If you want to customize URL generation
  298. logic (persist some query string argument, for example), this is
  299. right place to do it.
  300. :param endpoint:
  301. Flask endpoint name
  302. :param kwargs:
  303. Arguments for `url_for`
  304. """
  305. return url_for(endpoint, **kwargs)
  306. @property
  307. def _debug(self):
  308. if not self.admin or not self.admin.app:
  309. return False
  310. return self.admin.app.debug
  311. class AdminIndexView(BaseView):
  312. """
  313. Default administrative interface index page when visiting the ``/admin/`` URL.
  314. It can be overridden by passing your own view class to the ``Admin`` constructor::
  315. class MyHomeView(AdminIndexView):
  316. @expose('/')
  317. def index(self):
  318. arg1 = 'Hello'
  319. return self.render('admin/myhome.html', arg1=arg1)
  320. admin = Admin(index_view=MyHomeView())
  321. Also, you can change the root url from /admin to / with the following::
  322. admin = Admin(
  323. app,
  324. index_view=AdminIndexView(
  325. name='Home',
  326. template='admin/myhome.html',
  327. url='/'
  328. )
  329. )
  330. Default values for the index page are:
  331. * If a name is not provided, 'Home' will be used.
  332. * If an endpoint is not provided, will default to ``admin``
  333. * Default URL route is ``/admin``.
  334. * Automatically associates with static folder.
  335. * Default template is ``admin/index.html``
  336. """
  337. def __init__(self, name=None, category=None,
  338. endpoint=None, url=None,
  339. template='admin/index.html',
  340. menu_class_name=None,
  341. menu_icon_type=None,
  342. menu_icon_value=None):
  343. super(AdminIndexView, self).__init__(name or babel.lazy_gettext('Home'),
  344. category,
  345. endpoint or 'admin',
  346. '/admin' if url is None else url,
  347. 'static',
  348. menu_class_name=menu_class_name,
  349. menu_icon_type=menu_icon_type,
  350. menu_icon_value=menu_icon_value)
  351. self._template = template
  352. @expose()
  353. def index(self):
  354. return self.render(self._template)
  355. class Admin(object):
  356. """
  357. Collection of the admin views. Also manages menu structure.
  358. """
  359. def __init__(self, app=None, name=None,
  360. url=None, subdomain=None,
  361. index_view=None,
  362. translations_path=None,
  363. endpoint=None,
  364. static_url_path=None,
  365. base_template=None,
  366. template_mode=None,
  367. category_icon_classes=None):
  368. """
  369. Constructor.
  370. :param app:
  371. Flask application object
  372. :param name:
  373. Application name. Will be displayed in the main menu and as a page title. Defaults to "Admin"
  374. :param url:
  375. Base URL
  376. :param subdomain:
  377. Subdomain to use
  378. :param index_view:
  379. Home page view to use. Defaults to `AdminIndexView`.
  380. :param translations_path:
  381. Location of the translation message catalogs. By default will use the translations
  382. shipped with Flask-Admin.
  383. :param endpoint:
  384. Base endpoint name for index view. If you use multiple instances of the `Admin` class with
  385. a single Flask application, you have to set a unique endpoint name for each instance.
  386. :param static_url_path:
  387. Static URL Path. If provided, this specifies the default path to the static url directory for
  388. all its views. Can be overridden in view configuration.
  389. :param base_template:
  390. Override base HTML template for all static views. Defaults to `admin/base.html`.
  391. :param template_mode:
  392. Base template path. Defaults to `bootstrap2`. If you want to use
  393. Bootstrap 3 integration, change it to `bootstrap3`.
  394. :param category_icon_classes:
  395. A dict of category names as keys and html classes as values to be added to menu category icons.
  396. Example: {'Favorites': 'glyphicon glyphicon-star'}
  397. """
  398. self.app = app
  399. self.translations_path = translations_path
  400. self._views = []
  401. self._menu = []
  402. self._menu_categories = dict()
  403. self._menu_links = []
  404. if name is None:
  405. name = 'Admin'
  406. self.name = name
  407. self.index_view = index_view or AdminIndexView(endpoint=endpoint, url=url)
  408. self.endpoint = endpoint or self.index_view.endpoint
  409. self.url = url or self.index_view.url
  410. self.static_url_path = static_url_path
  411. self.subdomain = subdomain
  412. self.base_template = base_template or 'admin/base.html'
  413. self.template_mode = template_mode or 'bootstrap2'
  414. self.category_icon_classes = category_icon_classes or dict()
  415. # Add index view
  416. self._set_admin_index_view(index_view=index_view, endpoint=endpoint, url=url)
  417. # Register with application
  418. if app is not None:
  419. self._init_extension()
  420. def add_view(self, view):
  421. """
  422. Add a view to the collection.
  423. :param view:
  424. View to add.
  425. """
  426. # Add to views
  427. self._views.append(view)
  428. # If app was provided in constructor, register view with Flask app
  429. if self.app is not None:
  430. self.app.register_blueprint(view.create_blueprint(self))
  431. self._add_view_to_menu(view)
  432. def _set_admin_index_view(self, index_view=None,
  433. endpoint=None, url=None):
  434. """
  435. Add the admin index view.
  436. :param index_view:
  437. Home page view to use. Defaults to `AdminIndexView`.
  438. :param url:
  439. Base URL
  440. :param endpoint:
  441. Base endpoint name for index view. If you use multiple instances of the `Admin` class with
  442. a single Flask application, you have to set a unique endpoint name for each instance.
  443. """
  444. self.index_view = index_view or AdminIndexView(endpoint=endpoint, url=url)
  445. self.endpoint = endpoint or self.index_view.endpoint
  446. self.url = url or self.index_view.url
  447. # Add predefined index view
  448. # assume index view is always the first element of views.
  449. if len(self._views) > 0:
  450. self._views[0] = self.index_view
  451. else:
  452. self.add_view(self.index_view)
  453. def add_views(self, *args):
  454. """
  455. Add one or more views to the collection.
  456. Examples::
  457. admin.add_views(view1)
  458. admin.add_views(view1, view2, view3, view4)
  459. admin.add_views(*my_list)
  460. :param args:
  461. Argument list including the views to add.
  462. """
  463. for view in args:
  464. self.add_view(view)
  465. def add_link(self, link):
  466. """
  467. Add link to menu links collection.
  468. :param link:
  469. Link to add.
  470. """
  471. if link.category:
  472. self.add_menu_item(link, link.category)
  473. else:
  474. self._menu_links.append(link)
  475. def add_links(self, *args):
  476. """
  477. Add one or more links to the menu links collection.
  478. Examples::
  479. admin.add_links(link1)
  480. admin.add_links(link1, link2, link3, link4)
  481. admin.add_links(*my_list)
  482. :param args:
  483. Argument list including the links to add.
  484. """
  485. for link in args:
  486. self.add_link(link)
  487. def add_menu_item(self, menu_item, target_category=None):
  488. """
  489. Add menu item to menu tree hierarchy.
  490. :param menu_item:
  491. MenuItem class instance
  492. :param target_category:
  493. Target category name
  494. """
  495. if target_category:
  496. cat_text = as_unicode(target_category)
  497. category = self._menu_categories.get(cat_text)
  498. # create a new menu category if one does not exist already
  499. if category is None:
  500. category = MenuCategory(target_category)
  501. category.class_name = self.category_icon_classes.get(cat_text)
  502. self._menu_categories[cat_text] = category
  503. self._menu.append(category)
  504. category.add_child(menu_item)
  505. else:
  506. self._menu.append(menu_item)
  507. def _add_menu_item(self, menu_item, target_category):
  508. warnings.warn('Admin._add_menu_item is obsolete - use Admin.add_menu_item instead.')
  509. return self.add_menu_item(menu_item, target_category)
  510. def _add_view_to_menu(self, view):
  511. """
  512. Add a view to the menu tree
  513. :param view:
  514. View to add
  515. """
  516. self.add_menu_item(MenuView(view.name, view), view.category)
  517. def get_category_menu_item(self, name):
  518. return self._menu_categories.get(name)
  519. def init_app(self, app, index_view=None,
  520. endpoint=None, url=None):
  521. """
  522. Register all views with the Flask application.
  523. :param app:
  524. Flask application instance
  525. """
  526. self.app = app
  527. self._init_extension()
  528. # Register Index view
  529. if index_view is not None:
  530. self._set_admin_index_view(
  531. index_view=index_view,
  532. endpoint=endpoint,
  533. url=url
  534. )
  535. # Register views
  536. for view in self._views:
  537. app.register_blueprint(view.create_blueprint(self))
  538. def _init_extension(self):
  539. if not hasattr(self.app, 'extensions'):
  540. self.app.extensions = dict()
  541. admins = self.app.extensions.get('admin', [])
  542. for p in admins:
  543. if p.endpoint == self.endpoint:
  544. raise Exception(u'Cannot have two Admin() instances with same'
  545. u' endpoint name.')
  546. if p.url == self.url and p.subdomain == self.subdomain:
  547. raise Exception(u'Cannot assign two Admin() instances with same'
  548. u' URL and subdomain to the same application.')
  549. admins.append(self)
  550. self.app.extensions['admin'] = admins
  551. def menu(self):
  552. """
  553. Return the menu hierarchy.
  554. """
  555. return self._menu
  556. def menu_links(self):
  557. """
  558. Return menu links.
  559. """
  560. return self._menu_links