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.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. """Blog models."""
  4. from flask import g
  5. import time
  6. import re
  7. import glob
  8. import os
  9. from rophako.settings import Config
  10. import rophako.jsondb as JsonDB
  11. from rophako.log import logger
  12. def get_index():
  13. """Get the blog index.
  14. The index is the cache of available blog posts. It has the format:
  15. ```
  16. {
  17. 'post_id': {
  18. fid: Friendly ID for the blog post (for URLs)
  19. time: epoch time of the post
  20. sticky: the stickiness of the post (shows first on global views)
  21. author: the author user ID of the post
  22. categories: [ list of categories ]
  23. privacy: the privacy setting
  24. subject: the post subject
  25. },
  26. ...
  27. }
  28. ```
  29. """
  30. # Index doesn't exist?
  31. if not JsonDB.exists("blog/index"):
  32. return {}
  33. db = JsonDB.get("blog/index")
  34. # Hide any private posts if we aren't logged in.
  35. if not g.info["session"]["login"]:
  36. for post_id, data in db.items():
  37. if data["privacy"] == "private":
  38. del db[post_id]
  39. return db
  40. def get_categories():
  41. """Get the blog categories and their popularity."""
  42. index = get_index()
  43. # Group by tags.
  44. tags = {}
  45. for post, data in index.items():
  46. for tag in data["categories"]:
  47. if not tag in tags:
  48. tags[tag] = 0
  49. tags[tag] += 1
  50. return tags
  51. def get_entry(post_id):
  52. """Load a full blog entry."""
  53. if not JsonDB.exists("blog/entries/{}".format(post_id)):
  54. return None
  55. db = JsonDB.get("blog/entries/{}".format(post_id))
  56. # If no FID, set it to the ID.
  57. if len(db["fid"]) == 0:
  58. db["fid"] = str(post_id)
  59. # If no "format" option, set it to HTML (legacy)
  60. if db.get("format", "") == "":
  61. db["format"] = "html"
  62. return db
  63. def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
  64. privacy, ip, emoticons, comments, format, body):
  65. """Post (or update) a blog entry."""
  66. # Fetch the index.
  67. index = get_index()
  68. # Editing an existing post?
  69. if not post_id:
  70. post_id = get_next_id(index)
  71. logger.debug("Posting blog post ID {}".format(post_id))
  72. # Get a unique friendly ID.
  73. if not fid:
  74. # The default friendly ID = the subject.
  75. fid = subject.lower()
  76. fid = re.sub(r'[^A-Za-z0-9]', '-', fid)
  77. fid = re.sub(r'\-+', '-', fid)
  78. fid = fid.strip("-")
  79. logger.debug("Chosen friendly ID: {}".format(fid))
  80. # Make sure the friendly ID is unique!
  81. if len(fid):
  82. test = fid
  83. loop = 1
  84. logger.debug("Verifying the friendly ID is unique: {}".format(fid))
  85. while True:
  86. collision = False
  87. for k, v in index.items():
  88. # Skip the same post, for updates.
  89. if k == post_id: continue
  90. if v["fid"] == test:
  91. # Not unique.
  92. loop += 1
  93. test = fid + "_" + unicode(loop)
  94. collision = True
  95. logger.debug("Collision with existing post {}: {}".format(k, v["fid"]))
  96. break
  97. # Was there a collision?
  98. if collision:
  99. continue # Try again.
  100. # Nope!
  101. break
  102. fid = test
  103. # Write the post.
  104. JsonDB.commit("blog/entries/{}".format(post_id), dict(
  105. fid = fid,
  106. ip = ip,
  107. time = epoch or int(time.time()),
  108. categories = categories,
  109. sticky = False, # TODO: implement sticky
  110. comments = comments,
  111. emoticons = emoticons,
  112. avatar = avatar,
  113. privacy = privacy or "public",
  114. author = author,
  115. subject = subject,
  116. format = format,
  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.items():
  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. os.path.join(Config.site.site_root, "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. next_id = 1
  178. if len(sort) > 0:
  179. next_id = int(sort[-1]) + 1
  180. logger.debug("Highest post ID is: {}".format(next_id))
  181. # Sanity check!
  182. if next_id in index:
  183. raise Exception("Failed to get_next_id for the blog. Chosen ID is still in the index!")
  184. return next_id