Setup
In this third part about building a simple flower catalog with Flask, I
go over my use of
Flask-Admin to provide
a frontend interface to the database. To include the Flask-Admin
extension, you do the typical thing with your app's __init__.py file:
# import it
from flask_admin import Admin
# instantiate it
admin = Admin()
# initialize it as part of the app in the app factor function
def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
# ...
# Setup admin app context and ModelViews
from flower_store.models import Flower
from flower_store.admin_views import MyAdminIndexView, FlowerView
admin.init_app(app, index_view=MyAdminIndexView())
admin.add_view(FlowerView(Flower, db.session))
Flask-Admin integrates with existing database models. In
admin_views.py, I define versions of both the AdminIndexeView class
ModelView class. We see that the latter is passed both the Flower
model and the database session. At first, I followed the basic example
provided by the documentation. One little addition I made has to do with
how I set up users for my app:
class FlowerView(ModelView):
# form_base_class = SecureForm()
create_modal = True
edit_modal = True
def is_accessible(self):
return current_user.is_authenticated and current_user.is_admin
def inaccessible_callback(self, name):
return redirect(url_for("login"))
class MyAdminIndexView(AdminIndexView):
def is_accessible(self):
return current_user.is_authenticated and current_user.is_admin
Since I may want to implement accounts as a feature in the future, I
decided to setup a basic Users table with an is_admin attribute that
has a SQL type of Boolean. For now, I manage this entirely from the
command line.
Users and Logging In
Since adding users to a database and using Flask-Login seem to be a standard pair of elements in Flask tutorials, I won't go into much detail. I mostly repurposed code from Grinberg's tutorial. At the moment, since I don't want user accounts as a feature, none of this is displayed through the website.
As mentioned before, my User class for Flask-SQLAlchemy has the
following attribute:
is_admin = db.Column(db.Boolean, default=False)
And I have a function I expose to my flask shell environment that creates such a user for testing purposes:
# See Part 1 for all imports.
from dotenv import load_dotenv
from flower_store.models import User
def register(app):
# ...
@app.cli.command("setup-admin")
def setup_admin():
"""Creates an admin user account."""
# Ensure `user` table exists
if not db.inspect(db.engine).has_table("user"):
print("User table does not exist. Run `flask db upgrade`")
return
load_dotenv()
admin = os.environ.get("ADMIN")
admin_pwd = os.environ.get("ADMIN_PWD")
if not admin_pwd:
print("There is no admin user password in the local environment.")
return
test_admin = User(
username=admin,
email="davidrambo@mailfence.com",
is_admin=True,
)
# Check for existing "admin" user and delete if it exists.
user = User.query.filter_by(username=test_admin.username).first()
if user:
db.session.delete(user)
test_admin.set_password(admin_pwd)
db.session.add(test_admin)
db.session.commit()
As with the command that populates the flower table, this is run
via flask setup-admin.
Both logging in and out have to be done by entering the appropriate routes ("/login" and "/logout"). Both routes' view functions are in auth/routes.py. The one for logging in is basically the same as what Grinberg provides, except it redirects to the admin view if the user logging in is an admin:
if user.is_admin:
return redirect("admin")
It's a nice convenience.
Flask-Admin's Template
Once you've created a user with admin privileges and logged in as that user, Flask-Admin's frontend can be accessed by going to the "/admin" route. To customize it through Flask's templating system, Flask-Admin exposes its templates behind the scenes. Here's a minimal change I made:
{% extends 'admin/master.html' %} {% block body %}
<p>To log out, add "/logout" to the URL.</p>
{% endblock body %}
Uploading images
To upload images, I added an additional field to my FlowerView class:
form_extra_fields = {
"image_file": ImageUploadField(
"Image",
base_path=IMAGE_PATH,
namegen=image_rename,
# thumbnail_size=(125, 125, True),
max_size=(500, 500, True),
)
}
I adapted this from an
example
provided by Flask-Admin and the
documentation
for ImageUploadField. Initially, I tried to use Flask-Admin's
form_overrides variable, which didn't work. My thinking was that the
form, by default, is a text input box, since Flower.image_file takes a
string. This code snippet I arrived at works perfectly. I'll break it
down:
-
"image_file" names the field in the
Flowermodel to be manipulated. -
ImageUploadFieldleviesPillow(a fork ofPIL, the Python Image Library) to perform image manipulation. -
"Image" names the field in the Admin view.
-
base_path is where you want the file to be saved.
IMAGE_PATHis defined at the module level to get the directory path:IMAGE_PATH = os.path.join(os.path.dirname(__file__), "static/flower_imgs") -
namegen defines how the file name is to be generated. I wrote a little function called
image_renamein utils.py. It takes a tip from Corey Schafer's tutorial and adds some hex characters to the name provided by the user. It also ensures that the length is within the limit imposed by theFlowermodel'simage_filefield (30).
Automating asset deletion
The last thing I wanted to implement is a SQLAlchemy hook to delete images associated with flowers in the event of their deletion. Back in the app factory, I added the following code beneath the admin setup:
from sqlalchemy.event import listens_for
from flower_store.admin_views import IMAGE_PATH
@listens_for(Flower, "after_delete")
def del_image(mapper, connection, target):
if target.image_file:
# Delete image
try:
os.remove(os.path.join(IMAGE_PATH, target.image_file))
except OSError:
pass
Nice!