diff --git a/config-sample.py b/config-sample.py index 28a6906..1355960 100644 --- a/config-sample.py +++ b/config-sample.py @@ -4,6 +4,7 @@ import os _basedir = os.path.abspath(os.path.dirname(__file__)) +import datetime DEBUG = True @@ -63,6 +64,19 @@ 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 +# RSS feed settings. +RSS_TITLE = "Kirsle.net" +RSS_LINK = "http://www.kirsle.net/" +RSS_LANGUAGE = "en" +RSS_DESCRIPTION = "The web blog of Kirsle" +RSS_COPYRIGHT = "Copyright {}, Kirsle.net".format(str(datetime.datetime.now().strftime("%Y"))) +RSS_WEBMASTER = NOTIFY_ADDRESS[0] +RSS_IMAGE_TITLE = RSS_TITLE +RSS_IMAGE_URL = "http://www.kirsle.net/static/avatars/casey.png" +RSS_IMAGE_WIDTH = 96 +RSS_IMAGE_HEIGHT = 96 +RSS_IMAGE_DESCRIPTION = "Kirsle's Avatar" + ################################################################################ ## Photo Settings ## ################################################################################ diff --git a/rophako/__init__.py b/rophako/__init__.py index 2648aaf..01021d0 100644 --- a/rophako/__init__.py +++ b/rophako/__init__.py @@ -21,12 +21,14 @@ from rophako.modules.blog import mod as BlogModule from rophako.modules.photo import mod as PhotoModule from rophako.modules.comment import mod as CommentModule from rophako.modules.emoticons import mod as EmoticonsModule +from rophako.modules.contact import mod as ContactModule app.register_blueprint(AdminModule) app.register_blueprint(AccountModule) app.register_blueprint(BlogModule) app.register_blueprint(PhotoModule) app.register_blueprint(CommentModule) app.register_blueprint(EmoticonsModule) +app.register_blueprint(ContactModule) # Custom Jinja handler to support custom- and default-template folders for # rendering templates. @@ -114,6 +116,8 @@ def catchall(path): return send_file(abspath) elif not "." in path and os.path.isfile(abspath + ".html"): return rophako.utils.template(path + ".html") + elif not "." in path and os.path.isfile(abspath + "/index.html"): + return rophako.utils.template(path + "/index.html") return not_found("404") @@ -125,7 +129,6 @@ def index(): @app.errorhandler(404) def not_found(error): - print "NOT FOUND" return render_template('errors/404.html', **g.info), 404 diff --git a/rophako/model/blog.py b/rophako/model/blog.py index dea01f0..fdab459 100644 --- a/rophako/model/blog.py +++ b/rophako/model/blog.py @@ -46,26 +46,19 @@ def get_index(): return db -def __get_categories(): - """Get the blog categories cache. +def get_categories(): + """Get the blog categories and their popularity.""" + index = get_index() - The category cache is in the following format: + # Group by tags. + tags = {} + for post, data in index.iteritems(): + for tag in data["categories"]: + if not tag in tags: + tags[tag] = 0 + tags[tag] += 1 - ``` - { - 'category_name': { - 'post_id': 'friendly_id', - ... - }, - ... - } - ``` - """ - - # Index doesn't exist? - if not JsonDB.exists("blog/tags"): - return {} - return JsonDB.get("blog/tags") + return tags def get_entry(post_id): diff --git a/rophako/modules/blog.py b/rophako/modules/blog.py index c2bb02c..700e6ca 100644 --- a/rophako/modules/blog.py +++ b/rophako/modules/blog.py @@ -6,6 +6,8 @@ from flask import Blueprint, g, request, redirect, url_for, session, flash import re import datetime import calendar +import time +from xml.dom.minidom import Document import rophako.model.user as User import rophako.model.blog as Blog @@ -225,6 +227,85 @@ def delete(): return template("blog/delete.html") + +@mod.route("/rss") +def rss(): + """RSS feed for the blog.""" + doc = Document() + + rss = doc.createElement("rss") + rss.setAttribute("version", "2.0") + rss.setAttribute("xmlns:blogChannel", "http://backend.userland.com/blogChannelModule") + doc.appendChild(rss) + + channel = doc.createElement("channel") + rss.appendChild(channel) + + rss_time = "%a, %d %b %Y %H:%M:%S GMT" + + ###### + ## Channel Information + ###### + + today = time.strftime(rss_time, time.gmtime()) + + xml_add_text_tags(doc, channel, [ + ["title", RSS_TITLE], + ["link", RSS_LINK], + ["description", RSS_DESCRIPTION], + ["language", RSS_LANGUAGE], + ["copyright", RSS_COPYRIGHT], + ["pubDate", today], + ["lastBuildDate", today], + ["webmaster", RSS_WEBMASTER], + ]) + + ###### + ## Image Information + ###### + + image = doc.createElement("image") + channel.appendChild(image) + xml_add_text_tags(doc, image, [ + ["title", RSS_IMAGE_TITLE], + ["url", RSS_IMAGE_URL], + ["link", RSS_LINK], + ["width", RSS_IMAGE_WIDTH], + ["height", RSS_IMAGE_HEIGHT], + ["description", RSS_IMAGE_DESCRIPTION], + ]) + + ###### + ## Add the blog posts + ###### + + index = Blog.get_index() + posts = get_index_posts(index) + for post_id in posts[:BLOG_ENTRIES_PER_RSS]: + post = Blog.get_entry(post_id) + item = doc.createElement("item") + channel.appendChild(item) + xml_add_text_tags(doc, item, [ + ["title", post["subject"]], + ["link", url_for("blog.entry", fid=post["fid"])], + ["description", post["body"]], + ["pubDate", time.strftime(rss_time, time.gmtime(post["time"]))], + ]) + + return doc.toprettyxml(encoding="utf-8") + + +def xml_add_text_tags(doc, root_node, tags): + """RSS feed helper function. + + Add a collection of simple tag/text pairs to a root XML element.""" + for pair in tags: + name, value = pair + channelTag = doc.createElement(name) + channelTag.appendChild(doc.createTextNode(str(value))) + root_node.appendChild(channelTag) + + def partial_index(): """Partial template for including the index view of the blog.""" @@ -249,18 +330,8 @@ def partial_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)) + # Get the posts we want. + posts = get_index_posts(pool) # Handle pagination. offset = request.args.get("skip", 0) @@ -313,3 +384,41 @@ def partial_index(): g.info["posts"] = selected return template("blog/index.inc.html") + + +def get_index_posts(index): + """Helper function to get data for the blog index page.""" + # Separate the sticky posts from the normal ones. + sticky, normal = set(), set() + for post_id, data in index.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: index[x]["time"], reverse=True)) + posts.extend(sorted(normal, key=lambda x: index[x]["time"], reverse=True)) + return posts + +def partial_tags(): + """Get a listing of tags and their quantities for the nav bar.""" + tags = Blog.get_categories() + + # Sort the tags by popularity. + sort_tags = [ tag for tag in sorted(tags.keys(), key=lambda y: tags[y], reverse=True) ] + result = [] + has_small = False + for tag in sort_tags: + result.append(dict( + category=tag, + count=tags[tag], + small=tags[tag] < 3, # TODO: make this configurable + )) + if tags[tag] < 3: + has_small = True + + g.info["tags"] = result + g.info["has_small"] = has_small + return template("blog/categories.inc.html") \ No newline at end of file diff --git a/rophako/modules/comment.py b/rophako/modules/comment.py index 06c8d9b..bffaa32 100644 --- a/rophako/modules/comment.py +++ b/rophako/modules/comment.py @@ -14,7 +14,6 @@ from config import * mod = Blueprint("comment", __name__, url_prefix="/comments") -## TODO: emoticon support @mod.route("/") def index(): diff --git a/rophako/modules/contact.py b/rophako/modules/contact.py new file mode 100644 index 0000000..838db48 --- /dev/null +++ b/rophako/modules/contact.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +"""Endpoints for contacting the site owner.""" + +from flask import Blueprint, g, request, redirect, url_for, session, flash +import re +import time + +from rophako.utils import template, send_email +from rophako.log import logger +from config import * + +mod = Blueprint("contact", __name__, url_prefix="/contact") + + +@mod.route("/") +def index(): + return template("contact/index.html") + + +@mod.route("/send", methods=["POST"]) +def send(): + """Submitting the contact form.""" + name = request.form.get("name", "") or "Anonymous" + email = request.form.get("email", "") + subject = request.form.get("subject", "") or "[No Subject]" + message = request.form.get("message", "") + + # Spam traps. + trap1 = request.form.get("contact", "x") != "" + trap2 = request.form.get("website", "x") != "http://" + if trap1 or trap2: + flash("Wanna try that again?") + return redirect(url_for(".index")) + + # Message is required. + if len(message) == 0: + flash("The message is required.") + return redirect(url_for(".index")) + + # Send the e-mail. + send_email( + to=NOTIFY_ADDRESS, + subject="Contact Form on {}: {}".format(SITE_NAME, subject), + message="""A visitor to {site_name} has sent you a message! + +IP Address: {ip} +User Agent: {ua} +Referrer: {referer} +Name: {name} +E-mail: {email} +Subject: {subject} + +{message}""".format( + site_name=SITE_NAME, + ip=request.remote_addr, + ua=request.user_agent.string, + referer=request.headers.get("Referer", ""), + name=name, + email=email, + subject=subject, + message=message, + ) + ) + + flash("Your message has been delivered.") + return redirect(url_for("index")) \ No newline at end of file diff --git a/rophako/modules/kirsle_legacy.py b/rophako/modules/kirsle_legacy.py index 3a20f48..1242e28 100644 --- a/rophako/modules/kirsle_legacy.py +++ b/rophako/modules/kirsle_legacy.py @@ -2,14 +2,21 @@ # Legacy endpoint compatibility from kirsle.net. -from flask import request, redirect, url_for +from flask import g, request, redirect, url_for, flash +import re +import os + from rophako import app +from rophako.utils import template import rophako.model.blog as Blog +import rophako.jsondb as JsonDB + @app.route("/+") def google_plus(): return redirect("https://plus.google.com/+NoahPetherbridge/posts") + @app.route("/blog.html") def ancient_legacy_blog(): post_id = request.args.get("id", None) @@ -24,10 +31,67 @@ def ancient_legacy_blog(): 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("/rss.cgi") +def legacy_rss(): + return redirect(url_for("blog.rss")) + + +@app.route("/firered/") +@app.route("/firered") +def legacy_firered(page=""): + g.info["page"] = str(page) or "1" + return template("firered.html") + + +@app.route("/download", methods=["GET", "POST"]) +def legacy_download(): + form = None + if request.method == "POST": + form = request.form + else: + form = request.args + + method = form.get("method", "index") + project = form.get("project", "") + filename = form.get("file", "") + + root = "/home/kirsle/www/projects" + + if project and filename: + # Filter the sections. + project = re.sub(r'[^A-Za-z0-9]', '', project) # Project name is alphanumeric only. + filename = re.sub(r'[^A-Za-z0-9\-_\.]', '', filename) + + # Check that all the files exist. + if os.path.isdir(os.path.join(root, project)) and os.path.isfile(os.path.join(root, project, filename)): + # Hit counters. + hits = { "hits": 0 } + db = "data/downloads/{}-{}".format(project, filename) + if JsonDB.exists(db.format(project, filename)): + hits = JsonDB.get(db) + + # Actually getting the file? + if method == "get": + # Up the hit counter. + hits["hits"] += 1 + JsonDB.commit(db, hits) + + g.info["method"] = method + g.info["project"] = project + g.info["file"] = filename + g.info["hits"] = hits["hits"] + return template("download.html") + + flash("The file or project wasn't found.") + return redirect(url_for("index")) + + @app.route("/.html") def legacy_url(page): return "/{}".format(page) \ No newline at end of file diff --git a/rophako/www/blog/categories.inc.html b/rophako/www/blog/categories.inc.html new file mode 100644 index 0000000..26686aa --- /dev/null +++ b/rophako/www/blog/categories.inc.html @@ -0,0 +1,19 @@ +{% for tag in tags %} + {% if not tag["small"] %} + » {{ tag['category'] }} + ({{ tag['count'] }})
+ {% endif %} +{% endfor %} +{% if has_small %} + +
+ ¤ Show more... +
+{% endif %} \ No newline at end of file diff --git a/rophako/www/blog/nav-links.inc.html b/rophako/www/blog/nav-links.inc.html index c71fb94..2892be3 100644 --- a/rophako/www/blog/nav-links.inc.html +++ b/rophako/www/blog/nav-links.inc.html @@ -3,7 +3,7 @@ {% if can_older or can_newer %}
[ - RSS Feed | {# TODO! #} + RSS Feed | {% if can_earlier %} {% if category %} < Newer diff --git a/rophako/www/contact/index.html b/rophako/www/contact/index.html new file mode 100644 index 0000000..4fe6fc5 --- /dev/null +++ b/rophako/www/contact/index.html @@ -0,0 +1,48 @@ +{% extends "layout.html" %} +{% block title %}Contact Me{% endblock %} +{% block content %} + +

Contact Me

+ +You can use the form below to send me an e-mail.

+ +

+ + + + + + + + + + + + +
+ Your name:
+ (so I know who you are)
+ +
+ Your email:
+ (if you want a response)
+ +
+ Message subject:
+ (optional)
+

+ + Message:
+ (required)
+ +

+ +
+
+ If you can see these boxes, don't touch them.
+
+ +
+
+ +{% endblock %} \ No newline at end of file