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 @@

Admin Center

+

Blog

+ + + +

Users

+ 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 %} + +

Draft Entries

+ +{{ include_page("blog.partial_index", mode="drafts") | safe }} + +{% endblock %} diff --git a/rophako/modules/blog/templates/blog/entry.inc.html b/rophako/modules/blog/templates/blog/entry.inc.html index 33bc672..8f43cef 100644 --- a/rophako/modules/blog/templates/blog/entry.inc.html +++ b/rophako/modules/blog/templates/blog/entry.inc.html @@ -25,6 +25,15 @@
+ {% if post["privacy"] == "private" %} + [Private] + {% elif post["privacy"] == "draft" %} + [Draft] + {% endif %} + {% if post["sticky"] %} + [Sticky] + {% endif %} + Posted by {{ post["profile"]["name"] }} on {{ post["pretty_time"] }}
@@ -65,6 +74,14 @@ Blog {% endif %} + {% if post["privacy"] == "private" %} + | Private posts + {% endif %} + + {% if post["privacy"] == "draft" %} + | Drafts + {% endif %} + {% if session["login"] %} | Edit diff --git a/rophako/modules/blog/templates/blog/nav-links.inc.html b/rophako/modules/blog/templates/blog/nav-links.inc.html index 23347aa..431c074 100644 --- a/rophako/modules/blog/templates/blog/nav-links.inc.html +++ b/rophako/modules/blog/templates/blog/nav-links.inc.html @@ -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 %}
[ RSS Feed | @@ -8,7 +17,7 @@ {% if category %} < Newer {% else %} - < Newer + < Newer {% endif %} {% if can_older %} | {% endif %} @@ -18,10 +27,10 @@ {% if category %} Older > {% else %} - Older > + Older > {% endif %} {% endif %} ]
-{% endif %} \ No newline at end of file +{% endif %} diff --git a/rophako/modules/blog/templates/blog/private.html b/rophako/modules/blog/templates/blog/private.html new file mode 100644 index 0000000..003c1a2 --- /dev/null +++ b/rophako/modules/blog/templates/blog/private.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} +{% block title %}Draft Entries{% endblock %} +{% block content %} + +

Private Entries

+ +{{ include_page("blog.partial_index", mode="private") | safe }} + +{% endblock %} diff --git a/rophako/modules/blog/templates/blog/update.html b/rophako/modules/blog/templates/blog/update.html index 9765db5..bf6c8c6 100644 --- a/rophako/modules/blog/templates/blog/update.html +++ b/rophako/modules/blog/templates/blog/update.html @@ -56,12 +56,19 @@ +

Options:
+

- Time Stamp:
- / - / - @ - : - : -
- mm / dd / yyyy @ hh:mm:ss
+ + {% if post_id != "" %} + Reset Time Stamp:

+ {% endif %}