diff --git a/.env b/.env new file mode 100644 index 0000000..304fe69 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +MYSQL_HOST=localhost +MUSQL_PORT=3306 +MYSQL_USER=darkcat09 +MYSQL_PASSWORD= +MYSQL_DATABASE=${REPO_NAME_SNAKE} diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..49b46cf --- /dev/null +++ b/db/schema.sql @@ -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 +); diff --git a/flaskapp/admin.py b/flaskapp/admin.py new file mode 100644 index 0000000..5be80b0 --- /dev/null +++ b/flaskapp/admin.py @@ -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') diff --git a/flaskapp/app.py b/flaskapp/app.py index fb0e604..f71fa49 100644 --- a/flaskapp/app.py +++ b/flaskapp/app.py @@ -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,23 @@ 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', ''), + # '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 try: diff --git a/flaskapp/db.py b/flaskapp/db.py index 296f28f..ccb2eee 100644 --- a/flaskapp/db.py +++ b/flaskapp/db.py @@ -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() diff --git a/flaskapp/exts.py b/flaskapp/exts.py new file mode 100644 index 0000000..2b10bb4 --- /dev/null +++ b/flaskapp/exts.py @@ -0,0 +1,7 @@ +"""Flask extensions list""" + +from flaskext.mysql import MySQL # type: ignore + + +sql = MySQL(autocommit=True) +exts = [sql] diff --git a/flaskapp/forms.py b/flaskapp/forms.py new file mode 100644 index 0000000..14cde1c --- /dev/null +++ b/flaskapp/forms.py @@ -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()], + ) diff --git a/flaskapp/pages.py b/flaskapp/pages.py index e74b0da..459dfa4 100644 --- a/flaskapp/pages.py +++ b/flaskapp/pages.py @@ -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, + ) diff --git a/requirements.txt b/requirements.txt index 676759f..7b00ae4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/static/css/style.css b/static/css/style.css index 0d629db..cdaca35 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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%); +} diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..219eb96 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}Admin{% endblock %} + +{% block content %} +

Add a person to DB

+
+ {{ form.hidden_tag() }} + {% for field in form %} + {% if field.name != 'csrf_token' %} +
+ {{ field.label }} + {{ field }} +
+ {% endif %} + {% endfor %} +
+ +
+
+{% endblock %} diff --git a/templates/table.html b/templates/table.html new file mode 100644 index 0000000..252b2ea --- /dev/null +++ b/templates/table.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block title %}Database{% endblock %} + +{% block content %} +

Sample database

+ + + {% for row in rows %} + + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
{{ cell }}
+{% endblock %}