Added Flask-Demo and Flask AppBuilder tutorial

This commit is contained in:
2025-10-31 22:19:57 -04:00
parent 897ee0714f
commit 895dbbb811
9 changed files with 547 additions and 0 deletions

17
Flask-Demo/app.py Normal file
View File

@@ -0,0 +1,17 @@
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, world!</p>"
@app.route("/haha")
def second_example():
return "<p>Go dukes!</p>"
if __name__ == "__main__":
app.run(debug=True)

51
Flask-Demo/world.py Normal file
View File

@@ -0,0 +1,51 @@
"""Example web app that connects to PostgreSQL."""
from io import StringIO
import psycopg
from flask import Flask
app = Flask(__name__)
con = psycopg.connect(
host="data.cs.jmu.edu", user="demo", password="demo", dbname="world"
)
cur = con.cursor()
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
@app.route("/country/<code>")
def country_info(code: str):
_ = cur.execute(
"SELECT name, continent, region, population FROM country WHERE code = %s",
(code.upper(),),
)
row = cur.fetchone()
if not row:
return "<p>Country not found</p>"
name, cont, reg, pop = row
out = StringIO()
_ = out.write(
f"<p><strong>{name}</strong> is in <strong>{cont}</strong> in the <strong>{reg}</strong> region.</p>"
)
_ = out.write(
f"<p><strong>{name}</strong>'s population <strong>{pop:,d}</strong>.</p>"
)
return out.getvalue()
@app.route("/codes")
def multi_country():
_ = cur.execute("SELECT name, code population FROM country")
return f"<ul>{"".join(
f'<li><a href="/country/{code}">{name} {code}</a></li>'
for (name, code) in cur.fetchall()
)}</ul>"
if __name__ == "__main__":
app.run(debug=True)

View File

@@ -18,6 +18,7 @@
python-dotenv==1.1.1
psycopg==3.2.11
psycopg-binary==3.2.11
Flask-AppBuilder==5.0.1
'';
};
};

30
fab/README.md Normal file
View File

@@ -0,0 +1,30 @@
# webapp
This application is built using [Flask-AppBuilder](https://github.com/dpgaspar/Flask-AppBuilder) version 5.
## Files
* [app.py](app.py) -- Initialize the Flask application, database session, and appbuilder object.
* [config.py](config.py) -- Configure application settings such as the database URI, secret key, etc.
* [models.py](models.py) -- SQLAlchemy model classes (database tables) generated by `sqlacodegen`.
* [views.py](views.py) -- Flask-AppBuilder views that provide CRUD web interfaces for the models.
## Setup
Generate a random SECRET_KEY for use in config.py:
``` sh
python -c 'import secrets; print(secrets.token_hex())'
```
Create an admin user before running for the first time:
``` sh
flask fab create-admin
```
## Running
Start a local dev server with debugging and reloading:
``` sh
flask run --debug
```

17
fab/app.py Normal file
View File

@@ -0,0 +1,17 @@
"""Initialize the Flask application, database session, and appbuilder object."""
from flask import Flask
from flask_appbuilder import AppBuilder
from flask_appbuilder.utils.legacy import get_sqla_class
app = Flask(__name__)
app.config.from_object("config")
db = get_sqla_class()()
appbuilder = AppBuilder()
with app.app_context():
db.init_app(app)
appbuilder.init_app(app, db.session) # type: ignore
import views # noqa

23
fab/config.py Normal file
View File

@@ -0,0 +1,23 @@
"""Configure application settings such as the database URI, secret key, etc."""
import socket
# Determine whether connecting from on/off campus
try:
socket.gethostbyname("data.cs.jmu.edu")
HOST = "data.cs.jmu.edu"
except socket.gaierror:
HOST = "localhost"
# See https://flask-appbuilder.readthedocs.io/en/latest/config.html
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg://tamassno:113880616@localhost/sec2"
SECRET_KEY = "c46096d07cfae2ef311332eb98bc8336acaacf162427122b285ceb388a1cade1"
AUTH_TYPE = 1 # Database style (user/password)
APP_NAME = "Conference Review System"
APP_THEME = "readable.css"

275
fab/models.py Normal file
View File

@@ -0,0 +1,275 @@
import datetime
from typing import Optional, override
from flask_appbuilder import Model
from sqlalchemy import (
Column,
Date,
DateTime,
Enum,
ForeignKeyConstraint,
Identity,
Integer,
PrimaryKeyConstraint,
Table,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
class Affiliation(Model):
__tablename__ = "affiliation"
__table_args__ = (PrimaryKeyConstraint("org_name", name="affiliation_pkey"),)
org_name: Mapped[str] = mapped_column(Text, primary_key=True)
website: Mapped[str] = mapped_column(Text, nullable=False)
country: Mapped[str] = mapped_column(Text, nullable=False)
person_affiliation: Mapped[list["PersonAffiliation"]] = relationship(
"PersonAffiliation", back_populates="affiliation"
)
class Conference(Model):
__tablename__ = "conference"
__table_args__ = (PrimaryKeyConstraint("year", name="conference_pkey"),)
year: Mapped[int] = mapped_column(Integer, primary_key=True)
location: Mapped[str] = mapped_column(Text, nullable=False)
papers: Mapped[list["Paper"]] = relationship("Paper", back_populates="conference")
@override
def __str__(self) -> str:
return f"{self.location} {self.year}"
class Person(Model):
__tablename__ = "person"
__table_args__ = (PrimaryKeyConstraint("email", name="person_pkey"),)
email: Mapped[str] = mapped_column(Text, primary_key=True)
first_name: Mapped[str] = mapped_column(Text, nullable=False)
last_name: Mapped[str] = mapped_column(Text, nullable=False)
paper: Mapped[list["Paper"]] = relationship("Paper", back_populates="person")
person_affiliation: Mapped[list["PersonAffiliation"]] = relationship(
"PersonAffiliation", back_populates="person"
)
paper_author: Mapped[list["PaperAuthor"]] = relationship(
"PaperAuthor", back_populates="person"
)
class Topic(Model):
__tablename__ = "topic"
__table_args__ = (PrimaryKeyConstraint("topic_id", name="topic_pkey"),)
topic_id: Mapped[int] = mapped_column(
Integer,
Identity(
start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1
),
primary_key=True,
)
topic_name: Mapped[str] = mapped_column(Text, nullable=False)
papers: Mapped[list["Paper"]] = relationship(
"Paper", secondary="paper_topic", back_populates='topics'
)
reviewer: Mapped[list["Reviewer"]] = relationship(
"Reviewer", secondary="expertise", back_populates="topic"
)
@override
def __str__(self) -> str:
return f"{self.topic_name}"
class Paper(Model):
__tablename__ = "paper"
__table_args__ = (
ForeignKeyConstraint(
["contact_email"], ["person.email"], name="paper_contact_email_fkey"
),
ForeignKeyConstraint(["year"], ["conference.year"], name="paper_year_fkey"),
PrimaryKeyConstraint("paper_id", name="paper_pkey"),
)
paper_id: Mapped[int] = mapped_column(
Integer,
Identity(
start=1, increment=1, minvalue=1, maxvalue=2147483647, cycle=False, cache=1
),
primary_key=True,
)
title: Mapped[str] = mapped_column(Text, nullable=False)
abstract: Mapped[str] = mapped_column(Text, nullable=False)
filename: Mapped[str] = mapped_column(Text, nullable=False)
contact_email: Mapped[str] = mapped_column(Text, nullable=False)
year: Mapped[int] = mapped_column(Integer, nullable=False)
person: Mapped["Person"] = relationship("Person", back_populates="paper")
conference: Mapped["Conference"] = relationship(
"Conference", back_populates='papers'
)
topics: Mapped[list["Topic"]] = relationship(
"Topic", secondary="paper_topic", back_populates='papers'
)
history: Mapped[list["History"]] = relationship("History", back_populates="paper")
paper_author: Mapped[list["PaperAuthor"]] = relationship(
"PaperAuthor", back_populates="paper"
)
review: Mapped[list["Review"]] = relationship("Review", back_populates="paper")
@override
def __str__(self) -> str:
return f"Paper #{self.paper_id}: {self.title}"
class PersonAffiliation(Model):
__tablename__ = "person_affiliation"
__table_args__ = (
ForeignKeyConstraint(
["email"], ["person.email"], name="person_affiliation_email_fkey"
),
ForeignKeyConstraint(
["org_name"],
["affiliation.org_name"],
name="person_affiliation_org_name_fkey",
),
PrimaryKeyConstraint("email", "org_name", name="person_affiliation_pkey"),
)
email: Mapped[str] = mapped_column(Text, primary_key=True)
org_name: Mapped[str] = mapped_column(Text, primary_key=True)
from_date: Mapped[Optional[datetime.date]] = mapped_column(Date)
to_date: Mapped[Optional[datetime.date]] = mapped_column(Date)
person: Mapped["Person"] = relationship(
"Person", back_populates="person_affiliation"
)
affiliation: Mapped["Affiliation"] = relationship(
"Affiliation", back_populates="person_affiliation"
)
class Reviewer(Person):
__tablename__ = "reviewer"
__table_args__ = (
ForeignKeyConstraint(["email"], ["person.email"], name="reviewer_email_fkey"),
PrimaryKeyConstraint("email", name="reviewer_pkey"),
)
email: Mapped[str] = mapped_column(Text, primary_key=True)
phone: Mapped[Optional[str]] = mapped_column(Text)
topic: Mapped[list["Topic"]] = relationship(
"Topic", secondary="expertise", back_populates="reviewer"
)
review: Mapped[list["Review"]] = relationship("Review", back_populates="reviewer")
t_expertise = Table(
"expertise",
Model.metadata,
Column("email", Text, primary_key=True),
Column("topic_id", Integer, primary_key=True),
ForeignKeyConstraint(["email"], ["reviewer.email"], name="expertise_email_fkey"),
ForeignKeyConstraint(
["topic_id"], ["topic.topic_id"], name="expertise_topic_id_fkey"
),
PrimaryKeyConstraint("email", "topic_id", name="expertise_pkey"),
)
class History(Model):
__tablename__ = "history"
__table_args__ = (
ForeignKeyConstraint(
["paper_id"], ["paper.paper_id"], name="history_paper_id_fkey"
),
PrimaryKeyConstraint("paper_id", "timestamp", name="history_pkey"),
)
paper_id: Mapped[int] = mapped_column(Integer, primary_key=True)
timestamp: Mapped[datetime.datetime] = mapped_column(DateTime, primary_key=True)
paper_status: Mapped[str] = mapped_column(
Enum(
"SUBMITTED",
"UNDER_REVIEW",
"REVISION",
"RESUBMITTED",
"REJECTED",
"ACCEPTED",
"PUBLISHED",
name="status",
),
nullable=False,
)
notes: Mapped[Optional[str]] = mapped_column(Text)
paper: Mapped["Paper"] = relationship("Paper", back_populates="history")
@override
def __str__(self) -> str:
return f"History for #{self.paper_id} at {self.timestamp}"
class PaperAuthor(Model):
__tablename__ = "paper_author"
__table_args__ = (
ForeignKeyConstraint(
["email"], ["person.email"], name="paper_author_email_fkey"
),
ForeignKeyConstraint(
["paper_id"], ["paper.paper_id"], name="paper_author_paper_id_fkey"
),
PrimaryKeyConstraint("paper_id", "email", name="paper_author_pkey"),
)
paper_id: Mapped[int] = mapped_column(Integer, primary_key=True)
email: Mapped[str] = mapped_column(Text, primary_key=True)
rank: Mapped[int] = mapped_column(Integer, nullable=False, comment="author order")
person: Mapped["Person"] = relationship("Person", back_populates="paper_author")
paper: Mapped["Paper"] = relationship("Paper", back_populates="paper_author")
t_paper_topic = Table(
"paper_topic",
Model.metadata,
Column("paper_id", Integer, primary_key=True),
Column("topic_id", Integer, primary_key=True),
ForeignKeyConstraint(
["paper_id"], ["paper.paper_id"], name="paper_topic_paper_id_fkey"
),
ForeignKeyConstraint(
["topic_id"], ["topic.topic_id"], name="paper_topic_topic_id_fkey"
),
PrimaryKeyConstraint("paper_id", "topic_id", name="paper_topic_pkey"),
)
class Review(Model):
__tablename__ = "review"
__table_args__ = (
ForeignKeyConstraint(["email"], ["reviewer.email"], name="review_email_fkey"),
ForeignKeyConstraint(
["paper_id"], ["paper.paper_id"], name="review_paper_id_fkey"
),
PrimaryKeyConstraint("paper_id", "email", name="review_pkey"),
{"comment": "Scores range from 1 to 5"},
)
paper_id: Mapped[int] = mapped_column(Integer, primary_key=True)
email: Mapped[str] = mapped_column(Text, primary_key=True)
merit: Mapped[int] = mapped_column(Integer, nullable=False)
relevance: Mapped[int] = mapped_column(Integer, nullable=False)
readability: Mapped[int] = mapped_column(Integer, nullable=False)
originality: Mapped[int] = mapped_column(Integer, nullable=False)
author_comments: Mapped[str] = mapped_column(Text, nullable=False)
committee_comments: Mapped[Optional[str]] = mapped_column(Text)
reviewer: Mapped["Reviewer"] = relationship("Reviewer", back_populates="review")
paper: Mapped["Paper"] = relationship("Paper", back_populates="review")

54
fab/mvstubs.py Normal file
View File

@@ -0,0 +1,54 @@
"""Generate a basic ModelView class for each table in the database."""
import socket
import psycopg
# Determine whether connecting from on/off campus
try:
socket.gethostbyname("data.cs.jmu.edu")
HOST = "data.cs.jmu.edu"
except socket.gaierror:
HOST = "localhost"
# Get all tables and their columns
with psycopg.connect(
host=HOST, user="tamassno", dbname="sec2", password="113880616"
) as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'tamassno'
AND table_name NOT LIKE 'ab_%'
ORDER BY table_name, ordinal_position;
"""
)
schema = cur.fetchall()
# Build a dictionary of results by table
tables: dict[str, list[str]] = {}
for table_name, column_name in schema:
tables.setdefault(table_name, []).append(column_name)
# Generate Flask-AppBuilder ModelView classes
for table, columns in tables.items():
name = table.capitalize()
print(f"class {name}(ModelView):")
print(f" datamodel = SQLAInterface(models.{name})")
print(f" route_base = '/{table}'")
print(f" list_title = '{name}s'")
print(f" list_columns = {columns}")
print()
# Generate code to add each view to the app
for table in tables:
name = table.capitalize()
print("appbuilder.add_view(")
print(f" {name},")
print(f' "{name}s",')
print(' icon="fa-database",')
print(' category="Admin",')
print(")")
print()

79
fab/views.py Normal file
View File

@@ -0,0 +1,79 @@
from flask_appbuilder import ModelView
from flask_appbuilder.models.sqla.interface import SQLAInterface
import models
from app import appbuilder
class History(ModelView):
datamodel = SQLAInterface(models.History)
route_base = "/history"
list_title = "History"
list_columns = ["paper_id", "timestamp", "paper_status", "notes"]
class Topic(ModelView):
datamodel = SQLAInterface(models.Topic)
route_base = "/topic"
list_title = "Topics"
list_columns = ["topic_id", "topic_name"]
class Paper(ModelView):
datamodel = SQLAInterface(models.Paper)
route_base = "/paper"
list_title = "Papers"
list_columns = [
"paper_id",
"title",
"abstract",
"filename",
"contact_email",
"year",
]
add_exclude_columns = edit_exclude_columns = show_exclude_columns = [
"topics",
"history",
]
related_views = [Topic, History]
class Conference(ModelView):
datamodel = SQLAInterface(models.Conference)
route_base = "/conference"
list_title = "Conferences"
list_columns = ["year", "location"]
add_exclude_columns = edit_exclude_columns = show_exclude_columns = ["papers"]
related_views = [Paper]
appbuilder.add_view(
Conference,
"Conferences",
icon="fa-database",
category="Admin",
)
appbuilder.add_view(
History,
"History",
icon="fa-database",
category="Admin",
)
appbuilder.add_view(
Paper,
"Papers",
icon="fa-database",
category="Admin",
)
appbuilder.add_view(
Topic,
"Topics",
icon="fa-database",
category="Admin",
)