Working with DB, admin form, WTForms, .env
This commit is contained in:
parent
66133ac46c
commit
3f665df6a7
12 changed files with 263 additions and 25 deletions
5
.env
Normal file
5
.env
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
MYSQL_HOST=localhost
|
||||||
|
MUSQL_PORT=3306
|
||||||
|
MYSQL_USER=darkcat09
|
||||||
|
MYSQL_PASSWORD=
|
||||||
|
MYSQL_DATABASE=${REPO_NAME_SNAKE}
|
6
db/schema.sql
Normal file
6
db/schema.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS ${REPO_NAME_SNAKE} (
|
||||||
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
email VARCHAR(32) NOT NULL,
|
||||||
|
name VARCHAR(32) NOT NULL,
|
||||||
|
age INTEGER NOT NULL
|
||||||
|
);
|
43
flaskapp/admin.py
Normal file
43
flaskapp/admin.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
"""Admin routes"""
|
||||||
|
|
||||||
|
from flask import request, redirect
|
||||||
|
from flask import render_template
|
||||||
|
|
||||||
|
from . import routes
|
||||||
|
from . import forms
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
|
class RouteAdmin(routes.Routes):
|
||||||
|
"""Admin endpoints: GET and POST /add"""
|
||||||
|
|
||||||
|
def add_routes(self) -> None:
|
||||||
|
"""Add admin routes"""
|
||||||
|
|
||||||
|
@self.app.route('/add', methods=['GET', 'POST'])
|
||||||
|
def add_person():
|
||||||
|
|
||||||
|
form = forms.AddForm()
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template(
|
||||||
|
'admin.html',
|
||||||
|
form=form,
|
||||||
|
)
|
||||||
|
|
||||||
|
if form.pswd.data != '1234':
|
||||||
|
return 'Incorrect password', 403
|
||||||
|
|
||||||
|
cur = db.get_cursor()
|
||||||
|
cur.execute(
|
||||||
|
'insert into ${REPO_NAME_SNAKE} '
|
||||||
|
'(email,name,age) values (%s,%s,%s)',
|
||||||
|
(
|
||||||
|
form.email.data,
|
||||||
|
form.name.data,
|
||||||
|
form.age.data,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
return redirect('/db')
|
|
@ -5,16 +5,21 @@ import secrets
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Type, List
|
from typing import Type, List
|
||||||
|
|
||||||
from flask import Flask
|
from pathlib import Path
|
||||||
from flaskext.mysql import MySQL # type: ignore
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
from . import exts
|
||||||
from .routes import Routes
|
from .routes import Routes
|
||||||
from . import pages
|
from . import pages
|
||||||
|
from . import admin
|
||||||
from . import errors
|
from . import errors
|
||||||
from . import db
|
from . import db
|
||||||
|
|
||||||
routes: List[Type[Routes]] = [
|
routes: List[Type[Routes]] = [
|
||||||
pages.RoutePages,
|
pages.RoutePages,
|
||||||
|
admin.RouteAdmin,
|
||||||
errors.RouteErrors,
|
errors.RouteErrors,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -29,6 +34,11 @@ def create_app() -> Flask:
|
||||||
template_folder='../templates',
|
template_folder='../templates',
|
||||||
instance_relative_config=True,
|
instance_relative_config=True,
|
||||||
)
|
)
|
||||||
|
# Load sample configuation
|
||||||
|
if app.debug:
|
||||||
|
load_dotenv(
|
||||||
|
Path(__file__).parent.parent / '.env'
|
||||||
|
)
|
||||||
# Get the token from environment
|
# Get the token from environment
|
||||||
# or generate it using secrets
|
# or generate it using secrets
|
||||||
app.config['SECRET_KEY'] = os.getenv(
|
app.config['SECRET_KEY'] = os.getenv(
|
||||||
|
@ -36,16 +46,23 @@ def create_app() -> Flask:
|
||||||
secrets.token_hex(32),
|
secrets.token_hex(32),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Setup MySQL database
|
# Configurate MySQL database
|
||||||
|
db_name = os.getenv('MYSQL_DATABASE', '${REPO_NAME_SNAKE}')
|
||||||
app.config.update({
|
app.config.update({
|
||||||
'MYSQL_DATABASE_HOST': os.getenv('DB_HOST', 'localhost'),
|
'MYSQL_DATABASE_HOST': os.getenv('MYSQL_HOST', 'localhost'),
|
||||||
'MYSQL_DATABASE_PORT': parseint(os.getenv('DB_PORT'), 3306),
|
'MYSQL_DATABASE_PORT': parseint(os.getenv('MYSQL_PORT'), 3306),
|
||||||
'MYSQL_DATABASE_USER': os.getenv('DB_USER', 'root'),
|
'MYSQL_DATABASE_USER': os.getenv('MYSQL_USER', 'root'),
|
||||||
'MYSQL_DATABASE_PASSWORD': os.getenv('DB_PASSWORD', ''),
|
'MYSQL_DATABASE_PASSWORD': os.getenv('MYSQL_PASSWORD', ''),
|
||||||
'MYSQL_DATABASE_DB': os.getenv('DB_DATABASE', '${REPO_NAME_SNAKE}'),
|
# 'MYSQL_DATABASE_DB': db_name,
|
||||||
})
|
})
|
||||||
sql = MySQL(app)
|
|
||||||
db.set_db(sql)
|
# Load extensions
|
||||||
|
for ext in exts.exts:
|
||||||
|
ext.init_app(app)
|
||||||
|
|
||||||
|
db.create_db(db_name)
|
||||||
|
with app.app_context():
|
||||||
|
db.init_db()
|
||||||
|
|
||||||
# Create instance/ directory
|
# Create instance/ directory
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,25 +1,76 @@
|
||||||
"""Simple wrapper for getting/setting
|
"""Works with MySQL database objects"""
|
||||||
db key in global Flask object called `g`"""
|
|
||||||
|
|
||||||
from flask import g
|
from typing import List, Optional
|
||||||
from flaskext.mysql import MySQL # type: ignore
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from pymysql import Connection
|
||||||
|
from pymysql.cursors import Cursor
|
||||||
|
|
||||||
|
from . import exts
|
||||||
|
|
||||||
|
db_obj: List[Optional[Connection]] = [None]
|
||||||
|
|
||||||
|
|
||||||
def set_db(db: MySQL) -> None:
|
def init_db() -> None:
|
||||||
"""Add the database to `g` object
|
"""Initializes MySQL database
|
||||||
|
from schema.sql file"""
|
||||||
|
|
||||||
|
cur = get_cursor()
|
||||||
|
|
||||||
|
with current_app.open_resource('../db/schema.sql') as f:
|
||||||
|
|
||||||
|
schema: bytes = f.read() # type: ignore
|
||||||
|
schema_str = schema.decode('utf-8')
|
||||||
|
|
||||||
|
for query in schema_str.split(';'):
|
||||||
|
|
||||||
|
query = query.strip()
|
||||||
|
if query == '' or query.startswith('--'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
cur.execute(query)
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
|
||||||
|
def create_db(name: str) -> None:
|
||||||
|
"""Create the database if not exists
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db (MySQL): MySQL database
|
name (str): Database name
|
||||||
"""
|
"""
|
||||||
|
|
||||||
g.db = db
|
sql = get_db()
|
||||||
|
cur = sql.cursor(Cursor)
|
||||||
|
cur.execute(
|
||||||
|
f'create database if not exists {name}'
|
||||||
|
)
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
sql.select_db(name)
|
||||||
|
|
||||||
|
|
||||||
def get_db() -> MySQL:
|
def get_db() -> Connection:
|
||||||
"""Get the database from `g` object
|
"""Get MySQL connection object
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
MySQL database
|
MySQL database
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return g.db
|
if db_obj[0] is None:
|
||||||
|
db_obj[0] = exts.sql.connect()
|
||||||
|
return db_obj[0] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def get_cursor() -> Cursor:
|
||||||
|
"""Get MySQL database cursor object
|
||||||
|
for executing commands.
|
||||||
|
Equivalent to:
|
||||||
|
```
|
||||||
|
conn = db.get_db()
|
||||||
|
cur = conn.cursor()
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
return get_db().cursor()
|
||||||
|
|
7
flaskapp/exts.py
Normal file
7
flaskapp/exts.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
"""Flask extensions list"""
|
||||||
|
|
||||||
|
from flaskext.mysql import MySQL # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
sql = MySQL(autocommit=True)
|
||||||
|
exts = [sql]
|
25
flaskapp/forms.py
Normal file
25
flaskapp/forms.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"""Flask app forms"""
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm # type: ignore
|
||||||
|
from wtforms import StringField # type: ignore
|
||||||
|
from wtforms import IntegerField
|
||||||
|
from wtforms import PasswordField
|
||||||
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
|
|
||||||
|
class AddForm(FlaskForm):
|
||||||
|
"""Sample form for adding users to DB"""
|
||||||
|
|
||||||
|
pswd = PasswordField('Admin password (1234)')
|
||||||
|
email = StringField(
|
||||||
|
'Person\'s email',
|
||||||
|
validators=[DataRequired()],
|
||||||
|
)
|
||||||
|
name = StringField(
|
||||||
|
'Name',
|
||||||
|
validators=[DataRequired()],
|
||||||
|
)
|
||||||
|
age = IntegerField(
|
||||||
|
'Age',
|
||||||
|
validators=[DataRequired()],
|
||||||
|
)
|
|
@ -3,6 +3,7 @@
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
|
||||||
from . import routes
|
from . import routes
|
||||||
|
from . import db
|
||||||
|
|
||||||
|
|
||||||
class RoutePages(routes.Routes):
|
class RoutePages(routes.Routes):
|
||||||
|
@ -13,3 +14,16 @@ class RoutePages(routes.Routes):
|
||||||
@self.app.route('/')
|
@self.app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@self.app.route('/db')
|
||||||
|
def table():
|
||||||
|
|
||||||
|
cur = db.get_cursor()
|
||||||
|
cur.execute('SELECT * FROM ${REPO_NAME_SNAKE}')
|
||||||
|
rows = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'table.html',
|
||||||
|
rows=rows,
|
||||||
|
)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
flask==2.2.2
|
flask==2.2.2
|
||||||
flask-mysql==1.5.2
|
flask-mysql==1.5.2
|
||||||
|
flask-wtf==1.1.1
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
|
python-dotenv==0.21.1
|
||||||
|
|
|
@ -9,14 +9,17 @@ body {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
background: #fff;
|
--bg: #fff;
|
||||||
color: #000;
|
--fg: #000;
|
||||||
|
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
body {
|
body {
|
||||||
background: #202023;
|
--bg: #202023;
|
||||||
color: #eee;
|
--fg: #eee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,3 +32,29 @@ a {
|
||||||
a:hover {
|
a:hover {
|
||||||
filter: brightness(120%);
|
filter: brightness(120%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
form > div {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
form > div > label {
|
||||||
|
width: 50%;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
form > div > input {
|
||||||
|
width: 50%;
|
||||||
|
border: 1px solid var(--fg);
|
||||||
|
outline: none;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
form > div > input:hover,
|
||||||
|
form > div > input:focus {
|
||||||
|
filter: brightness(130%);
|
||||||
|
}
|
||||||
|
|
21
templates/admin.html
Normal file
21
templates/admin.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Admin{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Add a person to DB</h1>
|
||||||
|
<form action="/add" method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% for field in form %}
|
||||||
|
{% if field.name != 'csrf_token' %}
|
||||||
|
<div>
|
||||||
|
{{ field.label }}
|
||||||
|
{{ field }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<div>
|
||||||
|
<input type="submit" value="Add">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
18
templates/table.html
Normal file
18
templates/table.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Database{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Sample database</h1>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr>
|
||||||
|
{% for cell in row %}
|
||||||
|
<td>{{ cell }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
Loading…
Add table
Reference in a new issue