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.

230 lines
6.1KB

  1. # -*- coding: utf-8 -*-
  2. """Blog models."""
  3. from flask import g
  4. import time
  5. import re
  6. import glob
  7. import config
  8. import rophako.jsondb as JsonDB
  9. from rophako.log import logger
  10. def get_index():
  11. """Get the blog index.
  12. The index is the cache of available blog posts. It has the format:
  13. ```
  14. {
  15. 'post_id': {
  16. fid: Friendly ID for the blog post (for URLs)
  17. time: epoch time of the post
  18. sticky: the stickiness of the post (shows first on global views)
  19. author: the author user ID of the post
  20. categories: [ list of categories ]
  21. privacy: the privacy setting
  22. subject: the post subject
  23. },
  24. ...
  25. }
  26. ```
  27. """
  28. # Index doesn't exist?
  29. if not JsonDB.exists("blog/index"):
  30. return {}
  31. db = JsonDB.get("blog/index")
  32. # Hide any private posts if we aren't logged in.
  33. if not g.info["session"]["login"]:
  34. for post_id, data in db.iteritems():
  35. if data["privacy"] == "private":
  36. del db[post_id]
  37. return db
  38. def __get_categories():
  39. """Get the blog categories cache.
  40. The category cache is in the following format:
  41. ```
  42. {
  43. 'category_name': {
  44. 'post_id': 'friendly_id',
  45. ...
  46. },
  47. ...
  48. }
  49. ```
  50. """
  51. # Index doesn't exist?
  52. if not JsonDB.exists("blog/tags"):
  53. return {}
  54. return JsonDB.get("blog/tags")
  55. def get_entry(post_id):
  56. """Load a full blog entry."""
  57. if not JsonDB.exists("blog/entries/{}".format(post_id)):
  58. return None
  59. db = JsonDB.get("blog/entries/{}".format(post_id))
  60. # If no FID, set it to the ID.
  61. if len(db["fid"]) == 0:
  62. db["fid"] = str(post_id)
  63. return db
  64. def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
  65. privacy, ip, emoticons, comments, body):
  66. """Post (or update) a blog entry."""
  67. # Fetch the index.
  68. index = get_index()
  69. # Editing an existing post?
  70. if not post_id:
  71. post_id = get_next_id(index)
  72. logger.debug("Posting blog post ID {}".format(post_id))
  73. # Get a unique friendly ID.
  74. if not fid:
  75. # The default friendly ID = the subject.
  76. fid = subject.lower()
  77. fid = re.sub(r'[^A-Za-z0-9]', '-', fid)
  78. fid = re.sub(r'\-+', '-', fid)
  79. fid = fid.strip("-")
  80. logger.debug("Chosen friendly ID: {}".format(fid))
  81. # Make sure the friendly ID is unique!
  82. if len(fid):
  83. test = fid
  84. loop = 1
  85. logger.debug("Verifying the friendly ID is unique: {}".format(fid))
  86. while True:
  87. collision = False
  88. for k, v in index.iteritems():
  89. # Skip the same post, for updates.
  90. if k == post_id: continue
  91. if v["fid"] == test:
  92. # Not unique.
  93. loop += 1
  94. test = fid + "_" + unicode(loop)
  95. collision = True
  96. logger.debug("Collision with existing post {}: {}".format(k, v["fid"]))
  97. break
  98. # Was there a collision?
  99. if collision:
  100. continue # Try again.
  101. # Nope!
  102. break
  103. fid = test
  104. # Write the post.
  105. JsonDB.commit("blog/entries/{}".format(post_id), dict(
  106. fid = fid,
  107. ip = ip,
  108. time = epoch or int(time.time()),
  109. categories = categories,
  110. sticky = False, # TODO: implement sticky
  111. comments = comments,
  112. emoticons = emoticons,
  113. avatar = avatar,
  114. privacy = privacy or "public",
  115. author = author,
  116. subject = subject,
  117. body = body,
  118. ))
  119. # Update the index cache.
  120. index[post_id] = dict(
  121. fid = fid,
  122. time = epoch or int(time.time()),
  123. categories = categories,
  124. sticky = False, # TODO
  125. author = author,
  126. privacy = privacy or "public",
  127. subject = subject,
  128. )
  129. JsonDB.commit("blog/index", index)
  130. return post_id, fid
  131. def delete_entry(post_id):
  132. """Remove a blog entry."""
  133. # Fetch the blog information.
  134. index = get_index()
  135. post = get_entry(post_id)
  136. if post is None:
  137. logger.warning("Can't delete post {}, it doesn't exist!".format(post_id))
  138. # Delete the post.
  139. JsonDB.delete("blog/entries/{}".format(post_id))
  140. # Update the index cache.
  141. del index[str(post_id)] # Python JSON dict keys must be strings, never ints
  142. JsonDB.commit("blog/index", index)
  143. def resolve_id(fid):
  144. """Resolve a friendly ID to the blog ID number."""
  145. index = get_index()
  146. # If the ID is all numeric, it's the blog post ID directly.
  147. if re.match(r'^\d+$', fid):
  148. if fid in index:
  149. return int(fid)
  150. else:
  151. logger.error("Tried resolving blog post ID {} as an EntryID, but it wasn't there!".format(fid))
  152. return None
  153. # It's a friendly ID. Scan for it.
  154. for post_id, data in index.iteritems():
  155. if data["fid"] == fid:
  156. return int(post_id)
  157. logger.error("Friendly post ID {} wasn't found!".format(fid))
  158. return None
  159. def list_avatars():
  160. """Get a list of all the available blog avatars."""
  161. avatars = set()
  162. paths = [
  163. # Load avatars from both locations. We check the built-in set first,
  164. # so if you have matching names in your local site those will override.
  165. "rophako/www/static/avatars/*.*",
  166. "site/www/static/avatars/*.*",
  167. ]
  168. for path in paths:
  169. for filename in glob.glob(path):
  170. filename = filename.split("/")[-1]
  171. avatars.add(filename)
  172. return sorted(avatars, key=lambda x: x.lower())
  173. def get_next_id(index):
  174. """Get the next free ID for a blog post."""
  175. logger.debug("Getting next available blog ID number")
  176. sort = sorted(index.keys(), key=lambda x: int(x))
  177. logger.debug("Highest post ID is: {}".format(sort[-1]))
  178. next_id = int(sort[-1]) + 1
  179. # Sanity check!
  180. if next_id in index:
  181. raise Exception("Failed to get_next_id for the blog. Chosen ID is still in the index!")
  182. return next_id