From 43f9d78f8c00e13732b809519f0529ac293fd5ab Mon Sep 17 00:00:00 2001 From: Mitsuo Tokumori Date: Sun, 29 Oct 2023 07:41:04 -0500 Subject: Rename to ustayml (u-stayML) --- README.md | 2 +- flaskr/__init__.py | 44 ----------- flaskr/db.py | 49 ------------ flaskr/schema.sql | 17 ----- flaskr/static/favicon.ico | Bin 1001 -> 0 bytes flaskr/static/img/flask-powered.png | Bin 4890 -> 0 bytes flaskr/static/style.css | 142 ----------------------------------- flaskr/templates/auth/login.html | 15 ---- flaskr/templates/auth/register.html | 15 ---- flaskr/templates/base.html | 25 ------ flaskr/templates/blog/create.html | 15 ---- flaskr/templates/blog/index.html | 34 --------- flaskr/templates/blog/update.html | 20 ----- flaskr/views/auth.py | 102 ------------------------- flaskr/views/blog.py | 104 ------------------------- pyproject.toml | 6 +- tests/conftest.py | 4 +- tests/test_auth.py | 2 +- tests/test_blog.py | 2 +- tests/test_db.py | 4 +- tests/test_factory.py | 2 +- ustayml/__init__.py | 44 +++++++++++ ustayml/db.py | 49 ++++++++++++ ustayml/schema.sql | 17 +++++ ustayml/static/favicon.ico | Bin 0 -> 1001 bytes ustayml/static/img/flask-powered.png | Bin 0 -> 4890 bytes ustayml/static/logo_pucp.svg | 11 +++ ustayml/static/style.css | 142 +++++++++++++++++++++++++++++++++++ ustayml/templates/auth/login.html | 15 ++++ ustayml/templates/auth/register.html | 15 ++++ ustayml/templates/base.html | 25 ++++++ ustayml/templates/blog/create.html | 15 ++++ ustayml/templates/blog/index.html | 34 +++++++++ ustayml/templates/blog/update.html | 20 +++++ ustayml/views/auth.py | 102 +++++++++++++++++++++++++ ustayml/views/blog.py | 104 +++++++++++++++++++++++++ ustayml/views/dashboard.py | 0 37 files changed, 604 insertions(+), 593 deletions(-) delete mode 100644 flaskr/__init__.py delete mode 100644 flaskr/db.py delete mode 100644 flaskr/schema.sql delete mode 100644 flaskr/static/favicon.ico delete mode 100644 flaskr/static/img/flask-powered.png delete mode 100644 flaskr/static/style.css delete mode 100644 flaskr/templates/auth/login.html delete mode 100644 flaskr/templates/auth/register.html delete mode 100644 flaskr/templates/base.html delete mode 100644 flaskr/templates/blog/create.html delete mode 100644 flaskr/templates/blog/index.html delete mode 100644 flaskr/templates/blog/update.html delete mode 100644 flaskr/views/auth.py delete mode 100644 flaskr/views/blog.py create mode 100644 ustayml/__init__.py create mode 100644 ustayml/db.py create mode 100644 ustayml/schema.sql create mode 100644 ustayml/static/favicon.ico create mode 100644 ustayml/static/img/flask-powered.png create mode 100644 ustayml/static/logo_pucp.svg create mode 100644 ustayml/static/style.css create mode 100644 ustayml/templates/auth/login.html create mode 100644 ustayml/templates/auth/register.html create mode 100644 ustayml/templates/base.html create mode 100644 ustayml/templates/blog/create.html create mode 100644 ustayml/templates/blog/index.html create mode 100644 ustayml/templates/blog/update.html create mode 100644 ustayml/views/auth.py create mode 100644 ustayml/views/blog.py create mode 100644 ustayml/views/dashboard.py diff --git a/README.md b/README.md index d8f3a7e..2dc8520 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ python -m venv .venv source .venv/bin/activate pip install -r requirements.txt -flask --app flaskr run --debug +flask --app ustayml run --debug ``` ## Build diff --git a/flaskr/__init__.py b/flaskr/__init__.py deleted file mode 100644 index 25c2682..0000000 --- a/flaskr/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -import os - -from flask import Flask - - -def create_app(test_config=None): - # Create app object. Configuration files are relative to instance folder. - app = Flask(__name__, instance_relative_config=True) - - # Config - app.config.from_mapping( - SECRET_KEY='dev', - DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), - ) - - if test_config is None: - app.config.from_pyfile('config.py', silent=True) - else: - app.config.from_mapping(test_config) - - try: - os.makedirs(app.instance_path) - except OSError: - pass - - # Routes - @app.route('/hello') - def hello(): - return 'Hello, World!' - - # Register functions and blueprints - from . import db - db.init_app(app) - - from .views import auth - app.register_blueprint(auth.bp) - - from .views import blog - app.register_blueprint(blog.bp) - - # Extra - app.add_url_rule('/', endpoint='index') - - return app \ No newline at end of file diff --git a/flaskr/db.py b/flaskr/db.py deleted file mode 100644 index eb4e8b4..0000000 --- a/flaskr/db.py +++ /dev/null @@ -1,49 +0,0 @@ -import sqlite3 - -import click -from flask import current_app, g - - -def get_db(): - """Returns database connection - - sqlite3: https://docs.python.org/3/library/sqlite3.html - """ - if 'db' not in g: - g.db = sqlite3.connect( - current_app.config['DATABASE'], - detect_types=sqlite3.PARSE_DECLTYPES - ) - # Return rows that behave like dicts - g.db.row_factory = sqlite3.Row - - return g.db - - -def close_db(e=None): - db = g.pop('db', None) - - if db: - db.close() - -# CLI: -# https://flask.palletsprojects.com/en/3.0.x/cli/ - -def init_db(): - db = get_db() - - with current_app.open_resource('schema.sql') as f: - db.executescript(f.read().decode('utf8')) - - -@click.command('init-db') -def init_db_command(): - """Clear the existing data and create new tables.""" - init_db() - click.echo('Initialized the database.') - -# Register function with application - -def init_app(app): - app.teardown_appcontext(close_db) # callback after returning response - app.cli.add_command(init_db_command) \ No newline at end of file diff --git a/flaskr/schema.sql b/flaskr/schema.sql deleted file mode 100644 index be76d7e..0000000 --- a/flaskr/schema.sql +++ /dev/null @@ -1,17 +0,0 @@ -DROP TABLE IF EXISTS user; -DROP TABLE IF EXISTS post; - -CREATE TABLE user ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL -); - -CREATE TABLE post ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - author_id INTEGER NOT NULL, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - title TEXT NOT NULL, - body TEXT NOT NULL, - FOREIGN KEY (author_id) REFERENCES user (id) -); diff --git a/flaskr/static/favicon.ico b/flaskr/static/favicon.ico deleted file mode 100644 index aecf115..0000000 Binary files a/flaskr/static/favicon.ico and /dev/null differ diff --git a/flaskr/static/img/flask-powered.png b/flaskr/static/img/flask-powered.png deleted file mode 100644 index 9d20f17..0000000 Binary files a/flaskr/static/img/flask-powered.png and /dev/null differ diff --git a/flaskr/static/style.css b/flaskr/static/style.css deleted file mode 100644 index 1a73cc5..0000000 --- a/flaskr/static/style.css +++ /dev/null @@ -1,142 +0,0 @@ -html { - font-family: sans-serif; - background: #eee; - padding: 1rem; -} - -body { - max-width: 960px; - margin: 0 auto; - background: white; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - font-family: serif; - color: #377ba8; - margin: 1rem 0; -} - -a { - color: #377ba8; -} - -hr { - border: none; - border-top: 1px solid lightgray; -} - -nav { - background: lightgray; - display: flex; - align-items: center; - padding: 0 0.5rem; -} - -nav h1 { - flex: auto; - margin: 0; -} - -nav h1 a { - text-decoration: none; - padding: 0.25rem 0.5rem; -} - -nav ul { - display: flex; - list-style: none; - margin: 0; - padding: 0; -} - -nav ul li a, -nav ul li span, -header .action { - display: block; - padding: 0.5rem; -} - -.content { - padding: 0 1rem 1rem; -} - -.content > header { - border-bottom: 1px solid lightgray; - display: flex; - align-items: flex-end; -} - -.content > header h1 { - flex: auto; - margin: 1rem 0 0.25rem 0; -} - -.flash { - margin: 1em 0; - padding: 1em; - background: #cae6f6; - border: 1px solid #377ba8; -} - -.post > header { - display: flex; - align-items: flex-end; - font-size: 0.85em; -} - -.post > header > div:first-of-type { - flex: auto; -} - -.post > header h1 { - font-size: 1.5em; - margin-bottom: 0; -} - -.post .about { - color: slategray; - font-style: italic; -} - -.post .body { - white-space: pre-line; -} - -.content:last-child { - margin-bottom: 0; -} - -.content form { - margin: 1em 0; - display: flex; - flex-direction: column; -} - -.content label { - font-weight: bold; - margin-bottom: 0.5em; -} - -.content input, -.content textarea { - margin-bottom: 1em; -} - -.content textarea { - min-height: 12em; - resize: vertical; -} - -input.danger { - color: #cc2f2e; -} - -input[type="submit"] { - align-self: start; - min-width: 10em; -} diff --git a/flaskr/templates/auth/login.html b/flaskr/templates/auth/login.html deleted file mode 100644 index b7dd5dc..0000000 --- a/flaskr/templates/auth/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Log In{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-{% endblock %} \ No newline at end of file diff --git a/flaskr/templates/auth/register.html b/flaskr/templates/auth/register.html deleted file mode 100644 index a3c73cc..0000000 --- a/flaskr/templates/auth/register.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Register{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-{% endblock %} \ No newline at end of file diff --git a/flaskr/templates/base.html b/flaskr/templates/base.html deleted file mode 100644 index 6ea4864..0000000 --- a/flaskr/templates/base.html +++ /dev/null @@ -1,25 +0,0 @@ - -{% block title %}{% endblock %} - Flaskr - - - -
-
- {% block header %}{% endblock %} -
- {% for message in get_flashed_messages() %} -
{{ message }}
- {% endfor %} - {% block content %}{% endblock %} -
\ No newline at end of file diff --git a/flaskr/templates/blog/create.html b/flaskr/templates/blog/create.html deleted file mode 100644 index 88e31e4..0000000 --- a/flaskr/templates/blog/create.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}New Post{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-{% endblock %} diff --git a/flaskr/templates/blog/index.html b/flaskr/templates/blog/index.html deleted file mode 100644 index 0fb2e80..0000000 --- a/flaskr/templates/blog/index.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends 'base.html' %} - - -{% block header %} -

{% block title %}Posts{% endblock %}

- {% if g.user %} - New - {% endif %} -{% endblock %} - -{% block content %} - {% for post in posts %} -
-
-
- {% if post['title'].__len__() > 80 %} -

{{ post['title'][:80] }}

- {% else %} -

{{ post['title'] }}

- {% endif %} -
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
-
- {% if g.user['id'] == post['author_id'] %} - Edit - {% endif %} -
-

{{ post['body'] }}

-
- {% if not loop.last %} - {# Separate posts with a line #} -
- {% endif %} - {% endfor %} -{% endblock %} \ No newline at end of file diff --git a/flaskr/templates/blog/update.html b/flaskr/templates/blog/update.html deleted file mode 100644 index 7420f57..0000000 --- a/flaskr/templates/blog/update.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-
-
- -
-{% endblock %} \ No newline at end of file diff --git a/flaskr/views/auth.py b/flaskr/views/auth.py deleted file mode 100644 index 2dc32af..0000000 --- a/flaskr/views/auth.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Authentication blueprint""" - -import functools - -from flask import ( - Blueprint, flash, g, redirect, render_template, request, session, url_for -) -from werkzeug.security import check_password_hash, generate_password_hash -from flaskr.db import get_db - -bp = Blueprint('auth', __name__, url_prefix='/auth') - -@bp.route('/register', methods=('GET', 'POST')) -def register(): - if request.method == 'POST': - # Form validation - username = request.form['username'] - password = request.form['password'] - db = get_db() - error = None - - if not username: - error = 'Username is required.' - elif not password: - error = 'Password is required.' - - if error is None: - try: - # NOTE: don't use f-string here. Use `?` placeholders so that - # database library can escape the fields - # (otherwise SQL injection vulnerability) - db.execute( - "INSERT INTO user (username, password) VALUES (?, ?)", - (username, generate_password_hash(password)) - ) - db.commit() - except db.IntegrityError: - error = f"User {username} is already registered." - else: - return redirect(url_for("auth.login")) - - flash(error) - - return render_template('auth/register.html') - - -@bp.route('/login', methods=('GET', 'POST')) -def login(): - if request.method == 'POST': - username = request.form['username'] - password = request.form['password'] - db = get_db() - error = None - user = db.execute( - 'SELECT * FROM user WHERE username = ?', (username,) - ).fetchone() - - if user is None: - error = 'Incorrect username.' - elif not check_password_hash(user['password'], password): - error = 'Incorrect password.' - - if error is None: - session.clear() - session['user_id'] = user['id'] - return redirect(url_for('index')) - - flash(error) - - return render_template('auth/login.html') - - -# runs before the view function, no matter what URL is requested -@bp.before_app_request -def load_logged_in_user(): - user_id = session.get('user_id') - - if user_id is None: - g.user = None - else: - g.user = get_db().execute( - 'SELECT * FROM user WHERE id = ?', (user_id,) - ).fetchone() - - -@bp.route('/logout') -def logout(): - session.clear() - return redirect(url_for('index')) - - -# Define decorator to require authentication in other views -def login_required(view): - """view is a function that returns HTML (and is part of a blueprint)""" - @functools.wraps(view) - def wrapped_view(**kwargs): - if g.user is None: - return redirect(url_for('auth.login')) - - return view(**kwargs) - - return wrapped_view \ No newline at end of file diff --git a/flaskr/views/blog.py b/flaskr/views/blog.py deleted file mode 100644 index 6f728ea..0000000 --- a/flaskr/views/blog.py +++ /dev/null @@ -1,104 +0,0 @@ -from flask import ( - Blueprint, flash, g, redirect, render_template, request, url_for -) -from werkzeug.exceptions import abort - -from flaskr.views.auth import login_required -from flaskr.db import get_db - -# NOTE: no URL prefix -bp = Blueprint('blog', __name__) - - -@bp.route('/') -def index(): - db = get_db() - posts = db.execute( - 'SELECT p.id, title, body, created, author_id, username' - ' FROM post p JOIN user u ON p.author_id = u.id' - ' ORDER BY created DESC' - ).fetchall() - return render_template('blog/index.html', posts=posts) - - -@bp.route('/create', methods=('GET', 'POST')) -@login_required -def create(): - if request.method == 'POST': - title = request.form['title'] - body = request.form['body'] - error = None - - if not title: - error = 'Title is required.' - - if error is not None: - flash(error) - else: - db = get_db() - db.execute( - 'INSERT INTO post (title, body, author_id)' - ' VALUES (?, ?, ?)', - (title, body, g.user['id']) - ) - db.commit() - return redirect(url_for('blog.index')) - - return render_template('blog/create.html') - - -@bp.route('//update', methods=('GET', 'POST')) -@login_required -def update(id): - post = get_post(id) - - if request.method == 'POST': - title = request.form['title'] - body = request.form['body'] - error = None - - if not title: - error = 'Title is required.' - - if error is not None: - flash(error) - else: - db = get_db() - db.execute( - 'UPDATE post SET title = ?, body = ?' - ' WHERE id = ?', - (title, body, id) - ) - db.commit() - return redirect(url_for('blog.index')) - - return render_template('blog/update.html', post=post) - - -@bp.route('//delete', methods=('POST',)) -@login_required -def delete(id): - get_post(id) - db = get_db() - db.execute('DELETE FROM post WHERE id = ?', (id,)) - db.commit() - return redirect(url_for('blog.index')) - - -# Helper functions: - -def get_post(id, check_author=True): - post = get_db().execute( - 'SELECT p.id, title, body, created, author_id, username' - ' FROM post p JOIN user u ON p.author_id = u.id' - ' WHERE p.id = ?', - (id,) - ).fetchone() - - if post is None: - abort(404, f"Post id {id} doesn't exist.") - - if check_author and post['author_id'] != g.user['id']: - abort(403) - - return post diff --git a/pyproject.toml b/pyproject.toml index 5156197..e5df87f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "flaskr" +name = "ustayml" version = "1.0.0" -description = "The basic blog app built in the Flask tutorial." +description = "thesis project website" dependencies = [ "flask", ] @@ -15,4 +15,4 @@ testpaths = ["tests"] [tool.coverage.run] branch = true -source = ["flaskr"] \ No newline at end of file +source = ["ustayml"] \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 6a2d0a6..505c83d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,8 +10,8 @@ import os import tempfile import pytest -from flaskr import create_app -from flaskr.db import get_db, init_db +from ustayml import create_app +from ustayml.db import get_db, init_db with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: _data_sql = f.read().decode('utf8') diff --git a/tests/test_auth.py b/tests/test_auth.py index 401b61f..6684f43 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,6 +1,6 @@ import pytest from flask import g, session -from flaskr.db import get_db +from ustayml.db import get_db def test_register(client, app): diff --git a/tests/test_blog.py b/tests/test_blog.py index 75f67a5..06b6437 100644 --- a/tests/test_blog.py +++ b/tests/test_blog.py @@ -1,5 +1,5 @@ import pytest -from flaskr.db import get_db +from ustayml.db import get_db def test_index(client, auth): diff --git a/tests/test_db.py b/tests/test_db.py index 6ac4635..4e2f103 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,7 +1,7 @@ import sqlite3 import pytest -from flaskr.db import get_db +from ustayml.db import get_db def test_get_close_db(app): @@ -27,7 +27,7 @@ def test_init_db_command(runner, monkeypatch): def fake_init_db(): Recorder.called = True - monkeypatch.setattr('flaskr.db.init_db', fake_init_db) + monkeypatch.setattr('ustayml.db.init_db', fake_init_db) result = runner.invoke(args=['init-db']) assert 'Initialized' in result.output assert Recorder.called \ No newline at end of file diff --git a/tests/test_factory.py b/tests/test_factory.py index 024c19a..edc973c 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,4 +1,4 @@ -from flaskr import create_app +from ustayml import create_app def test_config(): diff --git a/ustayml/__init__.py b/ustayml/__init__.py new file mode 100644 index 0000000..1c411e7 --- /dev/null +++ b/ustayml/__init__.py @@ -0,0 +1,44 @@ +import os + +from flask import Flask + + +def create_app(test_config=None): + # Create app object. Configuration files are relative to instance folder. + app = Flask(__name__, instance_relative_config=True) + + # Config + app.config.from_mapping( + SECRET_KEY='dev', + DATABASE=os.path.join(app.instance_path, 'ustayml.sqlite'), + ) + + if test_config is None: + app.config.from_pyfile('config.py', silent=True) + else: + app.config.from_mapping(test_config) + + try: + os.makedirs(app.instance_path) + except OSError: + pass + + # Routes + @app.route('/hello') + def hello(): + return 'Hello, World!' + + # Register functions and blueprints + from . import db + db.init_app(app) + + from .views import auth + app.register_blueprint(auth.bp) + + from .views import blog + app.register_blueprint(blog.bp) + + # Extra + app.add_url_rule('/', endpoint='index') + + return app \ No newline at end of file diff --git a/ustayml/db.py b/ustayml/db.py new file mode 100644 index 0000000..eb4e8b4 --- /dev/null +++ b/ustayml/db.py @@ -0,0 +1,49 @@ +import sqlite3 + +import click +from flask import current_app, g + + +def get_db(): + """Returns database connection + + sqlite3: https://docs.python.org/3/library/sqlite3.html + """ + if 'db' not in g: + g.db = sqlite3.connect( + current_app.config['DATABASE'], + detect_types=sqlite3.PARSE_DECLTYPES + ) + # Return rows that behave like dicts + g.db.row_factory = sqlite3.Row + + return g.db + + +def close_db(e=None): + db = g.pop('db', None) + + if db: + db.close() + +# CLI: +# https://flask.palletsprojects.com/en/3.0.x/cli/ + +def init_db(): + db = get_db() + + with current_app.open_resource('schema.sql') as f: + db.executescript(f.read().decode('utf8')) + + +@click.command('init-db') +def init_db_command(): + """Clear the existing data and create new tables.""" + init_db() + click.echo('Initialized the database.') + +# Register function with application + +def init_app(app): + app.teardown_appcontext(close_db) # callback after returning response + app.cli.add_command(init_db_command) \ No newline at end of file diff --git a/ustayml/schema.sql b/ustayml/schema.sql new file mode 100644 index 0000000..be76d7e --- /dev/null +++ b/ustayml/schema.sql @@ -0,0 +1,17 @@ +DROP TABLE IF EXISTS user; +DROP TABLE IF EXISTS post; + +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL +); + +CREATE TABLE post ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + author_id INTEGER NOT NULL, + created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + title TEXT NOT NULL, + body TEXT NOT NULL, + FOREIGN KEY (author_id) REFERENCES user (id) +); diff --git a/ustayml/static/favicon.ico b/ustayml/static/favicon.ico new file mode 100644 index 0000000..aecf115 Binary files /dev/null and b/ustayml/static/favicon.ico differ diff --git a/ustayml/static/img/flask-powered.png b/ustayml/static/img/flask-powered.png new file mode 100644 index 0000000..9d20f17 Binary files /dev/null and b/ustayml/static/img/flask-powered.png differ diff --git a/ustayml/static/logo_pucp.svg b/ustayml/static/logo_pucp.svg new file mode 100644 index 0000000..8ffb164 --- /dev/null +++ b/ustayml/static/logo_pucp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ustayml/static/style.css b/ustayml/static/style.css new file mode 100644 index 0000000..1a73cc5 --- /dev/null +++ b/ustayml/static/style.css @@ -0,0 +1,142 @@ +html { + font-family: sans-serif; + background: #eee; + padding: 1rem; +} + +body { + max-width: 960px; + margin: 0 auto; + background: white; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: serif; + color: #377ba8; + margin: 1rem 0; +} + +a { + color: #377ba8; +} + +hr { + border: none; + border-top: 1px solid lightgray; +} + +nav { + background: lightgray; + display: flex; + align-items: center; + padding: 0 0.5rem; +} + +nav h1 { + flex: auto; + margin: 0; +} + +nav h1 a { + text-decoration: none; + padding: 0.25rem 0.5rem; +} + +nav ul { + display: flex; + list-style: none; + margin: 0; + padding: 0; +} + +nav ul li a, +nav ul li span, +header .action { + display: block; + padding: 0.5rem; +} + +.content { + padding: 0 1rem 1rem; +} + +.content > header { + border-bottom: 1px solid lightgray; + display: flex; + align-items: flex-end; +} + +.content > header h1 { + flex: auto; + margin: 1rem 0 0.25rem 0; +} + +.flash { + margin: 1em 0; + padding: 1em; + background: #cae6f6; + border: 1px solid #377ba8; +} + +.post > header { + display: flex; + align-items: flex-end; + font-size: 0.85em; +} + +.post > header > div:first-of-type { + flex: auto; +} + +.post > header h1 { + font-size: 1.5em; + margin-bottom: 0; +} + +.post .about { + color: slategray; + font-style: italic; +} + +.post .body { + white-space: pre-line; +} + +.content:last-child { + margin-bottom: 0; +} + +.content form { + margin: 1em 0; + display: flex; + flex-direction: column; +} + +.content label { + font-weight: bold; + margin-bottom: 0.5em; +} + +.content input, +.content textarea { + margin-bottom: 1em; +} + +.content textarea { + min-height: 12em; + resize: vertical; +} + +input.danger { + color: #cc2f2e; +} + +input[type="submit"] { + align-self: start; + min-width: 10em; +} diff --git a/ustayml/templates/auth/login.html b/ustayml/templates/auth/login.html new file mode 100644 index 0000000..b7dd5dc --- /dev/null +++ b/ustayml/templates/auth/login.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Log In{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} \ No newline at end of file diff --git a/ustayml/templates/auth/register.html b/ustayml/templates/auth/register.html new file mode 100644 index 0000000..a3c73cc --- /dev/null +++ b/ustayml/templates/auth/register.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Register{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} \ No newline at end of file diff --git a/ustayml/templates/base.html b/ustayml/templates/base.html new file mode 100644 index 0000000..1b2c384 --- /dev/null +++ b/ustayml/templates/base.html @@ -0,0 +1,25 @@ + +{% block title %}{% endblock %} - u-stayML + + + +
+
+ {% block header %}{% endblock %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block content %}{% endblock %} +
\ No newline at end of file diff --git a/ustayml/templates/blog/create.html b/ustayml/templates/blog/create.html new file mode 100644 index 0000000..88e31e4 --- /dev/null +++ b/ustayml/templates/blog/create.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}New Post{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} diff --git a/ustayml/templates/blog/index.html b/ustayml/templates/blog/index.html new file mode 100644 index 0000000..0fb2e80 --- /dev/null +++ b/ustayml/templates/blog/index.html @@ -0,0 +1,34 @@ +{% extends 'base.html' %} + + +{% block header %} +

{% block title %}Posts{% endblock %}

+ {% if g.user %} + New + {% endif %} +{% endblock %} + +{% block content %} + {% for post in posts %} +
+
+
+ {% if post['title'].__len__() > 80 %} +

{{ post['title'][:80] }}

+ {% else %} +

{{ post['title'] }}

+ {% endif %} +
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
+
+ {% if g.user['id'] == post['author_id'] %} + Edit + {% endif %} +
+

{{ post['body'] }}

+
+ {% if not loop.last %} + {# Separate posts with a line #} +
+ {% endif %} + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/ustayml/templates/blog/update.html b/ustayml/templates/blog/update.html new file mode 100644 index 0000000..7420f57 --- /dev/null +++ b/ustayml/templates/blog/update.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/ustayml/views/auth.py b/ustayml/views/auth.py new file mode 100644 index 0000000..92710a1 --- /dev/null +++ b/ustayml/views/auth.py @@ -0,0 +1,102 @@ +"""Authentication blueprint""" + +import functools + +from flask import ( + Blueprint, flash, g, redirect, render_template, request, session, url_for +) +from werkzeug.security import check_password_hash, generate_password_hash +from ustayml.db import get_db + +bp = Blueprint('auth', __name__, url_prefix='/auth') + +@bp.route('/register', methods=('GET', 'POST')) +def register(): + if request.method == 'POST': + # Form validation + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + + if not username: + error = 'Username is required.' + elif not password: + error = 'Password is required.' + + if error is None: + try: + # NOTE: don't use f-string here. Use `?` placeholders so that + # database library can escape the fields + # (otherwise SQL injection vulnerability) + db.execute( + "INSERT INTO user (username, password) VALUES (?, ?)", + (username, generate_password_hash(password)) + ) + db.commit() + except db.IntegrityError: + error = f"User {username} is already registered." + else: + return redirect(url_for("auth.login")) + + flash(error) + + return render_template('auth/register.html') + + +@bp.route('/login', methods=('GET', 'POST')) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + user = db.execute( + 'SELECT * FROM user WHERE username = ?', (username,) + ).fetchone() + + if user is None: + error = 'Incorrect username.' + elif not check_password_hash(user['password'], password): + error = 'Incorrect password.' + + if error is None: + session.clear() + session['user_id'] = user['id'] + return redirect(url_for('index')) + + flash(error) + + return render_template('auth/login.html') + + +# runs before the view function, no matter what URL is requested +@bp.before_app_request +def load_logged_in_user(): + user_id = session.get('user_id') + + if user_id is None: + g.user = None + else: + g.user = get_db().execute( + 'SELECT * FROM user WHERE id = ?', (user_id,) + ).fetchone() + + +@bp.route('/logout') +def logout(): + session.clear() + return redirect(url_for('index')) + + +# Define decorator to require authentication in other views +def login_required(view): + """view is a function that returns HTML (and is part of a blueprint)""" + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for('auth.login')) + + return view(**kwargs) + + return wrapped_view \ No newline at end of file diff --git a/ustayml/views/blog.py b/ustayml/views/blog.py new file mode 100644 index 0000000..57ba4ba --- /dev/null +++ b/ustayml/views/blog.py @@ -0,0 +1,104 @@ +from flask import ( + Blueprint, flash, g, redirect, render_template, request, url_for +) +from werkzeug.exceptions import abort + +from ustayml.views.auth import login_required +from ustayml.db import get_db + +# NOTE: no URL prefix +bp = Blueprint('blog', __name__) + + +@bp.route('/') +def index(): + db = get_db() + posts = db.execute( + 'SELECT p.id, title, body, created, author_id, username' + ' FROM post p JOIN user u ON p.author_id = u.id' + ' ORDER BY created DESC' + ).fetchall() + return render_template('blog/index.html', posts=posts) + + +@bp.route('/create', methods=('GET', 'POST')) +@login_required +def create(): + if request.method == 'POST': + title = request.form['title'] + body = request.form['body'] + error = None + + if not title: + error = 'Title is required.' + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + 'INSERT INTO post (title, body, author_id)' + ' VALUES (?, ?, ?)', + (title, body, g.user['id']) + ) + db.commit() + return redirect(url_for('blog.index')) + + return render_template('blog/create.html') + + +@bp.route('//update', methods=('GET', 'POST')) +@login_required +def update(id): + post = get_post(id) + + if request.method == 'POST': + title = request.form['title'] + body = request.form['body'] + error = None + + if not title: + error = 'Title is required.' + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + 'UPDATE post SET title = ?, body = ?' + ' WHERE id = ?', + (title, body, id) + ) + db.commit() + return redirect(url_for('blog.index')) + + return render_template('blog/update.html', post=post) + + +@bp.route('//delete', methods=('POST',)) +@login_required +def delete(id): + get_post(id) + db = get_db() + db.execute('DELETE FROM post WHERE id = ?', (id,)) + db.commit() + return redirect(url_for('blog.index')) + + +# Helper functions: + +def get_post(id, check_author=True): + post = get_db().execute( + 'SELECT p.id, title, body, created, author_id, username' + ' FROM post p JOIN user u ON p.author_id = u.id' + ' WHERE p.id = ?', + (id,) + ).fetchone() + + if post is None: + abort(404, f"Post id {id} doesn't exist.") + + if check_author and post['author_id'] != g.user['id']: + abort(403) + + return post diff --git a/ustayml/views/dashboard.py b/ustayml/views/dashboard.py new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3