diff --git a/rophako/app.py b/rophako/app.py
index adcf9e2..e72723b 100644
--- a/rophako/app.py
+++ b/rophako/app.py
@@ -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
diff --git a/rophako/model/blog.py b/rophako/model/blog.py
index 8a6c501..ca6f28f 100644
--- a/rophako/model/blog.py
+++ b/rophako/model/blog.py
@@ -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):
diff --git a/rophako/modules/admin/templates/admin/index.html b/rophako/modules/admin/templates/admin/index.html
index c1d043a..7ca9d9f 100644
--- a/rophako/modules/admin/templates/admin/index.html
+++ b/rophako/modules/admin/templates/admin/index.html
@@ -5,6 +5,16 @@
diff --git a/rophako/modules/blog/__init__.py b/rophako/modules/blog/__init__.py
index 44c105f..64f2d43 100644
--- a/rophako/modules/blog/__init__.py
+++ b/rophako/modules/blog/__init__.py
@@ -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/")
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
))
diff --git a/rophako/modules/blog/templates/blog/drafts.html b/rophako/modules/blog/templates/blog/drafts.html
new file mode 100644
index 0000000..6040b4a
--- /dev/null
+++ b/rophako/modules/blog/templates/blog/drafts.html
@@ -0,0 +1,9 @@
+{% extends "layout.html" %}
+{% block title %}Draft Entries{% endblock %}
+{% block content %}
+
+