
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)