Piki is a minimal wiki
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

page.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import difflib
  2. from django.conf import settings
  3. from django.utils.translation import gettext as _
  4. import fstools
  5. import json
  6. import logging
  7. from pages import messages, url_page
  8. import mycreole
  9. import os
  10. import shutil
  11. import time
  12. from . import timestamp_to_datetime
  13. logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__)
  14. SPLITCHAR = ":"
  15. HISTORY_FOLDER_NAME = 'history'
  16. def full_path_all_pages(expression="*"):
  17. system_pages = fstools.dirlist(settings.SYSTEM_PAGES_ROOT, expression=expression, rekursive=False)
  18. system_pages = [os.path.join(settings.PAGES_ROOT, os.path.basename(path)) for path in system_pages]
  19. pages = fstools.dirlist(settings.PAGES_ROOT, expression=expression, rekursive=False)
  20. rv = []
  21. for path in set(system_pages + pages):
  22. p = page_wrapped(None, path)
  23. if p.is_available():
  24. rv.append(path)
  25. return rv
  26. class base(dict):
  27. @property
  28. def rel_path(self):
  29. return os.path.basename(self._path).replace(2*SPLITCHAR, "/")
  30. def is_available(self):
  31. is_a = os.path.isfile(self.filename)
  32. if not is_a:
  33. logger.debug("Not available - %s", self.filename)
  34. return is_a
  35. def history_numbers_list(self):
  36. history_folder = os.path.join(self._path, HISTORY_FOLDER_NAME)
  37. return list(set([int(os.path.basename(filename)[:5]) for filename in fstools.filelist(history_folder)]))
  38. class meta_data(base):
  39. META_FILE_NAME = 'meta.json'
  40. #
  41. KEY_CREATION_TIME = "creation_time"
  42. KEY_CREATION_USER = "creation_user"
  43. KEY_MODIFIED_TIME = "modified_time"
  44. KEY_MODIFIED_USER = "modified_user"
  45. KEY_TAGS = "tags"
  46. def __init__(self, path, history_version=None):
  47. self._path = path
  48. self._history_version = history_version
  49. #
  50. # Load data from disk
  51. try:
  52. with open(self.filename, 'r') as fh:
  53. super().__init__(json.load(fh))
  54. except (FileNotFoundError, json.decoder.JSONDecodeError) as e:
  55. super().__init__()
  56. def delete(self):
  57. os.remove(self.filename)
  58. @property
  59. def filename(self):
  60. if not self._history_version:
  61. return os.path.join(self._path, self.META_FILE_NAME)
  62. else:
  63. return self.history_filename(self._history_version)
  64. def history_filename(self, history_version):
  65. return os.path.join(self._path, HISTORY_FOLDER_NAME, "%05d_%s" % (history_version, self.META_FILE_NAME))
  66. def update_required(self, tags):
  67. return tags != self.get(self.KEY_TAGS)
  68. def update(self, username, tags):
  69. if self._history_version:
  70. logger.error("A history version %05d can not be updated!", self._history_version)
  71. return False
  72. else:
  73. if username:
  74. self[self.KEY_MODIFIED_TIME] = int(time.time())
  75. self[self.KEY_MODIFIED_USER] = username
  76. #
  77. if self.KEY_CREATION_USER not in self:
  78. self[self.KEY_CREATION_USER] = self[self.KEY_MODIFIED_USER]
  79. if self.KEY_CREATION_TIME not in self:
  80. self[self.KEY_CREATION_TIME] = self[self.KEY_MODIFIED_TIME]
  81. if tags:
  82. self[self.KEY_TAGS] = tags
  83. #
  84. if username or tags:
  85. self.save()
  86. return True
  87. def save(self):
  88. if self._history_version:
  89. logger.error("A history version %05d can not be updated!", self._history_version)
  90. return False
  91. else:
  92. with open(self.filename, 'w') as fh:
  93. json.dump(self, fh, indent=4)
  94. return True
  95. def store_to_history(self, history_number):
  96. history_filename = self.history_filename(history_number)
  97. fstools.mkdir(os.path.dirname(history_filename))
  98. shutil.copy(self.filename, history_filename)
  99. class page_data(base):
  100. PAGE_FILE_NAME = 'page'
  101. def __init__(self, path, history_version=None):
  102. self._history_version = history_version
  103. self._path = path
  104. self._raw_page_src = None
  105. def _load_page_src(self):
  106. if self._raw_page_src is None:
  107. try:
  108. with open(self.filename, 'r') as fh:
  109. self._raw_page_src = fh.read()
  110. except FileNotFoundError:
  111. self._raw_page_src = ""
  112. def delete(self):
  113. os.remove(self.filename)
  114. def rename(self, page_name):
  115. # Change backslash to slash and remove double slashes
  116. page_name = page_name.replace("\\", "/")
  117. while "//" in page_name:
  118. page_name = page_name.replace("//", "/")
  119. # move path
  120. target_path = os.path.join(settings.PAGES_ROOT, page_name.replace("/", 2*SPLITCHAR))
  121. shutil.move(self._path, target_path)
  122. # set my path
  123. self._path = target_path
  124. def update_required(self, page_txt):
  125. return page_txt.replace("\r\n", "\n") != self.raw_page_src
  126. def update_page(self, page_txt):
  127. if self._history_version:
  128. logger.error("A history version %05d can not be updated!", self._history_version)
  129. return False
  130. else:
  131. # save the new page content
  132. fstools.mkdir(os.path.dirname(self.filename))
  133. with open(self.filename, 'w') as fh:
  134. fh.write(page_txt)
  135. self._raw_page_src = page_txt
  136. return True
  137. @property
  138. def filename(self):
  139. if not self._history_version:
  140. return os.path.join(self._path, self.PAGE_FILE_NAME)
  141. else:
  142. return self.history_filename(self._history_version)
  143. def history_filename(self, history_version):
  144. return os.path.join(self._path, HISTORY_FOLDER_NAME, "%05d_%s" % (history_version, self.PAGE_FILE_NAME))
  145. @property
  146. def rel_path(self):
  147. return os.path.basename(self._path).replace(2*SPLITCHAR, "/")
  148. @property
  149. def title(self):
  150. return os.path.basename(self._path).split(2*SPLITCHAR)[-1]
  151. @property
  152. def raw_page_src(self):
  153. self._load_page_src()
  154. return self._raw_page_src
  155. def store_to_history(self, history_number):
  156. history_filename = self.history_filename(history_number)
  157. fstools.mkdir(os.path.dirname(history_filename))
  158. shutil.copy(self.filename, history_filename)
  159. class page_wrapped(object):
  160. """
  161. This class holds different page and meta instances and decides which will be used in which case.
  162. """
  163. def __init__(self, request, path, history_version=None):
  164. """_summary_
  165. Args:
  166. request (_type_): The django request or None (if None, the page functionality is limited)
  167. path (_type_): A rel_path of the django page or the filesystem path to the page
  168. history_version (_type_, optional): The history version of the page to be created
  169. """
  170. self._request = request
  171. #
  172. page_path = self.__page_path__(path)
  173. # Page
  174. self._page = page_data(page_path, history_version=history_version)
  175. self._page_meta = meta_data(page_path, history_version=history_version)
  176. def __page_path__(self, path):
  177. if path.startswith(settings.PAGES_ROOT):
  178. # must be a filesystem path
  179. return path
  180. else:
  181. # must be a relative url
  182. return os.path.join(settings.PAGES_ROOT, path.replace("/", 2*SPLITCHAR))
  183. def __page_choose__(self):
  184. return self._page
  185. def __meta_choose__(self):
  186. return self._page_meta
  187. def __store_history__(self):
  188. if self._page.is_available():
  189. try:
  190. history_number = max(self._page.history_numbers_list()) + 1
  191. except ValueError:
  192. history_number = 1 # no history yet
  193. self._page.store_to_history(history_number)
  194. self._page_meta.store_to_history(history_number)
  195. #
  196. # meta_data
  197. #
  198. @property
  199. def creation_time(self):
  200. meta = self.__meta_choose__()
  201. rv = meta.get(meta.KEY_CREATION_TIME)
  202. return rv
  203. @property
  204. def creation_user(self):
  205. meta = self.__meta_choose__()
  206. rv = meta.get(meta.KEY_CREATION_USER)
  207. return rv
  208. def delete(self):
  209. self.__store_history__()
  210. self._page.delete()
  211. self._page_meta.delete()
  212. @property
  213. def modified_time(self):
  214. meta = self.__meta_choose__()
  215. rv = meta.get(meta.KEY_MODIFIED_TIME)
  216. return rv
  217. @property
  218. def modified_user(self):
  219. meta = self.__meta_choose__()
  220. rv = meta.get(meta.KEY_MODIFIED_USER)
  221. return rv
  222. def rename(self, page_name):
  223. self._page.rename(page_name)
  224. @property
  225. def tags(self):
  226. meta = self.__meta_choose__()
  227. rv = meta.get(meta.KEY_TAGS)
  228. return rv
  229. #
  230. # page
  231. #
  232. @property
  233. def attachment_path(self):
  234. page = self.__page_choose__()
  235. rv = page.attachment_path
  236. return rv
  237. def is_available(self):
  238. return self._page.is_available()
  239. def userpage_is_available(self):
  240. return self._page.is_available()
  241. @property
  242. def raw_page_src(self):
  243. page = self.__page_choose__()
  244. rv = page.raw_page_src
  245. return rv
  246. @property
  247. def rel_path(self):
  248. page = self.__page_choose__()
  249. rv = page.rel_path
  250. return rv
  251. def render_meta(self):
  252. page = self.__page_choose__()
  253. rv = page.render_meta(self.creation_time, self.modified_time, self.creation_user, self.modified_user, self.tags)
  254. return rv
  255. def render_to_html(self):
  256. page = self.__page_choose__()
  257. rv = page.render_to_html()
  258. return rv
  259. def render_text(self, request, txt):
  260. page = self.__page_choose__()
  261. rv = page.render_text(request, txt)
  262. return rv
  263. @property
  264. def title(self):
  265. page = self.__page_choose__()
  266. rv = page.title
  267. return rv
  268. def update_page(self, txt, tags):
  269. if self._page.update_required(txt) or self._page_meta.update_required(tags):
  270. rv = False
  271. # Store history
  272. self.__store_history__()
  273. username = None
  274. if self._page.update_required(txt):
  275. # Update page
  276. rv |= self._page.update_page(txt)
  277. # Identify username, to update meta
  278. try:
  279. if self._request.user.is_authenticated:
  280. username = self._request.user.username
  281. else:
  282. logger.warning("Page edit without having a logged in user. This is not recommended. Check your access definitions!")
  283. except AttributeError:
  284. logger.exception("Page edit without having a request object. Check programming!")
  285. rv |= self._page_meta.update(username, tags)
  286. # Update search index
  287. from pages.search import update_item
  288. update_item(self)
  289. return rv