16 changed files with 981 additions and 14 deletions
@ -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 |
@ -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") |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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" %} |
@ -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 %} |
@ -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 %} |
After Width: | Height: | Size: 7.0 KiB |
@ -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