Add the web blog views/models/controllers
This commit is contained in:
parent
80c20ec87b
commit
f9c2481499
|
@ -32,3 +32,11 @@ REDIS_HOST = "localhost"
|
|||
REDIS_PORT = 6379
|
||||
REDIS_DB = 0
|
||||
REDIS_PREFIX = "rophako:"
|
||||
|
||||
# Blog settings
|
||||
BLOG_ENTRIES_PER_PAGE = 5 # Number of entries to show per page
|
||||
BLOG_ENTRIES_PER_RSS = 5 # The same, but for the RSS feed
|
||||
BLOG_DEFAULT_CATEGORY = "Uncategorized"
|
||||
BLOG_DEFAULT_PRIVACY = "public"
|
||||
BLOG_TIME_FORMAT = "%A, %B %d %Y @ %I:%M:%S %p" # "Weekday, Month dd yyyy @ hh:mm:ss AM"
|
||||
BLOG_ALLOW_COMMENTS = True
|
|
@ -17,8 +17,10 @@ app.secret_key = config.SECRET_KEY
|
|||
# Load all the blueprints!
|
||||
from rophako.modules.admin import mod as AdminModule
|
||||
from rophako.modules.account import mod as AccountModule
|
||||
from rophako.modules.blog import mod as BlogModule
|
||||
app.register_blueprint(AdminModule)
|
||||
app.register_blueprint(AccountModule)
|
||||
app.register_blueprint(BlogModule)
|
||||
|
||||
# Custom Jinja handler to support custom- and default-template folders for
|
||||
# rendering templates.
|
||||
|
@ -28,6 +30,7 @@ app.jinja_loader = jinja2.ChoiceLoader([
|
|||
])
|
||||
|
||||
app.jinja_env.globals["csrf_token"] = rophako.utils.generate_csrf_token
|
||||
app.jinja_env.globals["include_page"] = rophako.utils.include
|
||||
|
||||
|
||||
@app.before_request
|
||||
|
@ -85,7 +88,6 @@ def before_request():
|
|||
@app.context_processor
|
||||
def after_request():
|
||||
"""Called just before render_template. Inject g.info into the template vars."""
|
||||
g.info["time_elapsed"] = "%.03f" % (time.time() - g.info["time"])
|
||||
return g.info
|
||||
|
||||
|
||||
|
@ -100,14 +102,13 @@ def catchall(path):
|
|||
if os.path.isfile(abspath):
|
||||
return send_file(abspath)
|
||||
elif not "." in path and os.path.isfile(abspath + ".html"):
|
||||
return render_template(path + ".html")
|
||||
return rophako.utils.template(path + ".html")
|
||||
|
||||
return not_found("404")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
print "INDEX PAGE"
|
||||
return catchall("index")
|
||||
|
||||
|
||||
|
|
230
rophako/model/blog.py
Normal file
230
rophako/model/blog.py
Normal file
|
@ -0,0 +1,230 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Blog models."""
|
||||
|
||||
from flask import g
|
||||
import time
|
||||
import re
|
||||
import glob
|
||||
|
||||
import config
|
||||
import rophako.jsondb as JsonDB
|
||||
from rophako.log import logger
|
||||
|
||||
def get_index():
|
||||
"""Get the blog index.
|
||||
|
||||
The index is the cache of available blog posts. It has the format:
|
||||
|
||||
```
|
||||
{
|
||||
'post_id': {
|
||||
fid: Friendly ID for the blog post (for URLs)
|
||||
time: epoch time of the post
|
||||
sticky: the stickiness of the post (shows first on global views)
|
||||
author: the author user ID of the post
|
||||
categories: [ list of categories ]
|
||||
privacy: the privacy setting
|
||||
subject: the post subject
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
# Index doesn't exist?
|
||||
if not JsonDB.exists("blog/index"):
|
||||
return {}
|
||||
db = JsonDB.get("blog/index")
|
||||
|
||||
# Hide any private posts if we aren't logged in.
|
||||
if not g.info["session"]["login"]:
|
||||
for post_id, data in db.iteritems():
|
||||
if data["privacy"] == "private":
|
||||
del db[post_id]
|
||||
|
||||
return db
|
||||
|
||||
|
||||
def __get_categories():
|
||||
"""Get the blog categories cache.
|
||||
|
||||
The category cache is in the following format:
|
||||
|
||||
```
|
||||
{
|
||||
'category_name': {
|
||||
'post_id': 'friendly_id',
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
# Index doesn't exist?
|
||||
if not JsonDB.exists("blog/tags"):
|
||||
return {}
|
||||
return JsonDB.get("blog/tags")
|
||||
|
||||
|
||||
def get_entry(post_id):
|
||||
"""Load a full blog entry."""
|
||||
if not JsonDB.exists("blog/entries/{}".format(post_id)):
|
||||
return None
|
||||
|
||||
db = JsonDB.get("blog/entries/{}".format(post_id))
|
||||
|
||||
# If no FID, set it to the ID.
|
||||
if len(db["fid"]) == 0:
|
||||
db["fid"] = str(post_id)
|
||||
|
||||
return db
|
||||
|
||||
|
||||
def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
|
||||
privacy, ip, emoticons, comments, body):
|
||||
"""Post (or update) a blog entry."""
|
||||
|
||||
# Fetch the index.
|
||||
index = get_index()
|
||||
|
||||
# Editing an existing post?
|
||||
if not post_id:
|
||||
post_id = get_next_id(index)
|
||||
|
||||
logger.debug("Posting blog post ID {}".format(post_id))
|
||||
|
||||
# Get a unique friendly ID.
|
||||
if not fid:
|
||||
# The default friendly ID = the subject.
|
||||
fid = subject.lower()
|
||||
fid = re.sub(r'[^A-Za-z0-9]', '-', fid)
|
||||
fid = re.sub(r'\-+', '-', fid)
|
||||
fid = fid.strip("-")
|
||||
logger.debug("Chosen friendly ID: {}".format(fid))
|
||||
|
||||
# Make sure the friendly ID is unique!
|
||||
if len(fid):
|
||||
test = fid
|
||||
loop = 1
|
||||
logger.debug("Verifying the friendly ID is unique: {}".format(fid))
|
||||
while True:
|
||||
collision = False
|
||||
|
||||
for k, v in index.iteritems():
|
||||
# Skip the same post, for updates.
|
||||
if k == post_id: continue
|
||||
|
||||
if v["fid"] == test:
|
||||
# Not unique.
|
||||
loop += 1
|
||||
test = fid + "_" + unicode(loop)
|
||||
collision = True
|
||||
logger.debug("Collision with existing post {}: {}".format(k, v["fid"]))
|
||||
break
|
||||
|
||||
# Was there a collision?
|
||||
if collision:
|
||||
continue # Try again.
|
||||
|
||||
# Nope!
|
||||
break
|
||||
fid = test
|
||||
|
||||
# Write the post.
|
||||
JsonDB.commit("blog/entries/{}".format(post_id), dict(
|
||||
fid = fid,
|
||||
ip = ip,
|
||||
time = epoch or int(time.time()),
|
||||
categories = categories,
|
||||
sticky = False, # TODO: implement sticky
|
||||
comments = comments,
|
||||
emoticons = emoticons,
|
||||
avatar = avatar,
|
||||
privacy = privacy or "public",
|
||||
author = author,
|
||||
subject = subject,
|
||||
body = body,
|
||||
))
|
||||
|
||||
# Update the index cache.
|
||||
index[post_id] = dict(
|
||||
fid = fid,
|
||||
time = epoch or int(time.time()),
|
||||
categories = categories,
|
||||
sticky = False, # TODO
|
||||
author = author,
|
||||
privacy = privacy or "public",
|
||||
subject = subject,
|
||||
)
|
||||
JsonDB.commit("blog/index", index)
|
||||
|
||||
return post_id, fid
|
||||
|
||||
|
||||
def delete_entry(post_id):
|
||||
"""Remove a blog entry."""
|
||||
# Fetch the blog information.
|
||||
index = get_index()
|
||||
post = get_entry(post_id)
|
||||
if post is None:
|
||||
logger.warning("Can't delete post {}, it doesn't exist!".format(post_id))
|
||||
|
||||
# Delete the post.
|
||||
JsonDB.delete("blog/entries/{}".format(post_id))
|
||||
|
||||
# Update the index cache.
|
||||
del index[str(post_id)] # Python JSON dict keys must be strings, never ints
|
||||
JsonDB.commit("blog/index", index)
|
||||
|
||||
|
||||
def resolve_id(fid):
|
||||
"""Resolve a friendly ID to the blog ID number."""
|
||||
index = get_index()
|
||||
|
||||
# If the ID is all numeric, it's the blog post ID directly.
|
||||
if re.match(r'^\d+$', fid):
|
||||
if fid in index:
|
||||
return int(fid)
|
||||
else:
|
||||
logger.error("Tried resolving blog post ID {} as an EntryID, but it wasn't there!".format(fid))
|
||||
return None
|
||||
|
||||
# It's a friendly ID. Scan for it.
|
||||
for post_id, data in index.iteritems():
|
||||
if data["fid"] == fid:
|
||||
return int(post_id)
|
||||
|
||||
logger.error("Friendly post ID {} wasn't found!".format(fid))
|
||||
return None
|
||||
|
||||
|
||||
def list_avatars():
|
||||
"""Get a list of all the available blog avatars."""
|
||||
avatars = set()
|
||||
paths = [
|
||||
# Load avatars from both locations. We check the built-in set first,
|
||||
# so if you have matching names in your local site those will override.
|
||||
"rophako/www/static/avatars/*.*",
|
||||
"site/www/static/avatars/*.*",
|
||||
]
|
||||
for path in paths:
|
||||
for filename in glob.glob(path):
|
||||
filename = filename.split("/")[-1]
|
||||
avatars.add(filename)
|
||||
|
||||
return sorted(avatars, key=lambda x: x.lower())
|
||||
|
||||
|
||||
def get_next_id(index):
|
||||
"""Get the next free ID for a blog post."""
|
||||
logger.debug("Getting next available blog ID number")
|
||||
sort = sorted(index.keys(), key=lambda x: int(x))
|
||||
logger.debug("Highest post ID is: {}".format(sort[-1]))
|
||||
next_id = int(sort[-1]) + 1
|
||||
|
||||
# Sanity check!
|
||||
if next_id in index:
|
||||
raise Exception("Failed to get_next_id for the blog. Chosen ID is still in the index!")
|
||||
return next_id
|
300
rophako/modules/blog.py
Normal file
300
rophako/modules/blog.py
Normal file
|
@ -0,0 +1,300 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Endpoints for the web blog."""
|
||||
|
||||
from flask import Blueprint, g, request, redirect, url_for, session, flash
|
||||
import re
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
import rophako.model.user as User
|
||||
import rophako.model.blog as Blog
|
||||
from rophako.utils import template, pretty_time, admin_required
|
||||
from rophako.log import logger
|
||||
from config import *
|
||||
|
||||
mod = Blueprint("blog", __name__, url_prefix="/blog")
|
||||
|
||||
@mod.route("/")
|
||||
def index():
|
||||
return template("blog/index.html")
|
||||
|
||||
|
||||
@mod.route("/category/<category>")
|
||||
def category(category):
|
||||
g.info["url_category"] = category
|
||||
return template("blog/index.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)
|
||||
if not post_id:
|
||||
flash("That blog post wasn't found.")
|
||||
return redirect(url_for(".index"))
|
||||
|
||||
# Look up the post.
|
||||
post = Blog.get_entry(post_id)
|
||||
post["post_id"] = post_id
|
||||
|
||||
# Get the author's information.
|
||||
post["profile"] = User.get_user(uid=post["author"])
|
||||
|
||||
# Pretty-print the time.
|
||||
post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"])
|
||||
|
||||
# TODO: count the comments for this post
|
||||
post["comment_count"] = 0
|
||||
|
||||
g.info["post"] = post
|
||||
return template("blog/entry.html")
|
||||
|
||||
|
||||
@mod.route("/entry")
|
||||
@mod.route("/index")
|
||||
def dummy():
|
||||
return redirect(url_for(".index"))
|
||||
|
||||
|
||||
@mod.route("/update", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
def update():
|
||||
"""Post/edit a blog entry."""
|
||||
|
||||
# Get our available avatars.
|
||||
g.info["avatars"] = Blog.list_avatars()
|
||||
|
||||
# Default vars.
|
||||
g.info.update(dict(
|
||||
post_id="",
|
||||
fid="",
|
||||
author=g.info["session"]["uid"],
|
||||
subject="",
|
||||
body="",
|
||||
avatar="",
|
||||
categories="",
|
||||
privacy=BLOG_DEFAULT_PRIVACY,
|
||||
emoticons=True,
|
||||
comments=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)
|
||||
if post_id:
|
||||
logger.info("Editing existing blog post {}".format(post_id))
|
||||
post = Blog.get_entry(post_id)
|
||||
g.info["post_id"] = post_id
|
||||
g.info["post"] = post
|
||||
|
||||
# Copy fields.
|
||||
for field in ["author", "fid", "subject", "body", "avatar",
|
||||
"categories", "privacy", "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")
|
||||
|
||||
# Get all the fields from the posted params.
|
||||
g.info["post_id"] = request.form.get("id")
|
||||
for field in ["fid", "subject", "body", "avatar", "categories", "privacy"]:
|
||||
g.info[field] = request.form.get(field)
|
||||
for boolean in ["emoticons", "comments"]:
|
||||
print "BOOL:", boolean, request.form.get(boolean)
|
||||
g.info[boolean] = True if request.form.get(boolean, None) == "true" else False
|
||||
print g.info[boolean]
|
||||
for number in ["author", "month", "day", "year", "hour", "min", "sec"]:
|
||||
g.info[number] = int(request.form.get(number, 0))
|
||||
|
||||
# What action are they doing?
|
||||
if action == "preview":
|
||||
g.info["preview"] = True
|
||||
elif action == "publish":
|
||||
# Publishing! Validate inputs first.
|
||||
invalid = False
|
||||
if len(g.info["body"]) == 0:
|
||||
invalid = True
|
||||
flash("You must enter a body for your blog post.")
|
||||
if len(g.info["subject"]) == 0:
|
||||
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, e:
|
||||
invalid = True
|
||||
flash("Invalid date/time: " + str(e))
|
||||
|
||||
# Format the categories.
|
||||
tags = []
|
||||
for tag in g.info["categories"].split(","):
|
||||
tags.append(tag.strip())
|
||||
|
||||
# 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,
|
||||
author = g.info["author"],
|
||||
subject = g.info["subject"],
|
||||
fid = g.info["fid"],
|
||||
avatar = g.info["avatar"],
|
||||
categories = tags,
|
||||
privacy = g.info["privacy"],
|
||||
ip = request.remote_addr,
|
||||
emoticons = g.info["emoticons"],
|
||||
comments = g.info["comments"],
|
||||
body = g.info["body"],
|
||||
)
|
||||
|
||||
return redirect(url_for(".entry", fid=new_fid))
|
||||
|
||||
|
||||
if type(g.info["categories"]) is list:
|
||||
g.info["categories"] = ", ".join(g.info["categories"])
|
||||
|
||||
return template("blog/update.html")
|
||||
|
||||
|
||||
@mod.route("/delete", methods=["GET", "POST"])
|
||||
def delete():
|
||||
"""Delete a blog post."""
|
||||
post_id = request.args.get("id")
|
||||
|
||||
# Resolve the post ID.
|
||||
post_id = Blog.resolve_id(post_id)
|
||||
if not post_id:
|
||||
flash("That blog post wasn't found.")
|
||||
return redirect(url_for(".index"))
|
||||
|
||||
if request.method == "POST":
|
||||
confirm = request.form.get("confirm")
|
||||
if confirm == "true":
|
||||
Blog.delete_entry(post_id)
|
||||
flash("The blog entry has been deleted.")
|
||||
return redirect(url_for(".index"))
|
||||
|
||||
# Get the entry's subject.
|
||||
post = Blog.get_entry(post_id)
|
||||
g.info["subject"] = post["subject"]
|
||||
g.info["post_id"] = post_id
|
||||
|
||||
return template("blog/delete.html")
|
||||
|
||||
def partial_index():
|
||||
"""Partial template for including the index view of the blog."""
|
||||
|
||||
# Get the blog index.
|
||||
index = Blog.get_index()
|
||||
pool = {} # The set of blog posts to show.
|
||||
|
||||
category = g.info.get("url_category", None)
|
||||
|
||||
# Are we narrowing by category?
|
||||
if category:
|
||||
# Narrow down the index to just those that match the category.
|
||||
for post_id, data in index.iteritems():
|
||||
if not category in data["categories"]:
|
||||
continue
|
||||
pool[post_id] = data
|
||||
|
||||
# No such category?
|
||||
if len(pool) == 0:
|
||||
flash("There are no posts with that category.")
|
||||
return redirect(url_for(".index"))
|
||||
else:
|
||||
pool = index
|
||||
|
||||
# Separate the sticky posts from the normal ones.
|
||||
sticky, normal = set(), set()
|
||||
for post_id, data in pool.iteritems():
|
||||
if data["sticky"]:
|
||||
sticky.add(post_id)
|
||||
else:
|
||||
normal.add(post_id)
|
||||
|
||||
# Sort the blog IDs by published time.
|
||||
posts = []
|
||||
posts.extend(sorted(sticky, key=lambda x: pool[x]["time"], reverse=True))
|
||||
posts.extend(sorted(normal, key=lambda x: pool[x]["time"], reverse=True))
|
||||
|
||||
# Handle pagination.
|
||||
offset = request.args.get("skip", 0)
|
||||
try: offset = int(offset)
|
||||
except: offset = 0
|
||||
|
||||
# Handle the offsets, and get those for the "older" and "earlier" posts.
|
||||
# "earlier" posts count down (towards index 0), "older" counts up.
|
||||
g.info["offset"] = offset
|
||||
g.info["earlier"] = offset - BLOG_ENTRIES_PER_PAGE if offset > 0 else 0
|
||||
g.info["older"] = offset + BLOG_ENTRIES_PER_PAGE
|
||||
if g.info["earlier"] < 0:
|
||||
g.info["earlier"] = 0
|
||||
if g.info["older"] < 0 or g.info["older"] > len(posts):
|
||||
g.info["older"] = 0
|
||||
g.info["count"] = 0
|
||||
|
||||
# Can we go to other pages?
|
||||
g.info["can_earlier"] = True if offset > 0 else False
|
||||
g.info["can_older"] = False if g.info["older"] == 0 else True
|
||||
|
||||
# Load the selected posts.
|
||||
selected = []
|
||||
stop = offset + BLOG_ENTRIES_PER_PAGE
|
||||
if stop > len(posts): stop = len(posts)
|
||||
for i in range(offset, stop):
|
||||
post_id = posts[i]
|
||||
post = Blog.get_entry(post_id)
|
||||
|
||||
post["post_id"] = post_id
|
||||
|
||||
# Get the author's information.
|
||||
post["profile"] = User.get_user(uid=post["author"])
|
||||
|
||||
post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"])
|
||||
|
||||
# TODO: count the comments for this post
|
||||
post["comment_count"] = 0
|
||||
|
||||
selected.append(post)
|
||||
g.info["count"] += 1
|
||||
|
||||
g.info["category"] = category
|
||||
g.info["posts"] = selected
|
||||
|
||||
return template("blog/index.inc.html")
|
|
@ -2,21 +2,31 @@
|
|||
|
||||
# Legacy endpoint compatibility from kirsle.net.
|
||||
|
||||
from flask import request, redirect
|
||||
from flask import request, redirect, url_for
|
||||
from rophako import app
|
||||
import rophako.model.blog as Blog
|
||||
|
||||
@app.route("/+")
|
||||
def google_plus():
|
||||
return redirect("https://plus.google.com/+NoahPetherbridge/posts")
|
||||
|
||||
@app.route("/blog.html")
|
||||
def legacy_blog():
|
||||
post_id = request.args.get("id", "")
|
||||
def ancient_legacy_blog():
|
||||
post_id = request.args.get("id", None)
|
||||
if post_id is None:
|
||||
return redirect(url_for("blog.index"))
|
||||
|
||||
# All of this is TO-DO.
|
||||
# friendly_id = get friendly ID
|
||||
# return redirect(...)
|
||||
return "TO-DO"
|
||||
# Look up the friendly ID.
|
||||
post = Blog.get_entry(post_id)
|
||||
if not post:
|
||||
flash("That blog entry wasn't found.")
|
||||
return redirect(url_for("blog.index"))
|
||||
|
||||
return redirect(url_for("blog.entry", fid=post["fid"]))
|
||||
|
||||
@app.route("/blog/kirsle/<fid>")
|
||||
def legacy_blog(fid):
|
||||
return redirect(url_for("blog.entry", fid=fid))
|
||||
|
||||
@app.route("/<page>.html")
|
||||
def legacy_url(page):
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask import g, session, request, render_template
|
||||
from flask import g, session, request, render_template, flash, redirect, url_for
|
||||
from functools import wraps
|
||||
import uuid
|
||||
import datetime
|
||||
import time
|
||||
import re
|
||||
import importlib
|
||||
|
||||
from rophako.log import logger
|
||||
|
||||
|
@ -41,6 +45,10 @@ def template(name, **kwargs):
|
|||
"""Render a template to the browser."""
|
||||
|
||||
html = render_template(name, **kwargs)
|
||||
|
||||
# Get the elapsed time for the request.
|
||||
time_elapsed = "%.03f" % (time.time() - g.info["time"])
|
||||
html = re.sub(r'\%time_elapsed\%', time_elapsed, html)
|
||||
return html
|
||||
|
||||
|
||||
|
@ -49,3 +57,22 @@ def generate_csrf_token():
|
|||
if "_csrf" not in session:
|
||||
session["_csrf"] = str(uuid.uuid4())
|
||||
return session["_csrf"]
|
||||
|
||||
|
||||
def include(endpoint, *args, **kwargs):
|
||||
"""Include another sub-page inside a template."""
|
||||
|
||||
# The 'endpoint' should be in the format 'module.function', i.e. 'blog.index'.
|
||||
module, function = endpoint.split(".")
|
||||
|
||||
# Dynamically import the module and call its function.
|
||||
m = importlib.import_module("rophako.modules.{}".format(module))
|
||||
html = getattr(m, function)(*args, **kwargs)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def pretty_time(time_format, unix):
|
||||
"""Pretty-print a time stamp."""
|
||||
date = datetime.datetime.fromtimestamp(unix)
|
||||
return date.strftime(time_format)
|
17
rophako/www/blog/delete.html
Normal file
17
rophako/www/blog/delete.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Delete Entry{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<h1>Delete Entry</h1>
|
||||
|
||||
<form name="editor" action="{{ url_for('blog.delete', id=post_id) }}" method="POST">
|
||||
<input type="hidden" name="token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="confirm" value="true">
|
||||
|
||||
Are you sure you want to delete the blog post,
|
||||
"{{ subject }}"?<p>
|
||||
|
||||
<button type="submit">Confirm Deletion</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
10
rophako/www/blog/entry.html
Normal file
10
rophako/www/blog/entry.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}{{ post["subject"] }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ post["subject"] }}</h1>
|
||||
|
||||
{% from "blog/entry.inc.html" import blog_entry %}
|
||||
{{ blog_entry(post) }}
|
||||
|
||||
{% endblock %}
|
69
rophako/www/blog/entry.inc.html
Normal file
69
rophako/www/blog/entry.inc.html
Normal file
|
@ -0,0 +1,69 @@
|
|||
{# Reusable template for showing a blog post content #}
|
||||
|
||||
{% macro blog_entry(post, from=None) %}
|
||||
|
||||
{% if from == "index" %}
|
||||
<a href="{{ url_for('blog.entry', fid=post['fid']) }}" class="blog-title-index">
|
||||
{{ post["subject"] }}
|
||||
</a><p>
|
||||
{% endif %}
|
||||
|
||||
<div class="blog-author">
|
||||
<a href="#">{# TODO #}
|
||||
{% if post["avatar"] %}
|
||||
<img src="/static/avatars/{{ post['avatar'] }}">
|
||||
{% else %}
|
||||
<img src="/static/avatars/default.png">
|
||||
{% endif %}
|
||||
</a><br>
|
||||
|
||||
<a href="#">
|
||||
{{ post["profile"]["username"] }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="blog-timestamp">
|
||||
Posted by <a href="#">{{ post["profile"]["name"] }}</a>
|
||||
on <span title="{{ post['time'] }}">{{ post["pretty_time"] }}</span>
|
||||
</div>
|
||||
|
||||
{{ post["body"] | safe }}
|
||||
|
||||
<p>
|
||||
<div class="clear">
|
||||
<strong>Categories:</strong>
|
||||
{% if post["categories"]|length == 0 %}
|
||||
<a href="{{ url_for('blog.category', category=Uncategorized) }}">Uncategorized</a>{# TODO hardcoded name #}
|
||||
{% else %}
|
||||
<ul class="blog-categories">
|
||||
{% for tag in post["categories"] %}
|
||||
<li><a href="{{ url_for('blog.category', category=tag) }}">{{ tag }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<p>
|
||||
|
||||
[
|
||||
{% if from == "index" %}
|
||||
{% if post["comments"] %}{# Allowed comments #}
|
||||
<a href="{{ url_for('blog.entry', fid=post['fid']) }}#comments">{{ post["comment_count"] }} comment{% if post["comment_count"] != 1 %}s{% endif %}</a>
|
||||
|
|
||||
<a href="{{ url_for('blog.entry', fid=post['fid']) }}#addcomment">Add comment</a>
|
||||
|
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('blog.entry', fid=post['fid']) }}">Permalink</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('blog.index') }}">Blog</a>
|
||||
{% endif %}
|
||||
|
||||
{% if session["login"] %}
|
||||
|
|
||||
<a href="{{ url_for('blog.update', id=post['post_id']) }}">Edit</a>
|
||||
|
|
||||
<a href="{{ url_for('blog.delete', id=post['post_id']) }}">Delete</a>
|
||||
{% endif %}
|
||||
]
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
13
rophako/www/blog/index.html
Normal file
13
rophako/www/blog/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Blog{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% if url_category %}
|
||||
<h1>Category: {{ url_category }}</h1>
|
||||
{% else %}
|
||||
<h1>My Blog</h1>
|
||||
{% endif %}
|
||||
|
||||
{{ include_page("blog.partial_index") | safe }}
|
||||
|
||||
{% endblock %}
|
13
rophako/www/blog/index.inc.html
Normal file
13
rophako/www/blog/index.inc.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% from "blog/entry.inc.html" import blog_entry %}
|
||||
|
||||
{% include "blog/nav-links.inc.html" %}
|
||||
|
||||
{% if count == 0 %}
|
||||
There are no blog posts yet.
|
||||
{% else %}
|
||||
{% for post in posts %}
|
||||
{{ blog_entry(post, from="index") }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% include "blog/nav-links.inc.html" %}
|
27
rophako/www/blog/nav-links.inc.html
Normal file
27
rophako/www/blog/nav-links.inc.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
{# Older/Newer links #}
|
||||
|
||||
{% if can_older or can_newer %}
|
||||
<div class="right">
|
||||
[
|
||||
<a href="/rss">RSS Feed</a> | {# TODO! #}
|
||||
{% if can_earlier %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
|
||||
{% if can_older %} | {% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if can_older %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
]
|
||||
</div>
|
||||
{% endif %}
|
152
rophako/www/blog/update.html
Normal file
152
rophako/www/blog/update.html
Normal file
|
@ -0,0 +1,152 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Update Blog{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% if preview %}
|
||||
<h1>Preview: {{ subject }}</h1>
|
||||
|
||||
{{ body|safe }}
|
||||
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
<h1>Update Blog</h1>
|
||||
|
||||
<form name="editor" action="{{ url_for('blog.update') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ post_id }}">
|
||||
<input type="hidden" name="author" value="{{ author }}">
|
||||
<input type="hidden" name="token" value="{{ csrf_token() }}">
|
||||
|
||||
<strong>Subject:</strong><br>
|
||||
<input type="text" size="80" name="subject" value="{{ subject }}"><p>
|
||||
|
||||
<strong>Friendly ID:</strong><br>
|
||||
You can leave this blank if this is a new post. It defaults to be based
|
||||
on the subject.<br>
|
||||
<input type="text" size="80" name="fid" value="{{ fid }}"><p>
|
||||
|
||||
<strong>Body:</strong><br>
|
||||
<textarea cols="80" rows="12" name="body">{{ body }}</textarea><br>
|
||||
<a href="/emoticons" target="_blank">Emoticon reference</a> (opens in new window)<p>
|
||||
|
||||
<strong>Avatar:</strong><br>
|
||||
<span id="avatar-preview"></span>
|
||||
<select name="avatar" id="avatar">
|
||||
<option value=""{% if avatar == "" %} selected{% endif %}>Use my profile picture</option>
|
||||
{% for pic in avatars %}
|
||||
<option value="{{ pic }}"{% if avatar == pic %} selected{% endif %}>{{ pic }}</option>
|
||||
{% endfor %}
|
||||
</select><p>
|
||||
|
||||
<strong>Categories:</strong><br>
|
||||
<small>Comma-separated list, e.g. General, HTML, Perl, Web Design</small><br>
|
||||
<input type="text" size="40" name="categories" value="{{ categories }}"><p>
|
||||
|
||||
<strong>Privacy:</strong><br>
|
||||
<select name="privacy">
|
||||
<option value="public"{% if privacy == "public" %} selected{% endif %}>
|
||||
Public: everybody can see this blog entry
|
||||
</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="emoticons" value="true"{% if emoticons %} checked{% endif %}>
|
||||
Enable graphical emoticons
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="comments" value="true"{% if comments %} checked{% endif %}>
|
||||
Enable comments on this entry
|
||||
</label><p>
|
||||
|
||||
<strong>Time Stamp:</strong><br>
|
||||
<input type="text" size="2" name="month" id="month" value="{{ month }}"> /
|
||||
<input type="text" size="2" name="day" id="day" value="{{ day }}"> /
|
||||
<input type="text" size="4" name="year" id="year" value="{{ year }}"> @
|
||||
<input type="text" size="2" name="hour" id="hour" value="{{ hour }}"> :
|
||||
<input type="text" size="2" name="min" id="min" value="{{ min }}"> :
|
||||
<input type="text" size="2" name="sec" id="sec" value="{{ sec }}"><br>
|
||||
mm / dd / yyyy @ hh:mm:ss<br>
|
||||
<label>
|
||||
<input type="checkbox" id="autoup" value="yes"{% if post_id == "" %} checked{% endif %}>
|
||||
Automatically update
|
||||
</label><p>
|
||||
|
||||
<button type="submit" name="action" value="preview">Preview</button>
|
||||
<button type="submit" name="action" value="publish">Publish Entry</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
var userPic = "/static/avatars/default.png"; // TODO
|
||||
|
||||
$(document).ready(function() {
|
||||
// Preview their selected avatar.
|
||||
updateAvatar();
|
||||
$("#avatar").on("change", updateAvatar);
|
||||
|
||||
// Start ticking the timestamp updater.
|
||||
setInterval(timestamps, 500)
|
||||
});
|
||||
|
||||
function updateAvatar() {
|
||||
var chosen = $("#avatar").val();
|
||||
|
||||
var picture = ""; // which pic to show
|
||||
if (chosen === "") {
|
||||
picture = userPic;
|
||||
}
|
||||
else {
|
||||
picture = "/static/avatars/" + chosen;
|
||||
}
|
||||
|
||||
// Show the pic
|
||||
if (picture.length) {
|
||||
$("#avatar-preview").html("<img src=\"" + picture + "\" alt=\"Preview\"><br>");
|
||||
}
|
||||
else {
|
||||
$("#avatar-preview").html("");
|
||||
}
|
||||
}
|
||||
|
||||
function timestamps() {
|
||||
function padout(num) {
|
||||
if (num < 10) {
|
||||
return '0' + num;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
if ($("#autoup").is(":checked")) {
|
||||
var d = new Date();
|
||||
var mon = d.getMonth(); // 0..11
|
||||
var day = d.getDate(); // 1..31
|
||||
var year = d.getFullYear(); // 2014
|
||||
var hour = d.getHours(); // 0..23
|
||||
var min = d.getMinutes(); // 0..59
|
||||
var sec = d.getSeconds(); // 0..59
|
||||
|
||||
// Adjust the dates.
|
||||
mon++;
|
||||
mon = padout(mon);
|
||||
day = padout(day);
|
||||
hour = padout(hour);
|
||||
min = padout(min);
|
||||
sec = padout(sec);
|
||||
|
||||
// Update the fields.
|
||||
$("#month").val(mon);
|
||||
$("#day").val(day);
|
||||
$("#year").val(year);
|
||||
$("#hour").val(hour);
|
||||
$("#min").val(min);
|
||||
$("#sec").val(sec);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
<h1>Welcome!</h1>
|
||||
|
||||
This is the Rophako CMS!
|
||||
This is the Rophako CMS!<p>
|
||||
|
||||
{{ include_page("blog.partial_index") | safe }}
|
||||
|
||||
{% endblock %}
|
BIN
rophako/www/static/avatars/default.png
Normal file
BIN
rophako/www/static/avatars/default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.0 KiB |
88
scripts/siikir-blog-migrate.py
Normal file
88
scripts/siikir-blog-migrate.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""Migrate blog database files from a PerlSiikir site to Rophako.
|
||||
|
||||
Usage: scripts/siikir-blog-migrate.py <path/to/siikir/db/root>
|
||||
|
||||
Rophako supports one global blog, so the blog of UserID 1 in Siikir is used."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import codecs
|
||||
import json
|
||||
import glob
|
||||
|
||||
sys.path.append(".")
|
||||
import rophako.jsondb as JsonDB
|
||||
|
||||
# Path to Siikir DB root.
|
||||
siikir = None
|
||||
|
||||
def main():
|
||||
if len(sys.argv) == 1:
|
||||
print "Usage: {} <path/to/siikir/db/root>".format(__file__)
|
||||
sys.exit(1)
|
||||
|
||||
global siikir
|
||||
siikir = sys.argv[1]
|
||||
print "Siikir DB:", siikir
|
||||
if raw_input("Confirm? [yN] ") != "y":
|
||||
sys.exit(1)
|
||||
|
||||
convert_index()
|
||||
#convert_tags()
|
||||
convert_posts()
|
||||
|
||||
|
||||
def convert_index():
|
||||
print "Converting blog index"
|
||||
index = json_get("blog/index/1.json")
|
||||
new = {}
|
||||
for post_id, data in index.iteritems():
|
||||
del data["id"]
|
||||
|
||||
# Enforce data types.
|
||||
data["author"] = int(data["author"])
|
||||
data["time"] = int(data["time"])
|
||||
data["sticky"] = bool(data["sticky"])
|
||||
|
||||
new[post_id] = data
|
||||
|
||||
JsonDB.commit("blog/index", new)
|
||||
|
||||
|
||||
def convert_tags():
|
||||
print "Converting tag index"
|
||||
index = json_get("blog/tags/1.json")
|
||||
JsonDB.commit("blog/tags", index)
|
||||
|
||||
|
||||
def convert_posts():
|
||||
print "Converting blog entries..."
|
||||
|
||||
for name in glob.glob(os.path.join(siikir, "blog/entries/1/*.json")):
|
||||
name = name.split("/")[-1]
|
||||
post = json_get("blog/entries/1/{}".format(name))
|
||||
post_id = post["id"]
|
||||
del post["id"]
|
||||
|
||||
# Enforce data types.
|
||||
post["time"] = int(post["time"])
|
||||
post["author"] = int(post["author"])
|
||||
post["comments"] = bool(post["comments"])
|
||||
post["sticky"] = bool(post["sticky"])
|
||||
post["emoticons"] = bool(post["emoticons"])
|
||||
|
||||
print "*", post["subject"]
|
||||
JsonDB.commit("blog/entries/{}".format(post_id), post)
|
||||
|
||||
|
||||
def json_get(document):
|
||||
fh = codecs.open(os.path.join(siikir, document), 'r', 'utf-8')
|
||||
text = fh.read()
|
||||
fh.close()
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in New Issue
Block a user