Flask-Admin

This is just practicing and learning together. It’s not easy to come across many tutorials for this so I’ve put a whole lot of code here (its not worthy of github). There is lots of clever things in this code that copilot helped find.

For this code to work you need a .env file:

SECRET_KEY=xxxx
FLASK_DEBUG=1
FLASK_ENV=development
SECURITY_PASSWORD_SALT="123456"

As for the libraries here is “requirements.txt”:

blinker==1.9.0
click==8.1.8
Flask==3.1.0
Flask-Admin==1.6.1
Flask-SQLAlchemy==3.1.1
Flask-WTF==1.2.2
greenlet==3.2.0
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
mypy-extensions==1.0.0
packaging==24.2
pathspec==0.12.1
platformdirs==4.3.7
python-dotenv==1.1.0
SQLAlchemy==2.0.40
typing_extensions==4.13.2
Werkzeug==3.1.3
WTForms==3.2.1

Here is the app.py file, so run with “flask run”:


from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin, AdminIndexView, expose, BaseView
from flask_admin.contrib.sqla import ModelView
from flask_admin.form import SecureForm
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, BooleanField, PasswordField
from wtforms.validators import DataRequired, Email, InputRequired, Optional
from dotenv import load_dotenv
import os
from flask_migrate import Migrate
from werkzeug.security import generate_password_hash
from wtforms_sqlalchemy.fields import QuerySelectField
from sqlalchemy.orm import joinedload
from sqlalchemy import func
from flask_login import UserMixin
#import uuid

load_dotenv()
from flask import redirect, url_for

app = Flask(__name__)
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///example.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECURITY_PASSWORD_SALT"] = (
    os.getenv("SECURITY_PASSWORD_SALT") or "random_salt_value"
)

db = SQLAlchemy(app)
migrate = Migrate(app, db)
admin = Admin(app, name="My Admin", template_mode="bootstrap4")

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(100), unique=True, nullable=False)
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(100), nullable=False)
    mobile = db.Column(db.String(100), nullable=True)
    password = db.Column(db.String(200), nullable=False)
    active = db.Column(db.Boolean, nullable=False, default=True)
    #fs_uniquifier = db.Column( db.String(64), unique=True, nullable=False, default=lambda: str(uuid.uuid4()))
    posts = db.relationship("Post", back_populates="user")

class CustomUserForm(FlaskForm, SecureForm):
    username = StringField("Username", validators=[DataRequired()])
    name = StringField("Full Name", validators=[DataRequired()])
    email = StringField("Email", validators=[DataRequired()])
    mobile = StringField("Mobile :")
    password = PasswordField("New Password:", validators=[Optional()])
    #active = BooleanField("Active:", validators=[DataRequired()])

class UserAdmin(ModelView):
    form = CustomUserForm
    column_exclude_list = ["password"]
    column_list = ["username", "name", "post_count"]
    can_create = True
    can_edit = True
    can_delete = True
    column_searchable_list = ["username", "name", "email"]
    column_filters = ["name", "email"]
    @property
    def post_count(self):
        return len(self.posts)
    column_formatters = {
        "post_count": lambda view, context, model, name: len(model.posts)
    }
    def on_model_change(self, form, model, is_created):
        if is_created and not form.password.data:
            raise ValueError("Password cannot be empty for new users")
        if form.password.data:
            model.password = generate_password_hash(form.password.data)

admin.add_view(UserAdmin(User, db.session, name="User Management"))


class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100))
    body = db.Column(db.Text)
    user_id = db.Column(db.ForeignKey("user.id"), nullable=False)
    user = db.relationship("User", back_populates="posts")

class PostView(ModelView):
    can_delete = True
    column_list = ["user_id", "title", "body"]
    form_columns = ["title", "body", "user_id"]
    column_searchable_list = ["title", "body", "user.username"]
    form_extra_fields = {
        "user_id": QuerySelectField(
            query_factory=lambda: User.query.all(),
            get_label="username",
            allow_blank=True,
            label='Username'
        )
    }
    column_formatters = {
        "user_id": lambda view, context, model, name: (
            model.user.username if model.user else ""
        ),
        "body": lambda view, context, model, name: (
            model.body[:50]+ "..." if len(model.body) > 50 else model.body
        ),
        "title": lambda view, context, model, name: (
            model.title[:50]+ "..." if len(model.title) > 50 else model.title
        )
    }
    column_labels = { 
        'user_id': 'Username',
        User.username: 'Username'
    }
    form_args = { 'user_id': { 'label': 'Username' } }

    def on_model_change(self, form, model, is_created):
        model.user_id = form.user_id.data.id

    def on_form_prefill(self, form, id):
        post = self.get_one(id)
        if post and post.user:
            form.user_id.data = post.user
    def get_query(self):
        return self.session.query(self.model).options(joinedload(Post.user))
    def get_count_query(self):
        return self.session.query(func.count('*')).select_from(self.model)
    def search_placeholder(self):
        return "Search by title, body, or username"
admin.add_view(PostView(Post, db.session))

    
# Initialise database
with app.app_context():
    db.create_all()


@app.route("/")
def home():
    return render_template("index.html")


if __name__ == "__main__":
    app.run(debug=True)