Commit a5e419ce authored by Serge S. Koval's avatar Serge S. Koval

Initial commit.

parents
*.swp
*.swo
*.pyc
*.*~
pyenv
#*#
build
source/_static*
source/_templates*
make.bat
venv
from flask import Flask, render_template
from flask.ext import extadmin
# Create custom admin view
class MyAdminView(extadmin.BaseView):
@extadmin.expose('/')
def index(self):
return render_template('myadmin.html', view=self)
class AnotherAdminView(extadmin.BaseView):
@extadmin.expose('/')
def index(self):
return render_template('anotheradmin.html', view=self)
# Create flask app
app = Flask(__name__, template_folder='templates')
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
if __name__ == '__main__':
# Create admin interface
admin = extadmin.Admin(app)
admin.add_view(MyAdminView())
admin.add_view(AnotherAdminView())
# Start app
app.debug = True
app.run()
{% extends 'admin/master.html' %}
{% block body %}
Hello World from AnotherMyAdmin!
{% endblock %}
{% extends 'admin/master.html' %}
{% block body %}
Hello World from MyAdmin!
{% endblock %}
from .base import expose, Admin, BaseView, AdminIndexView
from functools import wraps
from flask import Blueprint, render_template, url_for, abort
def expose(url='/', methods=('GET',)):
def wrap(f):
if not hasattr(f, '_urls'):
f._urls = []
f._urls.append((url, methods))
return f
return wrap
# Base views
def _wrap_view(f):
@wraps(f)
def inner(self, **kwargs):
h = self._handle_view(f.__name__, **kwargs)
if h is not None:
return h
return f(self, **kwargs)
return inner
class AdminViewMeta(type):
def __init__(cls, classname, bases, fields):
type.__init__(cls, classname, bases, fields)
# Gather exposed views
cls._urls = []
cls._default_view = None
for p in dir(cls):
attr = getattr(cls, p)
if hasattr(attr, '_urls'):
# Collect methods
for url, methods in attr._urls:
cls._urls.append((url, p, methods))
if url == '/':
cls._default_view = p
# Wrap views
setattr(cls, p, _wrap_view(attr))
# Default view
if cls._default_view is None and cls._urls:
raise Exception('Missing default view for the admin view %s' % classname)
class BaseView(object):
__metaclass__ = AdminViewMeta
def __init__(self, name=None, endpoint=None, url=None, static_folder=None):
self.name = name
self.endpoint = endpoint
self.url = url
self.static_folder = static_folder
self.admin = None
self._create_blueprint()
def _set_admin(self, admin):
self.admin = admin
def _create_blueprint(self):
# If endpoint name is not provided, get it from the class name
if self.endpoint is None:
self.endpoint = self.__class__.__name__.lower()
# If url is not provided, generate it from endpoint name
if self.url is None:
self.url = '/admin/%s' % self.endpoint
# If name is not povided, use capitalized endpoint name
if self.name is None:
self.name = self.endpoint.capitalize()
# Create blueprint and register rules
self.blueprint = Blueprint(self.endpoint, __name__,
url_prefix=self.url,
template_folder='templates',
static_folder=self.static_folder)
for url, name, methods in self._urls:
self.blueprint.add_url_rule(url,
name,
getattr(self, name),
methods=methods)
def is_accessible(self):
return True
def _handle_view(self, name, **kwargs):
if not self.is_accessible():
return abort(403)
class AdminIndexView(BaseView):
def __init__(self, name=None, endpoint=None, url=None):
super(AdminIndexView, self).__init__(name or 'Home', endpoint or 'admin', url or '/admin/', 'static')
@expose('/')
def index(self):
return render_template('admin/index.html', view=self)
class Admin(object):
def __init__(self, app, index_view=None):
self.app = app
self._views = []
if index_view is None:
index_view = AdminIndexView()
# Add predefined index view
self.add_view(index_view)
def add_view(self, view):
# Store in list of views and associate view with admin instance
self._views.append(view)
view._set_admin(self)
# Register blueprint
self.app.register_blueprint(view.blueprint)
@property
def menu(self):
# TODO: Precalculate URL - no need to get URLs for every request
return (('%s.%s' % (v.endpoint, v._default_view), v.url, v.name) for v in self._views if v.is_accessible())
from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty
from sqlalchemy.orm.interfaces import MANYTOONE
from wtforms.ext.sqlalchemy.orm import model_form, ModelConverter
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from flask import request, render_template, url_for, redirect
from flaskext import wtf
from .base import Admin, BaseView, expose
class AdminModelConverter(ModelConverter):
def __init__(self, session):
super(AdminModelConverter, self).__init__()
self.session = session
def convert(self, model, mapper, prop, field_args):
if isinstance(prop, RelationshipProperty):
kwargs = {
'validators': [],
'filters': [],
'default': None
}
if field_args:
kwargs.update(field_args)
if prop.direction is MANYTOONE:
def query_factory():
return self.session.query(prop.argument)
return QuerySelectField(query_factory=query_factory, **kwargs)
else:
# Ignore pk/fk
if isinstance(prop, ColumnProperty):
column = prop.columns[0]
if column.foreign_keys or column.primary_key:
return None
return super(AdminModelConverter, self).convert(model, mapper, prop, field_args)
class ModelView(BaseView):
def __init__(self, session, model, name=None, endpoint=None, url=None,
can_create=True, can_edit=True, can_delete=True,
list_template='admin/model/list.html',
edit_template='admin/model/edit.html',
create_template='admin/model/edit.html'):
super(ModelView, self).__init__(name, endpoint, url)
self.session = session
self.model = model
# Permissions
self.can_create = can_create
self.can_edit = can_edit
self.can_delete = can_delete
# Templates
self.list_template = list_template
self.edit_template = edit_template
self.create_template = create_template
# Scaffolding
self.list_columns = self.get_list_columns()
self.create_form = self.scaffold_create_form()
self.edit_form = self.scaffold_edit_form()
# Public API
def get_list_columns(self):
mapper = self.model._sa_class_manager.mapper
columns = []
for p in mapper.iterate_properties:
if isinstance(p, RelationshipProperty):
if p.direction is MANYTOONE:
self.list_columns.append(p.key)
elif isinstance(p, ColumnProperty):
# TODO: Check for multiple columns
column = p.columns[0]
if column.foreign_keys or column.primary_key:
continue
columns.append(p.key)
return columns
def scaffold_form(self):
return model_form(self.model, wtf.Form, None, converter=AdminModelConverter(self.session))
def scaffold_create_form(self):
return self.scaffold_form()
def scaffold_edit_form(self):
return self.scaffold_edit_form()
# Database-related API
def get_list(self):
return self.session.query(self.model)
def get_one(self, id):
return self.session.query(self.model).get(id)
# Model handlers
def create_model(self, form):
model = self.model()
form.populate_obj(model)
self.session.add(model)
self.session.commit()
def update_model(self, form, model):
form.populate_obj(model)
self.session.commit()
def delete_model(self, model):
self.session.delete(model)
self.session.commit()
# Views
@expose('/')
def index_view(self):
data = self.get_list()
return render_template(self.list_template, view=self, data=data)
@expose('/new/', methods=('GET', 'POST'))
def create_view(self):
if not self.can_create:
return redirect(url_for('.index_view'))
form = self.create_form(request.form)
if form.validate_on_submit():
self.create_model(form)
return redirect(url_for('.index_view'))
return render_template(self.create_template, view=self, form=form)
@expose('/edit/<int:id>/', methods=('GET', 'POST'))
def edit_view(self):
if not self.can_edit:
return redirect(url_for('.index_view'))
model = self.get_one(id)
if model is None:
return redirect(url_for('.index_view'))
form = self.edit_form(request.form, model)
if form.validate_on_submit():
self.update_model(form, model)
return redirect(url_for('.index_view'))
return render_template(self.edit_template, view=self, form=form)
@expose('/delete/<int:id>/')
def delete_view(self):
# TODO: Use post
if not self.can_delete:
return redirect(url_for('.index_view'))
model = self.get_one(id)
if model:
self.delete_model(model)
return redirect(url_for('.index_view'))
This diff is collapsed.
.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";}
.clearfix:after{clear:both;}
.hide-text{overflow:hidden;text-indent:100%;white-space:nowrap;}
.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;}
.hidden{display:none;visibility:hidden;}
.visible-phone{display:none;}
.visible-tablet{display:none;}
.visible-desktop{display:block;}
.hidden-phone{display:block;}
.hidden-tablet{display:block;}
.hidden-desktop{display:none;}
@media (max-width:767px){.visible-phone{display:block;} .hidden-phone{display:none;} .hidden-desktop{display:block;} .visible-desktop{display:none;}}@media (min-width:768px) and (max-width:979px){.visible-tablet{display:block;} .hidden-tablet{display:none;} .hidden-desktop{display:block;} .visible-desktop{display:none;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:18px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-group>label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .modal{position:absolute;top:10px;left:10px;right:10px;width:auto;margin:0;}.modal.fade.in{top:auto;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top{margin-left:-20px;margin-right:-20px;} .container{width:auto;} .row-fluid{width:100%;} .row{margin-left:0;} .row>[class*="span"],.row-fluid>[class*="span"]{float:none;display:block;width:auto;margin:0;} .thumbnails [class*="span"]{width:auto;} input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} .input-prepend input[class*="span"],.input-append input[class*="span"]{width:auto;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:20px;} .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.762430939%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid > .span12{width:99.999999993%;} .row-fluid > .span11{width:91.436464082%;} .row-fluid > .span10{width:82.87292817100001%;} .row-fluid > .span9{width:74.30939226%;} .row-fluid > .span8{width:65.74585634900001%;} .row-fluid > .span7{width:57.182320438000005%;} .row-fluid > .span6{width:48.618784527%;} .row-fluid > .span5{width:40.055248616%;} .row-fluid > .span4{width:31.491712705%;} .row-fluid > .span3{width:22.928176794%;} .row-fluid > .span2{width:14.364640883%;} .row-fluid > .span1{width:5.801104972%;} input,textarea,.uneditable-input{margin-left:0;} input.span12, textarea.span12, .uneditable-input.span12{width:714px;} input.span11, textarea.span11, .uneditable-input.span11{width:652px;} input.span10, textarea.span10, .uneditable-input.span10{width:590px;} input.span9, textarea.span9, .uneditable-input.span9{width:528px;} input.span8, textarea.span8, .uneditable-input.span8{width:466px;} input.span7, textarea.span7, .uneditable-input.span7{width:404px;} input.span6, textarea.span6, .uneditable-input.span6{width:342px;} input.span5, textarea.span5, .uneditable-input.span5{width:280px;} input.span4, textarea.span4, .uneditable-input.span4{width:218px;} input.span3, textarea.span3, .uneditable-input.span3{width:156px;} input.span2, textarea.span2, .uneditable-input.span2{width:94px;} input.span1, textarea.span1, .uneditable-input.span1{width:32px;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top{position:static;margin-bottom:18px;} .navbar-fixed-top .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .navbar .nav-collapse{clear:left;} .navbar .nav{float:none;margin:0 0 9px;} .navbar .nav>li{float:none;} .navbar .nav>li>a{margin-bottom:2px;} .navbar .nav>.divider-vertical{display:none;} .navbar .nav .nav-header{color:#999999;text-shadow:none;} .navbar .nav>li>a,.navbar .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .navbar .dropdown-menu li+li a{margin-bottom:2px;} .navbar .nav>li>a:hover,.navbar .dropdown-menu a:hover{background-color:#222222;} .navbar .dropdown-menu{position:static;top:auto;left:auto;float:none;display:block;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .navbar .dropdown-menu:before,.navbar .dropdown-menu:after{display:none;} .navbar .dropdown-menu .divider{display:none;} .navbar-form,.navbar-search{float:none;padding:9px 15px;margin:9px 0;border-top:1px solid #222222;border-bottom:1px solid #222222;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);} .navbar .nav.pull-right{float:none;margin-left:0;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;} .btn-navbar{display:block;} .nav-collapse{overflow:hidden;height:0;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:30px;} .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.564102564%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid > .span12{width:100%;} .row-fluid > .span11{width:91.45299145300001%;} .row-fluid > .span10{width:82.905982906%;} .row-fluid > .span9{width:74.358974359%;} .row-fluid > .span8{width:65.81196581200001%;} .row-fluid > .span7{width:57.264957265%;} .row-fluid > .span6{width:48.717948718%;} .row-fluid > .span5{width:40.170940171000005%;} .row-fluid > .span4{width:31.623931624%;} .row-fluid > .span3{width:23.076923077%;} .row-fluid > .span2{width:14.529914530000001%;} .row-fluid > .span1{width:5.982905983%;} input,textarea,.uneditable-input{margin-left:0;} input.span12, textarea.span12, .uneditable-input.span12{width:1160px;} input.span11, textarea.span11, .uneditable-input.span11{width:1060px;} input.span10, textarea.span10, .uneditable-input.span10{width:960px;} input.span9, textarea.span9, .uneditable-input.span9{width:860px;} input.span8, textarea.span8, .uneditable-input.span8{width:760px;} input.span7, textarea.span7, .uneditable-input.span7{width:660px;} input.span6, textarea.span6, .uneditable-input.span6{width:560px;} input.span5, textarea.span5, .uneditable-input.span5{width:460px;} input.span4, textarea.span4, .uneditable-input.span4{width:360px;} input.span3, textarea.span3, .uneditable-input.span3{width:260px;} input.span2, textarea.span2, .uneditable-input.span2{width:160px;} input.span1, textarea.span1, .uneditable-input.span1{width:60px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;}}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{% extends 'admin/master.html' %}
{% block body %}
{% endblock %}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Flask ExtAdmin{% endblock %}</title>
{% block head %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link href="{{ url_for('admin.static', filename='bootstrap/css/bootstrap.css') }}" rel="stylesheet">
<link href="{{ url_for('admin.static', filename='bootstrap/css/bootstrap-responsive.css') }}" rel="stylesheet">
{% endblock %}
</head>
<body>
{% block page_body %}
<div class="container-fluid">
<div class="row-fluid">
<div class="span1">
<ul class="nav nav-pills nav-stacked">
{% for url, raw_url, label in view.admin.menu %}
<li{% if raw_url == view.url %} class="active"{% endif %}>
<a href="{{ url_for(url) }}">{{ label }}</a>
</li>
{% endfor %}
</ul>
</div>
<div class="span10">
{% block body %}{% endblock %}
</div>
</div>
</div>
{% endblock %}
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script src="{{ url_for('admin.static', filename='bootstrap/js/bootstrap.min.js') }}" type="text/javascript"></script>
</body>
</html>
from setuptools import setup
setup(
name='Flask-ExtAdmin',
version='0.0.1',
url='https://github.com/MrJoes/flask-extadmin/',
license='BSD',
author='Serge S. Koval',
author_email='serge.koval+github@gmail.com',
description='Simple and extensible admin interface framework for Flask',
long_description=__doc__,
packages=['flask_extadmin'],
include_package_data=True,
zip_safe=False,
platforms='any',
install_requires=[
'Flask>=0.7',
'wtforms>=0.6.3',
],
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules'
]
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment