Added Flask-Demo and Flask AppBuilder tutorial
This commit is contained in:
17
Flask-Demo/app.py
Normal file
17
Flask-Demo/app.py
Normal 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
51
Flask-Demo/world.py
Normal 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)
|
||||
@@ -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
30
fab/README.md
Normal 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
17
fab/app.py
Normal 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
23
fab/config.py
Normal 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
275
fab/models.py
Normal 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
54
fab/mvstubs.py
Normal 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
79
fab/views.py
Normal 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",
|
||||
)
|
||||
Reference in New Issue
Block a user