Compare commits

...

3 commits

Author SHA1 Message Date
51e04f9f0f Cleanup 2023-02-16 18:21:15 +04:00
3f665df6a7 Working with DB, admin form, WTForms, .env 2023-02-16 18:19:58 +04:00
66133ac46c Makefile: pycache in root dir 2023-02-15 19:56:28 +04:00
13 changed files with 263 additions and 25 deletions

5
.env Normal file
View file

@ -0,0 +1,5 @@
MYSQL_HOST=localhost
MUSQL_PORT=3306
MYSQL_USER=darkcat09
MYSQL_PASSWORD=
MYSQL_DATABASE=${REPO_NAME_SNAKE}

View file

@ -13,4 +13,5 @@ check:
clean:
rm -rf flaskapp/__pycache__
rm -rf __pycache__
rm -rf .mypy_cache

6
db/schema.sql Normal file
View 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
View 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')

View file

@ -5,16 +5,21 @@ import secrets
from typing import Optional
from typing import Type, List
from flask import Flask
from flaskext.mysql import MySQL # type: ignore
from pathlib import Path
from dotenv import load_dotenv
from flask import Flask
from . import exts
from .routes import Routes
from . import pages
from . import admin
from . import errors
from . import db
routes: List[Type[Routes]] = [
pages.RoutePages,
admin.RouteAdmin,
errors.RouteErrors,
]
@ -29,6 +34,11 @@ def create_app() -> Flask:
template_folder='../templates',
instance_relative_config=True,
)
# Load sample configuation
if app.debug:
load_dotenv(
Path(__file__).parent.parent / '.env'
)
# Get the token from environment
# or generate it using secrets
app.config['SECRET_KEY'] = os.getenv(
@ -36,16 +46,22 @@ def create_app() -> Flask:
secrets.token_hex(32),
)
# Setup MySQL database
# Configurate MySQL database
db_name = os.getenv('MYSQL_DATABASE', '${REPO_NAME_SNAKE}')
app.config.update({
'MYSQL_DATABASE_HOST': os.getenv('DB_HOST', 'localhost'),
'MYSQL_DATABASE_PORT': parseint(os.getenv('DB_PORT'), 3306),
'MYSQL_DATABASE_USER': os.getenv('DB_USER', 'root'),
'MYSQL_DATABASE_PASSWORD': os.getenv('DB_PASSWORD', ''),
'MYSQL_DATABASE_DB': os.getenv('DB_DATABASE', '${REPO_NAME_SNAKE}'),
'MYSQL_DATABASE_HOST': os.getenv('MYSQL_HOST', 'localhost'),
'MYSQL_DATABASE_PORT': parseint(os.getenv('MYSQL_PORT'), 3306),
'MYSQL_DATABASE_USER': os.getenv('MYSQL_USER', 'root'),
'MYSQL_DATABASE_PASSWORD': os.getenv('MYSQL_PASSWORD', ''),
})
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
try:

View file

@ -1,25 +1,76 @@
"""Simple wrapper for getting/setting
db key in global Flask object called `g`"""
"""Works with MySQL database objects"""
from flask import g
from flaskext.mysql import MySQL # type: ignore
from typing import List, Optional
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:
"""Add the database to `g` object
def init_db() -> None:
"""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:
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:
"""Get the database from `g` object
def get_db() -> Connection:
"""Get MySQL connection object
Returns:
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
View 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
View 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 # type: ignore
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()],
)

View file

@ -3,6 +3,7 @@
from flask import render_template
from . import routes
from . import db
class RoutePages(routes.Routes):
@ -13,3 +14,16 @@ class RoutePages(routes.Routes):
@self.app.route('/')
def index():
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,
)

View file

@ -1,3 +1,5 @@
flask==2.2.2
flask-mysql==1.5.2
flask-wtf==1.1.1
gunicorn==20.1.0
python-dotenv==0.21.1

View file

@ -9,14 +9,17 @@ body {
justify-content: space-between;
align-items: center;
background: #fff;
color: #000;
--bg: #fff;
--fg: #000;
background: var(--bg);
color: var(--fg);
}
@media (prefers-color-scheme: dark) {
body {
background: #202023;
color: #eee;
--bg: #202023;
--fg: #eee;
}
}
@ -29,3 +32,29 @@ a {
a:hover {
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
View 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
View 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 %}