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.

232 lines
6.3KB

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