diff --git a/config-sample.py b/config-sample.py index 50448f1..0b7fa04 100644 --- a/config-sample.py +++ b/config-sample.py @@ -31,4 +31,12 @@ DB_ROOT = "db" REDIS_HOST = "localhost" REDIS_PORT = 6379 REDIS_DB = 0 -REDIS_PREFIX = "rophako:" \ No newline at end of file +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 \ No newline at end of file diff --git a/rophako/__init__.py b/rophako/__init__.py index e55ed25..ca98c6e 100644 --- a/rophako/__init__.py +++ b/rophako/__init__.py @@ -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") diff --git a/rophako/model/blog.py b/rophako/model/blog.py new file mode 100644 index 0000000..dea01f0 --- /dev/null +++ b/rophako/model/blog.py @@ -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 \ No newline at end of file diff --git a/rophako/modules/blog.py b/rophako/modules/blog.py new file mode 100644 index 0000000..6c755e6 --- /dev/null +++ b/rophako/modules/blog.py @@ -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/") +def category(category): + g.info["url_category"] = category + return template("blog/index.html") + + +@mod.route("/entry/") +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") diff --git a/rophako/modules/kirsle_legacy.py b/rophako/modules/kirsle_legacy.py index ee57a16..3a20f48 100644 --- a/rophako/modules/kirsle_legacy.py +++ b/rophako/modules/kirsle_legacy.py @@ -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/") +def legacy_blog(fid): + return redirect(url_for("blog.entry", fid=fid)) @app.route("/.html") def legacy_url(page): diff --git a/rophako/utils.py b/rophako/utils.py index 624be3c..a552510 100644 --- a/rophako/utils.py +++ b/rophako/utils.py @@ -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"] \ No newline at end of file + 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) \ No newline at end of file diff --git a/rophako/www/blog/delete.html b/rophako/www/blog/delete.html new file mode 100644 index 0000000..f7efe00 --- /dev/null +++ b/rophako/www/blog/delete.html @@ -0,0 +1,17 @@ +{% extends "layout.html" %} +{% block title %}Delete Entry{% endblock %} +{% block content %} + +

Delete Entry

+ +
+ + + + Are you sure you want to delete the blog post, + "{{ subject }}"?

+ + +

+ +{% endblock %} \ No newline at end of file diff --git a/rophako/www/blog/entry.html b/rophako/www/blog/entry.html new file mode 100644 index 0000000..805738c --- /dev/null +++ b/rophako/www/blog/entry.html @@ -0,0 +1,10 @@ +{% extends "layout.html" %} +{% block title %}{{ post["subject"] }}{% endblock %} +{% block content %} + +

{{ post["subject"] }}

+ +{% from "blog/entry.inc.html" import blog_entry %} +{{ blog_entry(post) }} + +{% endblock %} \ No newline at end of file diff --git a/rophako/www/blog/entry.inc.html b/rophako/www/blog/entry.inc.html new file mode 100644 index 0000000..2d7b007 --- /dev/null +++ b/rophako/www/blog/entry.inc.html @@ -0,0 +1,69 @@ +{# Reusable template for showing a blog post content #} + +{% macro blog_entry(post, from=None) %} + + {% if from == "index" %} + + {{ post["subject"] }} +

+ {% endif %} + +

+ +
+ Posted by {{ post["profile"]["name"] }} + on {{ post["pretty_time"] }} +
+ + {{ post["body"] | safe }} + +

+

+ Categories: + {% if post["categories"]|length == 0 %} + Uncategorized{# TODO hardcoded name #} + {% else %} +
    + {% for tag in post["categories"] %} +
  • {{ tag }}
  • + {% endfor %} +
+ {% endif %} +

+ + [ + {% if from == "index" %} + {% if post["comments"] %}{# Allowed comments #} + {{ post["comment_count"] }} comment{% if post["comment_count"] != 1 %}s{% endif %} + | + Add comment + | + {% endif %} + + Permalink + {% else %} + Blog + {% endif %} + + {% if session["login"] %} + | + Edit + | + Delete + {% endif %} + ] +

+ +{% endmacro %} \ No newline at end of file diff --git a/rophako/www/blog/index.html b/rophako/www/blog/index.html new file mode 100644 index 0000000..b6314c4 --- /dev/null +++ b/rophako/www/blog/index.html @@ -0,0 +1,13 @@ +{% extends "layout.html" %} +{% block title %}Blog{% endblock %} +{% block content %} + +{% if url_category %} +

Category: {{ url_category }}

+{% else %} +

My Blog

+{% endif %} + +{{ include_page("blog.partial_index") | safe }} + +{% endblock %} \ No newline at end of file diff --git a/rophako/www/blog/index.inc.html b/rophako/www/blog/index.inc.html new file mode 100644 index 0000000..8e7c700 --- /dev/null +++ b/rophako/www/blog/index.inc.html @@ -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" %} \ No newline at end of file diff --git a/rophako/www/blog/nav-links.inc.html b/rophako/www/blog/nav-links.inc.html new file mode 100644 index 0000000..c71fb94 --- /dev/null +++ b/rophako/www/blog/nav-links.inc.html @@ -0,0 +1,27 @@ +{# Older/Newer links #} + +{% if can_older or can_newer %} +
+ [ + RSS Feed | {# TODO! #} + {% if can_earlier %} + {% if category %} + < Newer + {% else %} + < Newer + {% endif %} + + {% if can_older %} | {% endif %} + {% endif %} + + {% if can_older %} + {% if category %} + Older > + {% else %} + Older > + {% endif %} + {% endif %} + + ] +
+{% endif %} \ No newline at end of file diff --git a/rophako/www/blog/update.html b/rophako/www/blog/update.html new file mode 100644 index 0000000..dcc3ad3 --- /dev/null +++ b/rophako/www/blog/update.html @@ -0,0 +1,152 @@ +{% extends "layout.html" %} +{% block title %}Update Blog{% endblock %} +{% block content %} + +{% if preview %} +

Preview: {{ subject }}

+ + {{ body|safe }} + +
+{% endif %} + +

Update Blog

+ +
+ + + + + Subject:
+

+ + Friendly ID:
+ You can leave this blank if this is a new post. It defaults to be based + on the subject.
+

+ + Body:
+
+ Emoticon reference (opens in new window)

+ + Avatar:
+ +

+ + Categories:
+ Comma-separated list, e.g. General, HTML, Perl, Web Design
+

+ + Privacy:
+

+ + Options:
+
+

+ + Time Stamp:
+ / + / + @ + : + : +
+ mm / dd / yyyy @ hh:mm:ss
+

+ + + +

+ +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/rophako/www/index.html b/rophako/www/index.html index b4b48c0..76fd760 100644 --- a/rophako/www/index.html +++ b/rophako/www/index.html @@ -4,6 +4,8 @@

Welcome!

-This is the Rophako CMS! +This is the Rophako CMS!

+ +{{ include_page("blog.partial_index") | safe }} {% endblock %} \ No newline at end of file diff --git a/rophako/www/static/avatars/default.png b/rophako/www/static/avatars/default.png new file mode 100644 index 0000000..b4e203c Binary files /dev/null and b/rophako/www/static/avatars/default.png differ diff --git a/scripts/siikir-blog-migrate.py b/scripts/siikir-blog-migrate.py new file mode 100644 index 0000000..a639d4c --- /dev/null +++ b/scripts/siikir-blog-migrate.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python + +"""Migrate blog database files from a PerlSiikir site to Rophako. + +Usage: scripts/siikir-blog-migrate.py + +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: {} ".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() \ No newline at end of file