Merge branch 'master' into oauth_redirect
# Conflicts: # realms/modules/auth/views.py
This commit is contained in:
commit
2ce6c2d314
|
@ -6,7 +6,7 @@ RUN apt-get install -y software-properties-common python-software-properties
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
RUN ln -s /usr/bin/nodejs /usr/bin/node && \
|
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
|
RUN useradd -ms /bin/bash wiki
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ ENV GEVENT_RESOLVER=ares
|
||||||
ENV REALMS_ENV=docker
|
ENV REALMS_ENV=docker
|
||||||
ENV REALMS_WIKI_PATH=/home/wiki/data/repo
|
ENV REALMS_WIKI_PATH=/home/wiki/data/repo
|
||||||
ENV REALMS_DB_URI='sqlite:////home/wiki/data/wiki.db'
|
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
|
RUN mkdir /home/wiki/data && touch /home/wiki/data/.a
|
||||||
VOLUME /home/wiki/data
|
VOLUME /home/wiki/data
|
||||||
|
@ -45,4 +44,3 @@ CMD . .venv/bin/activate && \
|
||||||
--bind 0.0.0.0:5000 \
|
--bind 0.0.0.0:5000 \
|
||||||
--chdir /home/wiki/realms-wiki \
|
--chdir /home/wiki/realms-wiki \
|
||||||
'realms:create_app()'
|
'realms:create_app()'
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import functools
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Set default encoding to UTF-8
|
# Set default encoding to UTF-8
|
||||||
|
@ -12,10 +13,10 @@ import httplib
|
||||||
import traceback
|
import traceback
|
||||||
import click
|
import click
|
||||||
from flask import Flask, request, render_template, url_for, redirect, g
|
from flask import Flask, request, render_template, url_for, redirect, g
|
||||||
from flask.ext.cache import Cache
|
from flask_cache import Cache
|
||||||
from flask.ext.login import LoginManager, current_user
|
from flask_login import LoginManager, current_user
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask.ext.assets import Environment, Bundle
|
from flask_assets import Environment, Bundle
|
||||||
from flask_ldap_login import LDAPLoginManager
|
from flask_ldap_login import LDAPLoginManager
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
|
@ -75,7 +76,9 @@ class Application(Flask):
|
||||||
|
|
||||||
# Click
|
# Click
|
||||||
if hasattr(sources, 'commands'):
|
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
|
# Hooks
|
||||||
if hasattr(sources, 'hooks'):
|
if hasattr(sources, 'hooks'):
|
||||||
|
@ -177,9 +180,7 @@ def create_app(config=None):
|
||||||
|
|
||||||
db.Model = declarative_base(metaclass=HookModelMeta, cls=HookMixin)
|
db.Model = declarative_base(metaclass=HookModelMeta, cls=HookMixin)
|
||||||
|
|
||||||
for status_code in httplib.responses:
|
app.register_error_handler(HTTPException, error_handler)
|
||||||
if status_code >= 400:
|
|
||||||
app.register_error_handler(status_code, error_handler)
|
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def init_g():
|
def init_g():
|
||||||
|
@ -287,9 +288,8 @@ class AppGroup(click.Group):
|
||||||
kwargs.setdefault('cls', AppGroup)
|
kwargs.setdefault('cls', AppGroup)
|
||||||
return click.Group.group(self, *args, **kwargs)
|
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
|
|
||||||
|
|
|
@ -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 realms.lib.util import random_string, in_virtualenv, green, yellow, red
|
||||||
from subprocess import call, Popen
|
from subprocess import call, Popen
|
||||||
from multiprocessing import cpu_count
|
from multiprocessing import cpu_count
|
||||||
|
@ -425,7 +425,7 @@ def clear_cache():
|
||||||
def test():
|
def test():
|
||||||
""" Run tests
|
""" 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]):
|
if not module_exists(mod[0]):
|
||||||
pip.main(['install', mod[1]])
|
pip.main(['install', mod[1]])
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class Config(object):
|
||||||
BASE_URL = 'http://localhost'
|
BASE_URL = 'http://localhost'
|
||||||
SITE_TITLE = "Realms"
|
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 = 'sqlite:////tmp/wiki.db'
|
||||||
# DB_URI = 'mysql://scott:tiger@localhost/mydatabase'
|
# DB_URI = 'mysql://scott:tiger@localhost/mydatabase'
|
||||||
# DB_URI = 'postgresql://scott:tiger@localhost/mydatabase'
|
# DB_URI = 'postgresql://scott:tiger@localhost/mydatabase'
|
||||||
|
@ -139,6 +139,7 @@ class Config(object):
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
ASSETS_DEBUG = False
|
ASSETS_DEBUG = False
|
||||||
SQLALCHEMY_ECHO = False
|
SQLALCHEMY_ECHO = False
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
MODULES = ['wiki', 'search', 'auth']
|
MODULES = ['wiki', 'search', 'auth']
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from flask.ext.sqlalchemy import DeclarativeMeta
|
from flask_sqlalchemy import DeclarativeMeta
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
@ -25,9 +25,15 @@ class HookMixinMeta(type):
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(cls, name, bases, attrs):
|
||||||
super_new = super(HookMixinMeta, cls).__new__
|
super_new = super(HookMixinMeta, cls).__new__
|
||||||
|
|
||||||
|
hookable = []
|
||||||
for key, value in attrs.items():
|
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):
|
if callable(value):
|
||||||
attrs[key] = hook_func(key, value)
|
attrs[key] = hook_func(key, value)
|
||||||
|
hookable.append(key)
|
||||||
|
attrs['_hookable'] = hookable
|
||||||
|
|
||||||
return super_new(cls, name, bases, attrs)
|
return super_new(cls, name, bases, attrs)
|
||||||
|
|
||||||
|
@ -37,9 +43,12 @@ class HookMixin(object):
|
||||||
|
|
||||||
_pre_hooks = {}
|
_pre_hooks = {}
|
||||||
_post_hooks = {}
|
_post_hooks = {}
|
||||||
|
_hookable = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def after(cls, method_name):
|
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):
|
def outer(f, *args, **kwargs):
|
||||||
cls._post_hooks.setdefault(method_name, []).append((f, args, kwargs))
|
cls._post_hooks.setdefault(method_name, []).append((f, args, kwargs))
|
||||||
return f
|
return f
|
||||||
|
@ -47,6 +56,8 @@ class HookMixin(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def before(cls, method_name):
|
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):
|
def outer(f, *args, **kwargs):
|
||||||
cls._pre_hooks.setdefault(method_name, []).append((f, args, kwargs))
|
cls._pre_hooks.setdefault(method_name, []).append((f, args, kwargs))
|
||||||
return f
|
return f
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from flask.ext.testing import TestCase
|
from flask_testing import TestCase
|
||||||
from realms.lib.util import random_string
|
from realms.lib.util import random_string
|
||||||
from realms import create_app
|
from realms import create_app
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from realms import login_manager
|
from realms import login_manager
|
||||||
from flask import request, flash, redirect
|
from flask import request, flash, redirect
|
||||||
from flask.ext.login import login_url
|
from flask_login import login_url
|
||||||
|
|
||||||
modules = set()
|
modules = set()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
from flask.ext.login import login_user
|
from flask_login import login_user
|
||||||
from realms import ldap
|
from realms import ldap
|
||||||
from flask_ldap_login import LDAPLoginForm
|
from flask_ldap_login import LDAPLoginForm
|
||||||
from ..models import BaseUser
|
from ..models import BaseUser
|
||||||
|
|
|
@ -2,10 +2,10 @@ import click
|
||||||
from realms.lib.util import random_string
|
from realms.lib.util import random_string
|
||||||
from realms.modules.auth.local.models import User
|
from realms.modules.auth.local.models import User
|
||||||
from realms.lib.util import green, red, yellow
|
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():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from flask import current_app, render_template
|
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 import login_manager, db
|
||||||
from realms.lib.model import Model
|
from realms.lib.model import Model
|
||||||
from ..models import BaseUser
|
from ..models import BaseUser
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from flask import current_app
|
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 import login_manager
|
||||||
from realms.lib.util import gravatar_url
|
from realms.lib.util import gravatar_url
|
||||||
from itsdangerous import URLSafeSerializer, BadSignature
|
from itsdangerous import URLSafeSerializer, BadSignature
|
||||||
|
|
|
@ -40,7 +40,7 @@ providers = {
|
||||||
'field_map': {
|
'field_map': {
|
||||||
'id': 'id',
|
'id': 'id',
|
||||||
'username': 'login',
|
'username': 'login',
|
||||||
'email': 'email'
|
'email': lambda(data): data.get('email') or data['login'] + '@users.noreply.github.com'
|
||||||
},
|
},
|
||||||
'token_name': 'access_token'
|
'token_name': 'access_token'
|
||||||
},
|
},
|
||||||
|
@ -118,6 +118,8 @@ class User(BaseUser):
|
||||||
def get_value(d, key):
|
def get_value(d, key):
|
||||||
if isinstance(key, basestring):
|
if isinstance(key, basestring):
|
||||||
return d.get(key)
|
return d.get(key)
|
||||||
|
elif callable(key):
|
||||||
|
return key(d)
|
||||||
# key should be list here
|
# key should be list here
|
||||||
val = d.get(key.pop(0))
|
val = d.get(key.pop(0))
|
||||||
if len(key) == 0:
|
if len(key) == 0:
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for, session
|
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
|
from realms.modules.auth.models import Auth
|
||||||
|
|
||||||
blueprint = Blueprint('auth', __name__)
|
blueprint = Blueprint('auth', __name__, template_folder='templates')
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/login", methods=['GET', 'POST'])
|
@blueprint.route("/login", methods=['GET', 'POST'])
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import click
|
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.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():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -13,27 +13,25 @@ def cli():
|
||||||
def rebuild_index():
|
def rebuild_index():
|
||||||
""" Rebuild search index
|
""" Rebuild search index
|
||||||
"""
|
"""
|
||||||
app = create_app()
|
if current_app.config.get('SEARCH_TYPE') == 'simple':
|
||||||
|
|
||||||
if app.config.get('SEARCH_TYPE') == 'simple':
|
|
||||||
click.echo("Search type is simple, try using elasticsearch.")
|
click.echo("Search type is simple, try using elasticsearch.")
|
||||||
return
|
return
|
||||||
|
|
||||||
with app.app_context():
|
# Wiki
|
||||||
# Wiki
|
search.delete_index('wiki')
|
||||||
search.delete_index('wiki')
|
wiki = Wiki(current_app.config['WIKI_PATH'])
|
||||||
wiki = Wiki(app.config['WIKI_PATH'])
|
for entry in wiki.get_index():
|
||||||
for entry in wiki.get_index():
|
page = wiki.get_page(entry['name'])
|
||||||
page = wiki.get_page(entry['name'])
|
if not page:
|
||||||
if not page:
|
# Some non-markdown files may have issues
|
||||||
# Some non-markdown files may have issues
|
continue
|
||||||
continue
|
# TODO add email?
|
||||||
name = filename_to_cname(page['path'])
|
# TODO I have concens about indexing the commit info from latest revision, see #148
|
||||||
# TODO add email?
|
info = next(page.history)
|
||||||
body = dict(name=name,
|
body = dict(name=page.name,
|
||||||
content=page.data,
|
content=page.data,
|
||||||
message=page.info['message'],
|
message=info['message'],
|
||||||
username=page.info['author'],
|
username=info['author'],
|
||||||
updated_on=entry['mtime'],
|
updated_on=entry['mtime'],
|
||||||
created_on=entry['ctime'])
|
created_on=entry['ctime'])
|
||||||
search.index_wiki(name, body)
|
search.index_wiki(page.name, body)
|
||||||
|
|
|
@ -14,7 +14,7 @@ def whoosh(app):
|
||||||
|
|
||||||
|
|
||||||
def elasticsearch(app):
|
def elasticsearch(app):
|
||||||
from flask.ext.elastic import Elastic
|
from flask_elastic import Elastic
|
||||||
fields = app.config.get('ELASTICSEARCH_FIELDS')
|
fields = app.config.get('ELASTICSEARCH_FIELDS')
|
||||||
return ElasticSearch(Elastic(app), fields)
|
return ElasticSearch(Elastic(app), fields)
|
||||||
|
|
||||||
|
|
|
@ -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
|
from realms import search as search_engine
|
||||||
|
|
||||||
blueprint = Blueprint('search', __name__)
|
blueprint = Blueprint('search', __name__, template_folder='templates')
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/_search')
|
@blueprint.route('/_search')
|
||||||
def 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'))
|
results = search_engine.wiki(request.args.get('q'))
|
||||||
return render_template('search/search.html', results=results)
|
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/mode-markdown.js',
|
||||||
'vendor/ace-builds/src/ext-keybinding_menu.js',
|
'vendor/ace-builds/src/ext-keybinding_menu.js',
|
||||||
'vendor/keymaster/keymaster.js',
|
'vendor/keymaster/keymaster.js',
|
||||||
'js/aced.js')
|
'wiki/js/aced.js')
|
||||||
|
|
|
@ -2,11 +2,9 @@ import os
|
||||||
import posixpath
|
import posixpath
|
||||||
import re
|
import re
|
||||||
import ghdiff
|
import ghdiff
|
||||||
import gittle.utils
|
|
||||||
import yaml
|
import yaml
|
||||||
from gittle import Gittle
|
|
||||||
from dulwich.object_store import tree_lookup_path
|
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.lib.util import cname_to_filename, filename_to_cname
|
||||||
from realms import cache
|
from realms import cache
|
||||||
from realms.lib.hook import HookMixin
|
from realms.lib.hook import HookMixin
|
||||||
|
@ -23,17 +21,13 @@ class Wiki(HookMixin):
|
||||||
default_committer_name = 'Anon'
|
default_committer_name = 'Anon'
|
||||||
default_committer_email = 'anon@anon.anon'
|
default_committer_email = 'anon@anon.anon'
|
||||||
index_page = 'home'
|
index_page = 'home'
|
||||||
gittle = None
|
|
||||||
repo = None
|
repo = None
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
try:
|
try:
|
||||||
self.gittle = Gittle(path)
|
self.repo = Repo(path)
|
||||||
except NotGitRepository:
|
except NotGitRepository:
|
||||||
self.gittle = Gittle.init(path)
|
self.repo = Repo.init(path, mkdir=True)
|
||||||
|
|
||||||
# Dulwich repo
|
|
||||||
self.repo = self.gittle.repo
|
|
||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
|
|
||||||
|
@ -46,20 +40,20 @@ class Wiki(HookMixin):
|
||||||
:param name: Committer name
|
:param name: Committer name
|
||||||
:param email: Committer email
|
:param email: Committer email
|
||||||
:param message: Commit message
|
: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:
|
:return:
|
||||||
"""
|
"""
|
||||||
# Dulwich and gittle seem to want us to encode ourselves at the moment. see #152
|
|
||||||
if isinstance(name, unicode):
|
if isinstance(name, unicode):
|
||||||
name = name.encode('utf-8')
|
name = name.encode('utf-8')
|
||||||
if isinstance(email, unicode):
|
if isinstance(email, unicode):
|
||||||
email = email.encode('utf-8')
|
email = email.encode('utf-8')
|
||||||
if isinstance(message, unicode):
|
if isinstance(message, unicode):
|
||||||
message = message.encode('utf-8')
|
message = message.encode('utf-8')
|
||||||
return self.gittle.commit(name=name,
|
author = committer = "%s <%s>" % (name, email)
|
||||||
email=email,
|
self.repo.stage(files)
|
||||||
message=message,
|
return self.repo.do_commit(message=message,
|
||||||
files=files)
|
committer=committer,
|
||||||
|
author=author)
|
||||||
|
|
||||||
def get_page(self, name, sha='HEAD'):
|
def get_page(self, name, sha='HEAD'):
|
||||||
"""Get page data, partials, commit info.
|
"""Get page data, partials, commit info.
|
||||||
|
@ -104,52 +98,93 @@ class WikiPage(HookMixin):
|
||||||
if cached:
|
if cached:
|
||||||
return 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)
|
cache.set(cache_key, data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def info(self):
|
def history(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):
|
|
||||||
"""Get page history.
|
"""Get page history.
|
||||||
|
|
||||||
:param limit: Limit history size.
|
History can take a long time to generate for repositories with many commits.
|
||||||
:return: list -- List of dicts
|
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()):
|
if not len(self.wiki.repo.open_index()):
|
||||||
# Index is empty, no commits
|
# Index is empty, no commits
|
||||||
return []
|
return
|
||||||
|
filename = filename or self.filename
|
||||||
versions = []
|
walker = iter(self.wiki.repo.get_walker(paths=[filename],
|
||||||
|
include=start_sha,
|
||||||
walker = self.wiki.repo.get_walker(paths=[self.filename], max_entries=limit)
|
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:
|
for entry in walker:
|
||||||
change_type = None
|
change_type = None
|
||||||
for change in entry.changes():
|
for change in entry.changes():
|
||||||
if change.old.path == self.filename:
|
if change.new.path == filename:
|
||||||
|
filename = change.old.path
|
||||||
change_type = change.type
|
change_type = change.type
|
||||||
elif change.new.path == self.filename:
|
break
|
||||||
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))
|
|
||||||
|
|
||||||
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
|
@property
|
||||||
def partials(self):
|
def partials(self):
|
||||||
|
@ -196,8 +231,14 @@ class WikiPage(HookMixin):
|
||||||
|
|
||||||
return username, email
|
return username, email
|
||||||
|
|
||||||
def _clear_cache(self):
|
def _invalidate_cache(self, save_history=None):
|
||||||
cache.delete_many(*(self._cache_key(p) for p in ['data', 'info']))
|
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):
|
def delete(self, username=None, email=None, message=None):
|
||||||
"""Delete page.
|
"""Delete page.
|
||||||
|
@ -211,14 +252,12 @@ class WikiPage(HookMixin):
|
||||||
if not message:
|
if not message:
|
||||||
message = "Deleted %s" % self.name
|
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))
|
os.remove(os.path.join(self.wiki.path, self.filename))
|
||||||
self.wiki.gittle.rm(self.filename)
|
|
||||||
commit = self.wiki.commit(name=username,
|
commit = self.wiki.commit(name=username,
|
||||||
email=email,
|
email=email,
|
||||||
message=message,
|
message=message,
|
||||||
files=[self.filename])
|
files=[self.filename])
|
||||||
self._clear_cache()
|
self._invalidate_cache()
|
||||||
return commit
|
return commit
|
||||||
|
|
||||||
def rename(self, new_name, username=None, email=None, message=None):
|
def rename(self, new_name, username=None, email=None, message=None):
|
||||||
|
@ -232,7 +271,7 @@ class WikiPage(HookMixin):
|
||||||
"""
|
"""
|
||||||
assert self.sha == 'HEAD'
|
assert self.sha == 'HEAD'
|
||||||
old_filename, new_filename = self.filename, cname_to_filename(new_name)
|
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
|
# old doesn't exist
|
||||||
return None
|
return None
|
||||||
elif old_filename == new_filename:
|
elif old_filename == new_filename:
|
||||||
|
@ -247,29 +286,25 @@ class WikiPage(HookMixin):
|
||||||
message = "Moved %s to %s" % (self.name, new_name)
|
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))
|
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,
|
commit = self.wiki.commit(name=username,
|
||||||
email=email,
|
email=email,
|
||||||
message=message,
|
message=message,
|
||||||
files=[old_filename, new_filename])
|
files=[old_filename, new_filename])
|
||||||
|
|
||||||
self._clear_cache()
|
old_history = cache.get(self._cache_key('history'))
|
||||||
|
self._invalidate_cache()
|
||||||
self.name = new_name
|
self.name = new_name
|
||||||
self.filename = new_filename
|
self.filename = new_filename
|
||||||
# We need to clear the cache for the new name as well as the old
|
# 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
|
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
|
"""Write page to git repo
|
||||||
|
|
||||||
:param content: Content of page.
|
:param content: Content of page.
|
||||||
:param message: Commit message.
|
:param message: Commit message.
|
||||||
:param create: Perform git add operation?
|
|
||||||
:param username: Commit Name.
|
:param username: Commit Name.
|
||||||
:param email: Commit Email.
|
:param email: Commit Email.
|
||||||
:return: Git commit sha1.
|
:return: Git commit sha1.
|
||||||
|
@ -283,9 +318,6 @@ class WikiPage(HookMixin):
|
||||||
with open(self.wiki.path + "/" + self.filename, 'w') as f:
|
with open(self.wiki.path + "/" + self.filename, 'w') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
if create:
|
|
||||||
self.wiki.gittle.add(self.filename)
|
|
||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
message = "Updated %s" % self.name
|
message = "Updated %s" % self.name
|
||||||
|
|
||||||
|
@ -296,7 +328,8 @@ class WikiPage(HookMixin):
|
||||||
message=message,
|
message=message,
|
||||||
files=[self.filename])
|
files=[self.filename])
|
||||||
|
|
||||||
self._clear_cache()
|
old_history = cache.get(self._cache_key('history'))
|
||||||
|
self._invalidate_cache(save_history=old_history)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def revert(self, commit_sha, message, username, email):
|
def revert(self, commit_sha, message, username, email):
|
||||||
|
@ -315,8 +348,7 @@ class WikiPage(HookMixin):
|
||||||
raise PageNotFound('Commit not found')
|
raise PageNotFound('Commit not found')
|
||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
commit_info = gittle.utils.git.commit_info(self.wiki.gittle[commit_sha.encode('latin-1')])
|
message = "Revert '%s' to %s" % (self.name, commit_sha[:7])
|
||||||
message = commit_info['message']
|
|
||||||
|
|
||||||
return self.write(new_page.data, message=message, username=username, email=email)
|
return self.write(new_page.data, message=message, username=username, email=email)
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
var PAGE_NAME = '{{ name }}';
|
var PAGE_NAME = '{{ name }}';
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/editor.js') }}"></script>
|
<script src="{{ url_for('wiki.static', filename='js/editor.js') }}"></script>
|
||||||
|
|
||||||
{% if partials %}
|
{% if partials %}
|
||||||
<script>
|
<script>
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if config.get('COLLABORATION') %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if config.get('COLLABORATION') == 'firepad' %}
|
{% if config.get('COLLABORATION') == 'firepad' %}
|
||||||
|
@ -34,11 +34,11 @@
|
||||||
</script>
|
</script>
|
||||||
<script src="https://cdn.firebase.com/js/client/1.0.17/firebase.js"></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="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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if config.get('COLLABORATION') == 'togetherjs' %}
|
{% 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>
|
<script src="https://togetherjs.com/togetherjs-min.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
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 %}
|
|
@ -41,7 +41,7 @@ class WikiTest(WikiBaseTest):
|
||||||
self.assert_200(rv)
|
self.assert_200(rv)
|
||||||
|
|
||||||
self.assert_context('name', 'test')
|
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')
|
eq_(self.get_context_variable('page').data, 'testing')
|
||||||
|
|
||||||
def test_history(self):
|
def test_history(self):
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import itertools
|
import itertools
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
from flask import abort, g, render_template, request, redirect, Blueprint, flash, url_for, current_app
|
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 realms.lib.util import to_canonical, remove_ext, gravatar_url
|
||||||
from .models import PageNotFound
|
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>")
|
@blueprint.route("/_commit/<sha>/<path:name>")
|
||||||
def commit(name, sha):
|
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()
|
return current_app.login_manager.unauthorized()
|
||||||
|
|
||||||
cname = to_canonical(name)
|
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>")
|
@blueprint.route(r"/_compare/<path:name>/<regex('\w+'):fsha><regex('\.{2,3}'):dots><regex('\w+'):lsha>")
|
||||||
def compare(name, fsha, dots, 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()
|
return current_app.login_manager.unauthorized()
|
||||||
|
|
||||||
diff = g.current_wiki.get_page(name, sha=lsha).compare(fsha)
|
diff = g.current_wiki.get_page(name, sha=lsha).compare(fsha)
|
||||||
|
@ -40,7 +42,7 @@ def revert():
|
||||||
commit = request.form.get('commit')
|
commit = request.form.get('commit')
|
||||||
message = request.form.get('message', "Reverting %s" % cname)
|
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
|
return dict(error=True, message="Anonymous posting not allowed"), 403
|
||||||
|
|
||||||
if cname in current_app.config.get('WIKI_LOCKED_PAGES'):
|
if cname in current_app.config.get('WIKI_LOCKED_PAGES'):
|
||||||
|
@ -62,13 +64,39 @@ def revert():
|
||||||
|
|
||||||
@blueprint.route("/_history/<path:name>")
|
@blueprint.route("/_history/<path:name>")
|
||||||
def history(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():
|
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
|
||||||
return current_app.login_manager.unauthorized()
|
return current_app.login_manager.unauthorized()
|
||||||
|
draw = int(request.args.get('draw', 0))
|
||||||
hist = g.current_wiki.get_page(name).get_history()
|
start = int(request.args.get('start', 0))
|
||||||
for item in hist:
|
length = int(request.args.get('length', 10))
|
||||||
|
page = g.current_wiki.get_page(name)
|
||||||
|
items = list(itertools.islice(page.history, start, start + length))
|
||||||
|
for item in items:
|
||||||
item['gravatar'] = gravatar_url(item['author_email'])
|
item['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>")
|
@blueprint.route("/_edit/<path:name>")
|
||||||
|
@ -85,7 +113,8 @@ def edit(name):
|
||||||
return render_template('wiki/edit.html',
|
return render_template('wiki/edit.html',
|
||||||
name=cname,
|
name=cname,
|
||||||
content=page.data,
|
content=page.data,
|
||||||
info=page.info,
|
# TODO: Remove this? See #148
|
||||||
|
info=next(page.history),
|
||||||
sha=page.sha,
|
sha=page.sha,
|
||||||
partials=page.partials)
|
partials=page.partials)
|
||||||
|
|
||||||
|
@ -137,7 +166,7 @@ def _tree_index(items, path=""):
|
||||||
@blueprint.route("/_index", defaults={"path": ""})
|
@blueprint.route("/_index", defaults={"path": ""})
|
||||||
@blueprint.route("/_index/<path:path>")
|
@blueprint.route("/_index/<path:path>")
|
||||||
def index(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()
|
return current_app.login_manager.unauthorized()
|
||||||
|
|
||||||
items = g.current_wiki.get_index()
|
items = g.current_wiki.get_index()
|
||||||
|
@ -158,7 +187,7 @@ def page_write(name):
|
||||||
if not cname:
|
if not cname:
|
||||||
return dict(error=True, message="Invalid name")
|
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
|
return dict(error=True, message="Anonymous posting not allowed"), 403
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
@ -168,7 +197,6 @@ def page_write(name):
|
||||||
|
|
||||||
sha = g.current_wiki.get_page(cname).write(request.form['content'],
|
sha = g.current_wiki.get_page(cname).write(request.form['content'],
|
||||||
message=request.form['message'],
|
message=request.form['message'],
|
||||||
create=True,
|
|
||||||
username=current_user.username,
|
username=current_user.username,
|
||||||
email=current_user.email)
|
email=current_user.email)
|
||||||
|
|
||||||
|
@ -202,7 +230,7 @@ def page_write(name):
|
||||||
@blueprint.route("/", defaults={'name': 'home'})
|
@blueprint.route("/", defaults={'name': 'home'})
|
||||||
@blueprint.route("/<path:name>")
|
@blueprint.route("/<path:name>")
|
||||||
def page(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()
|
return current_app.login_manager.unauthorized()
|
||||||
|
|
||||||
cname = to_canonical(name)
|
cname = to_canonical(name)
|
||||||
|
|
|
@ -2,5 +2,8 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<h1>Page Not Found</h1>
|
<h1>Page Not Found</h1>
|
||||||
|
{% if error is defined %}
|
||||||
|
<h4>{{ error.description }}</h4>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -58,7 +58,7 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% if current_user.is_authenticated() %}
|
{% if current_user.is_authenticated %}
|
||||||
<li class="dropdown user-avatar">
|
<li class="dropdown user-avatar">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||||
<span>
|
<span>
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
var User = {};
|
var User = {};
|
||||||
User.is_authenticated = {{ current_user.is_authenticated()|tojson }};
|
User.is_authenticated = {{ current_user.is_authenticated|tojson }};
|
||||||
{% for attr in ['username', 'email'] %}
|
{% for attr in ['username', 'email'] %}
|
||||||
User.{{ attr }} = {{ current_user[attr]|tojson }};
|
User.{{ attr }} = {{ current_user[attr]|tojson }};
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -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 %}
|
|
|
@ -1 +1 @@
|
||||||
__version__ = '0.8.0'
|
__version__ = '0.8.1'
|
||||||
|
|
14
setup.py
14
setup.py
|
@ -23,22 +23,22 @@ setup(name='realms-wiki',
|
||||||
version=__version__,
|
version=__version__,
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'Flask==0.10.1',
|
'Flask==0.11.1',
|
||||||
'Flask-Assets==0.10',
|
'Flask-Assets==0.11',
|
||||||
'Flask-Cache==0.13.1',
|
'Flask-Cache==0.13.1',
|
||||||
'Flask-Elastic==0.2',
|
'Flask-Elastic==0.2',
|
||||||
'Flask-Login==0.2.11',
|
'Flask-Login==0.3.2',
|
||||||
'Flask-OAuthlib==0.9.1',
|
'Flask-OAuthlib==0.9.3',
|
||||||
'Flask-SQLAlchemy==2.0',
|
'Flask-SQLAlchemy==2.1',
|
||||||
'Flask-WTF==0.10.2',
|
'Flask-WTF==0.12',
|
||||||
'PyYAML==3.11',
|
'PyYAML==3.11',
|
||||||
'bcrypt==1.0.2',
|
'bcrypt==1.0.2',
|
||||||
'beautifulsoup4==4.3.2',
|
'beautifulsoup4==4.3.2',
|
||||||
'click==3.3',
|
'click==3.3',
|
||||||
|
'dulwich==0.14.1',
|
||||||
'flask-ldap-login==0.3.0',
|
'flask-ldap-login==0.3.0',
|
||||||
'gevent==1.0.2',
|
'gevent==1.0.2',
|
||||||
'ghdiff==0.4',
|
'ghdiff==0.4',
|
||||||
'gittle==0.5.0',
|
|
||||||
'gunicorn==19.3',
|
'gunicorn==19.3',
|
||||||
'itsdangerous==0.24',
|
'itsdangerous==0.24',
|
||||||
'markdown2==2.3.1',
|
'markdown2==2.3.1',
|
||||||
|
|
Loading…
Reference in a new issue