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 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals, absolute_import
  3. """Blog models."""
  4. from flask import g
  5. import time
  6. import re
  7. import glob
  8. import os
  9. import sys
  10. if sys.version_info[0] > 2:
  11. def unicode(s):
  12. return s
  13. from rophako.settings import Config
  14. import rophako.jsondb as JsonDB
  15. from rophako.log import logger
  16. def get_index(drafts=False):
  17. """Get the blog index.
  18. The index is the cache of available blog posts. It has the format:
  19. ```
  20. {
  21. 'post_id': {
  22. fid: Friendly ID for the blog post (for URLs)
  23. time: epoch time of the post
  24. sticky: the stickiness of the post (shows first on global views)
  25. author: the author user ID of the post
  26. categories: [ list of categories ]
  27. privacy: the privacy setting
  28. subject: the post subject
  29. },
  30. ...
  31. }
  32. ```
  33. Args:
  34. drafts (bool): Whether to allow draft posts to be included in the index
  35. (for logged-in users only).
  36. """
  37. # Index doesn't exist?
  38. if not JsonDB.exists("blog/index"):
  39. return rebuild_index()
  40. db = JsonDB.get("blog/index")
  41. # Filter out posts that shouldn't be visible (draft/private)
  42. posts = list(db.keys())
  43. for post_id in posts:
  44. privacy = db[post_id]["privacy"]
  45. # Drafts are hidden universally so they can't be seen on any of the
  46. # normal blog routes.
  47. if privacy == "draft":
  48. if drafts is False or not g.info["session"]["login"]:
  49. del db[post_id]
  50. # Private posts are only visible to logged in users.
  51. elif privacy == "private" and not g.info["session"]["login"]:
  52. del db[post_id]
  53. return db
  54. def get_drafts():
  55. """Get the draft blog posts.
  56. Drafts are hidden from all places of the blog, just like private posts are
  57. (for non-logged-in users), so get_index() skips drafts and therefore
  58. resolve_id, etc. does too, making them invisible on the normal blog pages.
  59. This function is like get_index() except it *only* returns the drafts.
  60. """
  61. # Index doesn't exist?
  62. if not JsonDB.exists("blog/index"):
  63. return rebuild_index()
  64. db = JsonDB.get("blog/index")
  65. # Filter out only the draft posts.
  66. return {
  67. key: data for key, data in db.items() if data["privacy"] == "draft"
  68. }
  69. def get_private():
  70. """Get only the private blog posts.
  71. Since you can view only drafts, it made sense to have an easy way to view
  72. only private posts, too.
  73. This function is like get_index() except it *only* returns the private
  74. posts. It doesn't check for logged-in users, because the routes that view
  75. all private posts are login_required anyway.
  76. """
  77. # Index doesn't exist?
  78. if not JsonDB.exists("blog/index"):
  79. return rebuild_index()
  80. db = JsonDB.get("blog/index")
  81. # Filter out only the draft posts.
  82. return {
  83. key: data for key, data in db.items() if data["privacy"] == "private"
  84. }
  85. def rebuild_index():
  86. """Rebuild the index.json if it goes missing."""
  87. index = {}
  88. entries = JsonDB.list_docs("blog/entries")
  89. for post_id in entries:
  90. db = JsonDB.get("blog/entries/{}".format(post_id))
  91. update_index(post_id, db, index, False)
  92. JsonDB.commit("blog/index", index)
  93. return index
  94. def update_index(post_id, post, index=None, commit=True):
  95. """Update a post's meta-data in the index. This is also used for adding a
  96. new post to the index for the first time.
  97. * post_id: The ID number for the post
  98. * post: The DB object for a blog post
  99. * index: If you already have the index open, you can pass it here
  100. * commit: Write the DB after updating the index (default True)"""
  101. if index is None:
  102. index = get_index(drafts=True)
  103. index[post_id] = dict(
  104. fid = post["fid"],
  105. time = post["time"] or int(time.time()),
  106. categories = post["categories"],
  107. sticky = post["sticky"],
  108. author = post["author"],
  109. privacy = post["privacy"] or "public",
  110. subject = post["subject"],
  111. )
  112. if commit:
  113. JsonDB.commit("blog/index", index)
  114. def get_categories():
  115. """Get the blog categories and their popularity."""
  116. index = get_index()
  117. # Group by tags.
  118. tags = {}
  119. for post, data in index.items():
  120. for tag in data["categories"]:
  121. if not tag in tags:
  122. tags[tag] = 0
  123. tags[tag] += 1
  124. return tags
  125. def get_entry(post_id):
  126. """Load a full blog entry."""
  127. if not JsonDB.exists("blog/entries/{}".format(post_id)):
  128. return None
  129. db = JsonDB.get("blog/entries/{}".format(post_id))
  130. # If no FID, set it to the ID.
  131. if len(db["fid"]) == 0:
  132. db["fid"] = str(post_id)
  133. # If no "format" option, set it to HTML (legacy)
  134. if db.get("format", "") == "":
  135. db["format"] = "html"
  136. return db
  137. def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
  138. privacy, ip, emoticons, sticky, comments, format, body):
  139. """Post (or update) a blog entry."""
  140. # Fetch the index.
  141. index = get_index(drafts=True)
  142. # Editing an existing post?
  143. if not post_id:
  144. post_id = get_next_id(index)
  145. logger.debug("Posting blog post ID {}".format(post_id))
  146. # Get a unique friendly ID.
  147. if not fid:
  148. # The default friendly ID = the subject.
  149. fid = subject.lower()
  150. fid = re.sub(r'[^A-Za-z0-9]', '-', fid)
  151. fid = re.sub(r'\-+', '-', fid)
  152. fid = fid.strip("-")
  153. logger.debug("Chosen friendly ID: {}".format(fid))
  154. # Make sure the friendly ID is unique!
  155. if len(fid):
  156. test = fid
  157. loop = 1
  158. logger.debug("Verifying the friendly ID is unique: {}".format(fid))
  159. while True:
  160. collision = False
  161. for k, v in index.items():
  162. # Skip the same post, for updates.
  163. if k == post_id: continue
  164. if v["fid"] == test:
  165. # Not unique.
  166. loop += 1
  167. test = fid + "_" + unicode(loop)
  168. collision = True
  169. logger.debug("Collision with existing post {}: {}".format(k, v["fid"]))
  170. break
  171. # Was there a collision?
  172. if collision:
  173. continue # Try again.
  174. # Nope!
  175. break
  176. fid = test
  177. # DB body for the post.
  178. db = dict(
  179. fid = fid,
  180. ip = ip,
  181. time = epoch or int(time.time()),
  182. categories = categories,
  183. sticky = sticky,
  184. comments = comments,
  185. emoticons = emoticons,
  186. avatar = avatar,
  187. privacy = privacy or "public",
  188. author = author,
  189. subject = subject,
  190. format = format,
  191. body = body,
  192. )
  193. # Write the post.
  194. JsonDB.commit("blog/entries/{}".format(post_id), db)
  195. # Update the index cache.
  196. update_index(post_id, db, index)
  197. return post_id, fid
  198. def delete_entry(post_id):
  199. """Remove a blog entry."""
  200. # Fetch the blog information.
  201. index = get_index(drafts=True)
  202. post = get_entry(post_id)
  203. if post is None:
  204. logger.warning("Can't delete post {}, it doesn't exist!".format(post_id))
  205. # Delete the post.
  206. JsonDB.delete("blog/entries/{}".format(post_id))
  207. # Update the index cache.
  208. del index[str(post_id)] # Python JSON dict keys must be strings, never ints
  209. JsonDB.commit("blog/index", index)
  210. def resolve_id(fid, drafts=False):
  211. """Resolve a friendly ID to the blog ID number.
  212. Args:
  213. drafts (bool): Whether to allow draft IDs to be resolved (for
  214. logged-in users only).
  215. """
  216. index = get_index(drafts=drafts)
  217. # If the ID is all numeric, it's the blog post ID directly.
  218. if re.match(r'^\d+$', fid):
  219. if fid in index:
  220. return int(fid)
  221. else:
  222. logger.error("Tried resolving blog post ID {} as an EntryID, but it wasn't there!".format(fid))
  223. return None
  224. # It's a friendly ID. Scan for it.
  225. for post_id, data in index.items():
  226. if data["fid"] == fid:
  227. return int(post_id)
  228. logger.error("Friendly post ID {} wasn't found!".format(fid))
  229. return None
  230. def list_avatars():
  231. """Get a list of all the available blog avatars."""
  232. avatars = set()
  233. paths = [
  234. # Load avatars from both locations. We check the built-in set first,
  235. # so if you have matching names in your local site those will override.
  236. "rophako/www/static/avatars/*.*",
  237. os.path.join(Config.site.site_root, "static", "avatars", "*.*"),
  238. ]
  239. for path in paths:
  240. for filename in glob.glob(path):
  241. filename = filename.split("/")[-1]
  242. avatars.add(filename)
  243. return sorted(avatars, key=lambda x: x.lower())
  244. def get_next_id(index):
  245. """Get the next free ID for a blog post."""
  246. logger.debug("Getting next available blog ID number")
  247. sort = sorted(index.keys(), key=lambda x: int(x))
  248. next_id = 1
  249. if len(sort) > 0:
  250. next_id = int(sort[-1]) + 1
  251. logger.debug("Highest post ID is: {}".format(next_id))
  252. # Sanity check!
  253. if next_id in index:
  254. raise Exception("Failed to get_next_id for the blog. Chosen ID is still in the index!")
  255. return next_id