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.

blog.py 13KB

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