from __future__ import absolute_import import collections import itertools import sys from datetime import datetime from flask import abort, g, render_template, request, redirect, Blueprint, flash, url_for, current_app from flask_login import login_required, current_user from realms.lib.util import to_canonical, remove_ext, gravatar_url from .models import PageNotFound blueprint = Blueprint('wiki', __name__, template_folder='templates', static_folder='static', static_url_path='/static/wiki') @blueprint.route("/_commit//") def commit(name, sha): if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() cname = to_canonical(name) data = g.current_wiki.get_page(cname, sha=sha) if not data: abort(404) partials = _partials(data.imports, sha=sha) return render_template('wiki/page.html', name=name, page=data, commit=sha, partials=partials) @blueprint.route(r"/_compare//") def compare(name, fsha, dots, lsha): if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() diff = g.current_wiki.get_page(name, sha=lsha).compare(fsha) return render_template('wiki/compare.html', name=name, diff=diff, old=fsha, new=lsha) @blueprint.route("/_revert", methods=['POST']) @login_required def revert(): cname = to_canonical(request.form.get('name')) commit = request.form.get('commit') message = request.form.get('message', "Reverting %s" % cname) if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous: return dict(error=True, message="Anonymous posting not allowed"), 403 if cname in current_app.config.get('WIKI_LOCKED_PAGES'): return dict(error=True, message="Page is locked"), 403 try: sha = g.current_wiki.get_page(cname).revert(commit, message=message, username=current_user.username, email=current_user.email) except PageNotFound as e: return dict(error=True, message=e.message), 404 if sha: flash("Page reverted") return dict(sha=sha) @blueprint.route("/_history/") def history(name): if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() return render_template('wiki/history.html', name=name) @blueprint.route("/_history_data/") def history_data(name): """Ajax provider for paginated history data.""" if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() draw = int(request.args.get('draw', 0)) start = int(request.args.get('start', 0)) length = int(request.args.get('length', 10)) page = g.current_wiki.get_page(name) items = list(itertools.islice(page.history, start, start + length)) for item in items: item['gravatar'] = gravatar_url(item['author_email']) item['DT_RowId'] = item['sha'] date = datetime.fromtimestamp(item['time']) item['date'] = date.strftime(current_app.config.get('DATETIME_FORMAT', '%b %d, %Y %I:%M %p')) item['link'] = url_for('.commit', name=name, sha=item['sha']) total_records, hist_complete = page.history_cache if not hist_complete: # Force datatables to fetch more data when it gets to the end total_records += 1 return { 'draw': draw, 'recordsTotal': total_records, 'recordsFiltered': total_records, 'data': items, 'fully_loaded': hist_complete } @blueprint.route("/_edit/") @login_required def edit(name): cname = to_canonical(name) page = g.current_wiki.get_page(cname) if not page: # Page doesn't exist return redirect(url_for('wiki.create', name=cname)) g.assets['js'].append('editor.js') return render_template('wiki/edit.html', name=cname, content=page.data, # TODO: Remove this? See #148 info=next(page.history), sha=page.sha) def _partials(imports, sha='HEAD'): page_queue = collections.deque(imports) partials = collections.OrderedDict() while page_queue: page_name = page_queue.popleft() if page_name in partials: continue page = g.current_wiki.get_page(page_name, sha=sha) try: partials[page_name] = page.data except KeyError: partials[page_name] = "`Error importing wiki page '{0}'`".format(page_name) continue page_queue.extend(page.imports) # We want to retain the order (and reverse it) so that combining metadata from the imports works return list(reversed(partials.items())) @blueprint.route("/_partials") def partials(): if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() return {'partials': _partials(request.args.getlist('imports[]'))} @blueprint.route("/_create/", defaults={'name': None}) @blueprint.route("/_create/") @login_required def create(name): cname = to_canonical(name) if name else "" if cname and g.current_wiki.get_page(cname): # Page exists, edit instead return redirect(url_for('wiki.edit', name=cname)) g.assets['js'].append('editor.js') return render_template('wiki/edit.html', name=cname, content="", info={}) def _get_subdir(path, depth): parts = path.split('/', depth) if len(parts) > depth: return parts[-2] def _tree_index(items, path=""): depth = len(path.split("/")) items = sorted(items, key=lambda x: x['name']) for subdir, items in itertools.groupby(items, key=lambda x: _get_subdir(x['name'], depth)): if not subdir: for item in items: yield dict(item, dir=False) else: size = 0 ctime = sys.maxint mtime = 0 for item in items: size += item['size'] ctime = min(item['ctime'], ctime) mtime = max(item['mtime'], mtime) yield dict(name=path + subdir + "/", mtime=mtime, ctime=ctime, size=size, dir=True) @blueprint.route("/_index", defaults={"path": ""}) @blueprint.route("/_index/") def index(path): if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() items = g.current_wiki.get_index() if path: path = to_canonical(path) + "/" items = filter(lambda x: x['name'].startswith(path), items) if not request.args.get('flat', '').lower() in ['yes', '1', 'true']: items = _tree_index(items, path=path) return render_template('wiki/index.html', index=items, path=path) @blueprint.route("/", methods=['POST', 'PUT', 'DELETE']) @login_required def page_write(name): cname = to_canonical(name) if not cname: return dict(error=True, message="Invalid name") if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous: return dict(error=True, message="Anonymous posting not allowed"), 403 if request.method == 'POST': # Create if cname in current_app.config.get('WIKI_LOCKED_PAGES'): return dict(error=True, message="Page is locked"), 403 sha = g.current_wiki.get_page(cname).write(request.form['content'], message=request.form['message'], username=current_user.username, email=current_user.email) elif request.method == 'PUT': edit_cname = to_canonical(request.form['name']) if edit_cname in current_app.config.get('WIKI_LOCKED_PAGES'): return dict(error=True, message="Page is locked"), 403 if edit_cname != cname: g.current_wiki.get_page(cname).rename(edit_cname) sha = g.current_wiki.get_page(edit_cname).write(request.form['content'], message=request.form['message'], username=current_user.username, email=current_user.email) return dict(sha=sha) elif request.method == 'DELETE': # DELETE if cname in current_app.config.get('WIKI_LOCKED_PAGES'): return dict(error=True, message="Page is locked"), 403 sha = g.current_wiki.get_page(cname).delete(username=current_user.username, email=current_user.email) return dict(sha=sha) @blueprint.route("/", defaults={'name': 'home'}) @blueprint.route("/") def page(name): if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous: return current_app.login_manager.unauthorized() cname = to_canonical(name) if cname != name: return redirect(url_for('wiki.page', name=cname)) data = g.current_wiki.get_page(cname) if data: return render_template('wiki/page.html', name=cname, page=data, partials=_partials(data.imports)) else: return redirect(url_for('wiki.create', name=cname))