Add RSS feed, contact page, Kirsle.net stuff

This commit is contained in:
Noah 2014-04-06 18:02:34 -07:00
parent 4bb9b5d687
commit 9a4f74844d
10 changed files with 350 additions and 34 deletions

View File

@ -4,6 +4,7 @@
import os import os
_basedir = os.path.abspath(os.path.dirname(__file__)) _basedir = os.path.abspath(os.path.dirname(__file__))
import datetime
DEBUG = True 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_TIME_FORMAT = "%A, %B %d %Y @ %I:%M:%S %p" # "Weekday, Month dd yyyy @ hh:mm:ss AM"
BLOG_ALLOW_COMMENTS = True 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 ## ## Photo Settings ##
################################################################################ ################################################################################

View File

@ -21,12 +21,14 @@ from rophako.modules.blog import mod as BlogModule
from rophako.modules.photo import mod as PhotoModule from rophako.modules.photo import mod as PhotoModule
from rophako.modules.comment import mod as CommentModule from rophako.modules.comment import mod as CommentModule
from rophako.modules.emoticons import mod as EmoticonsModule from rophako.modules.emoticons import mod as EmoticonsModule
from rophako.modules.contact import mod as ContactModule
app.register_blueprint(AdminModule) app.register_blueprint(AdminModule)
app.register_blueprint(AccountModule) app.register_blueprint(AccountModule)
app.register_blueprint(BlogModule) app.register_blueprint(BlogModule)
app.register_blueprint(PhotoModule) app.register_blueprint(PhotoModule)
app.register_blueprint(CommentModule) app.register_blueprint(CommentModule)
app.register_blueprint(EmoticonsModule) app.register_blueprint(EmoticonsModule)
app.register_blueprint(ContactModule)
# 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.
@ -114,6 +116,8 @@ def catchall(path):
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 rophako.utils.template(path + ".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") return not_found("404")
@ -125,7 +129,6 @@ def index():
@app.errorhandler(404) @app.errorhandler(404)
def not_found(error): def not_found(error):
print "NOT FOUND"
return render_template('errors/404.html', **g.info), 404 return render_template('errors/404.html', **g.info), 404

View File

@ -46,26 +46,19 @@ def get_index():
return db return db
def __get_categories(): def get_categories():
"""Get the blog categories cache. """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
``` return tags
{
'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): def get_entry(post_id):

View File

@ -6,6 +6,8 @@ from flask import Blueprint, g, request, redirect, url_for, session, flash
import re import re
import datetime import datetime
import calendar import calendar
import time
from xml.dom.minidom import Document
import rophako.model.user as User import rophako.model.user as User
import rophako.model.blog as Blog import rophako.model.blog as Blog
@ -225,6 +227,85 @@ def delete():
return template("blog/delete.html") 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(): def partial_index():
"""Partial template for including the index view of the blog.""" """Partial template for including the index view of the blog."""
@ -249,18 +330,8 @@ def partial_index():
else: else:
pool = index pool = index
# Separate the sticky posts from the normal ones. # Get the posts we want.
sticky, normal = set(), set() posts = get_index_posts(pool)
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. # Handle pagination.
offset = request.args.get("skip", 0) offset = request.args.get("skip", 0)
@ -313,3 +384,41 @@ def partial_index():
g.info["posts"] = selected g.info["posts"] = selected
return template("blog/index.inc.html") 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")

View File

@ -14,7 +14,6 @@ from config import *
mod = Blueprint("comment", __name__, url_prefix="/comments") mod = Blueprint("comment", __name__, url_prefix="/comments")
## TODO: emoticon support
@mod.route("/") @mod.route("/")
def index(): def index():

View File

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

View File

@ -2,14 +2,21 @@
# Legacy endpoint compatibility from kirsle.net. # 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 import app
from rophako.utils import template
import rophako.model.blog as Blog import rophako.model.blog as Blog
import rophako.jsondb as JsonDB
@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 ancient_legacy_blog(): def ancient_legacy_blog():
post_id = request.args.get("id", None) post_id = request.args.get("id", None)
@ -24,10 +31,67 @@ def ancient_legacy_blog():
return redirect(url_for("blog.entry", fid=post["fid"])) return redirect(url_for("blog.entry", fid=post["fid"]))
@app.route("/blog/kirsle/<fid>") @app.route("/blog/kirsle/<fid>")
def legacy_blog(fid): def legacy_blog(fid):
return redirect(url_for("blog.entry", fid=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/<page>")
@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("/<page>.html") @app.route("/<page>.html")
def legacy_url(page): def legacy_url(page):
return "/{}".format(page) return "/{}".format(page)

View File

@ -0,0 +1,19 @@
{% for tag in tags %}
{% if not tag["small"] %}
&#0187; <a href="{{ url_for('blog.category', category=tag['category']) }}">{{ tag['category'] }}</a>
<small>({{ tag['count'] }})</small><br>
{% endif %}
{% endfor %}
{% if has_small %}
<div id="blog_show_more" style="display: none">
{% for tag in tags %}
{% if tag["small"] %}
&#0187; <a href="{{ url_for('blog.category', category=tag['category']) }}">{{ tag['category'] }}</a>
<small>({{ tag['count'] }})</small><br>
{% endif %}
{% endfor %}
</div>
<div id="blog_show_less" style="display: block">
&#0164; <a href="#" onClick="$('#blog_show_less').hide(); $('#blog_show_more').show(1000); return false">Show more...</a>
</div>
{% endif %}

View File

@ -3,7 +3,7 @@
{% if can_older or can_newer %} {% if can_older or can_newer %}
<div class="right"> <div class="right">
[ [
<a href="/rss">RSS Feed</a> | {# TODO! #} <a href="{{ url_for('blog.rss') }}">RSS Feed</a> |
{% if can_earlier %} {% if can_earlier %}
{% if category %} {% if category %}
<a href="{{ url_for('blog.category', category=category) }}?skip={{ earlier }}">&lt; Newer</a> <a href="{{ url_for('blog.category', category=category) }}?skip={{ earlier }}">&lt; Newer</a>

View File

@ -0,0 +1,48 @@
{% extends "layout.html" %}
{% block title %}Contact Me{% endblock %}
{% block content %}
<h1>Contact Me</h1>
You can use the form below to send me an e-mail.<p>
<form name="contact" action="{{ url_for('contact.send') }}" method="POST">
<input type="hidden" name="token" value="{{ csrf_token() }}">
<table border="0" cellspacing="0" cellpadding="2">
<tr>
<td width="50%" align="left" valign="middle">
<strong>Your name:</strong><br>
<small>(so I know who you are)</small><br>
<input type="text" size="40" name="name">
</td>
<td width="50%" align="left" valign="middle">
<strong>Your email:</strong><br>
<small>(if you want a response)</small><br>
<input type="email" size="40" name="email">
</td>
</tr>
<tr>
<td colspan="2" align="left" valign="middle">
<strong>Message subject:</strong><br>
<small>(optional)</small><br>
<input type="text" size="40" name="subject" style="width: 100%"><p>
<strong>Message:</strong><br>
<small>(required)</small><br>
<textarea cols="40" rows="12" name="message" style="width: 100%"></textarea>
</td>
</tr>
<tr>
<td colspan="2" align="right" valign="middle">
<button type="submit">Send Message</button>
</td>
</tr>
</table>
<div style="display: none">
If you can see these boxes, don't touch them.<br>
<input type="text" size="40" name="contact" value=""><br>
<input type="text" size="40" name="website" value="http://">
</div>
</form>
{% endblock %}