├── AUTHORS ├── LICENSE ├── MANIFEST ├── MANIFEST.in ├── NOTICE ├── README.rst ├── examples ├── __init__.py ├── simple.py └── xadmin_demo.db ├── flask_xadmin ├── __init__.py ├── examples │ ├── __init__.py │ └── simple.py ├── forms.py ├── templates │ ├── admin │ │ ├── edit_mode.html │ │ ├── files │ │ │ └── custom_file_list.html │ │ └── models │ │ │ ├── custom_create.html │ │ │ ├── custom_details.html │ │ │ ├── custom_edit.html │ │ │ └── custom_list.html │ ├── index.html │ └── security │ │ ├── _macros.html │ │ ├── _menu.html │ │ ├── base.html │ │ └── login_user.html ├── xadm_lib.py └── xadm_salib.py ├── requirements-dev.txt └── setup.py /AUTHORS: -------------------------------------------------------------------------------- 1 | Flask-Admin is maintained by Sedad Delalic -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 hexo- 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | xadmin\__init__.py 4 | xadmin\example_app.py 5 | xadmin\xadm_lib.py 6 | xadmin\xadm_salib.py 7 | xadmin\templates\index.html 8 | xadmin\templates\admin\edit_mode.html 9 | xadmin\templates\admin\files\custom_file_list.html 10 | xadmin\templates\admin\models\custom_create.html 11 | xadmin\templates\admin\models\custom_details.html 12 | xadmin\templates\admin\models\custom_edit.html 13 | xadmin\templates\admin\models\custom_list.html 14 | xadmin\templates\security\_macros.html 15 | xadmin\templates\security\_menu.html 16 | xadmin\templates\security\base.html 17 | xadmin\templates\security\login_user.html 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include MANIFEST 4 | include NOTICE 5 | 6 | recursive-include flask_xadmin/static * 7 | recursive-include flask_xadmin/templates * 8 | recursive-include flask_xadmin/examples * 9 | recursive-include flask_xadmin/translations * 10 | recursive-include flask_xadmin/tests * 11 | recursive-exclude flask_xadmin *.pyc -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Flask-xAdmin includes some bundled software to ease installation. 2 | 3 | Flask-Admin 4 | ======= 5 | 6 | Distributed under `Copyright (c) 2014, Serge S. Koval and contributors `_. 7 | 8 | Bootstrap 9 | ================= 10 | 11 | v3.1.0 and subsequent versions distributed under `MIT `_. 12 | Versions prior to v3.1.0 distributed under `APLv2 `_. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-xAdmin 2 | ============ 3 | 4 | Flask-xAdmin is a *extended-life batteries included*, simple-to-use `Flask `_ extension that lets you 5 | add flexible admin interface to Flask applications. 6 | Flask-xAdmin is standing on the shoulders of Flask, SQLAlchemy, Flask-Security and Flask-Admin. 7 | 8 | Introduction 9 | ------------ 10 | 11 | Flask-xAdmin is a *extended-life batteries-included*, simple-to-use `Flask `_ extension that lets you 12 | add admin interfaces to Flask applications. 13 | 14 | The goal of Flask-xAdmin is to give additional flexibility to Flask-Admin apps and make admin-app developer job easier. 15 | 16 | *Extended life* means that apps built with Flask-xAdmin are smaller and more resistant to database model changes due to development process, thus providing admin app extended lifetime & flexibility. 17 | 18 | Flask-xAdmin specific features are tested on `SQLAlchemy `_. 19 | 20 | Flask-xAdmin is a new project. 21 | 22 | Examples 23 | -------- 24 | Examples are included in the */examples* folder. Please feel free to add your own examples, or improve 25 | on some of the existing ones, and then submit them via GitHub as a *pull-request*. 26 | 27 | *simple.py* 28 | 29 | 1. start simple.py 30 | 2. open in browser http://127.0.0.1:8001/xadmin/note/ 31 | 3. username/password: admin@example.com/password 32 | 4. try to follow User relationship in Note table 33 | 5. click at user icon to enter edit mode 34 | 6. play ... 35 | 36 | User vadmin@example.com/password does not have edit mode role. 37 | You can try to login as vadmin@example.com to see difference. 38 | 39 | 40 | Documentation (todo) 41 | --------------------------- 42 | Flask-xAdmin documentation will be published at `https://flask-xadmin.readthedocs.io/en/latest/ `_. 43 | 44 | Installation 45 | ------------ 46 | To install Flask-xAdmin, simply:: 47 | 48 | pip install flask-xadmin 49 | 50 | Or alternatively, you can download the repository and install manually by doing:: 51 | 52 | git clone git@github.com:hexo-/flask-xadmin.git 53 | cd flask-xadmin 54 | python setup.py install 55 | 56 | Tests (TODO) 57 | ------------------- 58 | Test are run with *nose*. If you are not familiar with this package you can get some more info from `their website `_. 59 | 60 | To run the tests, from the project directory, simply:: 61 | 62 | pip install -r requirements-dev.txt 63 | nosetests 64 | 65 | You should see output similar to:: 66 | 67 | ............................................. 68 | ---------------------------------------------------------------------- 69 | Ran 102 tests in 13.132s 70 | 71 | OK 72 | 73 | Notes 74 | ----- 75 | This document is created from Flask-Admin/README.rst 76 | 77 | Special thanks to Serge S. Koval, author of Flask-Admin and contributors. 78 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexo-/flask-xadmin/4d35f5ae4fb13a71555ffb2746f47d00f96006d3/examples/__init__.py -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __author__ = 'dsedad' 3 | 4 | # Flask-xAdmin example 5 | # by Sedad Delalic 6 | # December 20, 2016 7 | 8 | 9 | from flask import Flask, current_app 10 | from flask import redirect 11 | from flask_sqlalchemy import SQLAlchemy 12 | from flask_security import RoleMixin, SQLAlchemyUserDatastore, UserMixin, utils 13 | 14 | from flask_xadmin.xadm_lib import xModelView, xFileAdmin 15 | from flask_xadmin.xadm_salib import Password 16 | from flask_xadmin import xadm_app, gen_xadmin 17 | 18 | 19 | # Initialize Flask and set some config values 20 | app = Flask(__name__) 21 | app.config['DEBUG']=True 22 | # Replace this with your own secret key 23 | app.config['SECRET_KEY'] = 'super-secret' 24 | # The database must exist (although it's fine if it's empty) before you attempt to access any page of the app 25 | # in your browser. 26 | # I used a PostgreSQL database, but you could use another type of database, including an in-memory SQLite database. 27 | # You'll need to connect as a user with sufficient privileges to create tables and read and write to them. 28 | # Replace this with your own database connection string. 29 | #xxxxx 30 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///xadmin_demo.db' 31 | 32 | # Set config values for Flask-Security. 33 | # We're using PBKDF2 with salt. 34 | app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512' 35 | # Replace this with your own salt. 36 | app.config['SECURITY_PASSWORD_SALT'] = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 37 | 38 | # xAdmin roles 39 | app.config['XADMIN_ROLE'] = 'flask-xadmin' 40 | app.config['XADMIN_EDIT_ROLE'] = 'flask-xadmin-edit' 41 | 42 | db = SQLAlchemy(app) 43 | app.register_blueprint(xadm_app) 44 | 45 | # Create a table to support a many-to-many relationship between Users and Roles 46 | roles_users = db.Table( 47 | 'roles_users', 48 | db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), 49 | db.Column('role_id', db.Integer(), db.ForeignKey('role.id')) 50 | ) 51 | 52 | 53 | # Role class 54 | class Role(db.Model, RoleMixin): 55 | """ Roles """ 56 | 57 | # Our Role has three fields, ID, name and description 58 | id = db.Column(db.Integer(), primary_key=True, doc="Role id") 59 | name = db.Column(db.String(80), unique=True, doc="Role name") 60 | description = db.Column(db.String(255), doc="Role description") 61 | 62 | # __str__ is required by Flask-Admin, so we can have human-readable values for the Role when editing a User. 63 | # If we were using Python 2.7, this would be __unicode__ instead. 64 | def __str__(self): 65 | return self.name 66 | 67 | # __hash__ is required to avoid the exception TypeError: unhashable type: 'Role' when saving a User 68 | def __hash__(self): 69 | return hash(self.name) 70 | 71 | 72 | # User class 73 | class User(db.Model, UserMixin): 74 | """ Application users """ 75 | # Our User has six fields: ID, email, password, active, confirmed_at and roles. The roles field represents a 76 | # many-to-many relationship using the roles_users table. Each user may have no role, one role, or multiple roles. 77 | id = db.Column(db.Integer, primary_key=True, doc="id") 78 | email = db.Column(db.String(255), unique=True, doc="Valid email") 79 | name = db.Column(db.String(255), doc="Full name") 80 | password = db.Column(Password(255), doc="Password") 81 | active = db.Column(db.Boolean(), doc="Only active users are allowed to log in") 82 | confirmed_at = db.Column(db.DateTime(), doc="User identity confirmation datetime") 83 | roles = db.relationship( 84 | 'Role', 85 | secondary=roles_users, 86 | backref=db.backref('users', lazy='dynamic') 87 | ) 88 | 89 | def __str__(self): 90 | return self.name 91 | 92 | class Note(db.Model): 93 | """ User Notes """ 94 | id = db.Column(db.String(32), primary_key=True, doc="Note id") 95 | note = db.Column(db.String(1024), doc="Note content") 96 | user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete='CASCADE')) 97 | user = db.relationship('User', backref='notes') 98 | 99 | def __str__(self): 100 | return self.note 101 | 102 | # Initialize the SQLAlchemy data store 103 | user_datastore = SQLAlchemyUserDatastore(db, User, Role) 104 | 105 | # Security is initialized in xAdmin 106 | 107 | # Executes before the first request is processed. 108 | @app.before_first_request 109 | def before_first_request(): 110 | 111 | # Create any database tables that don't exist yet. 112 | db.create_all() 113 | 114 | # Create the Roles "admin" and "end-user" -- unless they already exist 115 | user_datastore.find_or_create_role(name=current_app.config['XADMIN_EDIT_ROLE'], description='Administrator (edit)') 116 | user_datastore.find_or_create_role(name=current_app.config['XADMIN_ROLE'], description='Administrator (view)') 117 | user_datastore.find_or_create_role(name='end-user', description='End user') 118 | 119 | # Create two Users for testing purposes -- unless they already exists. 120 | # In each case, use Flask-Security utility function to encrypt the password. 121 | encrypted_password = utils.encrypt_password('password') 122 | if not user_datastore.get_user('someone@example.com'): 123 | user_datastore.create_user(email='someone@example.com', password=encrypted_password, name='User') 124 | if not user_datastore.get_user('vadmin@example.com'): 125 | user_datastore.create_user(email='vadmin@example.com', password=encrypted_password, name='Administrator (view only)') 126 | if not user_datastore.get_user('admin@example.com'): 127 | user_datastore.create_user(email='admin@example.com', password=encrypted_password, name='Administrator (edit)') 128 | 129 | # Commit any database changes; the User and Roles must exist before we can add a Role to the User 130 | db.session.commit() 131 | 132 | # Give one User has the "end-user" role, while the other has the "admin" role. (This will have no effect if the 133 | # Users already have these Roles.) Again, commit any database changes. 134 | user_datastore.add_role_to_user('someone@example.com', 'end-user') 135 | x=current_app 136 | user_datastore.add_role_to_user('vadmin@example.com', current_app.config['XADMIN_ROLE']) 137 | user_datastore.add_role_to_user('admin@example.com', current_app.config['XADMIN_ROLE']) 138 | user_datastore.add_role_to_user('admin@example.com', current_app.config['XADMIN_EDIT_ROLE']) 139 | 140 | db.session.commit() 141 | 142 | 143 | class myModelView(xModelView): 144 | column_exclude_list = 'password' 145 | # Customize your generic model -view 146 | pass 147 | 148 | class myFileAdmin(xFileAdmin): 149 | def doc(self): 150 | return "Various files" 151 | 152 | # Customize your File Admin model -view 153 | pass 154 | 155 | 156 | # IMPORTANT: Authorized users should have granted xadmin_superadmin role 157 | 158 | # Prepare view list 159 | views = [ 160 | myModelView(model=User, session=db.session, category='Entities'), 161 | myModelView(model=Role, session=db.session, category='Entities'), 162 | myModelView(model=Note, session=db.session, category='Entities'), 163 | myFileAdmin(base_path='.', name="Files", category='Files')] 164 | 165 | # Make an instance of flask-xadmin 166 | xadmin_obj = gen_xadmin(app = app, title = 'xAdmin', db=db, user_model=User, role_model=Role, views=views) 167 | 168 | @app.route('/') 169 | def home(): 170 | return redirect('/xadmin') 171 | 172 | if __name__ == "__main__": 173 | app.run(debug=True, port=8001) 174 | print('Try url: /xadmin') 175 | 176 | -------------------------------------------------------------------------------- /examples/xadmin_demo.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexo-/flask-xadmin/4d35f5ae4fb13a71555ffb2746f47d00f96006d3/examples/xadmin_demo.db -------------------------------------------------------------------------------- /flask_xadmin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '0.1.2' 3 | __author__ = 'Sedad Delalic' 4 | __email__ = 'dsedad@gmail.com' 5 | 6 | from flask import Blueprint, redirect 7 | from flask_security import Security, SQLAlchemyUserDatastore 8 | from flask_admin import Admin 9 | from flask_security import current_user, logout_user 10 | from flask import current_app 11 | from flask_xadmin.xadm_lib import set_edit_mode, is_super_admin 12 | from flask_xadmin.xadm_lib import xAdminIndexView, xEditModeView, xModelView, current_edit_mode, is_user_authenticated 13 | 14 | xadm_app = Blueprint('xadm_app', __name__, template_folder='templates') 15 | 16 | # wrap admin 17 | def gen_xadmin(app, title, db, user_model, role_model, views=[]): 18 | db.init_app(app) 19 | 20 | # init_login() 21 | user_datastore = SQLAlchemyUserDatastore(db=db, user_model=user_model, role_model=role_model) 22 | security = Security(app, user_datastore) 23 | xadmin = Admin(app, title, index_view=xAdminIndexView(url='/xadmin'), base_template='index.html') 24 | 25 | for v in views: 26 | xadmin.add_view(v) 27 | 28 | # Add view for enter/leave edit mode 29 | xadmin.add_view(xEditModeView(name='EditMode')) 30 | 31 | return xadmin 32 | 33 | @xadm_app.before_app_request 34 | def reset_views(): 35 | """ Before each request - reset permissions for views, regarding edit_mode """ 36 | 37 | if not is_user_authenticated(): 38 | set_edit_mode(False) 39 | else: 40 | if not is_super_admin(): 41 | logout_user() 42 | 43 | admins = current_app.extensions.get('admin', []) 44 | for adm in admins: 45 | for v in adm._views: 46 | if hasattr(v, 'set_permissions'): 47 | v.set_permissions(current_edit_mode()) 48 | 49 | 50 | @xadm_app.errorhandler(403) 51 | def page_not_found(e): 52 | return redirect('/') 53 | -------------------------------------------------------------------------------- /flask_xadmin/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexo-/flask-xadmin/4d35f5ae4fb13a71555ffb2746f47d00f96006d3/flask_xadmin/examples/__init__.py -------------------------------------------------------------------------------- /flask_xadmin/examples/simple.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __author__ = 'dsedad' 3 | 4 | # Flask-xAdmin example 5 | # by Sedad Delalic 6 | # December 20, 2016 7 | 8 | 9 | from flask import Flask, current_app 10 | from flask import redirect 11 | from flask_sqlalchemy import SQLAlchemy 12 | from flask_security import RoleMixin, SQLAlchemyUserDatastore, UserMixin, utils 13 | 14 | from flask_xadmin.xadm_lib import xModelView, xFileAdmin 15 | from flask_xadmin.xadm_salib import Password 16 | from flask_xadmin import xadm_app, gen_xadmin 17 | 18 | 19 | # Initialize Flask and set some config values 20 | app = Flask(__name__) 21 | app.config['DEBUG']=True 22 | # Replace this with your own secret key 23 | app.config['SECRET_KEY'] = 'super-secret' 24 | # The database must exist (although it's fine if it's empty) before you attempt to access any page of the app 25 | # in your browser. 26 | # I used a PostgreSQL database, but you could use another type of database, including an in-memory SQLite database. 27 | # You'll need to connect as a user with sufficient privileges to create tables and read and write to them. 28 | # Replace this with your own database connection string. 29 | #xxxxx 30 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///xadmin_demo.db' 31 | 32 | # Set config values for Flask-Security. 33 | # We're using PBKDF2 with salt. 34 | app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512' 35 | # Replace this with your own salt. 36 | app.config['SECURITY_PASSWORD_SALT'] = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 37 | 38 | # xAdmin roles 39 | app.config['XADMIN_ROLE'] = 'flask-xadmin' 40 | app.config['XADMIN_EDIT_ROLE'] = 'flask-xadmin-edit' 41 | 42 | db = SQLAlchemy(app) 43 | 44 | # Create a table to support a many-to-many relationship between Users and Roles 45 | roles_users = db.Table( 46 | 'roles_users', 47 | db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), 48 | db.Column('role_id', db.Integer(), db.ForeignKey('role.id')) 49 | ) 50 | 51 | 52 | # Role class 53 | class Role(db.Model, RoleMixin): 54 | """ Roles """ 55 | 56 | # Our Role has three fields, ID, name and description 57 | id = db.Column(db.Integer(), primary_key=True, doc="Role id") 58 | name = db.Column(db.String(80), unique=True, doc="Role name") 59 | description = db.Column(db.String(255), doc="Role description") 60 | 61 | # __str__ is required by Flask-Admin, so we can have human-readable values for the Role when editing a User. 62 | # If we were using Python 2.7, this would be __unicode__ instead. 63 | def __str__(self): 64 | return self.name 65 | 66 | # __hash__ is required to avoid the exception TypeError: unhashable type: 'Role' when saving a User 67 | def __hash__(self): 68 | return hash(self.name) 69 | 70 | 71 | # User class 72 | class User(db.Model, UserMixin): 73 | """ Application users """ 74 | # Our User has six fields: ID, email, password, active, confirmed_at and roles. The roles field represents a 75 | # many-to-many relationship using the roles_users table. Each user may have no role, one role, or multiple roles. 76 | id = db.Column(db.Integer, primary_key=True, doc="id") 77 | email = db.Column(db.String(255), unique=True, doc="Valid email") 78 | name = db.Column(db.String(255), doc="Full name") 79 | password = db.Column(Password(255), doc="Password") 80 | active = db.Column(db.Boolean(), doc="Only active users are allowed to log in") 81 | confirmed_at = db.Column(db.DateTime(), doc="User identity confirmation datetime") 82 | roles = db.relationship( 83 | 'Role', 84 | secondary=roles_users, 85 | backref=db.backref('users', lazy='dynamic') 86 | ) 87 | 88 | def __str__(self): 89 | return self.name 90 | 91 | class Note(db.Model): 92 | """ User Notes """ 93 | id = db.Column(db.String(32), primary_key=True, doc="Note id") 94 | note = db.Column(db.String(1024), doc="Note content") 95 | user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete='CASCADE')) 96 | user = db.relationship('User', backref='notes') 97 | 98 | def __str__(self): 99 | return self.note 100 | 101 | # Initialize the SQLAlchemy data store 102 | user_datastore = SQLAlchemyUserDatastore(db, User, Role) 103 | 104 | # Security is initialized in xAdmin 105 | 106 | # Executes before the first request is processed. 107 | @app.before_first_request 108 | def before_first_request(): 109 | 110 | # Create any database tables that don't exist yet. 111 | db.create_all() 112 | 113 | # Create the Roles "admin" and "end-user" -- unless they already exist 114 | user_datastore.find_or_create_role(name=current_app.config['XADMIN_EDIT_ROLE'], description='Administrator (edit)') 115 | user_datastore.find_or_create_role(name=current_app.config['XADMIN_ROLE'], description='Administrator (view)') 116 | user_datastore.find_or_create_role(name='end-user', description='End user') 117 | 118 | # Create two Users for testing purposes -- unless they already exists. 119 | # In each case, use Flask-Security utility function to encrypt the password. 120 | encrypted_password = utils.encrypt_password('password') 121 | if not user_datastore.get_user('someone@example.com'): 122 | user_datastore.create_user(email='someone@example.com', password=encrypted_password, name='User') 123 | if not user_datastore.get_user('vadmin@example.com'): 124 | user_datastore.create_user(email='vadmin@example.com', password=encrypted_password, name='Administrator (view only)') 125 | if not user_datastore.get_user('admin@example.com'): 126 | user_datastore.create_user(email='admin@example.com', password=encrypted_password, name='Administrator (edit)') 127 | 128 | # Commit any database changes; the User and Roles must exist before we can add a Role to the User 129 | db.session.commit() 130 | 131 | # Give one User has the "end-user" role, while the other has the "admin" role. (This will have no effect if the 132 | # Users already have these Roles.) Again, commit any database changes. 133 | user_datastore.add_role_to_user('someone@example.com', 'end-user') 134 | x=current_app 135 | user_datastore.add_role_to_user('vadmin@example.com', current_app.config['XADMIN_ROLE']) 136 | user_datastore.add_role_to_user('admin@example.com', current_app.config['XADMIN_ROLE']) 137 | user_datastore.add_role_to_user('admin@example.com', current_app.config['XADMIN_EDIT_ROLE']) 138 | 139 | note = Note(id='note4admin', user=user_datastore.get_user('admin@example.com'), note='Admin\'s note') 140 | db.session.add(note) 141 | 142 | note = Note(id='note4someone', user=user_datastore.get_user('someone@example.com'), note='Someone note') 143 | db.session.add(note) 144 | 145 | db.session.commit() 146 | 147 | 148 | # Here comes example of Flask-xAdmin ModelViews 149 | 150 | app.register_blueprint(xadm_app) 151 | 152 | class myModelView(xModelView): 153 | column_exclude_list = 'password' 154 | # Customize your generic model -view 155 | pass 156 | 157 | class myFileAdmin(xFileAdmin): 158 | def doc(self): 159 | return "Various files" 160 | 161 | # Customize your File Admin model -view 162 | pass 163 | 164 | 165 | # IMPORTANT: Authorized users should have granted flask-xadmin role (for view only) flask-xadmin-edit (for edit feature) 166 | 167 | # Prepare view list 168 | views = [ 169 | myModelView(model=User, session=db.session, category='Entities'), 170 | myModelView(model=Role, session=db.session, category='Entities'), 171 | myModelView(model=Note, session=db.session, category='Entities'), 172 | myFileAdmin(base_path='.', name="Files", category='Files')] 173 | 174 | # Make an instance of flask-xadmin 175 | xadmin_obj = gen_xadmin(app = app, title = 'xAdmin', db=db, user_model=User, role_model=Role, views=views) 176 | 177 | @app.route('/') 178 | def home(): 179 | return redirect('/xadmin') 180 | 181 | if __name__ == "__main__": 182 | app.run(debug=True, port=8001) 183 | print('Try url: /xadmin') 184 | 185 | -------------------------------------------------------------------------------- /flask_xadmin/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask_security.forms import Form,LoginForm 3 | from wtforms import SubmitField, PasswordField,HiddenField 4 | from flask_security.utils import verify_and_update_password 5 | from flask_security import current_user 6 | 7 | 8 | #Form to enter edit mode 9 | class EditModeForm(Form): 10 | password = PasswordField(u'Password', description='Please enter your password to enable edit mode') 11 | next = HiddenField() 12 | submit = SubmitField(u'Activate edit mode') 13 | 14 | def validate(self): 15 | if not Form.validate(self): 16 | return False 17 | 18 | if self.password.data.strip() == '': 19 | self.password.errors.append(u'Password is required') 20 | return False 21 | 22 | if not verify_and_update_password(self.password.data, current_user): 23 | self.password.errors.append(u'Wrong password') 24 | return False 25 | 26 | return True 27 | 28 | -------------------------------------------------------------------------------- /flask_xadmin/templates/admin/edit_mode.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'admin/master.html' %} 3 | {% include "security/_messages.html" %} 4 | 5 | {% block body %} 6 |
7 | {{ edit_mode_form.hidden_tag() }} 8 |
9 | {{ edit_mode_form.password(class="form-control", placeholder="password?") }} 10 |
11 | {% if edit_mode_form.password.errors %} 12 | {% for error in edit_mode_form.password.errors %} 13 | {{ error }} 14 | {% endfor %} 15 | {% endif %} 16 |
17 | {{ edit_mode_form.submit(class='btn btn-success') }} 18 |
19 | 20 |
21 | 22 | {% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /flask_xadmin/templates/admin/files/custom_file_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/file/list.html' %} 2 | {% block body %} 3 |

{{ admin_view.name }}

{{ admin_view.doc()}}   4 | 5 | {{super() }} 6 | 7 | {% endblock %} 8 | {% block model_menu_bar %} 9 | {{ super() }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /flask_xadmin/templates/admin/models/custom_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/model/create.html' %} 2 | {% block body %} 3 | {{ admin_view.name }} ({{ admin_view.doc()}}) 4 | 5 | {{super() }} 6 | {% endblock %} 7 | 8 | -------------------------------------------------------------------------------- /flask_xadmin/templates/admin/models/custom_details.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/model/details.html' %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | {{ lib.form_css() }} 6 | {% endblock %} 7 | 8 | {% block details_search %} 9 | {% endblock %} 10 | 11 | {% block navlinks %} 12 |

{{ admin_view.name }}

#{{ request.args.get('id') }} {{ admin_view.doc()}}   13 | {{ super() }} 14 | {% endblock %} 15 | 16 | {% block details_table %} 17 | 18 | {% call lib.form_tag(form) %} 19 |
20 | {{ lib.render_form_fields(form, form_opts=form_opts) }} 21 | 22 | {# 23 |
24 | 27 |
28 | #} 29 | 30 | {% block custom_field %}{% endblock %} 31 |
32 | {% endcall %} 33 | {% endblock %} 34 | 35 | {% block tail %} 36 | {{ super() }} 37 | {{ lib.form_js() }} 38 | 45 | {% endblock %} -------------------------------------------------------------------------------- /flask_xadmin/templates/admin/models/custom_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/model/edit.html' %} 2 | {% block body %} 3 |

{{ admin_view.name }}

#{{ request.args.get('id') }} {{ admin_view.doc()}}   4 | {{super() }} 5 | {% endblock %} 6 | 7 | -------------------------------------------------------------------------------- /flask_xadmin/templates/admin/models/custom_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/model/list.html' %} 2 | {% block body %} 3 |

{{ admin_view.name }}

{{ admin_view.doc()}}   4 | 5 | {{super() }} 6 | 7 | {% endblock %} 8 | {% block model_menu_bar %} 9 | {{ super() }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /flask_xadmin/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/base.html' %} 2 | 3 | {% block tail_js %} 4 | {{ super() }} 5 | {% if session.get('xadm_edit_mode', False) %} 6 | 11 | 12 | {% endif %} 13 | {% endblock %} 14 | 15 | {% block access_control %} 16 | {{ super() }} 17 | {% if (current_user.is_authenticated in (True, False) and current_user.is_authenticated) 18 | or (current_user.is_authenticated != False and current_user.is_authenticated()) %} 19 |
20 | 21 | {{ current_user.login }} 22 | 23 | 34 |
35 | {% else %} 36 | 37 | 40 | 41 | {% endif %} 42 | {% endblock %} 43 | 44 | {% block page_body %} 45 | {{ super() }} 46 | {% if request.endpoint == 'admin.index' %} 47 |
48 | 49 |
50 | 51 |
52 | {% if (current_user.is_authenticated in (True, False) and current_user.is_authenticated) 53 | or (current_user.is_authenticated != False and current_user.is_authenticated()) %} 54 | {% if current_user.has_role(config.get('XADMIN_ROLE', False)) %} 55 |

Dear "{{ current_user.name }}" welcome to xAdmin

56 | {% for cat in admin_view.admin._menu %} 57 |

58 | {% if cat._cached_url %} 59 | {{ cat.name }} 60 | {% else %} 61 | {% if cat._children.__len__() > 0 %} 62 | {{ cat.name }} 63 | {% endif %} 64 | {% endif %} 65 |

66 | 77 | {% endfor %} 78 | 79 | {% else %} 80 | 81 |
82 |

You are not authorized superuser!

83 |
84 | {% endif %} 85 | {% else %} 86 |
87 |

Welcome to xAdmin

88 |
89 | admin role: {{ config['XADMIN_ROLE'] }}, edit role: {{ config['XADMIN_EDIT_ROLE'] }} 90 |
91 |

92 | Warning: Only authorized users are allowed to be here. 93 | Please login using your credentials.

94 |
95 | {% endif %} 96 |
97 |
98 | {% endif %} 99 | {% endblock page_body %} -------------------------------------------------------------------------------- /flask_xadmin/templates/security/_macros.html: -------------------------------------------------------------------------------- 1 | {% macro render_field_with_errors(field) %} 2 |

3 | {{ field.label }} {{ field(**kwargs)|safe }} 4 | {% if field.errors %} 5 |

    6 | {% for error in field.errors %} 7 |
  • {{ error }}
  • 8 | {% endfor %} 9 |
10 | {% endif %} 11 |

12 | {% endmacro %} 13 | 14 | {% macro render_field(field) %} 15 |

{{ field(**kwargs)|safe }}

16 | {% endmacro %} -------------------------------------------------------------------------------- /flask_xadmin/templates/security/_menu.html: -------------------------------------------------------------------------------- 1 | 2 | {% if security.registerable or security.recoverable or security.confirmabled %} 3 |

Menu

4 |
    5 |
  • Login
  • 6 | {% if security.registerable %} 7 |
  • Register
  • 8 | {% endif %} 9 | {% if security.recoverable %} 10 |
  • Forgot password
  • 11 | {% endif %} 12 | {% if security.confirmable %} 13 |
  • Confirm account
  • 14 | {% endif %} 15 |
16 | {% endif %} 17 | -------------------------------------------------------------------------------- /flask_xadmin/templates/security/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Login 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 | 16 | {% block content %} 17 | {% endblock %} 18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /flask_xadmin/templates/security/login_user.html: -------------------------------------------------------------------------------- 1 | {% extends "security/base.html" %} 2 | {% from "security/_macros.html" import render_field_with_errors, render_field %} 3 | {% include "security/_messages.html" %} 4 | 5 | {% block content %} 6 | 7 | 8 | 9 |

xAdmin Login

10 |
11 |
12 | {{ login_user_form.hidden_tag() }} 13 |
14 | email: 15 | {{ login_user_form.email(class="form-control") }} 16 | password: 17 | {{ login_user_form.password(class="form-control") }} 18 | 19 |
20 | {{ login_user_form.remember }} 21 | Remember me 22 |
23 | 24 |
25 | {% if login_user_form.password.errors %} 26 | 27 | {% for error in login_user_form.password.errors %} 28 | {{ error }} 29 | {% endfor %} 30 | 31 | {% endif %} 32 | 33 | {% if login_user_form.email.errors %} 34 | 35 | {% for error in login_user_form.email.errors %} 36 | {{ error }} 37 | {% endfor %} 38 | 39 | {% endif %} 40 | 41 |
42 | 43 | 44 | {{ render_field(login_user_form.next) }} 45 | 46 | {{ login_user_form.submit(class='btn btn-success') }} 47 | 48 |
49 | 50 |
51 | 52 | 53 | 54 | 55 | 56 | {% endblock %} 57 | 58 | -------------------------------------------------------------------------------- /flask_xadmin/xadm_lib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __author__ = 'dsedad' 3 | 4 | from uuid import uuid4 5 | 6 | import inspect 7 | 8 | from flask_xadmin.xadm_salib import * 9 | from flask import flash, current_app 10 | from flask import redirect 11 | from flask import request 12 | from flask import url_for 13 | from flask import session 14 | from flask_admin.contrib.sqla import ModelView 15 | from flask_admin.helpers import get_redirect_target 16 | from flask_admin.form import FormOpts 17 | from flask_admin.model.base import get_mdict_item_or_list 18 | from flask_security import current_user, logout_user 19 | from flask_admin import Admin, expose, AdminIndexView, BaseView 20 | from sqlalchemy.ext.declarative import AbstractConcreteBase 21 | from flask_admin.contrib.fileadmin import FileAdmin 22 | from flask_xadmin.forms import EditModeForm 23 | from flask_security.utils import get_url 24 | from flask_security.utils import encrypt_password 25 | from wtforms import PasswordField 26 | 27 | #from config import XADMIN_ROLE, XADMIN_EDIT_ROLE 28 | 29 | LIST_TEMPLATE = 'admin/models/custom_list.html' 30 | FILE_LIST_TEMPLATE = 'admin/files/custom_file_list.html' 31 | DETAILS_TEMPLATE = 'admin/models/custom_details.html' 32 | EDIT_TEMPLATE = 'admin/models/custom_edit.html' 33 | CREATE_TEMPLATE = 'admin/models/custom_create.html' 34 | 35 | PAGE_SIZE = 10 36 | 37 | from flask_admin.contrib.sqla.form import AdminModelConverter 38 | from flask_admin.model.form import converts 39 | from wtforms import PasswordField 40 | from wtforms import widgets 41 | 42 | def xadmin_role(): 43 | role = current_app.config.get('XADMIN_ROLE') 44 | if role == None: 45 | role = 'flask-xadmin' 46 | current_app.config['XADMIN_ROLE'] = role 47 | return role 48 | 49 | 50 | def xadmin_edit_role(): 51 | role = current_app.config.get('XADMIN_EDIT_ROLE') 52 | if role == None: 53 | role = 'flask-xadmin-edit' 54 | current_app.config['XADMIN_EDIT_ROLE'] = role 55 | return role 56 | 57 | 58 | def is_user_authenticated(): 59 | """ 60 | Wrapper for user.is_authenticated 61 | :return: 62 | """ 63 | try: 64 | result = current_user.is_authenticated() 65 | except: 66 | result = current_user.is_authenticated 67 | return result 68 | 69 | 70 | def is_super_admin(): 71 | return current_user.has_role(xadmin_role()) 72 | 73 | def is_super_admin_edit(): 74 | return current_user.has_role(xadmin_edit_role()) 75 | 76 | class CustomPasswordInput(widgets.Input): 77 | input_type = 'password' 78 | 79 | def __init__(self, hide_value=True): 80 | self.hide_value = hide_value 81 | 82 | def __call__(self, field, **kwargs): 83 | return super(CustomPasswordInput, self).__call__(field, **kwargs) 84 | 85 | class CustomPasswordField(PasswordField): 86 | #custom password filed, does not hide value 87 | widget = CustomPasswordInput() 88 | 89 | class PasswordAdminModelConverter(AdminModelConverter): 90 | #custom model converter for converting Password column types to custom password filed form field 91 | @converts('Password') 92 | def conv_Password(self, field_args, **extra): 93 | field_args.setdefault('label', u'Password') 94 | return CustomPasswordField(**field_args) 95 | 96 | 97 | def current_edit_mode(): 98 | return session.get('xadm_edit_mode', False) 99 | 100 | def set_edit_mode(mode): 101 | if mode: 102 | if is_super_admin_edit(): 103 | session['xadm_edit_mode'] = True 104 | else: 105 | raise Exception(u'Not allowed') 106 | else: 107 | edit_mode = session.get('xadm_edit_mode', None) 108 | if edit_mode != None: 109 | session.pop('xadm_edit_mode') 110 | 111 | class BaseClass(AbstractConcreteBase): 112 | # table page size 113 | page_size = PAGE_SIZE 114 | details_modal = False 115 | 116 | def is_accessible(self): 117 | if is_user_authenticated(): 118 | return is_super_admin() 119 | 120 | # if view is not accessible redirect to login page 121 | def inaccessible_callback(self, name, **kwargs): 122 | return redirect(url_for('security.login', next=request.url)) 123 | 124 | list_template = LIST_TEMPLATE 125 | details_template = DETAILS_TEMPLATE 126 | edit_template = EDIT_TEMPLATE 127 | create_template = CREATE_TEMPLATE 128 | 129 | class xModelView(BaseClass, ModelView): 130 | column_display_pk = True 131 | read_only = False 132 | encrypt_password_fields = True 133 | _password_type_name = 'password' 134 | 135 | 136 | def on_model_change(self, form, model_obj, is_created): 137 | if self.encrypt_password_fields: 138 | model = inspect_sa(model_obj).mapper.class_ #we need model not model object 139 | keys = sa_type_keys(model, self._password_type_name) #column type is in lowercase 140 | for k in keys: 141 | password_changed = sa_column_changed(model_obj, k) 142 | if password_changed: 143 | password_field = getattr(form, k) 144 | setattr(model_obj, k, encrypt_password(password_field.data)) 145 | 146 | def set_permissions(self, edit_mode): 147 | """ 148 | edit_mode == True => allow edit, delete, create. Otherwise prevent edit, delete, create. 149 | :return: 150 | """ 151 | if not is_super_admin_edit(): 152 | edit_mode = False 153 | 154 | if not(edit_mode): 155 | self.can_create = False 156 | self.can_edit = False 157 | self.can_delete = False 158 | self.can_view_details = True 159 | else: 160 | if (hasattr(self, 'read_only') and self.read_only): 161 | self.can_create = False 162 | self.can_edit = False 163 | self.can_delete = False 164 | self.can_view_details = True 165 | else: 166 | self.can_create = True 167 | self.can_edit = True 168 | self.can_delete = True 169 | self.can_view_details = True 170 | # return dict(edit_mode=True) 171 | def doc(self): 172 | return inspect.getdoc(self.model).strip() 173 | 174 | @expose('/details/', methods=('GET', 'POST')) 175 | def details_view(self): 176 | 177 | return_url = get_redirect_target() or self.get_url('.index_view') 178 | id = get_mdict_item_or_list(request.args, 'id') 179 | if id is None: 180 | return redirect(return_url) 181 | model = self.get_one(id) 182 | if model is None: 183 | return redirect(return_url) 184 | form = self.edit_form(obj=model) 185 | form_opts = FormOpts(widget_args=self.form_widget_args, 186 | form_rules=self._form_edit_rules) 187 | 188 | self.on_form_prefill(form, id) 189 | 190 | return self.render(self.details_template, 191 | model=model, 192 | form=form, 193 | form_opts=form_opts, 194 | return_url=return_url) 195 | 196 | def scaffold_list_filters(self): 197 | cols = self.scaffold_list_columns() 198 | # Columns 199 | res_cols = [] 200 | # Relationships 201 | res_rels = [] 202 | for c in cols: 203 | col_type = sa_column_type(self.model, c) 204 | if col_type is None: 205 | res_rels.append(c) 206 | elif sa_column_type(self.model, c) not in ('password', 'guid', 'largebinary'): #If we use custom filed filter will not work 207 | res_cols.append(c) 208 | # Filter show list of columns, then list of relationships 209 | return res_cols + res_rels 210 | 211 | def get_form_columns(self, directions=[MANYTOMANY, ONETOMANY]): 212 | return self.scaffold_list_columns() + sa_relationships_keys(self.model, directions=directions) 213 | 214 | def get_column_searchable_list(self): 215 | return sa_column_searchable_list(self.model) 216 | 217 | def get_column_list(self): 218 | return self.scaffold_list_columns() 219 | 220 | def get_column_list_filters(self): 221 | return self.scaffold_list_filters() 222 | 223 | def get_column_descriptions(self): 224 | return sa_column_descriptions(self.model) 225 | 226 | def get_column_formatters(self): 227 | return gen_href_formatter(self.model) 228 | 229 | def get_column_details_list(self): 230 | return self.get_form_columns(directions=[MANYTOMANY, ONETOMANY]) 231 | 232 | def __init__(self, *args, **kwargs): 233 | # if not(self.column_formatters): 234 | # self.column_formatters = gen_href_formatter(model, relationship_names=['log_create_user']) 235 | self.model = kwargs.get('model') 236 | if not self.model: 237 | self.model = args[0] 238 | 239 | ahref_fmt = '%s' 240 | if not getattr(self, "column_formatters"): 241 | formatters = dict(self.get_column_formatters()) 242 | self.column_formatters = formatters 243 | 244 | if not getattr(self, "column_descriptions"): 245 | self.column_descriptions = self.get_column_descriptions() 246 | 247 | if not getattr(self, "column_filters"): 248 | self.column_filters = self.get_column_list_filters() 249 | pass 250 | 251 | if not getattr(self, "column_list"): 252 | self.column_list = self.get_column_list() 253 | 254 | if not getattr(self, "column_searchable_list"): 255 | self.column_searchable_list = self.get_column_searchable_list() 256 | 257 | if not getattr(self, "form_columns"): 258 | self.form_columns = self.get_form_columns(directions=[MANYTOMANY, ONETOMANY]) 259 | 260 | if not getattr(self, "column_details_list"): 261 | self.column_details_list = self.get_column_details_list() 262 | 263 | if self.encrypt_password_fields: 264 | self.model_form_converter = PasswordAdminModelConverter 265 | 266 | super(xModelView, self).__init__(*args, **kwargs) 267 | 268 | 269 | 270 | class xAdminIndexView(AdminIndexView): 271 | @expose('/') 272 | def index(self): 273 | return super(xAdminIndexView, self).index() 274 | 275 | @expose('/logout/') 276 | def logout_view(self): 277 | set_edit_mode(False) 278 | logout_user() 279 | return redirect(url_for('.index')) 280 | 281 | 282 | class xEditModeView(BaseView): 283 | def is_accessible(self): 284 | if is_user_authenticated(): 285 | return is_super_admin() 286 | return False 287 | 288 | def is_visible(self): 289 | return False 290 | 291 | # if view is not accessible redirect to login page 292 | def inaccessible_callback(self, name, **kwargs): 293 | return redirect(url_for('security.login', next=request.url)) 294 | 295 | #enter edit mode function 296 | @expose('/', methods=('GET', 'POST')) 297 | def change_mode(self): 298 | form = EditModeForm() 299 | if form.validate_on_submit(): 300 | set_edit_mode(True) 301 | flash(u'You are in EDIT mode. Be wise and careful!') 302 | return redirect(form.next.data) 303 | form.next.data = get_url(request.args.get('next')) or '/' 304 | return self.render('admin/edit_mode.html', edit_mode_form=form) 305 | 306 | # from flask_security.utils import verify_and_update_password 307 | #if request.method == 'GET': 308 | # return self.render('admin/edit_mode.html',edit_mode_form=form) 309 | #else: 310 | # password = request.form['password'] 311 | # # previous_page = request.form['previous_page'] 312 | # if verify_and_update_password(password, current_user): 313 | # session['xadm_edit_mode'] = True 314 | # flash(u'You are in EDIT mode. Be wise and careful!') 315 | # return redirect('/') 316 | # #return self.render('index.html') 317 | # else: 318 | # flash(u'Wrong password', category='error') 319 | # return self.render('admin/edit_mode.html',edit_mode_form=form) 320 | 321 | # exit edit mode function 322 | @expose('/leave_edit', methods=['GET']) 323 | def leave_edit(self): 324 | try: 325 | set_edit_mode(False) 326 | except: 327 | pass 328 | flash(u"You've left EDIT mode.") 329 | return redirect(request.referrer or '/') 330 | 331 | # Custom base file admin class 332 | class xFileAdmin(FileAdmin): 333 | list_template = FILE_LIST_TEMPLATE 334 | read_only = False 335 | def doc(self): 336 | return "" 337 | 338 | def is_accessible(self): 339 | if is_user_authenticated(): 340 | return is_super_admin() 341 | return False 342 | 343 | # if view is not accessible redirect to login page 344 | def inaccessible_callback(self, name, **kwargs): 345 | return redirect(url_for('security.login', next=request.url)) 346 | 347 | def set_permissions(self, edit_mode): 348 | """ 349 | edit_mode == True => allow edit, delete, create. Otherwise prevent edit, delete, create. 350 | :return: 351 | """ 352 | if not (edit_mode): 353 | self.can_download = True 354 | self.can_mkdir = False 355 | self.can_delete_dirs = False 356 | self.can_delete = False 357 | self.can_rename = False 358 | self.can_upload = False 359 | else: 360 | if (hasattr(self, 'read_only') and self.read_only): 361 | self.can_download = True 362 | self.can_mkdir = False 363 | self.can_delete_dirs = False 364 | self.can_delete = False 365 | self.can_rename = False 366 | self.can_upload = False 367 | else: 368 | self.can_download = True 369 | self.can_mkdir = True 370 | self.can_delete_dirs = True 371 | self.can_delete = True 372 | self.can_rename = True 373 | self.can_upload = True 374 | # return dict(edit_mode=True) 375 | -------------------------------------------------------------------------------- /flask_xadmin/xadm_salib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __author__ = 'dsedad' 3 | 4 | from sqlalchemy.inspection import inspect as inspect_sa 5 | from sqlalchemy.ext.hybrid import hybrid_property 6 | from sqlalchemy.orm.interfaces import MANYTOMANY, ONETOMANY, MANYTOONE 7 | from sqlalchemy.types import TypeDecorator, VARCHAR 8 | 9 | 10 | class Password(TypeDecorator): 11 | """Platform-independent password type.""" 12 | impl = VARCHAR 13 | 14 | 15 | def sa_column_changed(model, column): 16 | from sqlalchemy import inspect 17 | inspr = inspect(model) 18 | hist = getattr(inspr.attrs, column).history 19 | return hist.has_changes() 20 | 21 | def sa_view_url(model, vwtype='details'): 22 | tm = model 23 | return '{}.{}_view'.format(model.__name__.lower(), vwtype) 24 | 25 | 26 | def sa_relationship_key_pairs(relationship): 27 | remote = relationship.mapper.class_ 28 | local = relationship.parent.class_ 29 | srckeys = [] 30 | rmtkeys = [] 31 | for r in relationship.local_remote_pairs: 32 | srckeys.append(r[0].key) 33 | rmtkeys.append(r[1].key) 34 | return {"mapper": remote, "parent": local, "local": srckeys, "remote": rmtkeys, 35 | "direction": str(relationship.direction)} 36 | 37 | 38 | def sa_get_relationship_value(relationship): 39 | return relationship 40 | 41 | 42 | def sa_relationships(model, relation_name=None, directions=[MANYTOONE]): 43 | # Find relationship (filter by name, do not filter if filter_name = None) 44 | res = [] 45 | for r in inspect_sa(model).relationships: 46 | pass 47 | if relation_name is None: 48 | if r.direction in directions: 49 | res.append(r) 50 | elif relation_name.lower() == r.key.lower(): 51 | res.append(r) 52 | 53 | return res 54 | 55 | 56 | def sa_column_filters(model, mode='full'): 57 | list = sa_column_filterable_list(model) 58 | if mode == 'full': 59 | list += sa_relationships_keys(model) 60 | return list 61 | 62 | 63 | def sa_relationship_keys(relationship): 64 | return [rel.key for rel in relationship] 65 | 66 | 67 | def sa_relationships_keys(model, directions=[MANYTOONE]): 68 | return [rel.key for rel in sa_relationships(model, directions=directions)] 69 | 70 | 71 | def sa_hybrids(model): 72 | return [item for item in inspect_sa(model).all_orm_descriptors if type(item) == hybrid_property] 73 | 74 | 75 | def sa_hybrid_keys(model): 76 | return [item.__name__ for item in sa_hybrids(model)] 77 | 78 | 79 | def sa_column_type(model, column_name): 80 | sa_model = inspect_sa(model) 81 | col = sa_model.c.get(column_name) 82 | col_type = None 83 | if col is not None: 84 | col_type = type(col.type).__name__.lower() 85 | return col_type 86 | 87 | 88 | def sa_column_description(model_column): 89 | desc = "" 90 | try: 91 | desc = model_column.doc 92 | except: 93 | pass 94 | return desc 95 | 96 | 97 | def sa_columns(model): 98 | mapper = inspect_sa(model) 99 | return mapper.attrs 100 | 101 | 102 | def sa_column_keys(model): 103 | return [item.key for item in sa_columns(model)] 104 | 105 | def sa_type_keys(model, lowertype): 106 | keys = [] 107 | for c in sa_columns(model): 108 | typestr = sa_column_type(model, c.key) 109 | if typestr == lowertype: 110 | keys.append(c.key) 111 | return keys 112 | 113 | def sa_column_descriptions(model): 114 | result = {} 115 | for prop in sa_columns(model): 116 | desc = sa_column_description(prop) 117 | if desc: 118 | result[prop.key] = desc 119 | return result 120 | 121 | 122 | def sa_column_filterable_list(model): 123 | return [str(c.name) 124 | for c in model.__table__.columns 125 | if type(c.type).__name__.lower() not in ('guid', 'largebinary')] 126 | 127 | 128 | def sa_column_searchable_list(model): 129 | x = model.__table__.columns 130 | return [str(c.name) 131 | for c in model.__table__.columns 132 | if type(c.type).__name__ == 'String'] 133 | 134 | 135 | def sa_relationships4key(model, key, directions=[MANYTOONE]): 136 | res = [] 137 | for r in sa_relationships(model): 138 | kp = sa_relationship_key_pairs(r) 139 | if key in kp["local"] and r.direction in directions: 140 | res.append(r) 141 | return res 142 | 143 | def gen_href_formatter(model, relationship_names=None, ahref_fmt="%s"): 144 | from flask import url_for, Markup 145 | def _href_formatter(view, context, model, name): 146 | rels = sa_relationships(type(model), name) 147 | res = "" 148 | if rels: 149 | # Get first relationship if found more then one - 150 | r = rels[0] 151 | 152 | # Generate url path 153 | view_url = sa_view_url(r.mapper.class_) 154 | 155 | # Populate url args 156 | url_args = [] 157 | i = 0 158 | # Find mapping between local and remote keys 159 | r_kp = sa_relationship_key_pairs(r) 160 | for rmt in r_kp['remote']: 161 | url_args.append(getattr(model, r_kp['local'][i])) 162 | i += 1 163 | url_args_str = ",".join(str(e) for e in url_args) 164 | 165 | # Ignore if instance of referenced model view does not exists 166 | show_attr = getattr(model, name) 167 | 168 | # Python 3 compatibility 169 | try: 170 | show_value = show_attr.__unicode__() 171 | except: 172 | show_value = show_attr.__str__() 173 | 174 | try: 175 | url = url_for(view_url, id=url_args_str) 176 | if show_attr: 177 | res = Markup( 178 | ahref_fmt % ( 179 | url, 180 | show_value 181 | ) 182 | ) 183 | pass 184 | except: 185 | res = show_value 186 | return res 187 | 188 | # If relationship_names == None, generate for all relationships 189 | res = {} 190 | if relationship_names == None: 191 | for r in sa_relationships(model): 192 | res[r.key] = _href_formatter 193 | else: 194 | for r in relationship_names: 195 | for r in sa_relationships(model, relation_name=r): 196 | res[r.key] = _href_formatter 197 | return res 198 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | Flask-Admin 2 | Flask-SQLAlchemy 3 | Flask-Security 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Fix for older setuptools 2 | import re 3 | import os 4 | 5 | from setuptools import setup, find_packages 6 | 7 | 8 | def fpath(name): 9 | return os.path.join(os.path.dirname(__file__), name) 10 | 11 | 12 | def read(fname): 13 | return open(fpath(fname)).read() 14 | 15 | 16 | def desc(): 17 | info = read('README.rst') 18 | try: 19 | return info + '\n\n' + read('doc/changelog.rst') 20 | except IOError: 21 | return info 22 | 23 | 24 | # grep flask_admin/__init__.py since python 3.x cannot import it before using 2to3 25 | file_text = read(fpath('flask_xadmin/__init__.py')) 26 | 27 | 28 | def grep(attrname): 29 | pattern = r"{0}\W*=\W*'([^']+)'".format(attrname) 30 | strval, = re.findall(pattern, file_text) 31 | return strval 32 | 33 | 34 | setup( 35 | name='Flask-xAdmin', 36 | version=grep('__version__'), 37 | url='https://github.com/hexo-/flask-xadmin', 38 | license='MIT', 39 | author=grep('__author__'), 40 | author_email=grep('__email__'), 41 | description='eXtended Flask-Admin', 42 | long_description=desc(), 43 | packages=find_packages(), 44 | include_package_data=True, 45 | zip_safe=False, 46 | platforms='any', 47 | install_requires=[ 48 | 'Flask-Admin', 49 | 'Flask-SQLAlchemy', 50 | 'Flask-Security' 51 | ], 52 | tests_require=[ 53 | 'nose>=1.0', 54 | 'Flask-Admin', 55 | 'Flask-SQLAlchemy', 56 | 'Flask-Security', 57 | ], 58 | classifiers=[ 59 | 'Development Status :: 4 - Beta', 60 | 'Environment :: Web Environment', 61 | 'Intended Audience :: Developers', 62 | 'License :: OSI Approved :: MIT License', 63 | 'Operating System :: OS Independent', 64 | 'Programming Language :: Python', 65 | 'Topic :: Software Development :: Libraries :: Python Modules', 66 | 'Programming Language :: Python :: 2.7', 67 | 'Programming Language :: Python :: 2.6', 68 | 'Programming Language :: Python :: 3.3', 69 | 'Programming Language :: Python :: 3.4', 70 | 'Programming Language :: Python :: 3.5', 71 | ], 72 | test_suite='nose.collector' 73 | ) 74 | --------------------------------------------------------------------------------