A Python content management system designed for kirsle.net featuring a blog, comments and photo albums. https://rophako.kirsle.net/
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 

525 рядки
16 KiB

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