Implement blog drafts, privacy, sticky and bug fixes

This commit is contained in:
Noah 2016-06-03 14:44:34 -07:00
parent f1f69552d4
commit 91c7e3369e
9 changed files with 199 additions and 77 deletions

View File

@ -43,6 +43,9 @@ app.DEBUG = Config.site.debug == "true"
app.secret_key = bytes(Config.security.secret_key.encode("utf-8")) \ app.secret_key = bytes(Config.security.secret_key.encode("utf-8")) \
.decode(string_escape) .decode(string_escape)
# Make templates easier to edit live.
app.config["TEMPLATES_AUTO_RELOAD"] = True
# Security? # Security?
if Config.security.force_ssl == True: if Config.security.force_ssl == True:
app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_SECURE'] = True

View File

@ -18,7 +18,7 @@ from rophako.settings import Config
import rophako.jsondb as JsonDB import rophako.jsondb as JsonDB
from rophako.log import logger from rophako.log import logger
def get_index(): def get_index(drafts=False):
"""Get the blog index. """Get the blog index.
The index is the cache of available blog posts. It has the format: The index is the cache of available blog posts. It has the format:
@ -37,6 +37,10 @@ def get_index():
... ...
} }
``` ```
Args:
drafts (bool): Whether to allow draft posts to be included in the index
(for logged-in users only).
""" """
# Index doesn't exist? # Index doesn't exist?
@ -44,16 +48,67 @@ def get_index():
return rebuild_index() return rebuild_index()
db = JsonDB.get("blog/index") db = JsonDB.get("blog/index")
# Hide any private posts if we aren't logged in. # Filter out posts that shouldn't be visible (draft/private)
if not g.info["session"]["login"]: posts = list(db.keys())
posts = list(db.keys()) for post_id in posts:
for post_id in posts: privacy = db[post_id]["privacy"]
if db[post_id]["privacy"] == "private":
# Drafts are hidden universally so they can't be seen on any of the
# normal blog routes.
if privacy == "draft":
if drafts is False or not g.info["session"]["login"]:
del db[post_id] del db[post_id]
# Private posts are only visible to logged in users.
elif privacy == "private" and not g.info["session"]["login"]:
del db[post_id]
return db return db
def get_drafts():
"""Get the draft blog posts.
Drafts are hidden from all places of the blog, just like private posts are
(for non-logged-in users), so get_index() skips drafts and therefore
resolve_id, etc. does too, making them invisible on the normal blog pages.
This function is like get_index() except it *only* returns the drafts.
"""
# Index doesn't exist?
if not JsonDB.exists("blog/index"):
return rebuild_index()
db = JsonDB.get("blog/index")
# Filter out only the draft posts.
return {
key: data for key, data in db.items() if data["privacy"] == "draft"
}
def get_private():
"""Get only the private blog posts.
Since you can view only drafts, it made sense to have an easy way to view
only private posts, too.
This function is like get_index() except it *only* returns the private
posts. It doesn't check for logged-in users, because the routes that view
all private posts are login_required anyway.
"""
# Index doesn't exist?
if not JsonDB.exists("blog/index"):
return rebuild_index()
db = JsonDB.get("blog/index")
# Filter out only the draft posts.
return {
key: data for key, data in db.items() if data["privacy"] == "private"
}
def rebuild_index(): def rebuild_index():
"""Rebuild the index.json if it goes missing.""" """Rebuild the index.json if it goes missing."""
index = {} index = {}
@ -76,13 +131,13 @@ def update_index(post_id, post, index=None, commit=True):
* index: If you already have the index open, you can pass it here * index: If you already have the index open, you can pass it here
* commit: Write the DB after updating the index (default True)""" * commit: Write the DB after updating the index (default True)"""
if index is None: if index is None:
index = get_index() index = get_index(drafts=True)
index[post_id] = dict( index[post_id] = dict(
fid = post["fid"], fid = post["fid"],
time = post["time"] or int(time.time()), time = post["time"] or int(time.time()),
categories = post["categories"], categories = post["categories"],
sticky = False, # TODO sticky = post["sticky"],
author = post["author"], author = post["author"],
privacy = post["privacy"] or "public", privacy = post["privacy"] or "public",
subject = post["subject"], subject = post["subject"],
@ -125,11 +180,11 @@ def get_entry(post_id):
def post_entry(post_id, fid, epoch, author, subject, avatar, categories, def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
privacy, ip, emoticons, comments, format, body): privacy, ip, emoticons, sticky, comments, format, body):
"""Post (or update) a blog entry.""" """Post (or update) a blog entry."""
# Fetch the index. # Fetch the index.
index = get_index() index = get_index(drafts=True)
# Editing an existing post? # Editing an existing post?
if not post_id: if not post_id:
@ -180,7 +235,7 @@ def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
ip = ip, ip = ip,
time = epoch or int(time.time()), time = epoch or int(time.time()),
categories = categories, categories = categories,
sticky = False, # TODO: implement sticky sticky = sticky,
comments = comments, comments = comments,
emoticons = emoticons, emoticons = emoticons,
avatar = avatar, avatar = avatar,
@ -203,7 +258,7 @@ def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
def delete_entry(post_id): def delete_entry(post_id):
"""Remove a blog entry.""" """Remove a blog entry."""
# Fetch the blog information. # Fetch the blog information.
index = get_index() index = get_index(drafts=True)
post = get_entry(post_id) post = get_entry(post_id)
if post is None: if post is None:
logger.warning("Can't delete post {}, it doesn't exist!".format(post_id)) logger.warning("Can't delete post {}, it doesn't exist!".format(post_id))
@ -216,9 +271,14 @@ def delete_entry(post_id):
JsonDB.commit("blog/index", index) JsonDB.commit("blog/index", index)
def resolve_id(fid): def resolve_id(fid, drafts=False):
"""Resolve a friendly ID to the blog ID number.""" """Resolve a friendly ID to the blog ID number.
index = get_index()
Args:
drafts (bool): Whether to allow draft IDs to be resolved (for
logged-in users only).
"""
index = get_index(drafts=drafts)
# If the ID is all numeric, it's the blog post ID directly. # If the ID is all numeric, it's the blog post ID directly.
if re.match(r'^\d+$', fid): if re.match(r'^\d+$', fid):

View File

@ -5,6 +5,16 @@
<h1>Admin Center</h1> <h1>Admin Center</h1>
<h2>Blog</h2>
<ul>
<li><a href="{{ url_for('blog.update') }}">Post a new blog entry</a></li>
<li><a href="{{ url_for('blog.drafts') }}">View draft entries</a></li>
<li><a href="{{ url_for('blog.private') }}">View private entries</a></li>
</ul>
<h2>Users</h2>
<ul> <ul>
<li><a href="{{ url_for('admin.users') }}">View and Manage Users</a></li> <li><a href="{{ url_for('admin.users') }}">View and Manage Users</a></li>
</ul> </ul>

View File

@ -80,13 +80,24 @@ def category(category):
g.info["url_category"] = category g.info["url_category"] = category
return template("blog/index.html") return template("blog/index.html")
@mod.route("/drafts")
@login_required
def drafts():
"""View all of the draft blog posts."""
return template("blog/drafts.html")
@mod.route("/private")
@login_required
def private():
"""View all of the blog posts marked as private."""
return template("blog/private.html")
@mod.route("/entry/<fid>") @mod.route("/entry/<fid>")
def entry(fid): def entry(fid):
"""Endpoint to view a specific blog entry.""" """Endpoint to view a specific blog entry."""
# Resolve the friendly ID to a real ID. # Resolve the friendly ID to a real ID.
post_id = Blog.resolve_id(fid) post_id = Blog.resolve_id(fid, drafts=True)
if not post_id: if not post_id:
flash("That blog post wasn't found.") flash("That blog post wasn't found.")
return redirect(url_for(".index")) return redirect(url_for(".index"))
@ -165,21 +176,16 @@ def update():
avatar="", avatar="",
categories="", categories="",
privacy=Config.blog.default_privacy, privacy=Config.blog.default_privacy,
sticky=False,
emoticons=True, emoticons=True,
comments=Config.blog.allow_comments, comments=Config.blog.allow_comments,
month="",
day="",
year="",
hour="",
min="",
sec="",
preview=False, preview=False,
)) ))
# Editing an existing post? # Editing an existing post?
post_id = request.args.get("id", None) post_id = request.args.get("id", None)
if post_id: if post_id:
post_id = Blog.resolve_id(post_id) post_id = Blog.resolve_id(post_id, drafts=True)
if post_id: if post_id:
logger.info("Editing existing blog post {}".format(post_id)) logger.info("Editing existing blog post {}".format(post_id))
post = Blog.get_entry(post_id) post = Blog.get_entry(post_id)
@ -187,22 +193,11 @@ def update():
g.info["post"] = post g.info["post"] = post
# Copy fields. # Copy fields.
for field in ["author", "fid", "subject", "format", "format", for field in ["author", "fid", "subject", "time", "format",
"body", "avatar", "categories", "privacy", "body", "avatar", "categories", "privacy",
"emoticons", "comments"]: "sticky", "emoticons", "comments"]:
g.info[field] = post[field] g.info[field] = post[field]
# Dissect the time.
date = datetime.datetime.fromtimestamp(post["time"])
g.info.update(dict(
month="{:02d}".format(date.month),
day="{:02d}".format(date.day),
year=date.year,
hour="{:02d}".format(date.hour),
min="{:02d}".format(date.minute),
sec="{:02d}".format(date.second),
))
# Are we SUBMITTING the form? # Are we SUBMITTING the form?
if request.method == "POST": if request.method == "POST":
action = request.form.get("action") action = request.form.get("action")
@ -211,10 +206,9 @@ def update():
g.info["post_id"] = request.form.get("id") g.info["post_id"] = request.form.get("id")
for field in ["fid", "subject", "format", "body", "avatar", "categories", "privacy"]: for field in ["fid", "subject", "format", "body", "avatar", "categories", "privacy"]:
g.info[field] = request.form.get(field) g.info[field] = request.form.get(field)
for boolean in ["emoticons", "comments"]: for boolean in ["sticky", "emoticons", "comments"]:
g.info[boolean] = True if request.form.get(boolean, None) == "true" else False g.info[boolean] = True if request.form.get(boolean, None) == "true" else False
for number in ["author", "month", "day", "year", "hour", "min", "sec"]: g.info["author"] = int(g.info["author"])
g.info[number] = int(request.form.get(number, 0))
# What action are they doing? # What action are they doing?
if action == "preview": if action == "preview":
@ -240,20 +234,11 @@ def update():
invalid = True invalid = True
flash("You must enter a subject for your blog post.") flash("You must enter a subject for your blog post.")
# Make sure the times are valid. # Resetting the post's time stamp?
date = None if not request.form.get("id") or request.form.get("reset-time"):
try: g.info["time"] = float(time.time())
date = datetime.datetime( else:
g.info["year"], g.info["time"] = float(request.form.get("time", time.time()))
g.info["month"],
g.info["day"],
g.info["hour"],
g.info["min"],
g.info["sec"],
)
except ValueError as e:
invalid = True
flash("Invalid date/time: " + str(e))
# Format the categories. # Format the categories.
tags = [] tags = []
@ -262,12 +247,9 @@ def update():
# Okay to update? # Okay to update?
if invalid is False: if invalid is False:
# Convert the date into a Unix time stamp.
epoch = float(date.strftime("%s"))
new_id, new_fid = Blog.post_entry( new_id, new_fid = Blog.post_entry(
post_id = g.info["post_id"], post_id = g.info["post_id"],
epoch = epoch, epoch = g.info["time"],
author = g.info["author"], author = g.info["author"],
subject = g.info["subject"], subject = g.info["subject"],
fid = g.info["fid"], fid = g.info["fid"],
@ -276,6 +258,7 @@ def update():
privacy = g.info["privacy"], privacy = g.info["privacy"],
ip = remote_addr(), ip = remote_addr(),
emoticons = g.info["emoticons"], emoticons = g.info["emoticons"],
sticky = g.info["sticky"],
comments = g.info["comments"], comments = g.info["comments"],
format = g.info["format"], format = g.info["format"],
body = g.info["body"], body = g.info["body"],
@ -297,7 +280,7 @@ def delete():
post_id = request.args.get("id") post_id = request.args.get("id")
# Resolve the post ID. # Resolve the post ID.
post_id = Blog.resolve_id(post_id) post_id = Blog.resolve_id(post_id, drafts=True)
if not post_id: if not post_id:
flash("That blog post wasn't found.") flash("That blog post wasn't found.")
return redirect(url_for(".index")) return redirect(url_for(".index"))
@ -408,11 +391,30 @@ def xml_add_text_tags(doc, root_node, tags):
root_node.appendChild(channelTag) root_node.appendChild(channelTag)
def partial_index(template_name="blog/index.inc.html"): def partial_index(template_name="blog/index.inc.html", mode="normal"):
"""Partial template for including the index view of the blog.""" """Partial template for including the index view of the blog.
Args:
template_name (str): The name of the template to be rendered.
mode (str): The view mode of the posts, one of:
- normal: Only list public entries, or private posts for users
who are logged in.
- drafts: Only list draft entries for logged-in users.
"""
# Get the blog index. # Get the blog index.
index = Blog.get_index() if mode == "normal":
index = Blog.get_index()
elif mode == "drafts":
index = Blog.get_drafts()
elif mode == "private":
index = Blog.get_private()
else:
return "Invalid partial_index mode."
# Let the pages know what mode they're in.
g.info["mode"] = mode
pool = {} # The set of blog posts to show. pool = {} # The set of blog posts to show.
category = g.info.get("url_category", None) category = g.info.get("url_category", None)
@ -449,7 +451,7 @@ def partial_index(template_name="blog/index.inc.html"):
g.info["older"] = offset + int(Config.blog.entries_per_page) g.info["older"] = offset + int(Config.blog.entries_per_page)
if g.info["earlier"] < 0: if g.info["earlier"] < 0:
g.info["earlier"] = 0 g.info["earlier"] = 0
if g.info["older"] < 0 or g.info["older"] > len(posts): if g.info["older"] < 0 or g.info["older"] > len(posts) - 1:
g.info["older"] = 0 g.info["older"] = 0
g.info["count"] = 0 g.info["count"] = 0
@ -530,7 +532,7 @@ def partial_tags():
has_small = False has_small = False
for tag in sort_tags: for tag in sort_tags:
result.append(dict( result.append(dict(
category=tag, category=tag if len(tag) else Config.blog.default_category,
count=tags[tag], count=tags[tag],
small=tags[tag] < 3, # TODO: make this configurable small=tags[tag] < 3, # TODO: make this configurable
)) ))

View File

@ -0,0 +1,9 @@
{% extends "layout.html" %}
{% block title %}Draft Entries{% endblock %}
{% block content %}
<h1>Draft Entries</h1>
{{ include_page("blog.partial_index", mode="drafts") | safe }}
{% endblock %}

View File

@ -25,6 +25,15 @@
</div> </div>
<div class="blog-timestamp"> <div class="blog-timestamp">
{% if post["privacy"] == "private" %}
<span class="blog-entry-private">[Private]</span>
{% elif post["privacy"] == "draft" %}
<span class="blog-entry-draft">[Draft]</span>
{% endif %}
{% if post["sticky"] %}
<span class="blog-entry-sticky">[Sticky]</span>
{% endif %}
Posted by {{ post["profile"]["name"] }} Posted by {{ post["profile"]["name"] }}
on <span title="{{ post['time'] }}">{{ post["pretty_time"] }}</span> on <span title="{{ post['time'] }}">{{ post["pretty_time"] }}</span>
</div> </div>
@ -65,6 +74,14 @@
<a href="{{ url_for('blog.index') }}">Blog</a> <a href="{{ url_for('blog.index') }}">Blog</a>
{% endif %} {% endif %}
{% if post["privacy"] == "private" %}
| <a href="{{ url_for('blog.private') }}">Private posts</a>
{% endif %}
{% if post["privacy"] == "draft" %}
| <a href="{{ url_for('blog.drafts') }}">Drafts</a>
{% endif %}
{% if session["login"] %} {% if session["login"] %}
| |
<a href="{{ url_for('blog.update', id=post['post_id']) }}">Edit</a> <a href="{{ url_for('blog.update', id=post['post_id']) }}">Edit</a>

View File

@ -1,6 +1,15 @@
{# Older/Newer links #} {# Older/Newer links #}
{% if can_older or can_newer %} {# The relative blog index to link to #}
{% if mode == "drafts" %}
{% set blog_index = "blog.drafts" %}
{% elif mode == "private" %}
{% set blog_index = "blog.private" %}
{% else %}
{% set blog_index = "blog.index" %}
{% endif %}
{% if can_older or can_earlier %}
<div class="right"> <div class="right">
[ [
<a href="{{ url_for('blog.rss') }}">RSS Feed</a> | <a href="{{ url_for('blog.rss') }}">RSS Feed</a> |
@ -8,7 +17,7 @@
{% if category %} {% if category %}
<a href="{{ url_for('blog.category', category=category) }}?skip={{ earlier }}">&lt; Newer</a> <a href="{{ url_for('blog.category', category=category) }}?skip={{ earlier }}">&lt; Newer</a>
{% else %} {% else %}
<a href="{{ url_for('blog.index') }}?skip={{ earlier }}">&lt; Newer</a> <a href="{{ url_for(blog_index) }}?skip={{ earlier }}">&lt; Newer</a>
{% endif %} {% endif %}
{% if can_older %} | {% endif %} {% if can_older %} | {% endif %}
@ -18,10 +27,10 @@
{% if category %} {% if category %}
<a href="{{ url_for('blog.category', category=category) }}?skip={{ older }}">Older &gt;</a> <a href="{{ url_for('blog.category', category=category) }}?skip={{ older }}">Older &gt;</a>
{% else %} {% else %}
<a href="{{ url_for('blog.index') }}?skip={{ older }}">Older &gt;</a> <a href="{{ url_for(blog_index) }}?skip={{ older }}">Older &gt;</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
] ]
</div> </div>
{% endif %} {% endif %}

View File

@ -0,0 +1,9 @@
{% extends "layout.html" %}
{% block title %}Draft Entries{% endblock %}
{% block content %}
<h1>Private Entries</h1>
{{ include_page("blog.partial_index", mode="private") | safe }}
{% endblock %}

View File

@ -56,12 +56,19 @@
<option value="public"{% if privacy == "public" %} selected{% endif %}> <option value="public"{% if privacy == "public" %} selected{% endif %}>
Public: everybody can see this blog entry Public: everybody can see this blog entry
</option> </option>
<option value="draft"{% if privacy == "draft" %} selected{% endif %}>
Draft: don't show this on the blog anywhere
</option>
<option value="private"{% if privacy == "private" %} selected{% endif %}> <option value="private"{% if privacy == "private" %} selected{% endif %}>
Private: only site admins can see this blog entry Private: only site admins can see this blog entry
</option> </option>
</select><p> </select><p>
<strong>Options:</strong><br> <strong>Options:</strong><br>
<label>
<input type="checkbox" name="sticky" value="true"{% if sticky %} checked{% endif %}>
Make this post sticky (always on top)
</label><br>
<label> <label>
<input type="checkbox" name="emoticons" value="true"{% if emoticons %} checked{% endif %}> <input type="checkbox" name="emoticons" value="true"{% if emoticons %} checked{% endif %}>
Enable graphical emoticons Enable graphical emoticons
@ -71,18 +78,14 @@
Enable comments on this entry Enable comments on this entry
</label><p> </label><p>
<strong>Time Stamp:</strong><br> <input type="hidden" name="time" value="{{ time }}">
<input type="text" class="form-control input-sm inline" size="2" name="month" id="month" value="{{ month }}"> / {% if post_id != "" %}
<input type="text" class="form-control input-sm inline" size="2" name="day" id="day" value="{{ day }}"> / <strong>Reset Time Stamp:</strong><br>
<input type="text" class="form-control input-sm inline" size="4" name="year" id="year" value="{{ year }}"> @
<input type="text" class="form-control input-sm inline" size="2" name="hour" id="hour" value="{{ hour }}"> :
<input type="text" class="form-control input-sm inline" size="2" name="min" id="min" value="{{ min }}"> :
<input type="text" class="form-control input-sm inline" size="2" name="sec" id="sec" value="{{ sec }}"><br>
mm / dd / yyyy @ hh:mm:ss<br>
<label> <label>
<input type="checkbox" id="autoup" value="yes"{% if post_id == "" %} checked{% endif %}> <input type="checkbox" name="reset-time" value="yes"{% if post_id == "" %} checked{% endif %}>
Automatically update Reset the post's time stamp to the current time.
</label><p> </label><p>
{% endif %}
<button type="submit" class="btn btn-default" name="action" value="preview">Preview</button> <button type="submit" class="btn btn-default" name="action" value="preview">Preview</button>
<button type="submit" class="btn btn-primary" name="action" value="publish">Publish Entry</button> <button type="submit" class="btn btn-primary" name="action" value="publish">Publish Entry</button>