diff options
| -rw-r--r-- | README.md | 8 | ||||
| -rw-r--r-- | flaskr/__init__.py | 10 | ||||
| -rw-r--r-- | flaskr/db.py | 4 | ||||
| -rw-r--r-- | flaskr/templates/blog/create.html | 15 | ||||
| -rw-r--r-- | flaskr/templates/blog/index.html | 30 | ||||
| -rw-r--r-- | flaskr/templates/blog/update.html | 20 | ||||
| -rw-r--r-- | flaskr/views/auth.py | 2 | ||||
| -rw-r--r-- | flaskr/views/blog.py | 104 |
8 files changed, 187 insertions, 6 deletions
@@ -14,9 +14,10 @@ flask --app flaskr run --debug ## Learning resources -* Flask: https://flask.palletsprojects.com/en/3.0.x/#user-s-guide +* [Flask user guide](https://flask.palletsprojects.com/en/3.0.x/#user-s-guide) * DB: https://sqlite.org/lang.html -* Template: https://jinja.palletsprojects.com/templates/ +* [Jinja Template](https://jinja.palletsprojects.com/templates/) + * [For loops](https://jinja.palletsprojects.com/en/3.1.x/templates/#for) Concepts: @@ -34,7 +35,8 @@ Concepts: data that is rendered in HTML templates, so it's safe to render user input. * `{{ }}` denotes expressions (output) (similar to python) * `{% %}` denotes control flow statements (similar to pseudo-code) - * Automatically available: `g`, `url_for`, and more + * `{# #}` denotes a comment + * Automatically available: `g`, `url_for`, `request`, and more * In base.html you define "blocks" placeholder which are later defined in other templates that extend base.html diff --git a/flaskr/__init__.py b/flaskr/__init__.py index 0366365..3f068e8 100644 --- a/flaskr/__init__.py +++ b/flaskr/__init__.py @@ -7,7 +7,7 @@ def create_app(test_config=None): # Create app object. Configuration files are relative to instance folder. app = Flask(__name__, instance_relative_config=True) - # config + # Config app.config.from_mapping( SECRET_KEY='dev', DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), @@ -24,7 +24,7 @@ def create_app(test_config=None): pass # Routes - @app.route('/') + @app.route('/hello') def index(): return 'Hello, World!' @@ -35,4 +35,10 @@ def create_app(test_config=None): 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 index 7185da8..eb4e8b4 100644 --- a/flaskr/db.py +++ b/flaskr/db.py @@ -5,6 +5,10 @@ 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'], diff --git a/flaskr/templates/blog/create.html b/flaskr/templates/blog/create.html new file mode 100644 index 0000000..88e31e4 --- /dev/null +++ b/flaskr/templates/blog/create.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} + <h1>{% block title %}New Post{% endblock %}</h1> +{% endblock %} + +{% block content %} + <form method="post"> + <label for="title">Title</label> + <input name="title" id="title" value="{{ request.form['title'] }}" required> + <label for="body">Body</label> + <textarea name="body" id="body">{{ request.form['body'] }}</textarea> + <input type="submit" value="Save"> + </form> +{% endblock %} diff --git a/flaskr/templates/blog/index.html b/flaskr/templates/blog/index.html new file mode 100644 index 0000000..0d3ed17 --- /dev/null +++ b/flaskr/templates/blog/index.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + + +{% block header %} + <h1>{% block title %}Posts{% endblock %}</h1> + {% if g.user %} + <a class="action" href="{{ url_for('blog.create') }}">New</a> + {% endif %} +{% endblock %} + +{% block content %} + {% for post in posts %} + <article class="post"> + <header> + <div> + <h1>{{ post['title'] }}</h1> + <div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div> + </div> + {% if g.user['id'] == post['author_id'] %} + <a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a> + {% endif %} + </header> + <p class="body">{{ post['body'] }}</p> + </article> + {% if not loop.last %} + {# Separate posts with a line #} + <hr> + {% endif %} + {% endfor %} +{% endblock %}
\ No newline at end of file diff --git a/flaskr/templates/blog/update.html b/flaskr/templates/blog/update.html new file mode 100644 index 0000000..7420f57 --- /dev/null +++ b/flaskr/templates/blog/update.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} + +{% block header %} + <h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1> +{% endblock %} + +{% block content %} + <form method="post"> + <label for="title">Title</label> + <input name="title" id="title" + value="{{ request.form['title'] or post['title'] }}" required> + <label for="body">Body</label> + <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea> + <input type="submit" value="Save"> + </form> + <hr> + <form action="{{ url_for('blog.delete', id=post['id']) }}" method="post"> + <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');"> + </form> +{% endblock %}
\ No newline at end of file diff --git a/flaskr/views/auth.py b/flaskr/views/auth.py index 3689eba..2dc32af 100644 --- a/flaskr/views/auth.py +++ b/flaskr/views/auth.py @@ -31,7 +31,7 @@ def register(): # (otherwise SQL injection vulnerability) db.execute( "INSERT INTO user (username, password) VALUES (?, ?)", - (username, generate_password_hash(password)), + (username, generate_password_hash(password)) ) db.commit() except db.IntegrityError: diff --git a/flaskr/views/blog.py b/flaskr/views/blog.py new file mode 100644 index 0000000..6f728ea --- /dev/null +++ b/flaskr/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 flaskr.views.auth import login_required +from flaskr.db import get_db + +# NOTE: no URL prefix +bp = Blueprint('blog', __name__) + + [email protected]('/') +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) + + [email protected]('/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') + + [email protected]('/<int:id>/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) + + [email protected]('/<int:id>/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 |
