├── .gitattributes ├── .gitignore ├── README.md ├── flask-admin-modal.gif ├── requirements.txt ├── run.py └── templates └── admin └── model └── custom_list.html /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.py linguist-vendored=false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #PyCharm 2 | .idea/ 3 | 4 | # SQLite DB 5 | *.sqlite 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask instance folder 63 | instance/ 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # IPython Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # ========================= 97 | # Operating System Files 98 | # ========================= 99 | 100 | # OSX 101 | # ========================= 102 | 103 | .DS_Store 104 | .AppleDouble 105 | .LSOverride 106 | 107 | # Thumbnails 108 | ._* 109 | 110 | # Files that might appear in the root of a volume 111 | .DocumentRevisions-V100 112 | .fseventsd 113 | .Spotlight-V100 114 | .TemporaryItems 115 | .Trashes 116 | .VolumeIcon.icns 117 | 118 | # Directories potentially created on remote AFP share 119 | .AppleDB 120 | .AppleDesktop 121 | Network Trash Folder 122 | Temporary Items 123 | .apdisk 124 | 125 | # Windows 126 | # ========================= 127 | 128 | # Windows image file caches 129 | Thumbs.db 130 | ehthumbs.db 131 | 132 | # Folder config file 133 | Desktop.ini 134 | 135 | # Recycle Bin used on file shares 136 | $RECYCLE.BIN/ 137 | 138 | # Windows Installer files 139 | *.cab 140 | *.msi 141 | *.msm 142 | *.msp 143 | 144 | # Windows shortcuts 145 | *.lnk 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask-admin-modal 2 | 3 | Example, in response to this [StackOverflow](https://stackoverflow.com/q/47593195/2800058) question, 4 | of using a Bootstrap modal popup to update multiple records selected in a Flask-Admin batch action. 5 | 6 | ![Popup Demo](flask-admin-modal.gif) 7 | 8 | ## Installation 9 | 10 | ```bash 11 | git clone https://github.com/pjcunningham/flask-admin-modal.git 12 | cd flask-admin-modal 13 | 14 | # (optional, create a virtual environment) 15 | virtualenv venv && source venv/bin/activate 16 | 17 | # fetch dependencies; add '--user' if not using a virtualenv 18 | pip install -r requirements.txt 19 | 20 | # Set Flask environment variable e.g. Windows CMD 21 | set FLASK_APP=run.py 22 | 23 | # Create/Populate data 24 | flask create-database 25 | 26 | # launch app 27 | flask run 28 | ``` 29 | -------------------------------------------------------------------------------- /flask-admin-modal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pjcunningham/flask-admin-modal/28348cd443b5c7a829ba064c5308d2953b4b25d6/flask-admin-modal.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.0.4 2 | colorama==0.4.4 3 | Flask==2.0.3 4 | Flask-Admin==1.6.0 5 | Flask-SQLAlchemy==2.5.1 6 | greenlet==1.1.2 7 | itsdangerous==2.1.2 8 | Jinja2==3.1.1 9 | MarkupSafe==2.1.1 10 | SQLAlchemy==1.4.32 11 | Werkzeug==2.0.3 12 | WTForms==3.0.1 13 | 14 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import base64 3 | import random 4 | import string 5 | from flask import Flask, redirect, request, flash 6 | from flask.cli import click, with_appcontext 7 | from flask_admin.actions import action 8 | from flask_admin.helpers import get_redirect_target 9 | from flask_sqlalchemy import SQLAlchemy 10 | from flask_admin.contrib.sqla import ModelView 11 | from flask_admin import Admin, expose 12 | from wtforms import HiddenField, IntegerField, Form 13 | from wtforms.validators import InputRequired 14 | 15 | 16 | # Flask commands 17 | @click.command('create-database') 18 | @with_appcontext 19 | def create_database(): 20 | db.drop_all() 21 | db.create_all() 22 | 23 | for _ in range(0, 100): 24 | _cost = random.randrange(1, 1000) 25 | _project = Project( 26 | name=''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)), 27 | cost=_cost 28 | ) 29 | db.session.add(_project) 30 | 31 | db.session.commit() 32 | 33 | app = Flask(__name__) 34 | app.config['SECRET_KEY'] = '123456790' 35 | 36 | # Create in-memory database 37 | app.config['DATABASE_FILE'] = 'sample_db.sqlite' 38 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE'] 39 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 40 | db = SQLAlchemy(app) 41 | app.cli.add_command(create_database) 42 | 43 | 44 | # Flask views 45 | @app.route('/') 46 | def index(): 47 | return 'Click me to get to the projects!' 48 | 49 | 50 | class Project(db.Model): 51 | id = db.Column(db.Integer, primary_key=True) 52 | name = db.Column(db.String(255), nullable=False, unique=True) 53 | cost = db.Column(db.Integer(), nullable=False) 54 | 55 | def __str__(self): 56 | return unicode(self).encode('utf-8') 57 | 58 | def __unicode__(self): 59 | return "Name: {name}; Cost : {cost}".format(name=self.name, cost=self.cost) 60 | 61 | 62 | class ChangeForm(Form): 63 | ids = HiddenField() 64 | cost = IntegerField(validators=[InputRequired()]) 65 | 66 | 67 | class ProjectView(ModelView): 68 | # don't call the custom page list.html as you'll get a recursive call 69 | list_template = 'admin/model/custom_list.html' 70 | page_size = 10 71 | 72 | # omitting the third argument suppresses the confirmation alert 73 | @action('change_cost', 'Change Cost') 74 | def action_change_cost(self, ids): 75 | url = get_redirect_target() or self.get_url('.index_view') 76 | return redirect(url, code=307) 77 | 78 | @expose('/', methods=['POST']) 79 | def index(self): 80 | if request.method == 'POST': 81 | url = get_redirect_target() or self.get_url('.index_view') 82 | ids = request.form.getlist('rowid') 83 | joined_ids = ','.join(ids) 84 | change_form = ChangeForm() 85 | change_form.ids.data = joined_ids 86 | self._template_args['url'] = url 87 | self._template_args['change_form'] = change_form 88 | self._template_args['change_modal'] = True 89 | return self.index_view() 90 | 91 | @expose('/update/', methods=['POST']) 92 | def update_view(self): 93 | if request.method == 'POST': 94 | url = get_redirect_target() or self.get_url('.index_view') 95 | change_form = ChangeForm(request.form) 96 | if change_form.validate(): 97 | ids = change_form.ids.data.split(',') 98 | cost = change_form.cost.data 99 | _update_mappings = [{'id': rowid, 'cost': cost} for rowid in ids] 100 | db.session.bulk_update_mappings(Project, _update_mappings) 101 | db.session.commit() 102 | flash(f"Set cost for {len(ids)} record{'s' if len(ids) > 1 else ''} to {cost}.", category='info') 103 | return redirect(url) 104 | else: 105 | # Form didn't validate 106 | self._template_args['url'] = url 107 | self._template_args['change_form'] = change_form 108 | self._template_args['change_modal'] = True 109 | return self.index_view() 110 | 111 | 112 | admin = Admin(app, template_mode="bootstrap3") 113 | admin.add_view(ProjectView(Project, db.session)) 114 | 115 | -------------------------------------------------------------------------------- /templates/admin/model/custom_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/model/list.html' %} 2 | 3 | {% block body %} 4 | {{ super() }} 5 | 6 | 21 | {% endblock body %} 22 | 23 | {% block tail %} 24 | {{ super() }} 25 | 32 | {% endblock tail %} --------------------------------------------------------------------------------