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

@ -32,3 +32,11 @@ REDIS_HOST = "localhost"
REDIS_PORT = 6379 REDIS_PORT = 6379
REDIS_DB = 0 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! # Load all the blueprints!
from rophako.modules.admin import mod as AdminModule from rophako.modules.admin import mod as AdminModule
from rophako.modules.account import mod as AccountModule from rophako.modules.account import mod as AccountModule
from rophako.modules.blog import mod as BlogModule
app.register_blueprint(AdminModule) app.register_blueprint(AdminModule)
app.register_blueprint(AccountModule) app.register_blueprint(AccountModule)
app.register_blueprint(BlogModule)
# Custom Jinja handler to support custom- and default-template folders for # Custom Jinja handler to support custom- and default-template folders for
# rendering templates. # 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["csrf_token"] = rophako.utils.generate_csrf_token
app.jinja_env.globals["include_page"] = rophako.utils.include
@app.before_request @app.before_request
@ -85,7 +88,6 @@ def before_request():
@app.context_processor @app.context_processor
def after_request(): def after_request():
"""Called just before render_template. Inject g.info into the template vars.""" """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 return g.info
@ -100,14 +102,13 @@ def catchall(path):
if os.path.isfile(abspath): if os.path.isfile(abspath):
return send_file(abspath) return send_file(abspath)
elif not "." in path and os.path.isfile(abspath + ".html"): 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") return not_found("404")
@app.route("/") @app.route("/")
def index(): def index():
print "INDEX PAGE"
return catchall("index") 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. # Legacy endpoint compatibility from kirsle.net.
from flask import request, redirect from flask import request, redirect, url_for
from rophako import app from rophako import app
import rophako.model.blog as Blog
@app.route("/+") @app.route("/+")
def google_plus(): def google_plus():
return redirect("https://plus.google.com/+NoahPetherbridge/posts") return redirect("https://plus.google.com/+NoahPetherbridge/posts")
@app.route("/blog.html") @app.route("/blog.html")
def legacy_blog(): def ancient_legacy_blog():
post_id = request.args.get("id", "") post_id = request.args.get("id", None)
if post_id is None:
return redirect(url_for("blog.index"))
# All of this is TO-DO. # Look up the friendly ID.
# friendly_id = get friendly ID post = Blog.get_entry(post_id)
# return redirect(...) if not post:
return "TO-DO" 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") @app.route("/<page>.html")
def legacy_url(page): def legacy_url(page):

View File

@ -1,8 +1,12 @@
# -*- coding: utf-8 -*- # -*- 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 from functools import wraps
import uuid import uuid
import datetime
import time
import re
import importlib
from rophako.log import logger from rophako.log import logger
@ -41,6 +45,10 @@ def template(name, **kwargs):
"""Render a template to the browser.""" """Render a template to the browser."""
html = render_template(name, **kwargs) 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 return html
@ -49,3 +57,22 @@ def generate_csrf_token():
if "_csrf" not in session: if "_csrf" not in session:
session["_csrf"] = str(uuid.uuid4()) 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> <h1>Welcome!</h1>
This is the Rophako CMS! This is the Rophako CMS!<p>
{{ include_page("blog.partial_index") | safe }}
{% endblock %} {% 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()