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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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. 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.iteritems():
  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.iteritems():
  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. return db
  59. def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
  60. privacy, ip, emoticons, comments, body):
  61. """Post (or update) a blog entry."""
  62. # Fetch the index.
  63. index = get_index()
  64. # Editing an existing post?
  65. if not post_id:
  66. post_id = get_next_id(index)
  67. logger.debug("Posting blog post ID {}".format(post_id))
  68. # Get a unique friendly ID.
  69. if not fid:
  70. # The default friendly ID = the subject.
  71. fid = subject.lower()
  72. fid = re.sub(r'[^A-Za-z0-9]', '-', fid)
  73. fid = re.sub(r'\-+', '-', fid)
  74. fid = fid.strip("-")
  75. logger.debug("Chosen friendly ID: {}".format(fid))
  76. # Make sure the friendly ID is unique!
  77. if len(fid):
  78. test = fid
  79. loop = 1
  80. logger.debug("Verifying the friendly ID is unique: {}".format(fid))
  81. while True:
  82. collision = False
  83. for k, v in index.iteritems():
  84. # Skip the same post, for updates.
  85. if k == post_id: continue
  86. if v["fid"] == test:
  87. # Not unique.
  88. loop += 1
  89. test = fid + "_" + unicode(loop)
  90. collision = True
  91. logger.debug("Collision with existing post {}: {}".format(k, v["fid"]))
  92. break
  93. # Was there a collision?
  94. if collision:
  95. continue # Try again.
  96. # Nope!
  97. break
  98. fid = test
  99. # Write the post.
  100. JsonDB.commit("blog/entries/{}".format(post_id), dict(
  101. fid = fid,
  102. ip = ip,
  103. time = epoch or int(time.time()),
  104. categories = categories,
  105. sticky = False, # TODO: implement sticky
  106. comments = comments,
  107. emoticons = emoticons,
  108. avatar = avatar,
  109. privacy = privacy or "public",
  110. author = author,
  111. subject = subject,
  112. body = body,
  113. ))
  114. # Update the index cache.
  115. index[post_id] = dict(
  116. fid = fid,
  117. time = epoch or int(time.time()),
  118. categories = categories,
  119. sticky = False, # TODO
  120. author = author,
  121. privacy = privacy or "public",
  122. subject = subject,
  123. )
  124. JsonDB.commit("blog/index", index)
  125. return post_id, fid
  126. def delete_entry(post_id):
  127. """Remove a blog entry."""
  128. # Fetch the blog information.
  129. index = get_index()
  130. post = get_entry(post_id)
  131. if post is None:
  132. logger.warning("Can't delete post {}, it doesn't exist!".format(post_id))
  133. # Delete the post.
  134. JsonDB.delete("blog/entries/{}".format(post_id))
  135. # Update the index cache.
  136. del index[str(post_id)] # Python JSON dict keys must be strings, never ints
  137. JsonDB.commit("blog/index", index)
  138. def resolve_id(fid):
  139. """Resolve a friendly ID to the blog ID number."""
  140. index = get_index()
  141. # If the ID is all numeric, it's the blog post ID directly.
  142. if re.match(r'^\d+$', fid):
  143. if fid in index:
  144. return int(fid)
  145. else:
  146. logger.error("Tried resolving blog post ID {} as an EntryID, but it wasn't there!".format(fid))
  147. return None
  148. # It's a friendly ID. Scan for it.
  149. for post_id, data in index.iteritems():
  150. if data["fid"] == fid:
  151. return int(post_id)
  152. logger.error("Friendly post ID {} wasn't found!".format(fid))
  153. return None
  154. def list_avatars():
  155. """Get a list of all the available blog avatars."""
  156. avatars = set()
  157. paths = [
  158. # Load avatars from both locations. We check the built-in set first,
  159. # so if you have matching names in your local site those will override.
  160. "rophako/www/static/avatars/*.*",
  161. os.path.join(config.SITE_ROOT, "static", "avatars", "*.*"),
  162. ]
  163. for path in paths:
  164. for filename in glob.glob(path):
  165. filename = filename.split("/")[-1]
  166. avatars.add(filename)
  167. return sorted(avatars, key=lambda x: x.lower())
  168. def get_next_id(index):
  169. """Get the next free ID for a blog post."""
  170. logger.debug("Getting next available blog ID number")
  171. sort = sorted(index.keys(), key=lambda x: int(x))
  172. next_id = 1
  173. if len(sort) > 0:
  174. next_id = int(sort[-1]) + 1
  175. logger.debug("Highest post ID is: {}".format(next_id))
  176. # Sanity check!
  177. if next_id in index:
  178. raise Exception("Failed to get_next_id for the blog. Chosen ID is still in the index!")
  179. return next_id