Merge branch 'master' into oauth_redirect

# Conflicts:
#	realms/modules/auth/views.py
This commit is contained in:
Chase Sterling 2016-09-04 12:25:57 -04:00
commit 2ce6c2d314
41 changed files with 350 additions and 213 deletions

View file

@ -6,7 +6,7 @@ RUN apt-get install -y software-properties-common python-software-properties
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN ln -s /usr/bin/nodejs /usr/bin/node && \
npm install -g bower
npm install -g bower clean-css
RUN useradd -ms /bin/bash wiki
@ -28,7 +28,6 @@ ENV GEVENT_RESOLVER=ares
ENV REALMS_ENV=docker
ENV REALMS_WIKI_PATH=/home/wiki/data/repo
ENV REALMS_DB_URI='sqlite:////home/wiki/data/wiki.db'
ENV REALMS_SQLALCHEMY_DATABASE_URI=${REALMS_DB_URI}
RUN mkdir /home/wiki/data && touch /home/wiki/data/.a
VOLUME /home/wiki/data
@ -45,4 +44,3 @@ CMD . .venv/bin/activate && \
--bind 0.0.0.0:5000 \
--chdir /home/wiki/realms-wiki \
'realms:create_app()'

View file

@ -1,3 +1,4 @@
import functools
import sys
# Set default encoding to UTF-8
@ -12,10 +13,10 @@ import httplib
import traceback
import click
from flask import Flask, request, render_template, url_for, redirect, g
from flask.ext.cache import Cache
from flask.ext.login import LoginManager, current_user
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.assets import Environment, Bundle
from flask_cache import Cache
from flask_login import LoginManager, current_user
from flask_sqlalchemy import SQLAlchemy
from flask_assets import Environment, Bundle
from flask_ldap_login import LDAPLoginManager
from functools import update_wrapper
from werkzeug.routing import BaseConverter
@ -75,7 +76,9 @@ class Application(Flask):
# Click
if hasattr(sources, 'commands'):
cli.add_command(sources.commands.cli, name=module_name)
if sources.commands.cli.name == 'cli':
sources.commands.cli.name = module_name
cli.add_command(sources.commands.cli)
# Hooks
if hasattr(sources, 'hooks'):
@ -177,9 +180,7 @@ def create_app(config=None):
db.Model = declarative_base(metaclass=HookModelMeta, cls=HookMixin)
for status_code in httplib.responses:
if status_code >= 400:
app.register_error_handler(status_code, error_handler)
app.register_error_handler(HTTPException, error_handler)
@app.before_request
def init_g():
@ -287,9 +288,8 @@ class AppGroup(click.Group):
kwargs.setdefault('cls', AppGroup)
return click.Group.group(self, *args, **kwargs)
flask_cli = AppGroup()
cli = AppGroup()
# Decorator to be used in modules instead of click.group
cli_group = functools.partial(click.group, cls=AppGroup)
@flask_cli.group()
def cli():
pass

View file

@ -1,4 +1,4 @@
from realms import config, create_app, db, __version__, flask_cli as cli, cache
from realms import config, create_app, db, __version__, cli, cache
from realms.lib.util import random_string, in_virtualenv, green, yellow, red
from subprocess import call, Popen
from multiprocessing import cpu_count
@ -425,7 +425,7 @@ def clear_cache():
def test():
""" Run tests
"""
for mod in [('flask.ext.testing', 'Flask-Testing'), ('nose', 'nose'), ('blinker', 'blinker')]:
for mod in [('flask_testing', 'Flask-Testing'), ('nose', 'nose'), ('blinker', 'blinker')]:
if not module_exists(mod[0]):
pip.main(['install', mod[1]])

View file

@ -23,7 +23,7 @@ class Config(object):
BASE_URL = 'http://localhost'
SITE_TITLE = "Realms"
# https://pythonhosted.org/Flask-SQLAlchemy/config.html#connection-uri-format
# http://flask-sqlalchemy.pocoo.org/config/#connection-uri-format
DB_URI = 'sqlite:////tmp/wiki.db'
# DB_URI = 'mysql://scott:tiger@localhost/mydatabase'
# DB_URI = 'postgresql://scott:tiger@localhost/mydatabase'
@ -139,6 +139,7 @@ class Config(object):
DEBUG = False
ASSETS_DEBUG = False
SQLALCHEMY_ECHO = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
MODULES = ['wiki', 'search', 'auth']

View file

@ -1,4 +1,4 @@
from flask.ext.sqlalchemy import DeclarativeMeta
from flask_sqlalchemy import DeclarativeMeta
from functools import wraps
@ -25,9 +25,15 @@ class HookMixinMeta(type):
def __new__(cls, name, bases, attrs):
super_new = super(HookMixinMeta, cls).__new__
hookable = []
for key, value in attrs.items():
# Disallow hooking methods which start with an underscore (allow __init__ etc. still)
if key.startswith('_') and not key.startswith('__'):
continue
if callable(value):
attrs[key] = hook_func(key, value)
hookable.append(key)
attrs['_hookable'] = hookable
return super_new(cls, name, bases, attrs)
@ -37,9 +43,12 @@ class HookMixin(object):
_pre_hooks = {}
_post_hooks = {}
_hookable = []
@classmethod
def after(cls, method_name):
assert method_name in cls._hookable, "'%s' not a hookable method of '%s'" % (method_name, cls.__name__)
def outer(f, *args, **kwargs):
cls._post_hooks.setdefault(method_name, []).append((f, args, kwargs))
return f
@ -47,6 +56,8 @@ class HookMixin(object):
@classmethod
def before(cls, method_name):
assert method_name in cls._hookable, "'%s' not a hookable method of '%s'" % (method_name, cls.__name__)
def outer(f, *args, **kwargs):
cls._pre_hooks.setdefault(method_name, []).append((f, args, kwargs))
return f

View file

@ -1,7 +1,7 @@
import os
import shutil
import tempfile
from flask.ext.testing import TestCase
from flask_testing import TestCase
from realms.lib.util import random_string
from realms import create_app

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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'])

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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')

View file

@ -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)

View file

@ -6,7 +6,7 @@
var PAGE_NAME = '{{ name }}';
</script>
<script src="{{ url_for('static', filename='js/editor.js') }}"></script>
<script src="{{ url_for('wiki.static', filename='js/editor.js') }}"></script>
{% if partials %}
<script>
@ -25,7 +25,7 @@
{% endif %}
{% if config.get('COLLABORATION') %}
<script src="{{ url_for('static', filename='js/collaboration/main.js') }}"></script>
<script src="{{ url_for('wiki.static', filename='js/collaboration/main.js') }}"></script>
{% endif %}
{% if config.get('COLLABORATION') == 'firepad' %}
@ -34,11 +34,11 @@
</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('static', filename='js/collaboration/firepad.js') }}"></script>
<script src="{{ url_for('wiki.static', filename='js/collaboration/firepad.js') }}"></script>
{% endif %}
{% if config.get('COLLABORATION') == 'togetherjs' %}
<script src="{{ url_for('static', filename='js/collaboration/togetherjs.js') }}"></script>
<script src="{{ url_for('wiki.static', filename='js/collaboration/togetherjs.js') }}"></script>
<script src="https://togetherjs.com/togetherjs-min.js"></script>
{% endif %}

View 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" />&nbsp;&nbsp;' + data.author
}
},
{
"data": null,
"render": function (data) {
return '<a href="' + data.link + '" class="label label-primary">View</a>&nbsp;&nbsp;' + 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 %}

View file

@ -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):

View file

@ -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)

View file

@ -2,5 +2,8 @@
{% block body %}
<h1>Page Not Found</h1>
{% if error is defined %}
<h4>{{ error.description }}</h4>
{% endif %}
{% endblock %}
{% endblock %}

View file

@ -58,7 +58,7 @@
</div>
</form>
</li>
{% if current_user.is_authenticated() %}
{% if current_user.is_authenticated %}
<li class="dropdown user-avatar">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span>
@ -109,7 +109,7 @@
{% endfor %}
var User = {};
User.is_authenticated = {{ current_user.is_authenticated()|tojson }};
User.is_authenticated = {{ current_user.is_authenticated|tojson }};
{% for attr in ['username', 'email'] %}
User.{{ attr }} = {{ current_user[attr]|tojson }};
{% endfor %}

View file

@ -1,60 +0,0 @@
{% 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">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Revision Message</th>
<th>Date</th>
</tr>
</thead>
{% for h in history %}
<tr>
<td class="checkbox-cell text-center">
{% if h.type != 'delete' %}
<input type="checkbox" name="versions[]" value="{{ h.sha }}" />
{% endif %}
</td>
<td><img src="{{ h.gravatar }}?s=20" class="avatar"/> {{ h.author }}</td>
<td><a href="{{ url_for('wiki.commit', name=name, sha=h.sha) }}" class='label label-primary'>View</a> {{ h.message }} </td>
<td>{{ h.time|datetime }}</td>
</tr>
{% endfor %}
</table>
<p>
<a class="btn btn-default btn-sm compare-revisions">Compare Revisions</a>
</p>
{% endblock %}
{% block js %}
<script>
$(function(){
$('.revision-tbl :checkbox').change(function () {
var $cs=$(this).closest('.revision-tbl').find(':checkbox:checked');
if ($cs.length > 2) {
this.checked=false;
}
});
$(".compare-revisions").click(function(){
var $cs = $('.revision-tbl').find(':checkbox:checked');
if ($cs.length != 2) return;
var revs = [];
$.each($cs, function(i, v){
revs.push(v.value);
});
revs.reverse();
revs = revs.join("..");
location.href = "{{ config.RELATIVE_PATH }}/_compare/{{ name }}/" + revs;
});
});
</script>
{% endblock %}

View file

@ -1 +1 @@
__version__ = '0.8.0'
__version__ = '0.8.1'

View file

@ -23,22 +23,22 @@ setup(name='realms-wiki',
version=__version__,
packages=find_packages(),
install_requires=[
'Flask==0.10.1',
'Flask-Assets==0.10',
'Flask==0.11.1',
'Flask-Assets==0.11',
'Flask-Cache==0.13.1',
'Flask-Elastic==0.2',
'Flask-Login==0.2.11',
'Flask-OAuthlib==0.9.1',
'Flask-SQLAlchemy==2.0',
'Flask-WTF==0.10.2',
'Flask-Login==0.3.2',
'Flask-OAuthlib==0.9.3',
'Flask-SQLAlchemy==2.1',
'Flask-WTF==0.12',
'PyYAML==3.11',
'bcrypt==1.0.2',
'beautifulsoup4==4.3.2',
'click==3.3',
'dulwich==0.14.1',
'flask-ldap-login==0.3.0',
'gevent==1.0.2',
'ghdiff==0.4',
'gittle==0.5.0',
'gunicorn==19.3',
'itsdangerous==0.24',
'markdown2==2.3.1',