@@ -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"]: | |||
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): | |||
@@ -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. | |||
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 | |||
)) | |||
@@ -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,10 +27,10 @@ | |||
{% 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 %} | |||
] | |||
</div> | |||
{% endif %} | |||
{% endif %} |
@@ -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> | |||