1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
|
.. _admin-interface:
Admin Interface
===============
Many web applications ship with an "admin area", where priveleged users can
view and modify content. By introspecting your application's models, flask-peewee
can provide you with straightforward, easily-extensible forms for managing your
application content.
Here's a screen-shot of the admin dashboard:
.. image:: fp-admin.jpg
Getting started
---------------
To get started with the admin, there are just a couple steps:
1. Instantiate an :py:class:`Auth` backend for your project -- this component is responsible for providing the security for the admin area
.. code-block:: python
from flask import Flask
from flask_peewee.auth import Auth
from flask_peewee.db import Database
app = Flask(__name__)
db = Database(app)
# needed for authentication
auth = Auth(app, db)
2. Instantiate an :py:class:`Admin` object
.. code-block:: python
# continued from above...
from flask_peewee.admin import Admin
admin = Admin(app, auth)
3. Register any :py:class:`ModelAdmin` or :py:class:`AdminPanel` objects you would like to expose via the admin
.. code-block:: python
# continuing... assuming "Blog" and "Entry" models
admin.register(Blog) # register "Blog" with vanilla ModelAdmin
admin.register(Entry, EntryAdmin) # register "Entry" with a custom ModelAdmin subclass
# assume we have an "AdminPanel" called "NotePanel"
admin.register_panel('Notes', NotePanel)
4. Call :py:meth:`Admin.setup()`, which registers the admin blueprint and configures the urls
.. code-block:: python
# after all models and panels are registered, configure the urls
admin.setup()
.. note::
For a complete example, check the :ref:`example` which ships with the project.
Customizing how models are displayed
------------------------------------
We'll use the "Message" model taken from the `example app <https://github.com/coleifer/flask-peewee/tree/master/example>`_,
which looks like this:
.. code-block:: python
class Message(db.Model):
user = ForeignKeyField(User)
content = TextField()
pub_date = DateTimeField(default=datetime.datetime.now)
def __unicode__(self):
return '%s: %s' % (self.user, self.content)
If we were to simply register this model with the admin, it would look something
like this:
.. code-block:: python
admin = Admin(app, auth)
admin.register(Message)
admin.setup()
.. image:: fp-message-admin.jpg
A quick way to improve the appearance of this view is to specify which columns
to display. To start customizing how the ``Message`` model is displayed in the
admin, we'll subclass :py:class:`ModelAdmin`.
.. code-block:: python
from flask_peewee.admin import ModelAdmin
class MessageAdmin(ModelAdmin):
columns = ('user', 'content', 'pub_date',)
admin.register(Message, MessageAdmin)
admin.setup()
Now the admin shows all the columns and they can be clicked to sort the data:
.. image:: fp-message-admin-2.jpg
Suppose privacy is a big concern, and under no circumstances should a user be
able to see another user's messages -- even in the admin. This can be done by overriding
the :py:meth:`~ModelAdmin.get_query` method:
.. code-block:: python
def get_query(self):
return self.model.select().where(self.model.user == g.user)
Now a user will only be able to see and edit their own messages.
Overriding Admin Templates
^^^^^^^^^^^^^^^^^^^^^^^^^^
Use the :py:meth:`ModelAdmin.get_template_overrides` method to override templates
for an individual ``Model``:
.. code-block:: python
class MessageAdmin(ModelAdmin):
columns = ('user', 'content', 'pub_date',)
def get_template_overrides(self):
# override the edit template with a custom one
return {'edit': 'messages/admin/edit.html'}
admin.register(Message, MessageAdmin)
This instructs the admin to use a custom template for the edit page in the Message
admin. That template is stored in the application's templates. It might look
something like this:
.. code-block:: jinja
{% extends "admin/models/edit.html" %} {# override the default edit template #}
{# override any blocks here #}
There are five templates that can be overridden:
* index
* add
* edit
* delete
* export
Nicer display for Foreign Key fields
------------------------------------
If you have a model that foreign keys to another, by default the related model
instances are displayed in a <select> input.
This can be problematic if you have a large list of models to search (causes slow
load time, hurts the database). To mitigate this pain, foreign key lookups can
be done using a paginated widget that supports type-ahead searching.
Setting this up is very easy:
.. code-block:: python
class MessageAdmin(ModelAdmin):
columns = ('user', 'content', 'pub_date',)
foreign_key_lookups = {'user': 'username'}
When flask-peewee sees the ``foreign_key_lookups`` it will use the special modal
window to select instances. This applies to both filters and model forms:
Filters
^^^^^^^
1. Select a user by clicking the "Select..." button
.. image:: fp-admin-filter.png
2. A modal window with a paginated list and typeahead search appers:
.. image:: fp-admin-modal.png
3. The button now indicates the selected user, clicking again will reload the dialog:
.. image:: fp-admin-btn.png
Admin ModelForms
^^^^^^^^^^^^^^^^
The interface is the same as with the filters, except the foreign key field is
replaced by a simple button:
.. image:: fp-admin-btn-form.png
Creating admin panels
---------------------
:py:class:`AdminPanel` classes provide a way of extending the admin dashboard with arbitrary functionality.
These are displayed as "panels" on the admin dashboard with a customizable
template. They may additionally, however, define any views and urls. These
views will automatically be protected by the same authentication used throughout
the admin area.
Some example use-cases for AdminPanels might be:
* Display some at-a-glance functionality in the dashboard, like stats on new
user signups.
* Provide a set of views that should only be visible to site administrators,
for example a mailing-list app.
* Control global site settings, turn on and off features, etc.
Referring to the `example app <https://github.com/coleifer/flask-peewee/tree/master/example>`_,
we'll look at a simple panel that allows administrators to leave "notes" in the admin area:
.. image:: fp-note-panel.jpg
.. image:: fp-note-panel-2.jpg
Here's what the panel class looks like:
.. code-block:: python
class NotePanel(AdminPanel):
template_name = 'admin/notes.html'
def get_urls(self):
return (
('/create/', self.create),
)
def create(self):
if request.method == 'POST':
if request.form.get('message'):
Note.create(
user=auth.get_logged_in_user(),
message=request.form['message'],
)
next = request.form.get('next') or self.dashboard_url()
return redirect(next)
def get_context(self):
return {
'note_list': Note.select().order_by(Note.created_date.desc()).limit(3)
}
When the admin dashboard is rendered (``/admin/``), all panels are rendered using
the templates the specify. The template is rendered with the context provided
by the panel's ``get_context`` method.
And the template:
.. code-block:: python
{% extends "admin/panels/default.html" %}
{% block panel_content %}
{% for note in note_list %}
<p>{{ note.user.username }}: {{ note.message }}</p>
{% endfor %}
<form method="post" action="{{ url_for(panel.get_url_name('create')) }}">
<input type="hidden" value="{{ request.url }}" />
<p><textarea name="message"></textarea></p>
<p><button type="submit" class="btn small">Save</button></p>
</form>
{% endblock %}
A panel can provide as many urls and views as you like. These views will all be
protected by the same authentication as other parts of the admin area.
Handling File Uploads
---------------------
Flask and wtforms both provide support for handling file uploads (on the server
and generating form fields). Peewee, however, does not have a "file field" --
generally I store a path to a file on disk and thus use a ``CharField`` for
the storage.
Here's a very simple example of a "photo" model and a ``ModelAdmin`` that enables
file uploads.
.. code-block:: models.py
# models.py
import datetime
import os
from flask import Markup
from peewee import *
from werkzeug import secure_filename
from app import app, db
class Photo(db.Model):
image = CharField()
def __unicode__(self):
return self.image
def save_image(self, file_obj):
self.image = secure_filename(file_obj.filename)
full_path = os.path.join(app.config['MEDIA_ROOT'], self.image)
file_obj.save(full_path)
self.save()
def url(self):
return os.path.join(app.config['MEDIA_URL'], self.image)
def thumb(self):
return Markup('<img src="%s" style="height: 80px;" />' % self.url())
.. code-block:: python
# admin.py
from flask import request
from flask_peewee.admin import Admin, ModelAdmin
from wtforms.fields import FileField, HiddenField
from wtforms.form import Form
from app import app, db
from auth import auth
from models import Photo
admin = Admin(app, auth)
class PhotoAdmin(ModelAdmin):
columns = ['image', 'thumb']
def get_form(self, adding=False):
class PhotoForm(Form):
image = HiddenField()
image_file = FileField(u'Image file')
return PhotoForm
def save_model(self, instance, form, adding=False):
instance = super(PhotoAdmin, self).save_model(instance, form, adding)
if 'image_file' in request.files:
file = request.files['image_file']
instance.save_image(file)
return instance
admin.register(Photo, PhotoAdmin)
|