Merge pull request #2 from kirsle/feature/blog-drafts-and-privacy
Many blog improvements
This commit is contained in:
commit
646620fa92
|
@ -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
|
||||
|
|
|
@ -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"]:
|
||||
# Filter out posts that shouldn't be visible (draft/private)
|
||||
posts = list(db.keys())
|
||||
for post_id in posts:
|
||||
if db[post_id]["privacy"] == "private":
|
||||
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):
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
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
|
||||
))
|
||||
|
|
9
rophako/modules/blog/templates/blog/drafts.html
Normal file
9
rophako/modules/blog/templates/blog/drafts.html
Normal 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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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 }}">< Newer</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('blog.index') }}?skip={{ earlier }}">< Newer</a>
|
||||
<a href="{{ url_for(blog_index) }}?skip={{ earlier }}">< Newer</a>
|
||||
{% endif %}
|
||||
|
||||
{% if can_older %} | {% endif %}
|
||||
|
@ -18,7 +27,7 @@
|
|||
{% if category %}
|
||||
<a href="{{ url_for('blog.category', category=category) }}?skip={{ older }}">Older ></a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('blog.index') }}?skip={{ older }}">Older ></a>
|
||||
<a href="{{ url_for(blog_index) }}?skip={{ older }}">Older ></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
|
9
rophako/modules/blog/templates/blog/private.html
Normal file
9
rophako/modules/blog/templates/blog/private.html
Normal 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 %}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user