A Python content management system designed for kirsle.net featuring a blog, comments and photo albums. https://rophako.kirsle.net/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

480 lines
15 KiB

  1. # -*- coding: utf-8 -*-
  2. """Endpoints for the web blog."""
  3. from flask import Blueprint, g, request, redirect, url_for, session, flash, make_response
  4. import re
  5. import datetime
  6. import calendar
  7. import time
  8. from xml.dom.minidom import Document
  9. import rophako.model.user as User
  10. import rophako.model.blog as Blog
  11. import rophako.model.comment as Comment
  12. import rophako.model.emoticons as Emoticons
  13. from rophako.utils import template, render_markdown, pretty_time, login_required
  14. from rophako.log import logger
  15. from config import *
  16. mod = Blueprint("blog", __name__, url_prefix="/blog")
  17. @mod.route("/")
  18. def index():
  19. return template("blog/index.html")
  20. @mod.route("/category/<category>")
  21. def category(category):
  22. g.info["url_category"] = category
  23. return template("blog/index.html")
  24. @mod.route("/entry/<fid>")
  25. def entry(fid):
  26. """Endpoint to view a specific blog entry."""
  27. # Resolve the friendly ID to a real ID.
  28. post_id = Blog.resolve_id(fid)
  29. if not post_id:
  30. flash("That blog post wasn't found.")
  31. return redirect(url_for(".index"))
  32. # Look up the post.
  33. post = Blog.get_entry(post_id)
  34. post["post_id"] = post_id
  35. # Render the body.
  36. if post["format"] == "markdown":
  37. post["rendered_body"] = render_markdown(post["body"])
  38. else:
  39. post["rendered_body"] = post["body"]
  40. # Render emoticons.
  41. if post["emoticons"]:
  42. post["rendered_body"] = Emoticons.render(post["rendered_body"])
  43. # Get the author's information.
  44. post["profile"] = User.get_user(uid=post["author"])
  45. post["photo"] = User.get_picture(uid=post["author"])
  46. post["photo_url"] = PHOTO_ROOT_PUBLIC
  47. # Pretty-print the time.
  48. post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"])
  49. # Count the comments for this post
  50. post["comment_count"] = Comment.count_comments("blog-{}".format(post_id))
  51. # Inject information about this post's siblings.
  52. index = Blog.get_index()
  53. siblings = [None, None] # previous, next
  54. sorted_ids = map(lambda y: int(y), sorted(index.keys(), key=lambda x: index[x]["time"], reverse=True))
  55. for i in range(0, len(sorted_ids)):
  56. if sorted_ids[i] == post_id:
  57. # Found us!
  58. if i > 0:
  59. # We have an older post.
  60. siblings[0] = index[ str(sorted_ids[i-1]) ]
  61. if i < len(sorted_ids) - 1:
  62. # We have a newer post.
  63. siblings[1] = index[ str(sorted_ids[i+1]) ]
  64. post["siblings"] = siblings
  65. g.info["post"] = post
  66. return template("blog/entry.html")
  67. @mod.route("/entry")
  68. @mod.route("/index")
  69. def dummy():
  70. return redirect(url_for(".index"))
  71. @mod.route("/update", methods=["GET", "POST"])
  72. @login_required
  73. def update():
  74. """Post/edit a blog entry."""
  75. # Get our available avatars.
  76. g.info["avatars"] = Blog.list_avatars()
  77. g.info["userpic"] = User.get_picture(uid=g.info["session"]["uid"])
  78. # Default vars.
  79. g.info.update(dict(
  80. post_id="",
  81. fid="",
  82. author=g.info["session"]["uid"],
  83. subject="",
  84. body="",
  85. format="markdown",
  86. avatar="",
  87. categories="",
  88. privacy=BLOG_DEFAULT_PRIVACY,
  89. emoticons=True,
  90. comments=BLOG_ALLOW_COMMENTS,
  91. month="",
  92. day="",
  93. year="",
  94. hour="",
  95. min="",
  96. sec="",
  97. preview=False,
  98. ))
  99. # Editing an existing post?
  100. post_id = request.args.get("id", None)
  101. if post_id:
  102. post_id = Blog.resolve_id(post_id)
  103. if post_id:
  104. logger.info("Editing existing blog post {}".format(post_id))
  105. post = Blog.get_entry(post_id)
  106. g.info["post_id"] = post_id
  107. g.info["post"] = post
  108. # Copy fields.
  109. for field in ["author", "fid", "subject", "format", "body", "avatar",
  110. "categories", "privacy", "emoticons", "comments"]:
  111. g.info[field] = post[field]
  112. # Dissect the time.
  113. date = datetime.datetime.fromtimestamp(post["time"])
  114. g.info.update(dict(
  115. month="{:02d}".format(date.month),
  116. day="{:02d}".format(date.day),
  117. year=date.year,
  118. hour="{:02d}".format(date.hour),
  119. min="{:02d}".format(date.minute),
  120. sec="{:02d}".format(date.second),
  121. ))
  122. # Are we SUBMITTING the form?
  123. if request.method == "POST":
  124. action = request.form.get("action")
  125. # Get all the fields from the posted params.
  126. g.info["post_id"] = request.form.get("id")
  127. for field in ["fid", "subject", "body", "avatar", "categories", "privacy"]:
  128. g.info[field] = request.form.get(field)
  129. for boolean in ["emoticons", "comments"]:
  130. g.info[boolean] = True if request.form.get(boolean, None) == "true" else False
  131. for number in ["author", "month", "day", "year", "hour", "min", "sec"]:
  132. g.info[number] = int(request.form.get(number, 0))
  133. # What action are they doing?
  134. if action == "preview":
  135. g.info["preview"] = True
  136. # Render markdown?
  137. if g.info["format"] == "markdown":
  138. g.info["rendered_body"] = render_markdown(g.info["body"])
  139. else:
  140. g.info["rendered_body"] = g.info["body"]
  141. # Render emoticons.
  142. if g.info["emoticons"]:
  143. g.info["rendered_body"] = Emoticons.render(g.info["rendered_body"])
  144. elif action == "publish":
  145. # Publishing! Validate inputs first.
  146. invalid = False
  147. if len(g.info["body"]) == 0:
  148. invalid = True
  149. flash("You must enter a body for your blog post.")
  150. if len(g.info["subject"]) == 0:
  151. invalid = True
  152. flash("You must enter a subject for your blog post.")
  153. # Make sure the times are valid.
  154. date = None
  155. try:
  156. date = datetime.datetime(
  157. g.info["year"],
  158. g.info["month"],
  159. g.info["day"],
  160. g.info["hour"],
  161. g.info["min"],
  162. g.info["sec"],
  163. )
  164. except ValueError, e:
  165. invalid = True
  166. flash("Invalid date/time: " + str(e))
  167. # Format the categories.
  168. tags = []
  169. for tag in g.info["categories"].split(","):
  170. tags.append(tag.strip())
  171. # Okay to update?
  172. if invalid is False:
  173. # Convert the date into a Unix time stamp.
  174. epoch = float(date.strftime("%s"))
  175. new_id, new_fid = Blog.post_entry(
  176. post_id = g.info["post_id"],
  177. epoch = epoch,
  178. author = g.info["author"],
  179. subject = g.info["subject"],
  180. fid = g.info["fid"],
  181. avatar = g.info["avatar"],
  182. categories = tags,
  183. privacy = g.info["privacy"],
  184. ip = request.remote_addr,
  185. emoticons = g.info["emoticons"],
  186. comments = g.info["comments"],
  187. format = g.info["format"],
  188. body = g.info["body"],
  189. )
  190. return redirect(url_for(".entry", fid=new_fid))
  191. if type(g.info["categories"]) is list:
  192. g.info["categories"] = ", ".join(g.info["categories"])
  193. return template("blog/update.html")
  194. @mod.route("/delete", methods=["GET", "POST"])
  195. @login_required
  196. def delete():
  197. """Delete a blog post."""
  198. post_id = request.args.get("id")
  199. # Resolve the post ID.
  200. post_id = Blog.resolve_id(post_id)
  201. if not post_id:
  202. flash("That blog post wasn't found.")
  203. return redirect(url_for(".index"))
  204. if request.method == "POST":
  205. confirm = request.form.get("confirm")
  206. if confirm == "true":
  207. Blog.delete_entry(post_id)
  208. flash("The blog entry has been deleted.")
  209. return redirect(url_for(".index"))
  210. # Get the entry's subject.
  211. post = Blog.get_entry(post_id)
  212. g.info["subject"] = post["subject"]
  213. g.info["post_id"] = post_id
  214. return template("blog/delete.html")
  215. @mod.route("/rss")
  216. def rss():
  217. """RSS feed for the blog."""
  218. doc = Document()
  219. rss = doc.createElement("rss")
  220. rss.setAttribute("version", "2.0")
  221. rss.setAttribute("xmlns:blogChannel", "http://backend.userland.com/blogChannelModule")
  222. doc.appendChild(rss)
  223. channel = doc.createElement("channel")
  224. rss.appendChild(channel)
  225. rss_time = "%a, %d %b %Y %H:%M:%S GMT"
  226. ######
  227. ## Channel Information
  228. ######
  229. today = time.strftime(rss_time, time.gmtime())
  230. xml_add_text_tags(doc, channel, [
  231. ["title", RSS_TITLE],
  232. ["link", RSS_LINK],
  233. ["description", RSS_DESCRIPTION],
  234. ["language", RSS_LANGUAGE],
  235. ["copyright", RSS_COPYRIGHT],
  236. ["pubDate", today],
  237. ["lastBuildDate", today],
  238. ["webmaster", RSS_WEBMASTER],
  239. ])
  240. ######
  241. ## Image Information
  242. ######
  243. image = doc.createElement("image")
  244. channel.appendChild(image)
  245. xml_add_text_tags(doc, image, [
  246. ["title", RSS_IMAGE_TITLE],
  247. ["url", RSS_IMAGE_URL],
  248. ["link", RSS_LINK],
  249. ["width", RSS_IMAGE_WIDTH],
  250. ["height", RSS_IMAGE_HEIGHT],
  251. ["description", RSS_IMAGE_DESCRIPTION],
  252. ])
  253. ######
  254. ## Add the blog posts
  255. ######
  256. index = Blog.get_index()
  257. posts = get_index_posts(index)
  258. for post_id in posts[:BLOG_ENTRIES_PER_RSS]:
  259. post = Blog.get_entry(post_id)
  260. item = doc.createElement("item")
  261. channel.appendChild(item)
  262. # Render the body.
  263. if post["format"] == "markdown":
  264. post["rendered_body"] = render_markdown(post["body"])
  265. else:
  266. post["rendered_body"] = post["body"]
  267. # Render emoticons.
  268. if post["emoticons"]:
  269. post["rendered_body"] = Emoticons.render(post["rendered_body"])
  270. xml_add_text_tags(doc, item, [
  271. ["title", post["subject"]],
  272. ["link", url_for("blog.entry", fid=post["fid"], _external=True)],
  273. ["description", post["rendered_body"]],
  274. ["pubDate", time.strftime(rss_time, time.gmtime(post["time"]))],
  275. ])
  276. resp = make_response(doc.toprettyxml(encoding="utf-8"))
  277. resp.headers["Content-Type"] = "application/rss+xml; charset=utf-8"
  278. return resp
  279. def xml_add_text_tags(doc, root_node, tags):
  280. """RSS feed helper function.
  281. Add a collection of simple tag/text pairs to a root XML element."""
  282. for pair in tags:
  283. name, value = pair
  284. channelTag = doc.createElement(name)
  285. channelTag.appendChild(doc.createTextNode(unicode(value)))
  286. root_node.appendChild(channelTag)
  287. def partial_index():
  288. """Partial template for including the index view of the blog."""
  289. # Get the blog index.
  290. index = Blog.get_index()
  291. pool = {} # The set of blog posts to show.
  292. category = g.info.get("url_category", None)
  293. # Are we narrowing by category?
  294. if category:
  295. # Narrow down the index to just those that match the category.
  296. for post_id, data in index.iteritems():
  297. if not category in data["categories"]:
  298. continue
  299. pool[post_id] = data
  300. # No such category?
  301. if len(pool) == 0:
  302. flash("There are no posts with that category.")
  303. return redirect(url_for(".index"))
  304. else:
  305. pool = index
  306. # Get the posts we want.
  307. posts = get_index_posts(pool)
  308. # Handle pagination.
  309. offset = request.args.get("skip", 0)
  310. try: offset = int(offset)
  311. except: offset = 0
  312. # Handle the offsets, and get those for the "older" and "earlier" posts.
  313. # "earlier" posts count down (towards index 0), "older" counts up.
  314. g.info["offset"] = offset
  315. g.info["earlier"] = offset - BLOG_ENTRIES_PER_PAGE if offset > 0 else 0
  316. g.info["older"] = offset + BLOG_ENTRIES_PER_PAGE
  317. if g.info["earlier"] < 0:
  318. g.info["earlier"] = 0
  319. if g.info["older"] < 0 or g.info["older"] > len(posts):
  320. g.info["older"] = 0
  321. g.info["count"] = 0
  322. # Can we go to other pages?
  323. g.info["can_earlier"] = True if offset > 0 else False
  324. g.info["can_older"] = False if g.info["older"] == 0 else True
  325. # Load the selected posts.
  326. selected = []
  327. stop = offset + BLOG_ENTRIES_PER_PAGE
  328. if stop > len(posts): stop = len(posts)
  329. index = 1 # Let each post know its position on-page.
  330. for i in range(offset, stop):
  331. post_id = posts[i]
  332. post = Blog.get_entry(post_id)
  333. post["post_id"] = post_id
  334. # Render the body.
  335. if post["format"] == "markdown":
  336. post["rendered_body"] = render_markdown(post["body"])
  337. else:
  338. post["rendered_body"] = post["body"]
  339. # Render emoticons.
  340. if post["emoticons"]:
  341. post["rendered_body"] = Emoticons.render(post["rendered_body"])
  342. # Get the author's information.
  343. post["profile"] = User.get_user(uid=post["author"])
  344. post["photo"] = User.get_picture(uid=post["author"])
  345. post["photo_url"] = PHOTO_ROOT_PUBLIC
  346. post["pretty_time"] = pretty_time(BLOG_TIME_FORMAT, post["time"])
  347. # Count the comments for this post
  348. post["comment_count"] = Comment.count_comments("blog-{}".format(post_id))
  349. post["position_index"] = index
  350. index += 1
  351. selected.append(post)
  352. g.info["count"] += 1
  353. g.info["category"] = category
  354. g.info["posts"] = selected
  355. return template("blog/index.inc.html")
  356. def get_index_posts(index):
  357. """Helper function to get data for the blog index page."""
  358. # Separate the sticky posts from the normal ones.
  359. sticky, normal = set(), set()
  360. for post_id, data in index.iteritems():
  361. if data["sticky"]:
  362. sticky.add(post_id)
  363. else:
  364. normal.add(post_id)
  365. # Sort the blog IDs by published time.
  366. posts = []
  367. posts.extend(sorted(sticky, key=lambda x: index[x]["time"], reverse=True))
  368. posts.extend(sorted(normal, key=lambda x: index[x]["time"], reverse=True))
  369. return posts
  370. def partial_tags():
  371. """Get a listing of tags and their quantities for the nav bar."""
  372. tags = Blog.get_categories()
  373. # Sort the tags by popularity.
  374. sort_tags = [ tag for tag in sorted(tags.keys(), key=lambda y: tags[y], reverse=True) ]
  375. result = []
  376. has_small = False
  377. for tag in sort_tags:
  378. result.append(dict(
  379. category=tag,
  380. count=tags[tag],
  381. small=tags[tag] < 3, # TODO: make this configurable
  382. ))
  383. if tags[tag] < 3:
  384. has_small = True
  385. g.info["tags"] = result
  386. g.info["has_small"] = has_small
  387. return template("blog/categories.inc.html")