Git based wiki inspired by Gollum
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.

277 lines
9.7KB

  1. from __future__ import absolute_import
  2. import collections
  3. import itertools
  4. import sys
  5. from datetime import datetime
  6. from flask import abort, g, render_template, request, redirect, Blueprint, flash, url_for, current_app
  7. from flask_login import login_required, current_user
  8. from realms.lib.util import to_canonical, remove_ext, gravatar_url
  9. from .models import PageNotFound
  10. blueprint = Blueprint('wiki', __name__, template_folder='templates',
  11. static_folder='static', static_url_path='/static/wiki')
  12. @blueprint.route("/_commit/<sha>/<path:name>")
  13. def commit(name, sha):
  14. if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
  15. return current_app.login_manager.unauthorized()
  16. cname = to_canonical(name)
  17. data = g.current_wiki.get_page(cname, sha=sha)
  18. if not data:
  19. abort(404)
  20. partials = _partials(data.imports, sha=sha)
  21. return render_template('wiki/page.html', name=name, page=data, commit=sha, partials=partials)
  22. @blueprint.route(r"/_compare/<path:name>/<regex('\w+'):fsha><regex('\.{2,3}'):dots><regex('\w+'):lsha>")
  23. def compare(name, fsha, dots, lsha):
  24. if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
  25. return current_app.login_manager.unauthorized()
  26. diff = g.current_wiki.get_page(name, sha=lsha).compare(fsha)
  27. return render_template('wiki/compare.html',
  28. name=name, diff=diff, old=fsha, new=lsha)
  29. @blueprint.route("/_revert", methods=['POST'])
  30. @login_required
  31. def revert():
  32. cname = to_canonical(request.form.get('name'))
  33. commit = request.form.get('commit')
  34. message = request.form.get('message', "Reverting %s" % cname)
  35. if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous:
  36. return dict(error=True, message="Anonymous posting not allowed"), 403
  37. if cname in current_app.config.get('WIKI_LOCKED_PAGES'):
  38. return dict(error=True, message="Page is locked"), 403
  39. try:
  40. sha = g.current_wiki.get_page(cname).revert(commit,
  41. message=message,
  42. username=current_user.username,
  43. email=current_user.email)
  44. except PageNotFound as e:
  45. return dict(error=True, message=e.message), 404
  46. if sha:
  47. flash("Page reverted")
  48. return dict(sha=sha)
  49. @blueprint.route("/_history/<path:name>")
  50. def history(name):
  51. if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
  52. return current_app.login_manager.unauthorized()
  53. return render_template('wiki/history.html', name=name)
  54. @blueprint.route("/_history_data/<path:name>")
  55. def history_data(name):
  56. """Ajax provider for paginated history data."""
  57. if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
  58. return current_app.login_manager.unauthorized()
  59. draw = int(request.args.get('draw', 0))
  60. start = int(request.args.get('start', 0))
  61. length = int(request.args.get('length', 10))
  62. page = g.current_wiki.get_page(name)
  63. items = list(itertools.islice(page.history, start, start + length))
  64. for item in items:
  65. item['gravatar'] = gravatar_url(item['author_email'])
  66. item['DT_RowId'] = item['sha']
  67. date = datetime.fromtimestamp(item['time'])
  68. item['date'] = date.strftime(current_app.config.get('DATETIME_FORMAT', '%b %d, %Y %I:%M %p'))
  69. item['link'] = url_for('.commit', name=name, sha=item['sha'])
  70. total_records, hist_complete = page.history_cache
  71. if not hist_complete:
  72. # Force datatables to fetch more data when it gets to the end
  73. total_records += 1
  74. return {
  75. 'draw': draw,
  76. 'recordsTotal': total_records,
  77. 'recordsFiltered': total_records,
  78. 'data': items,
  79. 'fully_loaded': hist_complete
  80. }
  81. @blueprint.route("/_edit/<path:name>")
  82. @login_required
  83. def edit(name):
  84. cname = to_canonical(name)
  85. page = g.current_wiki.get_page(cname)
  86. if not page:
  87. # Page doesn't exist
  88. return redirect(url_for('wiki.create', name=cname))
  89. g.assets['js'].append('editor.js')
  90. return render_template('wiki/edit.html',
  91. name=cname,
  92. content=page.data,
  93. # TODO: Remove this? See #148
  94. info=next(page.history),
  95. sha=page.sha)
  96. def _partials(imports, sha='HEAD'):
  97. page_queue = collections.deque(imports)
  98. partials = collections.OrderedDict()
  99. while page_queue:
  100. page_name = page_queue.popleft()
  101. if page_name in partials:
  102. continue
  103. page = g.current_wiki.get_page(page_name, sha=sha)
  104. try:
  105. partials[page_name] = page.data
  106. except KeyError:
  107. partials[page_name] = "`Error importing wiki page '{0}'`".format(page_name)
  108. continue
  109. page_queue.extend(page.imports)
  110. # We want to retain the order (and reverse it) so that combining metadata from the imports works
  111. return list(reversed(partials.items()))
  112. @blueprint.route("/_partials")
  113. def partials():
  114. if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
  115. return current_app.login_manager.unauthorized()
  116. return {'partials': _partials(request.args.getlist('imports[]'))}
  117. @blueprint.route("/_create/", defaults={'name': None})
  118. @blueprint.route("/_create/<path:name>")
  119. @login_required
  120. def create(name):
  121. cname = to_canonical(name) if name else ""
  122. if cname and g.current_wiki.get_page(cname):
  123. # Page exists, edit instead
  124. return redirect(url_for('wiki.edit', name=cname))
  125. g.assets['js'].append('editor.js')
  126. return render_template('wiki/edit.html',
  127. name=cname,
  128. content="",
  129. info={})
  130. def _get_subdir(path, depth):
  131. parts = path.split('/', depth)
  132. if len(parts) > depth:
  133. return parts[-2]
  134. def _tree_index(items, path=""):
  135. depth = len(path.split("/"))
  136. items = sorted(items, key=lambda x: x['name'])
  137. for subdir, items in itertools.groupby(items, key=lambda x: _get_subdir(x['name'], depth)):
  138. if not subdir:
  139. for item in items:
  140. yield dict(item, dir=False)
  141. else:
  142. size = 0
  143. ctime = sys.maxint
  144. mtime = 0
  145. for item in items:
  146. size += item['size']
  147. ctime = min(item['ctime'], ctime)
  148. mtime = max(item['mtime'], mtime)
  149. yield dict(name=path + subdir + "/",
  150. mtime=mtime,
  151. ctime=ctime,
  152. size=size,
  153. dir=True)
  154. @blueprint.route("/_index", defaults={"path": ""})
  155. @blueprint.route("/_index/<path:path>")
  156. def index(path):
  157. if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
  158. return current_app.login_manager.unauthorized()
  159. items = g.current_wiki.get_index()
  160. if path:
  161. path = to_canonical(path) + "/"
  162. items = filter(lambda x: x['name'].startswith(path), items)
  163. if not request.args.get('flat', '').lower() in ['yes', '1', 'true']:
  164. items = _tree_index(items, path=path)
  165. return render_template('wiki/index.html', index=items, path=path)
  166. @blueprint.route("/<path:name>", methods=['POST', 'PUT', 'DELETE'])
  167. @login_required
  168. def page_write(name):
  169. cname = to_canonical(name)
  170. if not cname:
  171. return dict(error=True, message="Invalid name")
  172. if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous:
  173. return dict(error=True, message="Anonymous posting not allowed"), 403
  174. if request.method == 'POST':
  175. # Create
  176. if cname in current_app.config.get('WIKI_LOCKED_PAGES'):
  177. return dict(error=True, message="Page is locked"), 403
  178. sha = g.current_wiki.get_page(cname).write(request.form['content'],
  179. message=request.form['message'],
  180. username=current_user.username,
  181. email=current_user.email)
  182. elif request.method == 'PUT':
  183. edit_cname = to_canonical(request.form['name'])
  184. if edit_cname in current_app.config.get('WIKI_LOCKED_PAGES'):
  185. return dict(error=True, message="Page is locked"), 403
  186. if edit_cname != cname:
  187. g.current_wiki.get_page(cname).rename(edit_cname)
  188. sha = g.current_wiki.get_page(edit_cname).write(request.form['content'],
  189. message=request.form['message'],
  190. username=current_user.username,
  191. email=current_user.email)
  192. return dict(sha=sha)
  193. elif request.method == 'DELETE':
  194. # DELETE
  195. if cname in current_app.config.get('WIKI_LOCKED_PAGES'):
  196. return dict(error=True, message="Page is locked"), 403
  197. sha = g.current_wiki.get_page(cname).delete(username=current_user.username,
  198. email=current_user.email)
  199. return dict(sha=sha)
  200. @blueprint.route("/", defaults={'name': 'home'})
  201. @blueprint.route("/<path:name>")
  202. def page(name):
  203. if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
  204. return current_app.login_manager.unauthorized()
  205. cname = to_canonical(name)
  206. if cname != name:
  207. return redirect(url_for('wiki.page', name=cname))
  208. data = g.current_wiki.get_page(cname)
  209. if data:
  210. return render_template('wiki/page.html', name=cname, page=data, partials=_partials(data.imports))
  211. else:
  212. return redirect(url_for('wiki.create', name=cname))