Add the web blog views/models/controllers

This commit is contained in:
Noah 2014-03-28 01:23:12 -07:00
parent 80c20ec87b
commit f9c2481499
16 changed files with 981 additions and 14 deletions

View File

@ -31,4 +31,12 @@ DB_ROOT = "db"
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0
REDIS_PREFIX = "rophako:"
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

View File

@ -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
View 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
View 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")

View File

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

View File

@ -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
@ -48,4 +56,23 @@ def generate_csrf_token():
"""Generator for CSRF tokens."""
if "_csrf" not in session:
session["_csrf"] = str(uuid.uuid4())
return session["_csrf"]
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)

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

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

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

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

View 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" %}

View 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 }}">&lt; Newer</a>
{% else %}
<a href="{{ url_for('blog.index') }}?skip={{ earlier }}">&lt; Newer</a>
{% endif %}
{% if can_older %} | {% endif %}
{% endif %}
{% if can_older %}
{% if category %}
<a href="{{ url_for('blog.category', category=category) }}?skip={{ older }}">Older &gt;</a>
{% else %}
<a href="{{ url_for('blog.index') }}?skip={{ older }}">Older &gt;</a>
{% endif %}
{% endif %}
]
</div>
{% endif %}

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View 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()