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.
 
 
 
 
 

270 lines
7.4 KiB

  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():
  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. """
  34. # Index doesn't exist?
  35. if not JsonDB.exists("blog/index"):
  36. return rebuild_index()
  37. db = JsonDB.get("blog/index")
  38. # Hide any private posts if we aren't logged in.
  39. if not g.info["session"]["login"]:
  40. posts = list(db.keys())
  41. for post_id in posts:
  42. if db[post_id]["privacy"] == "private":
  43. del db[post_id]
  44. return db
  45. def rebuild_index():
  46. """Rebuild the index.json if it goes missing."""
  47. index = {}
  48. entries = JsonDB.list_docs("blog/entries")
  49. for post_id in entries:
  50. db = JsonDB.get("blog/entries/{}".format(post_id))
  51. update_index(post_id, db, index, False)
  52. JsonDB.commit("blog/index", index)
  53. return index
  54. def update_index(post_id, post, index=None, commit=True):
  55. """Update a post's meta-data in the index. This is also used for adding a
  56. new post to the index for the first time.
  57. * post_id: The ID number for the post
  58. * post: The DB object for a blog post
  59. * index: If you already have the index open, you can pass it here
  60. * commit: Write the DB after updating the index (default True)"""
  61. if index is None:
  62. index = get_index()
  63. index[post_id] = dict(
  64. fid = post["fid"],
  65. time = post["time"] or int(time.time()),
  66. categories = post["categories"],
  67. sticky = False, # TODO
  68. author = post["author"],
  69. privacy = post["privacy"] or "public",
  70. subject = post["subject"],
  71. )
  72. if commit:
  73. JsonDB.commit("blog/index", index)
  74. def get_categories():
  75. """Get the blog categories and their popularity."""
  76. index = get_index()
  77. # Group by tags.
  78. tags = {}
  79. for post, data in index.items():
  80. for tag in data["categories"]:
  81. if not tag in tags:
  82. tags[tag] = 0
  83. tags[tag] += 1
  84. return tags
  85. def get_entry(post_id):
  86. """Load a full blog entry."""
  87. if not JsonDB.exists("blog/entries/{}".format(post_id)):
  88. return None
  89. db = JsonDB.get("blog/entries/{}".format(post_id))
  90. # If no FID, set it to the ID.
  91. if len(db["fid"]) == 0:
  92. db["fid"] = str(post_id)
  93. # If no "format" option, set it to HTML (legacy)
  94. if db.get("format", "") == "":
  95. db["format"] = "html"
  96. return db
  97. def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
  98. privacy, ip, emoticons, comments, format, body):
  99. """Post (or update) a blog entry."""
  100. # Fetch the index.
  101. index = get_index()
  102. # Editing an existing post?
  103. if not post_id:
  104. post_id = get_next_id(index)
  105. logger.debug("Posting blog post ID {}".format(post_id))
  106. # Get a unique friendly ID.
  107. if not fid:
  108. # The default friendly ID = the subject.
  109. fid = subject.lower()
  110. fid = re.sub(r'[^A-Za-z0-9]', '-', fid)
  111. fid = re.sub(r'\-+', '-', fid)
  112. fid = fid.strip("-")
  113. logger.debug("Chosen friendly ID: {}".format(fid))
  114. # Make sure the friendly ID is unique!
  115. if len(fid):
  116. test = fid
  117. loop = 1
  118. logger.debug("Verifying the friendly ID is unique: {}".format(fid))
  119. while True:
  120. collision = False
  121. for k, v in index.items():
  122. # Skip the same post, for updates.
  123. if k == post_id: continue
  124. if v["fid"] == test:
  125. # Not unique.
  126. loop += 1
  127. test = fid + "_" + unicode(loop)
  128. collision = True
  129. logger.debug("Collision with existing post {}: {}".format(k, v["fid"]))
  130. break
  131. # Was there a collision?
  132. if collision:
  133. continue # Try again.
  134. # Nope!
  135. break
  136. fid = test
  137. # DB body for the post.
  138. db = dict(
  139. fid = fid,
  140. ip = ip,
  141. time = epoch or int(time.time()),
  142. categories = categories,
  143. sticky = False, # TODO: implement sticky
  144. comments = comments,
  145. emoticons = emoticons,
  146. avatar = avatar,
  147. privacy = privacy or "public",
  148. author = author,
  149. subject = subject,
  150. format = format,
  151. body = body,
  152. )
  153. # Write the post.
  154. JsonDB.commit("blog/entries/{}".format(post_id), db)
  155. # Update the index cache.
  156. update_index(post_id, db, index)
  157. return post_id, fid
  158. def delete_entry(post_id):
  159. """Remove a blog entry."""
  160. # Fetch the blog information.
  161. index = get_index()
  162. post = get_entry(post_id)
  163. if post is None:
  164. logger.warning("Can't delete post {}, it doesn't exist!".format(post_id))
  165. # Delete the post.
  166. JsonDB.delete("blog/entries/{}".format(post_id))
  167. # Update the index cache.
  168. del index[str(post_id)] # Python JSON dict keys must be strings, never ints
  169. JsonDB.commit("blog/index", index)
  170. def resolve_id(fid):
  171. """Resolve a friendly ID to the blog ID number."""
  172. index = get_index()
  173. # If the ID is all numeric, it's the blog post ID directly.
  174. if re.match(r'^\d+$', fid):
  175. if fid in index:
  176. return int(fid)
  177. else:
  178. logger.error("Tried resolving blog post ID {} as an EntryID, but it wasn't there!".format(fid))
  179. return None
  180. # It's a friendly ID. Scan for it.
  181. for post_id, data in index.items():
  182. if data["fid"] == fid:
  183. return int(post_id)
  184. logger.error("Friendly post ID {} wasn't found!".format(fid))
  185. return None
  186. def list_avatars():
  187. """Get a list of all the available blog avatars."""
  188. avatars = set()
  189. paths = [
  190. # Load avatars from both locations. We check the built-in set first,
  191. # so if you have matching names in your local site those will override.
  192. "rophako/www/static/avatars/*.*",
  193. os.path.join(Config.site.site_root, "static", "avatars", "*.*"),
  194. ]
  195. for path in paths:
  196. for filename in glob.glob(path):
  197. filename = filename.split("/")[-1]
  198. avatars.add(filename)
  199. return sorted(avatars, key=lambda x: x.lower())
  200. def get_next_id(index):
  201. """Get the next free ID for a blog post."""
  202. logger.debug("Getting next available blog ID number")
  203. sort = sorted(index.keys(), key=lambda x: int(x))
  204. next_id = 1
  205. if len(sort) > 0:
  206. next_id = int(sort[-1]) + 1
  207. logger.debug("Highest post ID is: {}".format(next_id))
  208. # Sanity check!
  209. if next_id in index:
  210. raise Exception("Failed to get_next_id for the blog. Chosen ID is still in the index!")
  211. return next_id