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: clean:
rm -rf flaskapp/__pycache__ rm -rf flaskapp/__pycache__
rm -rf __pycache__
rm -rf .mypy_cache 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 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,22 @@ 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}'),
}) })
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:

View file

@ -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
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 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,
)

View file

@ -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

View file

@ -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
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 %}