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")) \
.decode(string_escape)
# Make templates easier to edit live.
app.config["TEMPLATES_AUTO_RELOAD"] = True
# Security?
if Config.security.force_ssl == True:
app.config['SESSION_COOKIE_SECURE'] = True

View File

@ -18,7 +18,7 @@ from rophako.settings import Config
import rophako.jsondb as JsonDB
from rophako.log import logger
def get_index():
def get_index(drafts=False):
"""Get the blog index.
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?
@ -44,16 +48,67 @@ def get_index():
return rebuild_index()
db = JsonDB.get("blog/index")
# Hide any private posts if we aren't logged in.
if not g.info["session"]["login"]:
posts = list(db.keys())
for post_id in posts:
if db[post_id]["privacy"] == "private":
# Filter out posts that shouldn't be visible (draft/private)
posts = list(db.keys())
for post_id in posts:
privacy = db[post_id]["privacy"]
# 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]
# Private posts are only visible to logged in users.
elif privacy == "private" and not g.info["session"]["login"]:
del db[post_id]
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():
"""Rebuild the index.json if it goes missing."""
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
* commit: Write the DB after updating the index (default True)"""
if index is None:
index = get_index()
index = get_index(drafts=True)
index[post_id] = dict(
fid = post["fid"],
time = post["time"] or int(time.time()),
categories = post["categories"],
sticky = False, # TODO
sticky = post["sticky"],
author = post["author"],
privacy = post["privacy"] or "public",
subject = post["subject"],
@ -125,11 +180,11 @@ def get_entry(post_id):
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."""
# Fetch the index.
index = get_index()
index = get_index(drafts=True)
# Editing an existing post?
if not post_id:
@ -180,7 +235,7 @@ def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
ip = ip,
time = epoch or int(time.time()),
categories = categories,
sticky = False, # TODO: implement sticky
sticky = sticky,
comments = comments,
emoticons = emoticons,
avatar = avatar,
@ -203,7 +258,7 @@ def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
def delete_entry(post_id):
"""Remove a blog entry."""
# Fetch the blog information.
index = get_index()
index = get_index(drafts=True)
post = get_entry(post_id)
if post is None:
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)
def resolve_id(fid):
"""Resolve a friendly ID to the blog ID number."""
index = get_index()
def resolve_id(fid, drafts=False):
"""Resolve a friendly ID to the blog ID number.
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 re.match(r'^\d+$', fid):

View File

@ -5,6 +5,16 @@
<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>
<li><a href="{{ url_for('admin.users') }}">View and Manage Users</a></li>
</ul>

View File

@ -80,13 +80,24 @@ def category(category):
g.info["url_category"] = category
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>")
def entry(fid):
"""Endpoint to view a specific blog entry."""
# 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:
flash("That blog post wasn't found.")
return redirect(url_for(".index"))
@ -165,21 +176,16 @@ def update():
avatar="",
categories="",
privacy=Config.blog.default_privacy,
sticky=False,
emoticons=True,
comments=Config.blog.allow_comments,
month="",
day="",
year="",
hour="",
min="",
sec="",
preview=False,
))
# Editing an existing post?
post_id = request.args.get("id", None)
if post_id:
post_id = Blog.resolve_id(post_id)
post_id = Blog.resolve_id(post_id, drafts=True)
if post_id:
logger.info("Editing existing blog post {}".format(post_id))
post = Blog.get_entry(post_id)
@ -187,22 +193,11 @@ def update():
g.info["post"] = post
# Copy fields.
for field in ["author", "fid", "subject", "format", "format",
for field in ["author", "fid", "subject", "time", "format",
"body", "avatar", "categories", "privacy",
"emoticons", "comments"]:
"sticky", "emoticons", "comments"]:
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?
if request.method == "POST":
action = request.form.get("action")
@ -211,10 +206,9 @@ def update():
g.info["post_id"] = request.form.get("id")
for field in ["fid", "subject", "format", "body", "avatar", "categories", "privacy"]:
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
for number in ["author", "month", "day", "year", "hour", "min", "sec"]:
g.info[number] = int(request.form.get(number, 0))
g.info["author"] = int(g.info["author"])
# What action are they doing?
if action == "preview":
@ -240,20 +234,11 @@ def update():
invalid = True
flash("You must enter a subject for your blog post.")
# Make sure the times are valid.
date = None
try:
date = datetime.datetime(
g.info["year"],
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))
# Resetting the post's time stamp?
if not request.form.get("id") or request.form.get("reset-time"):
g.info["time"] = float(time.time())
else:
g.info["time"] = float(request.form.get("time", time.time()))
# Format the categories.
tags = []
@ -262,12 +247,9 @@ def update():
# Okay to update?
if invalid is False:
# Convert the date into a Unix time stamp.
epoch = float(date.strftime("%s"))
new_id, new_fid = Blog.post_entry(
post_id = g.info["post_id"],
epoch = epoch,
epoch = g.info["time"],
author = g.info["author"],
subject = g.info["subject"],
fid = g.info["fid"],
@ -276,6 +258,7 @@ def update():
privacy = g.info["privacy"],
ip = remote_addr(),
emoticons = g.info["emoticons"],
sticky = g.info["sticky"],
comments = g.info["comments"],
format = g.info["format"],
body = g.info["body"],
@ -297,7 +280,7 @@ def delete():
post_id = request.args.get("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:
flash("That blog post wasn't found.")
return redirect(url_for(".index"))
@ -408,11 +391,30 @@ def xml_add_text_tags(doc, root_node, tags):
root_node.appendChild(channelTag)
def partial_index(template_name="blog/index.inc.html"):
"""Partial template for including the index view of the blog."""
def partial_index(template_name="blog/index.inc.html", mode="normal"):
"""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.
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.
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)
if 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["count"] = 0
@ -530,7 +532,7 @@ def partial_tags():
has_small = False
for tag in sort_tags:
result.append(dict(
category=tag,
category=tag if len(tag) else Config.blog.default_category,
count=tags[tag],
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 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"] }}
on <span title="{{ post['time'] }}">{{ post["pretty_time"] }}</span>
</div>
@ -65,6 +74,14 @@
<a href="{{ url_for('blog.index') }}">Blog</a>
{% 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"] %}
|
<a href="{{ url_for('blog.update', id=post['post_id']) }}">Edit</a>

View File

@ -1,6 +1,15 @@
{# 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">
[
<a href="{{ url_for('blog.rss') }}">RSS Feed</a> |
@ -8,7 +17,7 @@
{% if category %}
<a href="{{ url_for('blog.category', category=category) }}?skip={{ earlier }}">&lt; Newer</a>
{% else %}
<a href="{{ url_for('blog.index') }}?skip={{ earlier }}">&lt; Newer</a>
<a href="{{ url_for(blog_index) }}?skip={{ earlier }}">&lt; Newer</a>
{% endif %}
{% if can_older %} | {% endif %}
@ -18,10 +27,10 @@
{% if category %}
<a href="{{ url_for('blog.category', category=category) }}?skip={{ older }}">Older &gt;</a>
{% 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 %}
]
</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 %}>
Public: everybody can see this blog entry
</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 %}>
Private: only site admins can see this blog entry
</option>
</select><p>
<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>
<input type="checkbox" name="emoticons" value="true"{% if emoticons %} checked{% endif %}>
Enable graphical emoticons
@ -71,18 +78,14 @@
Enable comments on this entry
</label><p>
<strong>Time Stamp:</strong><br>
<input type="text" class="form-control input-sm inline" size="2" name="month" id="month" value="{{ month }}"> /
<input type="text" class="form-control input-sm inline" size="2" name="day" id="day" value="{{ day }}"> /
<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>
<input type="hidden" name="time" value="{{ time }}">
{% if post_id != "" %}
<strong>Reset Time Stamp:</strong><br>
<label>
<input type="checkbox" id="autoup" value="yes"{% if post_id == "" %} checked{% endif %}>
Automatically update
<input type="checkbox" name="reset-time" value="yes"{% if post_id == "" %} checked{% endif %}>
Reset the post's time stamp to the current time.
</label><p>
{% endif %}
<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>