diff --git a/LICENSE b/LICENSE index 50712b5..e270514 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,28 @@ -MIT License +Copyright 2010 Pallets -Copyright (c) 2023 PV Tejas +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/flaskr/__init__.py b/flaskr/__init__.py index ed7d989..33779f8 100644 --- a/flaskr/__init__.py +++ b/flaskr/__init__.py @@ -8,6 +8,7 @@ def create_app(test_config=None): app.config.from_mapping( SECRET_KEY='dev', DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), + ALLOW_REGISTER=True, ) app.wsgi_app = ProxyFix( diff --git a/flaskr/auth.py b/flaskr/auth.py index e41998c..6b95143 100644 --- a/flaskr/auth.py +++ b/flaskr/auth.py @@ -1,7 +1,7 @@ import functools from flask import ( - Blueprint, flash, g, redirect, render_template, request, session, url_for + Blueprint, flash, g, redirect, render_template, request, session, url_for, current_app ) from werkzeug.security import check_password_hash, generate_password_hash @@ -11,6 +11,8 @@ bp = Blueprint('auth', __name__, url_prefix='/auth') @bp.route('/register', methods=('GET', 'POST')) def register(): + if not current_app.config["ALLOW_REGISTER"]: + return "Admin only", 403 if request.method == 'POST': username = request.form['username'] password = request.form['password'] diff --git a/flaskr/blog.py b/flaskr/blog.py index cfa78d8..8082d2f 100644 --- a/flaskr/blog.py +++ b/flaskr/blog.py @@ -7,6 +7,7 @@ from flaskr.auth import login_required from flaskr.db import get_db import markdown +import datetime bp = Blueprint('blog', __name__) @@ -20,11 +21,30 @@ def index(): ).fetchall() posts = [] for post in db_posts: + if post['created'] > datetime.datetime.utcnow(): + continue post = dict(post) post['body'] = markdown.markdown(post['body']) posts.append(post) return render_template('blog/index.html', posts=posts) +@bp.route('/firehose') +def firehose(): + db = get_db() + 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() + posts = [] + for post in db_posts: + if post['created'] > datetime.datetime.utcnow(): + continue + post = dict(post) + post['body'] = markdown.markdown(post['body']) + posts.append(post) + return render_template('blog/firehose.html', posts=posts) + @bp.route('/create',methods=('GET', 'POST')) @login_required def create(): @@ -66,6 +86,12 @@ def get_post(id, check_author=True): return post +@bp.route('//') +def individual_post(id): + post = dict(get_post(id, False)) + post['body'] = markdown.markdown(post['body']) + return render_template('blog/post.html', post=post) + @bp.route('//update', methods=('GET', 'POST')) @login_required def update(id): @@ -74,19 +100,23 @@ def update(id): if request.method == 'POST': title = request.form['title'] body = request.form['body'] + created = request.form['created'] error = None if not title: error = 'Title is required.' + if not created: + error = "Created is required." + if error is not None: flash(error) else: db = get_db() db.execute( - 'UPDATE post SET title = ?, body = ?' + 'UPDATE post SET title = ?, body = ?, created = ?' ' WHERE id = ?', - (title, body, id) + (title, body, created, id) ) db.commit() return redirect(url_for('blog.index')) diff --git a/flaskr/static/favicon.png b/flaskr/static/favicon.png index 910818c..b8cb77d 100644 Binary files a/flaskr/static/favicon.png and b/flaskr/static/favicon.png differ diff --git a/flaskr/static/style.css b/flaskr/static/style.css index a45a73b..89c7015 100644 --- a/flaskr/static/style.css +++ b/flaskr/static/style.css @@ -1,20 +1,30 @@ html { font-family: sans-serif; background: #eee; padding: 1rem; } body { max-width: 960px; margin: 0 auto; background: white; } h1 { font-family: serif; color: #377ba8; margin: 1rem 0; } +h2 { color: #377ba8; margin: 1rem 0; } +h3 { color: #377ba8; margin: 1rem 0; } +h4 { color: #377ba8; margin: 1rem 0; } +h5 { color: #377ba8; margin: 1rem 0; } +h6 { 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; } +nav ul li a, nav ul li span, header .action { display: block;} .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 > header h1 { margin-bottom: 0; } +.post > h2 { margin-bottom: 0; } +.post > h3 { margin-bottom: 0; } +.post > h4 { margin-bottom: 0; } +.post > h5 { margin-bottom: 0; } +.post > h6 { margin-bottom: 0; } .post .about { color: slategray; font-style: italic; } .post .body { white-space: pre-line; } .content:last-child { margin-bottom: 0; } diff --git a/flaskr/templates/base.html b/flaskr/templates/base.html index a2c26cd..ae63dea 100644 --- a/flaskr/templates/base.html +++ b/flaskr/templates/base.html @@ -1,16 +1,13 @@ -{% block title %}{% endblock %} - Flaskr +{% block title %}{% endblock %} - blogsparkinfinite diff --git a/flaskr/templates/blog/firehose.html b/flaskr/templates/blog/firehose.html new file mode 100644 index 0000000..21e3178 --- /dev/null +++ b/flaskr/templates/blog/firehose.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block header %} +

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

+ {% if g.user %} + New + {% endif %} +{% endblock %} + +{% block content %} + {% for post in posts %} +
+
+
+

{{ post['title'] }}

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

{{ post['body']|safe }}

+
+ {% if not loop.last %} +
+ {% endif %} + {% endfor %} +{% endblock %} diff --git a/flaskr/templates/blog/index.html b/flaskr/templates/blog/index.html index ffd9fbc..f50de7f 100644 --- a/flaskr/templates/blog/index.html +++ b/flaskr/templates/blog/index.html @@ -12,14 +12,13 @@
-

{{ post['title'] }}

+

{{ post['title'] }}

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

{{ post['body']|safe }}

{% if not loop.last %}
diff --git a/flaskr/templates/blog/post.html b/flaskr/templates/blog/post.html new file mode 100644 index 0000000..c2a612c --- /dev/null +++ b/flaskr/templates/blog/post.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block header %} +

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

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

{{ post['body']|safe }}

+
+{% endblock %} diff --git a/flaskr/templates/blog/update.html b/flaskr/templates/blog/update.html index af66322..6ba221f 100644 --- a/flaskr/templates/blog/update.html +++ b/flaskr/templates/blog/update.html @@ -11,6 +11,9 @@ value="{{ request.form['title'] or post['title'] }}" required> + +
diff --git a/requirements.txt b/requirements.txt index 3f319fd..055a19a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,11 +8,11 @@ dnspython==2.3.0 email-validator==2.0.0.post2 exceptiongroup==1.1.1 Flask==2.3.2 --e git+https://gitlab.com/pvtejas/based4tech.git@4be89bd767a7c5a84ab62fbf4ad924ae1af077f1#egg=flaskr +# -e git+https://gitlab.com/pvtejas/based4tech.git@4be89bd767a7c5a84ab62fbf4ad924ae1af077f1#egg=flaskr gunicorn==20.1.0 h11==0.14.0 httpcore==0.17.0 -httptools==0.5.0 +httptools httpx==0.24.0 idna==3.4 iniconfig==2.0.0 @@ -20,20 +20,20 @@ itsdangerous==2.1.2 Jinja2==3.1.2 Markdown==3.4.3 MarkupSafe==2.1.2 -orjson==3.8.11 +orjson packaging==23.1 pluggy==1.0.0 pyproject_hooks==1.0.0 pytest==7.3.2 python-dotenv==1.0.0 python-multipart==0.0.6 -PyYAML==6.0 +# PyYAML==6.0 sniffio==1.3.0 tomli==2.0.1 typing_extensions==4.5.0 -ujson==5.7.0 +ujson uvicorn==0.22.0 -uvloop==0.17.0 +uvloop watchfiles==0.19.0 websockets==11.0.3 Werkzeug==2.3.3 diff --git a/tests/conftest.py b/tests/conftest.py index 2db6e29..836a5ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ def app(): app = create_app({ 'TESTING': True, 'DATABASE': db_path, + 'ALLOW_REGISTER': True, }) with app.app_context(): diff --git a/tests/test_auth.py b/tests/test_auth.py index d9b9b9e..1f45590 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -14,6 +14,10 @@ def test_register(client, app): "SELECT * FROM user WHERE USERNAME = 'a'", ).fetchone() is not None + app.config["ALLOW_REGISTER"] = False + response = client.get('/auth/register') + assert b"Admin only" in response.data + @pytest.mark.parametrize(('username', 'password', 'message'), ( ('', '', b'Username is required.'), ('a', '', b'Password is required.'), diff --git a/tests/test_blog.py b/tests/test_blog.py index 2479428..548363a 100644 --- a/tests/test_blog.py +++ b/tests/test_blog.py @@ -3,17 +3,37 @@ from flaskr.db import get_db def test_index(client, auth): response = client.get('/') - assert b"Log In" in response.data - assert b"Register" in response.data + assert b"Log In" not in response.data + assert b"Register" not in response.data auth.login() response = client.get('/') assert b'Log Out' in response.data assert b'test title' in response.data assert b'by test on 2018-01-01' in response.data + assert b'test\nbody' not in response.data + assert b'href="/1/update"' in response.data + assert b'href="/1/"' in response.data + +def test_firehose(client, auth): + response = client.get('/') + assert b"Log In" not in response.data + assert b"Register" not in response.data + + auth.login() + response = client.get('/firehose') + assert b'Log Out' in response.data + assert b'test title' in response.data + assert b'by test on 2018-01-01' in response.data assert b'test\nbody' in response.data assert b'href="/1/update"' in response.data +def test_individual_page(client, auth): + response = client.get('/1/') + assert b'test title' in response.data + assert b'by test on 2018-01-01' in response.data + assert b'test\nbody' in response.data + @pytest.mark.parametrize('path', ( '/create', '/1/update', @@ -58,7 +78,7 @@ def test_create(client, auth, app): def test_update(client, auth, app): auth.login() assert client.get('/1/update').status_code == 200 - client.post('/1/update', data={'title': 'updated', 'body': ''}) + client.post('/1/update', data={'title': 'updated', 'body': '', 'created': '1970-01-01 00:00:00'}) with app.app_context(): db = get_db() @@ -71,7 +91,7 @@ def test_update(client, auth, app): )) def test_create_update_validate(client, auth, path): auth.login() - response = client.post(path, data={'title': '', 'body': ''}) + response = client.post(path, data={'title': '', 'body': '', 'created': '1970-01-01 00:00:00'}) assert b'Title is required.' in response.data def test_delete(client, auth, app):