diff --git a/flaskr/__init__.py b/flaskr/__init__.py index 7d7ec81..33779f8 100644 --- a/flaskr/__init__.py +++ b/flaskr/__init__.py @@ -8,7 +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=False, + ALLOW_REGISTER=True, ) app.wsgi_app = ProxyFix( diff --git a/flaskr/blog.py b/flaskr/blog.py index 523cfaf..61b0715 100644 --- a/flaskr/blog.py +++ b/flaskr/blog.py @@ -5,6 +5,7 @@ from werkzeug.exceptions import abort from flaskr.auth import login_required from flaskr.db import get_db +from flaskr.generate_rss import generate_feed import markdown import datetime @@ -28,6 +29,23 @@ def index(): 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(): @@ -49,6 +67,7 @@ def create(): (title, body, g.user['id']) ) db.commit() + generate_feed(db) return redirect(url_for('blog.index')) return render_template('blog/create.html') @@ -69,6 +88,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): @@ -96,6 +121,7 @@ def update(id): (title, body, created, id) ) db.commit() + generate_feed(db) return redirect(url_for('blog.index')) return render_template('blog/update.html', post=post) @@ -108,3 +134,7 @@ def delete(id): db.execute('DELETE FROM post WHERE id = ?',(id,)) db.commit() return redirect(url_for('blog.index')) + +@bp.route('/rss/', methods=('GET',)) +def redirect_rss(): + return redirect(url_for('static', filename='rss.xml')) diff --git a/flaskr/generate_rss.py b/flaskr/generate_rss.py new file mode 100644 index 0000000..b06327b --- /dev/null +++ b/flaskr/generate_rss.py @@ -0,0 +1,69 @@ +from datetime import datetime +import sqlite3 + +from bs4 import BeautifulSoup +from markdown import markdown + +def soup_append_multiple_tags(soup: BeautifulSoup, children: dict) -> None: + for name, string in children.items(): + soup.append(soup.new_tag(name)) + soup.find_all(name)[-1].string = string + +def post_find_first_plain_line(post: str) -> str: + while post: + if post[0].isalnum(): + break + end = post.find("\n") + post = post[end+1:] + end = post.find("\n") + return post[:end] + +def generate_feed(db): + soup = BeautifulSoup(features="xml") + + soup.append(soup.new_tag("rss", version="2.0")) + rss = soup.rss + rss["xmlns:atom"] = "http://www.w3.org/2005/Atom" + rss["xmlns:content"] = "http://purl.org/rss/1.0/modules/content/" + + rss.append(soup.new_tag("channel")) + channel = rss.channel + + channel_children = { + "description": "Blog posts from allsparkinfinite's blog", + "link": "https://blog.allsparkinfinite.name", + "title": "blogsparkinfinite", + "docs": "https://rssboard.org/rss-specification", + "language": "en-IN", + "lastBuildDate": datetime.now().strftime("%a, %d %b %Y %T +0530"), + "ttl": "240", + } + soup_append_multiple_tags(channel, channel_children) + + channel.append(channel.new_tag("atom:link")) + channel.find_all("atom:link")[-1]["href"] = "https://blog.allsparkinfinite.name/rss/" + channel.find_all("atom:link")[-1]["rel"] = "self" + channel.find_all("atom:link")[-1]["type"] = "application/rss+xml" + + channel.append(channel.new_tag("atom:link")) + channel.find_all("atom:link")[-1]["href"] = "https://blog.allsparkinfinite.name/" + channel.find_all("atom:link")[-1]["rel"] = "alternate" + channel.find_all("atom:link")[-1]["type"] = "text/html" + + posts = db.execute("SELECT * FROM post").fetchall() + posts.reverse() + + for post in posts: + channel.append(soup.new_tag("item")) + item = channel.find_all("item")[-1] + item_children = { + "description": post_find_first_plain_line(post[4]), + "guid": f"https://blog.allsparkinfinite.name/{post[0]}/", + "pubDate": post[2].strftime("%a, %d %b %Y %T GMT"), + "title": post[3], + "content:encoded": markdown(post[4]), + } + soup_append_multiple_tags(item, item_children) + + with open("flaskr/static/rss.xml", "w") as f: + f.write(soup.prettify()) diff --git a/flaskr/static/style.css b/flaskr/static/style.css index 63f3f5b..dd1a44d 100644 --- a/flaskr/static/style.css +++ b/flaskr/static/style.css @@ -12,7 +12,8 @@ nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5r 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 { display: block; padding: 0.5rem;} +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; } @@ -34,3 +35,4 @@ nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } .content textarea { min-height: 12em; resize: vertical; } input.danger { color: #cc2f2e; } input[type=submit] { align-self: start; min-width: 10em; } +pre { white-space: pre-wrap; } \ No newline at end of file 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/tests/test_blog.py b/tests/test_blog.py index 810ade1..548363a 100644 --- a/tests/test_blog.py +++ b/tests/test_blog.py @@ -11,9 +11,29 @@ def test_index(client, auth): 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',