Merge branch 'master' into oauth_redirect
# Conflicts: # realms/modules/auth/views.py
This commit is contained in:
commit
2ce6c2d314
41 changed files with 350 additions and 213 deletions
|
@ -1,6 +1,6 @@
|
|||
from realms import login_manager
|
||||
from flask import request, flash, redirect
|
||||
from flask.ext.login import login_url
|
||||
from flask_login import login_url
|
||||
|
||||
modules = set()
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from flask import render_template
|
||||
from flask.ext.login import login_user
|
||||
from flask_login import login_user
|
||||
from realms import ldap
|
||||
from flask_ldap_login import LDAPLoginForm
|
||||
from ..models import BaseUser
|
||||
|
|
|
@ -2,10 +2,10 @@ import click
|
|||
from realms.lib.util import random_string
|
||||
from realms.modules.auth.local.models import User
|
||||
from realms.lib.util import green, red, yellow
|
||||
from realms import flask_cli
|
||||
from realms import cli_group
|
||||
|
||||
|
||||
@flask_cli.group(short_help="Auth Module")
|
||||
@cli_group(short_help="Auth Module")
|
||||
def cli():
|
||||
pass
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from flask import current_app, render_template
|
||||
from flask.ext.login import logout_user, login_user
|
||||
from flask_login import logout_user, login_user
|
||||
from realms import login_manager, db
|
||||
from realms.lib.model import Model
|
||||
from ..models import BaseUser
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from flask import current_app
|
||||
from flask.ext.login import UserMixin, logout_user, AnonymousUserMixin
|
||||
from flask_login import UserMixin, logout_user, AnonymousUserMixin
|
||||
from realms import login_manager
|
||||
from realms.lib.util import gravatar_url
|
||||
from itsdangerous import URLSafeSerializer, BadSignature
|
||||
|
|
|
@ -40,7 +40,7 @@ providers = {
|
|||
'field_map': {
|
||||
'id': 'id',
|
||||
'username': 'login',
|
||||
'email': 'email'
|
||||
'email': lambda(data): data.get('email') or data['login'] + '@users.noreply.github.com'
|
||||
},
|
||||
'token_name': 'access_token'
|
||||
},
|
||||
|
@ -118,6 +118,8 @@ class User(BaseUser):
|
|||
def get_value(d, key):
|
||||
if isinstance(key, basestring):
|
||||
return d.get(key)
|
||||
elif callable(key):
|
||||
return key(d)
|
||||
# key should be list here
|
||||
val = d.get(key.pop(0))
|
||||
if len(key) == 0:
|
||||
|
|
29
realms/modules/auth/templates/auth/ldap/login.html
Normal file
29
realms/modules/auth/templates/auth/ldap/login.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% from 'macros.html' import render_form, render_field %}
|
||||
{% if config.get('AUTH_LOCAL_ENABLE') %}
|
||||
<button type="button" class="btn btn-info" data-toggle="modal" data-target="#ldap-modal">
|
||||
<i class="fa fa-folder-open-o"></i> Login with LDAP
|
||||
</button>
|
||||
|
||||
<div class="modal fade" id="ldap-modal" tabindex="-1" role="dialog" aria-labelledby="ldap-login">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="ldap-login">LDAP Login</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% call render_form(form, action_url=url_for('auth.ldap.login'), action_text='Login', btn_class='btn btn-primary') %}
|
||||
{{ render_field(form.username, placeholder='Username', type='text', required=1) }}
|
||||
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<h3><i class="fa fa-folder-open-o"></i> LDAP Login</h3>
|
||||
{% call render_form(form, action_url=url_for('auth.ldap.login'), action_text='Login', btn_class='btn btn-primary') %}
|
||||
{{ render_field(form.username, placeholder='Username', type='text', required=1) }}
|
||||
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
|
||||
{% endcall %}
|
||||
{% endif %}
|
5
realms/modules/auth/templates/auth/local/login.html
Normal file
5
realms/modules/auth/templates/auth/local/login.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
{% from 'macros.html' import render_form, render_field %}
|
||||
{% call render_form(form, action_url=url_for('auth.local.login'), action_text='Login', btn_class='btn btn-primary') %}
|
||||
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
|
||||
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
|
||||
{% endcall %}
|
4
realms/modules/auth/templates/auth/login.html
Normal file
4
realms/modules/auth/templates/auth/login.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block body %}
|
||||
{{ forms|safe }}
|
||||
{% endblock %}
|
13
realms/modules/auth/templates/auth/register.html
Normal file
13
realms/modules/auth/templates/auth/register.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% from 'macros.html' import render_form, render_field %}
|
||||
{% block body %}
|
||||
{% call render_form(form, action_url=url_for('auth.local.register'), action_text='Register', btn_class='btn btn-primary') %}
|
||||
{{ render_field(form.username, placeholder='Username', type='username', **{"required": 1, "data-parsley-type": "alphanum"}) }}
|
||||
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
|
||||
{{ render_field(form.password, placeholder='Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}
|
||||
{{ render_field(form.confirm, placeholder='Confirm Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}
|
||||
{% if config.RECAPTCHA_ENABLE %}
|
||||
{{ render_field(form.recaptcha) }}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
5
realms/modules/auth/templates/auth/settings.html
Normal file
5
realms/modules/auth/templates/auth/settings.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% from 'macros.html' import render_form, render_field %}
|
||||
{% block body %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,8 +1,8 @@
|
|||
from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for, session
|
||||
from flask.ext.login import logout_user
|
||||
from flask_login import logout_user
|
||||
from realms.modules.auth.models import Auth
|
||||
|
||||
blueprint = Blueprint('auth', __name__)
|
||||
blueprint = Blueprint('auth', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@blueprint.route("/login", methods=['GET', 'POST'])
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import click
|
||||
from realms import create_app, search, flask_cli
|
||||
from flask import current_app
|
||||
from realms import search, cli_group
|
||||
from realms.modules.wiki.models import Wiki
|
||||
from realms.lib.util import filename_to_cname
|
||||
|
||||
|
||||
@flask_cli.group(short_help="Search Module")
|
||||
@cli_group(short_help="Search Module")
|
||||
def cli():
|
||||
pass
|
||||
|
||||
|
@ -13,27 +13,25 @@ def cli():
|
|||
def rebuild_index():
|
||||
""" Rebuild search index
|
||||
"""
|
||||
app = create_app()
|
||||
|
||||
if app.config.get('SEARCH_TYPE') == 'simple':
|
||||
if current_app.config.get('SEARCH_TYPE') == 'simple':
|
||||
click.echo("Search type is simple, try using elasticsearch.")
|
||||
return
|
||||
|
||||
with app.app_context():
|
||||
# Wiki
|
||||
search.delete_index('wiki')
|
||||
wiki = Wiki(app.config['WIKI_PATH'])
|
||||
for entry in wiki.get_index():
|
||||
page = wiki.get_page(entry['name'])
|
||||
if not page:
|
||||
# Some non-markdown files may have issues
|
||||
continue
|
||||
name = filename_to_cname(page['path'])
|
||||
# TODO add email?
|
||||
body = dict(name=name,
|
||||
content=page.data,
|
||||
message=page.info['message'],
|
||||
username=page.info['author'],
|
||||
updated_on=entry['mtime'],
|
||||
created_on=entry['ctime'])
|
||||
search.index_wiki(name, body)
|
||||
# Wiki
|
||||
search.delete_index('wiki')
|
||||
wiki = Wiki(current_app.config['WIKI_PATH'])
|
||||
for entry in wiki.get_index():
|
||||
page = wiki.get_page(entry['name'])
|
||||
if not page:
|
||||
# Some non-markdown files may have issues
|
||||
continue
|
||||
# TODO add email?
|
||||
# TODO I have concens about indexing the commit info from latest revision, see #148
|
||||
info = next(page.history)
|
||||
body = dict(name=page.name,
|
||||
content=page.data,
|
||||
message=info['message'],
|
||||
username=info['author'],
|
||||
updated_on=entry['mtime'],
|
||||
created_on=entry['ctime'])
|
||||
search.index_wiki(page.name, body)
|
||||
|
|
|
@ -14,7 +14,7 @@ def whoosh(app):
|
|||
|
||||
|
||||
def elasticsearch(app):
|
||||
from flask.ext.elastic import Elastic
|
||||
from flask_elastic import Elastic
|
||||
fields = app.config.get('ELASTICSEARCH_FIELDS')
|
||||
return ElasticSearch(Elastic(app), fields)
|
||||
|
||||
|
|
18
realms/modules/search/templates/search/search.html
Normal file
18
realms/modules/search/templates/search/search.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block body %}
|
||||
{% if results %}
|
||||
<h3>Results for <em class="text-info">{{ request.args.get('q') }}</em></h3>
|
||||
<div class="list-group">
|
||||
{% for r in results %}
|
||||
<a href="{{ url_for('wiki.page', name=r['name']) }}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">{{ r['name'] }}</h4>
|
||||
<p class="list-group-item-text">
|
||||
{{ r['content'][:100] }}
|
||||
</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<h3>No results found for <em class="text-info">{{ request.args.get('q') }}</em></h3>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -1,10 +1,14 @@
|
|||
from flask import render_template, request, Blueprint
|
||||
from flask import render_template, request, Blueprint, current_app
|
||||
from flask.ext.login import current_user
|
||||
from realms import search as search_engine
|
||||
|
||||
blueprint = Blueprint('search', __name__)
|
||||
blueprint = Blueprint('search', __name__, template_folder='templates')
|
||||
|
||||
|
||||
@blueprint.route('/_search')
|
||||
def search():
|
||||
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
|
||||
return current_app.login_manager.unauthorized()
|
||||
|
||||
results = search_engine.wiki(request.args.get('q'))
|
||||
return render_template('search/search.html', results=results)
|
||||
|
|
|
@ -7,4 +7,4 @@ assets.register('editor.js',
|
|||
'vendor/ace-builds/src/mode-markdown.js',
|
||||
'vendor/ace-builds/src/ext-keybinding_menu.js',
|
||||
'vendor/keymaster/keymaster.js',
|
||||
'js/aced.js')
|
||||
'wiki/js/aced.js')
|
||||
|
|
|
@ -2,11 +2,9 @@ import os
|
|||
import posixpath
|
||||
import re
|
||||
import ghdiff
|
||||
import gittle.utils
|
||||
import yaml
|
||||
from gittle import Gittle
|
||||
from dulwich.object_store import tree_lookup_path
|
||||
from dulwich.repo import NotGitRepository
|
||||
from dulwich.repo import Repo, NotGitRepository
|
||||
from realms.lib.util import cname_to_filename, filename_to_cname
|
||||
from realms import cache
|
||||
from realms.lib.hook import HookMixin
|
||||
|
@ -23,17 +21,13 @@ class Wiki(HookMixin):
|
|||
default_committer_name = 'Anon'
|
||||
default_committer_email = 'anon@anon.anon'
|
||||
index_page = 'home'
|
||||
gittle = None
|
||||
repo = None
|
||||
|
||||
def __init__(self, path):
|
||||
try:
|
||||
self.gittle = Gittle(path)
|
||||
self.repo = Repo(path)
|
||||
except NotGitRepository:
|
||||
self.gittle = Gittle.init(path)
|
||||
|
||||
# Dulwich repo
|
||||
self.repo = self.gittle.repo
|
||||
self.repo = Repo.init(path, mkdir=True)
|
||||
|
||||
self.path = path
|
||||
|
||||
|
@ -46,20 +40,20 @@ class Wiki(HookMixin):
|
|||
:param name: Committer name
|
||||
:param email: Committer email
|
||||
:param message: Commit message
|
||||
:param files: list of file names that should be committed
|
||||
:param files: list of file names that will be staged for commit
|
||||
:return:
|
||||
"""
|
||||
# Dulwich and gittle seem to want us to encode ourselves at the moment. see #152
|
||||
if isinstance(name, unicode):
|
||||
name = name.encode('utf-8')
|
||||
if isinstance(email, unicode):
|
||||
email = email.encode('utf-8')
|
||||
if isinstance(message, unicode):
|
||||
message = message.encode('utf-8')
|
||||
return self.gittle.commit(name=name,
|
||||
email=email,
|
||||
message=message,
|
||||
files=files)
|
||||
author = committer = "%s <%s>" % (name, email)
|
||||
self.repo.stage(files)
|
||||
return self.repo.do_commit(message=message,
|
||||
committer=committer,
|
||||
author=author)
|
||||
|
||||
def get_page(self, name, sha='HEAD'):
|
||||
"""Get page data, partials, commit info.
|
||||
|
@ -104,52 +98,93 @@ class WikiPage(HookMixin):
|
|||
if cached:
|
||||
return cached
|
||||
|
||||
data = self.wiki.gittle.get_commit_files(self.sha, paths=[self.filename]).get(self.filename).get('data')
|
||||
mode, sha = tree_lookup_path(self.wiki.repo.get_object, self.wiki.repo[self.sha].tree, self.filename)
|
||||
data = self.wiki.repo[sha].data
|
||||
cache.set(cache_key, data)
|
||||
return data
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
cache_key = self._cache_key('info')
|
||||
cached = cache.get(cache_key)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
info = self.get_history(limit=1)[0]
|
||||
cache.set(cache_key, info)
|
||||
return info
|
||||
|
||||
def get_history(self, limit=100):
|
||||
def history(self):
|
||||
"""Get page history.
|
||||
|
||||
:param limit: Limit history size.
|
||||
:return: list -- List of dicts
|
||||
History can take a long time to generate for repositories with many commits.
|
||||
This returns an iterator to avoid having to load them all at once, and caches
|
||||
as it goes.
|
||||
|
||||
:return: iter -- Iterator over dicts
|
||||
|
||||
"""
|
||||
cache_head = []
|
||||
cache_tail = cache.get(self._cache_key('history')) or [{'_cache_missing': True}]
|
||||
while True:
|
||||
if not cache_tail:
|
||||
return
|
||||
for index, cached_rev in enumerate(cache_tail):
|
||||
if cached_rev.get("_cache_missing"):
|
||||
break
|
||||
else:
|
||||
yield cached_rev
|
||||
cache_head.extend(cache_tail[:index])
|
||||
cache_tail = cache_tail[index+1:]
|
||||
start_sha = cached_rev.get('sha')
|
||||
end_sha = cache_tail[0].get('sha') if cache_tail else None
|
||||
for rev in self._iter_revs(start_sha=start_sha, end_sha=end_sha, filename=cached_rev.get('filename')):
|
||||
cache_head.append(rev)
|
||||
placeholder = {
|
||||
'_cache_missing': True,
|
||||
'sha': rev['sha'],
|
||||
'filename': rev['new_filename']
|
||||
}
|
||||
cache.set(self._cache_key('history'), cache_head + [placeholder] + cache_tail)
|
||||
yield rev
|
||||
cache.set(self._cache_key('history'), cache_head + cache_tail)
|
||||
|
||||
def _iter_revs(self, start_sha=None, end_sha=None, filename=None):
|
||||
if end_sha:
|
||||
end_sha = [end_sha]
|
||||
if not len(self.wiki.repo.open_index()):
|
||||
# Index is empty, no commits
|
||||
return []
|
||||
|
||||
versions = []
|
||||
|
||||
walker = self.wiki.repo.get_walker(paths=[self.filename], max_entries=limit)
|
||||
return
|
||||
filename = filename or self.filename
|
||||
walker = iter(self.wiki.repo.get_walker(paths=[filename],
|
||||
include=start_sha,
|
||||
exclude=end_sha,
|
||||
follow=True))
|
||||
if start_sha:
|
||||
# If we are not starting from HEAD, we already have the start commit
|
||||
next(walker)
|
||||
filename = self.filename
|
||||
for entry in walker:
|
||||
change_type = None
|
||||
for change in entry.changes():
|
||||
if change.old.path == self.filename:
|
||||
if change.new.path == filename:
|
||||
filename = change.old.path
|
||||
change_type = change.type
|
||||
elif change.new.path == self.filename:
|
||||
change_type = change.type
|
||||
author_name, author_email = entry.commit.author.rstrip('>').split('<')
|
||||
versions.append(dict(
|
||||
author=author_name.strip(),
|
||||
author_email=author_email,
|
||||
time=entry.commit.author_time,
|
||||
message=entry.commit.message,
|
||||
sha=entry.commit.id,
|
||||
type=change_type))
|
||||
break
|
||||
|
||||
return versions
|
||||
author_name, author_email = entry.commit.author.rstrip('>').split('<')
|
||||
r = dict(author=author_name.strip(),
|
||||
author_email=author_email,
|
||||
time=entry.commit.author_time,
|
||||
message=entry.commit.message,
|
||||
sha=entry.commit.id,
|
||||
type=change_type,
|
||||
new_filename=change.new.path,
|
||||
old_filename=change.old.path)
|
||||
yield r
|
||||
|
||||
@property
|
||||
def history_cache(self):
|
||||
"""Get info about the history cache.
|
||||
|
||||
:return: tuple -- (cached items, cache complete?)
|
||||
"""
|
||||
cached_revs = cache.get(self._cache_key('history'))
|
||||
if not cached_revs:
|
||||
return 0, False
|
||||
elif any(rev.get('_cache_missing') for rev in cached_revs):
|
||||
return len(cached_revs) - 1, False
|
||||
return len(cached_revs), True
|
||||
|
||||
@property
|
||||
def partials(self):
|
||||
|
@ -196,8 +231,14 @@ class WikiPage(HookMixin):
|
|||
|
||||
return username, email
|
||||
|
||||
def _clear_cache(self):
|
||||
cache.delete_many(*(self._cache_key(p) for p in ['data', 'info']))
|
||||
def _invalidate_cache(self, save_history=None):
|
||||
cache.delete(self._cache_key('data'))
|
||||
if save_history:
|
||||
if not save_history[0].get('_cache_missing'):
|
||||
save_history = [{'_cache_missing': True}] + save_history
|
||||
cache.set(self._cache_key('history'), save_history)
|
||||
else:
|
||||
cache.delete(self._cache_key('history'))
|
||||
|
||||
def delete(self, username=None, email=None, message=None):
|
||||
"""Delete page.
|
||||
|
@ -211,14 +252,12 @@ class WikiPage(HookMixin):
|
|||
if not message:
|
||||
message = "Deleted %s" % self.name
|
||||
|
||||
# gittle.rm won't actually remove the file, have to do it ourselves
|
||||
os.remove(os.path.join(self.wiki.path, self.filename))
|
||||
self.wiki.gittle.rm(self.filename)
|
||||
commit = self.wiki.commit(name=username,
|
||||
email=email,
|
||||
message=message,
|
||||
files=[self.filename])
|
||||
self._clear_cache()
|
||||
self._invalidate_cache()
|
||||
return commit
|
||||
|
||||
def rename(self, new_name, username=None, email=None, message=None):
|
||||
|
@ -232,7 +271,7 @@ class WikiPage(HookMixin):
|
|||
"""
|
||||
assert self.sha == 'HEAD'
|
||||
old_filename, new_filename = self.filename, cname_to_filename(new_name)
|
||||
if old_filename not in self.wiki.gittle.index:
|
||||
if old_filename not in self.wiki.repo.open_index():
|
||||
# old doesn't exist
|
||||
return None
|
||||
elif old_filename == new_filename:
|
||||
|
@ -247,29 +286,25 @@ class WikiPage(HookMixin):
|
|||
message = "Moved %s to %s" % (self.name, new_name)
|
||||
|
||||
os.rename(os.path.join(self.wiki.path, old_filename), os.path.join(self.wiki.path, new_filename))
|
||||
|
||||
self.wiki.gittle.add(new_filename)
|
||||
self.wiki.gittle.rm(old_filename)
|
||||
|
||||
commit = self.wiki.commit(name=username,
|
||||
email=email,
|
||||
message=message,
|
||||
files=[old_filename, new_filename])
|
||||
|
||||
self._clear_cache()
|
||||
old_history = cache.get(self._cache_key('history'))
|
||||
self._invalidate_cache()
|
||||
self.name = new_name
|
||||
self.filename = new_filename
|
||||
# We need to clear the cache for the new name as well as the old
|
||||
self._clear_cache()
|
||||
self._invalidate_cache(save_history=old_history)
|
||||
|
||||
return commit
|
||||
|
||||
def write(self, content, message=None, create=False, username=None, email=None):
|
||||
def write(self, content, message=None, username=None, email=None):
|
||||
"""Write page to git repo
|
||||
|
||||
:param content: Content of page.
|
||||
:param message: Commit message.
|
||||
:param create: Perform git add operation?
|
||||
:param username: Commit Name.
|
||||
:param email: Commit Email.
|
||||
:return: Git commit sha1.
|
||||
|
@ -283,9 +318,6 @@ class WikiPage(HookMixin):
|
|||
with open(self.wiki.path + "/" + self.filename, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
if create:
|
||||
self.wiki.gittle.add(self.filename)
|
||||
|
||||
if not message:
|
||||
message = "Updated %s" % self.name
|
||||
|
||||
|
@ -296,7 +328,8 @@ class WikiPage(HookMixin):
|
|||
message=message,
|
||||
files=[self.filename])
|
||||
|
||||
self._clear_cache()
|
||||
old_history = cache.get(self._cache_key('history'))
|
||||
self._invalidate_cache(save_history=old_history)
|
||||
return ret
|
||||
|
||||
def revert(self, commit_sha, message, username, email):
|
||||
|
@ -315,8 +348,7 @@ class WikiPage(HookMixin):
|
|||
raise PageNotFound('Commit not found')
|
||||
|
||||
if not message:
|
||||
commit_info = gittle.utils.git.commit_info(self.wiki.gittle[commit_sha.encode('latin-1')])
|
||||
message = commit_info['message']
|
||||
message = "Revert '%s' to %s" % (self.name, commit_sha[:7])
|
||||
|
||||
return self.write(new_page.data, message=message, username=username, email=email)
|
||||
|
||||
|
|
430
realms/modules/wiki/static/js/aced.js
Normal file
430
realms/modules/wiki/static/js/aced.js
Normal file
|
@ -0,0 +1,430 @@
|
|||
function Aced(settings) {
|
||||
var id,
|
||||
options,
|
||||
editor,
|
||||
element,
|
||||
preview,
|
||||
previewWrapper,
|
||||
profile,
|
||||
autoInterval,
|
||||
themes,
|
||||
themeSelect,
|
||||
loadedThemes = {};
|
||||
|
||||
settings = settings || {};
|
||||
|
||||
options = {
|
||||
sanitize: true,
|
||||
preview: null,
|
||||
editor: null,
|
||||
theme: 'idle_fingers',
|
||||
themePath: '/static/vendor/ace-builds/src',
|
||||
mode: 'markdown',
|
||||
autoSave: true,
|
||||
autoSaveInterval: 5000,
|
||||
syncPreview: false,
|
||||
keyMaster: false,
|
||||
submit: function(data){ alert(data); },
|
||||
showButtonBar: false,
|
||||
themeSelect: null,
|
||||
submitBtn: null,
|
||||
renderer: null,
|
||||
info: null
|
||||
};
|
||||
|
||||
themes = {
|
||||
chrome: "Chrome",
|
||||
clouds: "Clouds",
|
||||
clouds_midnight: "Clouds Midnight",
|
||||
cobalt: "Cobalt",
|
||||
crimson_editor: "Crimson Editor",
|
||||
dawn: "Dawn",
|
||||
dreamweaver: "Dreamweaver",
|
||||
eclipse: "Eclipse",
|
||||
idle_fingers: "idleFingers",
|
||||
kr_theme: "krTheme",
|
||||
merbivore: "Merbivore",
|
||||
merbivore_soft: "Merbivore Soft",
|
||||
mono_industrial: "Mono Industrial",
|
||||
monokai: "Monokai",
|
||||
pastel_on_dark: "Pastel on Dark",
|
||||
solarized_dark: "Solarized Dark",
|
||||
solarized_light: "Solarized Light",
|
||||
textmate: "TextMate",
|
||||
tomorrow: "Tomorrow",
|
||||
tomorrow_night: "Tomorrow Night",
|
||||
tomorrow_night_blue: "Tomorrow Night Blue",
|
||||
tomorrow_night_bright: "Tomorrow Night Bright",
|
||||
tomorrow_night_eighties: "Tomorrow Night 80s",
|
||||
twilight: "Twilight",
|
||||
vibrant_ink: "Vibrant Ink"
|
||||
};
|
||||
|
||||
function editorId() {
|
||||
return "aced." + id;
|
||||
}
|
||||
|
||||
function infoKey() {
|
||||
return editorId() + ".info";
|
||||
}
|
||||
|
||||
function gc() {
|
||||
// Clean up localstorage
|
||||
store.forEach(function(key, val) {
|
||||
var re = new RegExp("aced\.(.*?)\.info");
|
||||
var info = re.exec(key);
|
||||
if (!info || !val.time) {
|
||||
return;
|
||||
}
|
||||
|
||||
var id = info[1];
|
||||
|
||||
// Remove week+ old stuff
|
||||
var now = new Date().getTime() / 1000;
|
||||
|
||||
if (now > (val.time + 604800)) {
|
||||
store.remove(key);
|
||||
store.remove('aced.' + id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildThemeSelect() {
|
||||
var $sel = $("<select class='aced-theme-sel' data-placeholder='Theme'></select>");
|
||||
$sel.append('<option></option>');
|
||||
$.each(themes, function(k, v) {
|
||||
$sel.append("<option value='" + k + "'>" + v + "</option>");
|
||||
});
|
||||
return $("<div/>").html($sel);
|
||||
}
|
||||
|
||||
function toJquery(o) {
|
||||
return (typeof o == 'string') ? $("#" + o) : $(o);
|
||||
}
|
||||
|
||||
function initProfile() {
|
||||
profile = {theme: ''};
|
||||
|
||||
try {
|
||||
// Need to merge in any undefined/new properties from last release
|
||||
// Meaning, if we add new features they may not have them in profile
|
||||
profile = $.extend(true, profile, store.get('aced.profile'));
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
function updateProfile(obj) {
|
||||
profile = $.extend(null, profile, obj);
|
||||
store.set('profile', profile);
|
||||
}
|
||||
|
||||
function render(content) {
|
||||
return (options.renderer) ? options.renderer(content) : content;
|
||||
}
|
||||
|
||||
function bindKeyboard() {
|
||||
// CMD+s TO SAVE DOC
|
||||
key('command+s, ctrl+s', function (e) {
|
||||
submit();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
var saveCommand = {
|
||||
name: "save",
|
||||
bindKey: {
|
||||
mac: "Command-S",
|
||||
win: "Ctrl-S"
|
||||
},
|
||||
exec: function () {
|
||||
submit();
|
||||
}
|
||||
};
|
||||
editor.commands.addCommand(saveCommand);
|
||||
}
|
||||
|
||||
function info(info) {
|
||||
if (info) {
|
||||
store.set(infoKey(), info);
|
||||
}
|
||||
return store.get(infoKey());
|
||||
}
|
||||
|
||||
function val(val) {
|
||||
// Alias func
|
||||
if (val) {
|
||||
editor.getSession().setValue(val);
|
||||
}
|
||||
return editor.getSession().getValue();
|
||||
}
|
||||
|
||||
function discardDraft() {
|
||||
stopAutoSave();
|
||||
store.remove(editorId());
|
||||
store.remove(infoKey());
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function save() {
|
||||
store.set(editorId(), val());
|
||||
}
|
||||
|
||||
function submit() {
|
||||
store.remove(editorId());
|
||||
store.remove(editorId() + ".info");
|
||||
options.submit(val());
|
||||
}
|
||||
|
||||
function autoSave() {
|
||||
if (options.autoSave) {
|
||||
autoInterval = setInterval(function () {
|
||||
save();
|
||||
}, options.autoSaveInterval);
|
||||
} else {
|
||||
stopAutoSave();
|
||||
}
|
||||
}
|
||||
|
||||
function stopAutoSave() {
|
||||
if (autoInterval){
|
||||
clearInterval(autoInterval)
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreview() {
|
||||
if (!preview) {
|
||||
return;
|
||||
}
|
||||
preview.html(render(val()));
|
||||
$('pre code', preview).each(function(i, e) {
|
||||
hljs.highlightBlock(e)
|
||||
});
|
||||
}
|
||||
|
||||
function getScrollHeight($prevFrame) {
|
||||
// Different browsers attach the scrollHeight of a document to different
|
||||
// elements, so handle that here.
|
||||
if ($prevFrame[0].scrollHeight !== undefined) {
|
||||
return $prevFrame[0].scrollHeight;
|
||||
} else if ($prevFrame.find('html')[0].scrollHeight !== undefined &&
|
||||
$prevFrame.find('html')[0].scrollHeight !== 0) {
|
||||
return $prevFrame.find('html')[0].scrollHeight;
|
||||
} else {
|
||||
return $prevFrame.find('body')[0].scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function getPreviewWrapper(obj) {
|
||||
// Attempts to get the wrapper for preview based on overflow prop
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
if (obj.css('overflow') == 'auto' || obj.css('overflow') == 'scroll') {
|
||||
return obj;
|
||||
} else {
|
||||
return getPreviewWrapper(obj.parent());
|
||||
}
|
||||
}
|
||||
|
||||
function syncPreview() {
|
||||
|
||||
var editorScrollRange = (editor.getSession().getLength());
|
||||
|
||||
var previewScrollRange = (getScrollHeight(preview));
|
||||
|
||||
// Find how far along the editor is (0 means it is scrolled to the top, 1
|
||||
// means it is at the bottom).
|
||||
var scrollFactor = editor.getFirstVisibleRow() / editorScrollRange;
|
||||
|
||||
// Set the scroll position of the preview pane to match. jQuery will
|
||||
// gracefully handle out-of-bounds values.
|
||||
|
||||
previewWrapper.scrollTop(scrollFactor * previewScrollRange);
|
||||
}
|
||||
|
||||
function asyncLoad(filename, cb) {
|
||||
(function (d, t) {
|
||||
|
||||
var leScript = d.createElement(t)
|
||||
, scripts = d.getElementsByTagName(t)[0];
|
||||
|
||||
leScript.async = 1;
|
||||
leScript.src = filename;
|
||||
scripts.parentNode.insertBefore(leScript, scripts);
|
||||
|
||||
leScript.onload = function () {
|
||||
cb && cb();
|
||||
}
|
||||
|
||||
}(document, 'script'));
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
var cb = function(theme) {
|
||||
editor.setTheme('ace/theme/'+theme);
|
||||
updateProfile({theme: theme});
|
||||
};
|
||||
|
||||
if (loadedThemes[theme]) {
|
||||
cb(theme);
|
||||
} else {
|
||||
asyncLoad(options.themePath + "/theme-" + theme + ".js", function () {
|
||||
cb(theme);
|
||||
loadedThemes[theme] = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initSyncPreview() {
|
||||
if (!preview || !options.syncPreview) return;
|
||||
previewWrapper = getPreviewWrapper(preview);
|
||||
window.onload = function () {
|
||||
/**
|
||||
* Bind synchronization of preview div to editor scroll and change
|
||||
* of editor cursor position.
|
||||
*/
|
||||
editor.session.on('changeScrollTop', syncPreview);
|
||||
editor.session.selection.on('changeCursor', syncPreview);
|
||||
};
|
||||
}
|
||||
|
||||
function initProps() {
|
||||
// Id of editor
|
||||
if (typeof settings == 'string') {
|
||||
settings = { editor: settings };
|
||||
}
|
||||
|
||||
if ('theme' in profile && profile['theme']) {
|
||||
settings['theme'] = profile['theme'];
|
||||
}
|
||||
|
||||
if (settings['preview'] && !settings.hasOwnProperty('syncPreview')) {
|
||||
settings['syncPreview'] = true;
|
||||
}
|
||||
|
||||
$.extend(options, settings);
|
||||
|
||||
if (options.editor) {
|
||||
element = toJquery(options.editor);
|
||||
}
|
||||
|
||||
$.each(options, function(k, v){
|
||||
if (element.data(k.toLowerCase())) {
|
||||
options[k] = element.data(k.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
if (options.themeSelect) {
|
||||
themeSelect = toJquery(options.themeSelect);
|
||||
}
|
||||
|
||||
if (options.submitBtn) {
|
||||
var submitBtn = toJquery(options.submitBtn);
|
||||
submitBtn.click(function(){
|
||||
submit();
|
||||
});
|
||||
}
|
||||
|
||||
if (options.preview) {
|
||||
preview = toJquery(options.preview);
|
||||
|
||||
// Enable sync unless set otherwise
|
||||
if (!settings.hasOwnProperty('syncPreview')) {
|
||||
options['syncPreview'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!element.attr('id')) {
|
||||
// No id, make one!
|
||||
id = Math.random().toString(36).substring(7);
|
||||
element.attr('id', id);
|
||||
} else {
|
||||
id = element.attr('id')
|
||||
}
|
||||
}
|
||||
|
||||
function initEditor() {
|
||||
editor = ace.edit(id);
|
||||
setTheme(profile.theme || options.theme);
|
||||
editor.getSession().setMode('ace/mode/' + options.mode);
|
||||
if (store.get(editorId()) && store.get(editorId()) != val()) {
|
||||
editor.getSession().setValue(store.get(editorId()));
|
||||
}
|
||||
editor.getSession().setUseWrapMode(true);
|
||||
editor.getSession().setTabSize(2);
|
||||
editor.getSession().setUseSoftTabs(true);
|
||||
editor.setShowPrintMargin(false);
|
||||
editor.renderer.setShowInvisibles(true);
|
||||
editor.renderer.setShowGutter(false);
|
||||
|
||||
if (options.showButtonBar) {
|
||||
var $btnBar = $('<div class="aced-button-bar aced-button-bar-top">' + buildThemeSelect().html() + ' <button type="button" class="btn btn-primary btn-xs aced-save">Save</button></div>')
|
||||
element.find('.ace_content').before($btnBar);
|
||||
|
||||
$(".aced-save", $btnBar).click(function(){
|
||||
submit();
|
||||
});
|
||||
|
||||
if ($.fn.chosen) {
|
||||
$('select', $btnBar).chosen().change(function(){
|
||||
setTheme($(this).val());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (options.keyMaster) {
|
||||
bindKeyboard();
|
||||
}
|
||||
|
||||
if (preview) {
|
||||
editor.getSession().on('change', function (e) {
|
||||
renderPreview();
|
||||
});
|
||||
renderPreview();
|
||||
}
|
||||
|
||||
if (themeSelect) {
|
||||
themeSelect
|
||||
.find('li > a')
|
||||
.bind('click', function (e) {
|
||||
setTheme($(e.target).data('value'));
|
||||
$(e.target).blur();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (options.info) {
|
||||
// If no info exists, save it to storage
|
||||
if (!store.get(infoKey())) {
|
||||
store.set(infoKey(), options.info);
|
||||
} else {
|
||||
// Check info in storage against one passed in
|
||||
// for possible changes in data that may have occurred
|
||||
var info = store.get(infoKey());
|
||||
if (info['sha'] != options.info['sha'] && !info['ignore']) {
|
||||
// Data has changed since start of draft
|
||||
$(document).trigger('shaMismatch');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(this).trigger('ready');
|
||||
}
|
||||
|
||||
function init() {
|
||||
gc();
|
||||
initProfile();
|
||||
initProps();
|
||||
initEditor();
|
||||
initSyncPreview();
|
||||
autoSave();
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
return {
|
||||
editor: editor,
|
||||
submit: submit,
|
||||
val: val,
|
||||
discard: discardDraft,
|
||||
info: info
|
||||
};
|
||||
}
|
52
realms/modules/wiki/static/js/collaboration/firepad.js
Normal file
52
realms/modules/wiki/static/js/collaboration/firepad.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
// Helper to get hash from end of URL or generate a random one.
|
||||
function getExampleRef() {
|
||||
var ref = new Firebase('https://' + Config['FIREBASE_HOSTNAME']);
|
||||
var hash = window.location.hash.replace(/^#fp-/, '');
|
||||
if (hash) {
|
||||
ref = ref.child(hash);
|
||||
} else {
|
||||
ref = ref.push(); // generate unique location.
|
||||
window.location = window.location + '#fp-' + ref.name(); // add it as a hash to the URL.
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
function initFirepad() {
|
||||
var new_ = true;
|
||||
if (window.location.hash.lastIndexOf('#fp-', 0) === 0) {
|
||||
new_ = false;
|
||||
}
|
||||
var firepadRef = getExampleRef();
|
||||
var session = aced.editor.session;
|
||||
var content;
|
||||
|
||||
if (new_) {
|
||||
content = session.getValue();
|
||||
}
|
||||
|
||||
// Firepad wants an empty editor
|
||||
session.setValue('');
|
||||
|
||||
//// Create Firepad.
|
||||
var firepad = Firepad.fromACE(firepadRef, aced.editor, {
|
||||
defaultText: content
|
||||
});
|
||||
|
||||
firepad.on('ready', function() {
|
||||
startCollaboration();
|
||||
});
|
||||
|
||||
$(document).on('end-collaboration', function() {
|
||||
firepad.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('loading-collaboration', function() {
|
||||
initFirepad(true);
|
||||
});
|
||||
|
||||
$(function(){
|
||||
if (window.location.hash.lastIndexOf('#fp-', 0) === 0) {
|
||||
loadingCollaboration();
|
||||
}
|
||||
});
|
36
realms/modules/wiki/static/js/collaboration/main.js
Normal file
36
realms/modules/wiki/static/js/collaboration/main.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
var $startCollaborationBtn = $('#start-collaboration');
|
||||
var $endCollaborationBtn = $('#end-collaboration');
|
||||
var $loadingCollaborationBtn = $('#loading-collaboration');
|
||||
|
||||
function loadingCollaboration() {
|
||||
$endCollaborationBtn.hide();
|
||||
$startCollaborationBtn.hide();
|
||||
$loadingCollaborationBtn.show();
|
||||
$(document).trigger('loading-collaboration');
|
||||
}
|
||||
|
||||
function startCollaboration() {
|
||||
$loadingCollaborationBtn.hide();
|
||||
$startCollaborationBtn.hide();
|
||||
$endCollaborationBtn.show();
|
||||
$(document).trigger('start-collaboration');
|
||||
}
|
||||
|
||||
function endCollaboration() {
|
||||
$loadingCollaborationBtn.hide();
|
||||
$endCollaborationBtn.hide();
|
||||
$startCollaborationBtn.show();
|
||||
$(document).trigger('end-collaboration');
|
||||
}
|
||||
|
||||
$(function() {
|
||||
$startCollaborationBtn.click(function(e) {
|
||||
loadingCollaboration();
|
||||
e.preventDefault();
|
||||
});
|
||||
$endCollaborationBtn.click(function(e) {
|
||||
endCollaboration();
|
||||
e.preventDefault();
|
||||
|
||||
});
|
||||
});
|
28
realms/modules/wiki/static/js/collaboration/togetherjs.js
Normal file
28
realms/modules/wiki/static/js/collaboration/togetherjs.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
$(document).on('loading-collaboration', function() {
|
||||
TogetherJS();
|
||||
});
|
||||
|
||||
$(document).on('end-collaboration', function() {
|
||||
TogetherJS();
|
||||
});
|
||||
|
||||
TogetherJSConfig_toolName = "Collaboration";
|
||||
TogetherJSConfig_suppressJoinConfirmation = true;
|
||||
|
||||
if (User.is_authenticated) {
|
||||
TogetherJSConfig_getUserName = function () {
|
||||
return User.username;
|
||||
};
|
||||
|
||||
TogetherJSConfig_getUserAvatar = function () {
|
||||
return User.avatar;
|
||||
};
|
||||
}
|
||||
|
||||
TogetherJSConfig_on_ready = function () {
|
||||
startCollaboration();
|
||||
};
|
||||
|
||||
TogetherJSConfig_on_close = function () {
|
||||
//endCollaboration();
|
||||
};
|
119
realms/modules/wiki/static/js/editor.js
Normal file
119
realms/modules/wiki/static/js/editor.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
var $entry_markdown_header = $("#entry-markdown-header");
|
||||
var $entry_preview_header = $("#entry-preview-header");
|
||||
var $entry_markdown = $(".entry-markdown");
|
||||
var $entry_preview = $(".entry-preview");
|
||||
var $page_name = $("#page-name");
|
||||
var $page_message = $("#page-message");
|
||||
|
||||
// Tabs
|
||||
$entry_markdown_header.click(function(){
|
||||
$entry_markdown.addClass('active');
|
||||
$entry_preview.removeClass('active');
|
||||
});
|
||||
|
||||
$entry_preview_header.click(function(){
|
||||
$entry_preview.addClass('active');
|
||||
$entry_markdown.removeClass('active');
|
||||
});
|
||||
|
||||
$(document).on('shaMismatch', function() {
|
||||
bootbox.dialog({
|
||||
title: "Page has changed",
|
||||
message: "This page has changed and differs from your draft. What do you want to do?",
|
||||
buttons: {
|
||||
ignore: {
|
||||
label: "Ignore",
|
||||
className: "btn-default",
|
||||
callback: function() {
|
||||
var info = aced.info();
|
||||
info['ignore'] = true;
|
||||
aced.info(info);
|
||||
}
|
||||
},
|
||||
discard: {
|
||||
label: "Discard Draft",
|
||||
className: "btn-danger",
|
||||
callback: function() {
|
||||
aced.discard();
|
||||
}
|
||||
},
|
||||
changes: {
|
||||
label: "Show Diff",
|
||||
className: "btn-primary",
|
||||
callback: function() {
|
||||
bootbox.alert("Draft diff not done! Sorry");
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$(function(){
|
||||
$("#discard-draft-btn").click(function() {
|
||||
aced.discard();
|
||||
});
|
||||
|
||||
$(".entry-markdown .floatingheader").click(function(){
|
||||
aced.editor.focus();
|
||||
});
|
||||
|
||||
$("#delete-page-btn").click(function() {
|
||||
bootbox.confirm('Are you sure you want to delete this page?', function(result) {
|
||||
if (result) {
|
||||
deletePage();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var deletePage = function() {
|
||||
var pageName = $page_name.val();
|
||||
var path = Config['RELATIVE_PATH'] + '/' + pageName;
|
||||
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: path,
|
||||
}).done(function(data) {
|
||||
var msg = 'Deleted page: ' + pageName;
|
||||
bootbox.alert(msg, function() {
|
||||
location.href = '/';
|
||||
});
|
||||
}).fail(function(data, status, error) {
|
||||
bootbox.alert('Error deleting page!');
|
||||
});
|
||||
};
|
||||
|
||||
var aced = new Aced({
|
||||
editor: $('#entry-markdown-content').find('.editor').attr('id'),
|
||||
renderer: function(md) { return MDR.convert(md) },
|
||||
info: Commit.info,
|
||||
submit: function(content) {
|
||||
var data = {
|
||||
name: $page_name.val().replace(/^\/*/g, "").replace(/\/+/g, "/"),
|
||||
message: $page_message.val(),
|
||||
content: content
|
||||
};
|
||||
|
||||
// If renaming an existing page, use the old page name for the URL to PUT to
|
||||
var subPath = (PAGE_NAME) ? PAGE_NAME : data['name'];
|
||||
var path = Config['RELATIVE_PATH'] + '/' + subPath;
|
||||
var newPath = Config['RELATIVE_PATH'] + '/' + data['name'];
|
||||
|
||||
var type = (Commit.info['sha']) ? "PUT" : "POST";
|
||||
|
||||
$.ajax({
|
||||
type: type,
|
||||
url: path,
|
||||
data: data,
|
||||
dataType: 'json'
|
||||
}).always(function(data, status, error) {
|
||||
var res = data['responseJSON'];
|
||||
if (res && res['error']) {
|
||||
$page_name.addClass('parsley-error');
|
||||
bootbox.alert("<h3>" + res['message'] + "</h3>");
|
||||
} else {
|
||||
location.href = newPath;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
16
realms/modules/wiki/templates/wiki/compare.html
Normal file
16
realms/modules/wiki/templates/wiki/compare.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block body %}
|
||||
|
||||
<h2>History for <strong>{{ name }}</strong></h2>
|
||||
<div class="pull-right">
|
||||
<a href="{{ url_for('wiki.commit', name=name, sha=old) }}" class="btn btn-default btn-sm">View Old</a>
|
||||
<a href="{{ url_for('wiki.commit', name=name, sha=new) }}" class="btn btn-info btn-sm">View New</a>
|
||||
</div>
|
||||
<p>
|
||||
<a class="btn btn-default btn-sm" href="{{ url_for('wiki.history', name=name) }}">Back to History</a>
|
||||
</p>
|
||||
{{ diff|safe }}
|
||||
<p>
|
||||
<a class="btn btn-default btn-sm" href="{{ url_for('wiki.history', name=name) }}">Back to History</a>
|
||||
</p>
|
||||
{% endblock %}
|
173
realms/modules/wiki/templates/wiki/edit.html
Normal file
173
realms/modules/wiki/templates/wiki/edit.html
Normal file
|
@ -0,0 +1,173 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block js %}
|
||||
<script>
|
||||
var Commit = {};
|
||||
Commit.info = {{ info|tojson }};
|
||||
|
||||
var PAGE_NAME = '{{ name }}';
|
||||
</script>
|
||||
<script src="{{ url_for('wiki.static', filename='js/editor.js') }}"></script>
|
||||
|
||||
{% if partials %}
|
||||
<script>
|
||||
$(function() {
|
||||
{% for name, value in partials.items() %}
|
||||
{% if name and value %}
|
||||
try {
|
||||
Handlebars.registerPartial({{ name|tojson|safe }}, {{ value.data|tojson|safe }});
|
||||
} catch (e) {
|
||||
// no data?
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if config.get('COLLABORATION') %}
|
||||
<script src="{{ url_for('wiki.static', filename='js/collaboration/main.js') }}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% if config.get('COLLABORATION') == 'firepad' %}
|
||||
<script>
|
||||
Config['FIREBASE_HOSTNAME'] = {{ config.get('FIREBASE_HOSTNAME')|tojson }};
|
||||
</script>
|
||||
<script src="https://cdn.firebase.com/js/client/1.0.17/firebase.js"></script>
|
||||
<script src="https://cdn.firebase.com/libs/firepad/1.0.0/firepad.min.js"></script>
|
||||
<script src="{{ url_for('wiki.static', filename='js/collaboration/firepad.js') }}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% if config.get('COLLABORATION') == 'togetherjs' %}
|
||||
<script src="{{ url_for('wiki.static', filename='js/collaboration/togetherjs.js') }}"></script>
|
||||
<script src="https://togetherjs.com/togetherjs-min.js"></script>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="app-wrap">
|
||||
<div id="app-controls" class="row">
|
||||
<div class="col-xs-4 col-md-3">
|
||||
<input id="page-name" type="text" class="form-control input-sm" name="name"
|
||||
placeholder="Name" value="{{- name -}}" />
|
||||
</div>
|
||||
<div class="col-xs-4 col-md-3">
|
||||
<input id="page-message" type="text" class="form-control input-sm" name="page-message"
|
||||
placeholder="Comment" value="" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-xs-4 text-right">
|
||||
|
||||
{% if config.get('COLLABORATION') %}
|
||||
<div class="btn-group">
|
||||
<button style='display:none' class="btn btn-danger btn-sm" id="end-collaboration">
|
||||
<i class="fa fa-comments-o"></i>
|
||||
<span class="hidden-xs">End Collaboration</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button style='display:none' class="btn btn-default btn-sm" id="loading-collaboration" type="button">
|
||||
<i class="fa fa-cog fa-spin"></i>
|
||||
<span class="hidden-xs">Loading</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="dropdown btn-group">
|
||||
<button class="btn btn-default btn-sm dropdown-toggle" type="button" id="editor-actions"
|
||||
data-toggle="dropdown" title="Actions">
|
||||
<i class="fa fa-cog"></i>
|
||||
<span class="hidden-xs hidden-sm">Actions <i class="fa fa-caret-down"></i></span>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="editor-actions">
|
||||
{% if config.get('COLLABORATION') %}
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" href="#" id="start-collaboration">Collaborate</a>
|
||||
</li>
|
||||
<li role="presentation" class="divider"></li>
|
||||
{% endif %}
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" href="#" id="discard-draft-btn">Delete Draft</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" href="#" id="delete-page-btn">Delete Page</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="dropdown btn-group">
|
||||
<button id="theme-list-btn" type="button" class="dropdown-toggle btn btn-default btn-sm"
|
||||
data-toggle="dropdown" title="Change Theme">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
<span class="hidden-xs hidden-sm">Theme <i class="fa fa-caret-down"></i></span>
|
||||
</button>
|
||||
<ul id="theme-list" class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="theme-list">
|
||||
<li><a tabindex="-1" href="#" data-value="chrome" >Chrome</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="clouds" >Clouds</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="clouds_midnight" >Clouds Midnight</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="cobalt" >Cobalt</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="crimson_editor" >Crimson Editor</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="dawn" class="selected">Dawn</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="dreamweaver" >Dreamweaver</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="eclipse" >Eclipse</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="idle_fingers" >idleFingers</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="kr_theme" >krTheme</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="merbivore" >Merbivore</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="merbivore_soft" >Merbivore Soft</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="mono_industrial" >Mono Industrial</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="monokai" >Monokai</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="pastel_on_dark">Pastel on Dark</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="solarized_dark" >Solarized Dark</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="solarized_light" >Solarized Light</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="textmate" >TextMate</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="tomorrow" >Tomorrow</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="tomorrow_night">Tomorrow Night</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="tomorrow_night_blue" >Tomorrow Night Blue</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="tomorrow_night_bright" >Tomorrow Night Bright</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="tomorrow_night_eighties" >Tomorrow Night 80s</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="twilight" >Twilight</a></li>
|
||||
<li><a tabindex="-1" href="#" data-value="vibrant_ink" >Vibrant Ink</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
{% if name in config['WIKI_LOCKED_PAGES'] %}
|
||||
<a class="btn btn-danger btn-sm">
|
||||
<i class="fa fa-lock"></i>
|
||||
<span class="hidden-xs">Locked</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a id="submit-btn" class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-save"></i>
|
||||
<span class="hidden-xs">Save</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="entry-markdown active">
|
||||
<header class="floatingheader" id="entry-markdown-header">
|
||||
<small>Markdown</small>
|
||||
<a class="markdown-help" href=""><span class="hidden">What is Markdown?</span></a>
|
||||
</header>
|
||||
<section id="entry-markdown-content" class="entry-markdown-content">
|
||||
<div id="editor-{{ name | b64encode }}" data-submitbtn='submit-btn' data-themeselect="theme-list" data-mode="markdown"
|
||||
data-preview="preview" class="editor">{{ content }}</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="entry-preview">
|
||||
<header class="floatingheader" id="entry-preview-header">
|
||||
<small>Preview</small>
|
||||
</header>
|
||||
<section class="entry-preview-content">
|
||||
<div id="preview"></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
120
realms/modules/wiki/templates/wiki/history.html
Normal file
120
realms/modules/wiki/templates/wiki/history.html
Normal file
|
@ -0,0 +1,120 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block body %}
|
||||
|
||||
<h2>History for <strong>{{ name }}</strong></h2>
|
||||
<p>
|
||||
<a class="btn btn-default btn-sm compare-revisions">Compare Revisions</a>
|
||||
</p>
|
||||
|
||||
<table class="table table-bordered revision-tbl dataTable DTTT_selectable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Revision Message</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="3" style="text-align: center">Loading file history...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<a class="btn btn-default btn-sm compare-revisions">Compare Revisions</a>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<style type="text/css">
|
||||
table.dataTable td {
|
||||
transition: background-color 0.5s linear, color 0.5s linear;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
table.dataTable tr.active td {
|
||||
transition: background-color 0.1s linear, color 0.1s linear;
|
||||
transition-delay: 0s
|
||||
}
|
||||
table.dataTable tbody tr:hover {
|
||||
background-color: #d8d8d8 !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var selected = [];
|
||||
var selected_pos = [];
|
||||
|
||||
$('.dataTable').dataTable({
|
||||
serverSide: true,
|
||||
ajax: {
|
||||
url: '{{ url_for('.history_data', name=name) }}',
|
||||
dataSrc: function (data) {
|
||||
$('.dataTable').data('fully_loaded', data.fully_loaded);
|
||||
return data.data
|
||||
}
|
||||
},
|
||||
ordering: false,
|
||||
bFilter: false,
|
||||
columns: [
|
||||
{
|
||||
"data": null,
|
||||
"render": function (data) {
|
||||
return '<img src="' + data.gravatar + '?s=20" class="avatar" /> ' + data.author
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": null,
|
||||
"render": function (data) {
|
||||
return '<a href="' + data.link + '" class="label label-primary">View</a> ' + data.message
|
||||
}
|
||||
},
|
||||
{ "data": "date" }
|
||||
],
|
||||
rowCallback: function( row, data, index ) {
|
||||
index += $('.dataTable').DataTable().page.info().start;
|
||||
$(row).data('index', index);
|
||||
if ( $.inArray(data.DT_RowId, selected) !== -1 ) {
|
||||
$(row).addClass('active');
|
||||
}
|
||||
},
|
||||
infoCallback: function( settings, start, end, max, total, pre ) {
|
||||
if (!$('.dataTable').data('fully_loaded')) {
|
||||
total += "+"
|
||||
}
|
||||
return "Showing " + start +" to "+ end + " of " + total + " revisions.";
|
||||
}
|
||||
});
|
||||
|
||||
$('.dataTable tbody').on('click', 'tr', function () {
|
||||
var id = this.id;
|
||||
var selected_index = $.inArray(id, selected);
|
||||
|
||||
if ( selected_index === -1 ) {
|
||||
selected.push( id );
|
||||
selected_pos.push( $(this).data('index') );
|
||||
if ( selected.length > 2) {
|
||||
// Only 2 selected at once
|
||||
var shifted = selected.shift();
|
||||
selected_pos.shift();
|
||||
$('#' + shifted).removeClass('active');
|
||||
}
|
||||
} else {
|
||||
selected.splice( selected_index, 1 );
|
||||
selected_pos.splice( selected_index, 1);
|
||||
}
|
||||
|
||||
$(this).toggleClass('active');
|
||||
});
|
||||
$(".compare-revisions").click(function(){
|
||||
if (selected.length != 2) return;
|
||||
if (selected_pos[1] > selected_pos[0]) {
|
||||
selected.reverse()
|
||||
}
|
||||
revs = selected.join("..");
|
||||
location.href = "{{ config.RELATIVE_PATH }}/_compare/{{ name }}/" + revs;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
43
realms/modules/wiki/templates/wiki/index.html
Normal file
43
realms/modules/wiki/templates/wiki/index.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.data-table').dataTable({'aaSorting': [[0, "asc"], [1, "asc"]]});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h2>Index of <a href="{{ url_for('wiki.index') }}">/</a>
|
||||
{%- set parts = path.split('/') -%}
|
||||
{%- for dir in parts if dir -%}
|
||||
<a href="{{ url_for('wiki.index', path='/'.join(parts[:loop.index])) }}">{{ dir }}/</a>
|
||||
{%- endfor -%}
|
||||
</h2>
|
||||
<table class="table table-bordered data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 1px;"></th>
|
||||
<th>Name</th>
|
||||
<th class="hidden-xs">Bytes</th>
|
||||
<th class="hidden-xs hidden-sm">Created</th>
|
||||
<th>Modified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for file in index %}
|
||||
<tr>
|
||||
{% if file['dir'] %}
|
||||
<td><i class="fa fa-folder-open-o"><span style="display:none;">Dir</span></i></td>
|
||||
<td><a href="{{ url_for('wiki.index', path=file['name']) }}">{{ file['name'][path|length:] }}</a></td>
|
||||
{% else %}
|
||||
<td><i class="fa fa-file-text-o"><span style="display:none;">Page</span></i></td>
|
||||
<td><a href="{{ url_for('wiki.page', name=file['name']) }}">{{ file['name'][path|length:] }}</a></td>
|
||||
{% endif %}
|
||||
<td class="hidden-xs">{{ file['size'] }}</td>
|
||||
<td>{{ file['ctime']|datetime }}</td>
|
||||
<td class="hidden-xs hidden-sm">{{ file['mtime']|datetime }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
34
realms/modules/wiki/templates/wiki/page.html
Normal file
34
realms/modules/wiki/templates/wiki/page.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block page_menu %}
|
||||
<div class="controls pull-right">
|
||||
<a class="btn btn-default btn-sm" href="{{ url_for('wiki.edit', name=name) }}">Edit</a>
|
||||
<a class="btn btn-default btn-sm" href="{{ url_for('wiki.history', name=name) }}">History</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% if commit %}
|
||||
<div id="page-action-bar">
|
||||
<form method="POST" action="{{ url_for('wiki.revert') }}" class="ajax-form" data-redirect="{{ url_for('wiki.page', name=name) }}">
|
||||
<input type="hidden" value="{{ name }}" name="name" />
|
||||
<input type="hidden" value="{{ commit }}" name="commit" />
|
||||
<input type="submit" class="btn btn-danger btn-sm" title="Revert back to this revision" value="Revert" />
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="page-content"></div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
$(function(){
|
||||
{% if partials %}
|
||||
{% for name, value in partials.items() %}
|
||||
Handlebars.registerPartial({{ name|tojson|safe }}, {{ value.data|tojson|safe }});
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
$("#page-content").html(MDR.convert({{ page.data|tojson|safe }})).show();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -41,7 +41,7 @@ class WikiTest(WikiBaseTest):
|
|||
self.assert_200(rv)
|
||||
|
||||
self.assert_context('name', 'test')
|
||||
eq_(self.get_context_variable('page').info['message'], 'test message')
|
||||
eq_(next(self.get_context_variable('page').history)['message'], 'test message')
|
||||
eq_(self.get_context_variable('page').data, 'testing')
|
||||
|
||||
def test_history(self):
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
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.ext.login import login_required, current_user
|
||||
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__)
|
||||
blueprint = Blueprint('wiki', __name__, template_folder='templates',
|
||||
static_folder='static', static_url_path='/static/wiki')
|
||||
|
||||
|
||||
@blueprint.route("/_commit/<sha>/<path:name>")
|
||||
def commit(name, sha):
|
||||
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
|
||||
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
|
||||
return current_app.login_manager.unauthorized()
|
||||
|
||||
cname = to_canonical(name)
|
||||
|
@ -25,7 +27,7 @@ def commit(name, sha):
|
|||
|
||||
@blueprint.route(r"/_compare/<path:name>/<regex('\w+'):fsha><regex('\.{2,3}'):dots><regex('\w+'):lsha>")
|
||||
def compare(name, fsha, dots, lsha):
|
||||
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
|
||||
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)
|
||||
|
@ -40,7 +42,7 @@ def revert():
|
|||
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():
|
||||
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'):
|
||||
|
@ -62,13 +64,39 @@ def revert():
|
|||
|
||||
@blueprint.route("/_history/<path:name>")
|
||||
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/<path:name>")
|
||||
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()
|
||||
|
||||
hist = g.current_wiki.get_page(name).get_history()
|
||||
for item in hist:
|
||||
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'])
|
||||
return render_template('wiki/history.html', name=name, history=hist)
|
||||
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/<path:name>")
|
||||
|
@ -85,7 +113,8 @@ def edit(name):
|
|||
return render_template('wiki/edit.html',
|
||||
name=cname,
|
||||
content=page.data,
|
||||
info=page.info,
|
||||
# TODO: Remove this? See #148
|
||||
info=next(page.history),
|
||||
sha=page.sha,
|
||||
partials=page.partials)
|
||||
|
||||
|
@ -137,7 +166,7 @@ def _tree_index(items, path=""):
|
|||
@blueprint.route("/_index", defaults={"path": ""})
|
||||
@blueprint.route("/_index/<path:path>")
|
||||
def index(path):
|
||||
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
|
||||
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
|
||||
return current_app.login_manager.unauthorized()
|
||||
|
||||
items = g.current_wiki.get_index()
|
||||
|
@ -158,7 +187,7 @@ def page_write(name):
|
|||
if not cname:
|
||||
return dict(error=True, message="Invalid name")
|
||||
|
||||
if not current_app.config.get('ALLOW_ANON') and current_user.is_anonymous():
|
||||
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':
|
||||
|
@ -168,7 +197,6 @@ def page_write(name):
|
|||
|
||||
sha = g.current_wiki.get_page(cname).write(request.form['content'],
|
||||
message=request.form['message'],
|
||||
create=True,
|
||||
username=current_user.username,
|
||||
email=current_user.email)
|
||||
|
||||
|
@ -202,7 +230,7 @@ def page_write(name):
|
|||
@blueprint.route("/", defaults={'name': 'home'})
|
||||
@blueprint.route("/<path:name>")
|
||||
def page(name):
|
||||
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
|
||||
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous:
|
||||
return current_app.login_manager.unauthorized()
|
||||
|
||||
cname = to_canonical(name)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue