From e3ae3a1ccb4743d591ca3e10890591672bf5c8cf Mon Sep 17 00:00:00 2001
From: Chase Sterling
Date: Thu, 7 Jul 2016 02:49:52 -0400
Subject: [PATCH 01/24] Lazy load history view with pagination. refs #149
---
realms/modules/wiki/models.py | 103 +++++++++++++++++++++--------
realms/modules/wiki/views.py | 17 ++++-
realms/templates/wiki/history.html | 16 +++++
3 files changed, 107 insertions(+), 29 deletions(-)
diff --git a/realms/modules/wiki/models.py b/realms/modules/wiki/models.py
index 3c3a160..7531d1d 100644
--- a/realms/modules/wiki/models.py
+++ b/realms/modules/wiki/models.py
@@ -1,3 +1,5 @@
+import collections
+import itertools
import os
import posixpath
import re
@@ -98,37 +100,14 @@ class WikiPage(object):
cache.set(cache_key, info)
return info
- def get_history(self, limit=100):
+ @property
+ def history(self):
"""Get page history.
- :param limit: Limit history size.
:return: list -- List of dicts
"""
- 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)
- for entry in walker:
- change_type = None
- for change in entry.changes():
- if change.old.path == self.filename:
- 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))
-
- return versions
+ return PageHistory(self, self._cache_key('history'))
@property
def partials(self):
@@ -319,3 +298,75 @@ class WikiPage(object):
# We'll get a KeyError if self.sha isn't in the repo, or if self.filename isn't in the tree of our commit
return False
return True
+
+
+class PageHistory(collections.Sequence):
+ """Acts like a list, but dynamically loads and caches history revisions as requested."""
+ def __init__(self, page, cache_key):
+ self.page = page
+ self.cache_key = cache_key
+ self._store = cache.get(cache_key) or []
+ if not self._store:
+ self._iter_rest = self._get_rest()
+ elif self._store[-1] == 'TAIL':
+ self._iter_rest = None
+ else:
+ self._iter_rest = self._get_rest(self._store[-1]['sha'])
+
+ def __iter__(self):
+ # Iterate over the revisions already cached
+ for r in self._store:
+ if r == 'TAIL':
+ return
+ yield r
+ # Iterate over the revisions yet to be discovered
+ if self._iter_rest:
+ try:
+ for r in self._iter_rest:
+ self._store.append(r)
+ yield r
+ self._store.append('TAIL')
+ finally:
+ # Make sure we cache the results whether or not the iteration was completed
+ cache.set(self.cache_key, self._store)
+
+ def _get_rest(self, start_sha=None):
+ if not len(self.page.wiki.repo.open_index()):
+ # Index is empty, no commits
+ return
+ walker = iter(self.page.wiki.repo.get_walker(paths=[self.page.filename], include=start_sha, follow=True))
+ if start_sha:
+ # If we are not starting from HEAD, we already have the start commit
+ print(next(walker))
+ filename = self.page.filename
+ for entry in walker:
+ change_type = None
+ for change in entry.changes():
+ if change.new.path == filename:
+ filename = change.old.path
+ change_type = change.type
+ break
+
+ 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)
+ yield r
+
+ def __getitem__(self, index):
+ if isinstance(index, slice):
+ return list(itertools.islice(self, index.start, index.stop, index.step))
+ else:
+ try:
+ return next(itertools.islice(self, index, index+1))
+ except StopIteration:
+ raise IndexError
+
+ def __len__(self):
+ if not self._store or self._store[-1] != 'TAIL':
+ # Force generation of all revisions
+ list(self)
+ return len(self._store) - 1 # Don't include the TAIL sentinel
diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py
index 40571b2..d850ff5 100644
--- a/realms/modules/wiki/views.py
+++ b/realms/modules/wiki/views.py
@@ -64,11 +64,22 @@ def revert():
def history(name):
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
return current_app.login_manager.unauthorized()
-
+ ITEMS_PER_PAGE = 15
+ page = int(request.args.get('page', 1))
+ start_index = (page - 1) * ITEMS_PER_PAGE
hist = g.current_wiki.get_page(name).get_history()
- for item in hist:
+ # Grab one extra item to see if there is a next page
+ items = hist[start_index:start_index+ITEMS_PER_PAGE+1]
+ more = False
+ if len(items) > ITEMS_PER_PAGE:
+ more = True
+ items.pop()
+ if page > 1 and not items:
+ abort(404)
+ for item in items:
item['gravatar'] = gravatar_url(item['author_email'])
- return render_template('wiki/history.html', name=name, history=hist)
+ return render_template('wiki/history.html', name=name, history=items,
+ pagination={'page': page, 'more': more})
@blueprint.route("/_edit/")
diff --git a/realms/templates/wiki/history.html b/realms/templates/wiki/history.html
index bdc0cc7..c5c253d 100644
--- a/realms/templates/wiki/history.html
+++ b/realms/templates/wiki/history.html
@@ -28,6 +28,22 @@
{% endfor %}
+
+ {% if pagination.page > 1 or pagination.more %}
+
+ {% if pagination.page > 1 %}
+
<
+ {% else %}
+ <
+ {% endif %}
+ {{ pagination.page }}
+ {% if pagination.more %}
+
>
+ {% else %}
+ >
+ {% endif %}
+
+ {% endif %}
Compare Revisions
From 74452fe58f1e79effa47c06dcee49b04397c717f Mon Sep 17 00:00:00 2001
From: Chase Sterling
Date: Thu, 7 Jul 2016 02:50:45 -0400
Subject: [PATCH 02/24] Add ability for 404 page to show error description.
---
realms/modules/wiki/views.py | 2 +-
realms/templates/errors/404.html | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py
index d850ff5..632b643 100644
--- a/realms/modules/wiki/views.py
+++ b/realms/modules/wiki/views.py
@@ -75,7 +75,7 @@ def history(name):
more = True
items.pop()
if page > 1 and not items:
- abort(404)
+ abort(404, 'Page is past end of history.')
for item in items:
item['gravatar'] = gravatar_url(item['author_email'])
return render_template('wiki/history.html', name=name, history=items,
diff --git a/realms/templates/errors/404.html b/realms/templates/errors/404.html
index 66bfd47..11e4044 100644
--- a/realms/templates/errors/404.html
+++ b/realms/templates/errors/404.html
@@ -2,5 +2,8 @@
{% block body %}
Page Not Found
+ {% if error is defined %}
+ {{ error.description }}
+ {% endif %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
From 35471d3c3fe1984c6a470f5e62f690da8fb6c406 Mon Sep 17 00:00:00 2001
From: Chase Sterling
Date: Thu, 7 Jul 2016 17:30:49 -0400
Subject: [PATCH 03/24] Don't add a separate class to iterate history
---
realms/modules/wiki/models.py | 122 +++++++++++++---------------------
1 file changed, 47 insertions(+), 75 deletions(-)
diff --git a/realms/modules/wiki/models.py b/realms/modules/wiki/models.py
index 7531d1d..a73dbaa 100644
--- a/realms/modules/wiki/models.py
+++ b/realms/modules/wiki/models.py
@@ -96,7 +96,7 @@ class WikiPage(object):
if cached:
return cached
- info = self.get_history(limit=1)[0]
+ info = next(self.history)
cache.set(cache_key, info)
return info
@@ -104,10 +104,54 @@ class WikiPage(object):
def history(self):
"""Get page history.
- :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.
+
+ :return: iter -- Iterator over dicts
"""
- return PageHistory(self, self._cache_key('history'))
+ cache_complete = False
+ cached_revs = cache.get(self._cache_key('history')) or []
+ start_sha = None
+ if cached_revs:
+ if cached_revs[-1] == 'TAIL':
+ del cached_revs[-1]
+ cache_complete = True
+ else:
+ start_sha = cached_revs[-1]['sha']
+ for rev in cached_revs:
+ yield rev
+ if cache_complete:
+ return
+ if not len(self.wiki.repo.open_index()):
+ # Index is empty, no commits
+ return
+ walker = iter(self.wiki.repo.get_walker(paths=[self.filename], include=start_sha, follow=True))
+ if start_sha:
+ # If we are not starting from HEAD, we already have the start commit
+ next(walker)
+ filename = self.filename
+ try:
+ for entry in walker:
+ change_type = None
+ for change in entry.changes():
+ if change.new.path == filename:
+ filename = change.old.path
+ change_type = change.type
+ break
+
+ 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)
+ cached_revs.append(r)
+ yield r
+ cached_revs.append('TAIL')
+ finally:
+ cache.set(self._cache_key('history'), cached_revs)
@property
def partials(self):
@@ -298,75 +342,3 @@ class WikiPage(object):
# We'll get a KeyError if self.sha isn't in the repo, or if self.filename isn't in the tree of our commit
return False
return True
-
-
-class PageHistory(collections.Sequence):
- """Acts like a list, but dynamically loads and caches history revisions as requested."""
- def __init__(self, page, cache_key):
- self.page = page
- self.cache_key = cache_key
- self._store = cache.get(cache_key) or []
- if not self._store:
- self._iter_rest = self._get_rest()
- elif self._store[-1] == 'TAIL':
- self._iter_rest = None
- else:
- self._iter_rest = self._get_rest(self._store[-1]['sha'])
-
- def __iter__(self):
- # Iterate over the revisions already cached
- for r in self._store:
- if r == 'TAIL':
- return
- yield r
- # Iterate over the revisions yet to be discovered
- if self._iter_rest:
- try:
- for r in self._iter_rest:
- self._store.append(r)
- yield r
- self._store.append('TAIL')
- finally:
- # Make sure we cache the results whether or not the iteration was completed
- cache.set(self.cache_key, self._store)
-
- def _get_rest(self, start_sha=None):
- if not len(self.page.wiki.repo.open_index()):
- # Index is empty, no commits
- return
- walker = iter(self.page.wiki.repo.get_walker(paths=[self.page.filename], include=start_sha, follow=True))
- if start_sha:
- # If we are not starting from HEAD, we already have the start commit
- print(next(walker))
- filename = self.page.filename
- for entry in walker:
- change_type = None
- for change in entry.changes():
- if change.new.path == filename:
- filename = change.old.path
- change_type = change.type
- break
-
- 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)
- yield r
-
- def __getitem__(self, index):
- if isinstance(index, slice):
- return list(itertools.islice(self, index.start, index.stop, index.step))
- else:
- try:
- return next(itertools.islice(self, index, index+1))
- except StopIteration:
- raise IndexError
-
- def __len__(self):
- if not self._store or self._store[-1] != 'TAIL':
- # Force generation of all revisions
- list(self)
- return len(self._store) - 1 # Don't include the TAIL sentinel
From 242e317e6564e5eca1de67a995b305704d381ab3 Mon Sep 17 00:00:00 2001
From: Chase Sterling
Date: Thu, 7 Jul 2016 17:39:37 -0400
Subject: [PATCH 04/24] Make the history view pagination prettier.
---
realms/modules/wiki/views.py | 10 ++++----
realms/templates/wiki/history.html | 38 +++++++++++++++++-------------
2 files changed, 27 insertions(+), 21 deletions(-)
diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py
index 632b643..a50df83 100644
--- a/realms/modules/wiki/views.py
+++ b/realms/modules/wiki/views.py
@@ -64,14 +64,14 @@ def revert():
def history(name):
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
return current_app.login_manager.unauthorized()
- ITEMS_PER_PAGE = 15
+ items_per_page = 15
page = int(request.args.get('page', 1))
- start_index = (page - 1) * ITEMS_PER_PAGE
- hist = g.current_wiki.get_page(name).get_history()
+ start_index = (page - 1) * items_per_page
+ hist = g.current_wiki.get_page(name).history
# Grab one extra item to see if there is a next page
- items = hist[start_index:start_index+ITEMS_PER_PAGE+1]
+ items = list(itertools.islice(hist, start_index, start_index+items_per_page+1))
more = False
- if len(items) > ITEMS_PER_PAGE:
+ if len(items) > items_per_page:
more = True
items.pop()
if page > 1 and not items:
diff --git a/realms/templates/wiki/history.html b/realms/templates/wiki/history.html
index c5c253d..c80ccd5 100644
--- a/realms/templates/wiki/history.html
+++ b/realms/templates/wiki/history.html
@@ -15,6 +15,27 @@
Date |
+ {% if pagination.page > 1 or pagination.more %}
+
+
+
+ |
+
+ {% endif %}
+
{% for h in history %}
@@ -27,23 +48,8 @@
| {{ h.time|datetime }} |
{% endfor %}
+
-
- {% if pagination.page > 1 or pagination.more %}
-
- {% if pagination.page > 1 %}
-
<
- {% else %}
- <
- {% endif %}
- {{ pagination.page }}
- {% if pagination.more %}
-
>
- {% else %}
- >
- {% endif %}
-
- {% endif %}
Compare Revisions
From 0bcfaba807b7888e335e20b0687ae4ddf298625c Mon Sep 17 00:00:00 2001
From: Chase Sterling
Date: Thu, 7 Jul 2016 17:54:15 -0400
Subject: [PATCH 05/24] Remove WikiPage.info refs #148
---
realms/modules/search/commands.py | 6 ++++--
realms/modules/wiki/models.py | 11 -----------
realms/modules/wiki/tests.py | 2 +-
realms/modules/wiki/views.py | 3 ++-
4 files changed, 7 insertions(+), 15 deletions(-)
diff --git a/realms/modules/search/commands.py b/realms/modules/search/commands.py
index 43507df..c11edb8 100644
--- a/realms/modules/search/commands.py
+++ b/realms/modules/search/commands.py
@@ -30,10 +30,12 @@ def rebuild_index():
continue
name = filename_to_cname(page['path'])
# TODO add email?
+ # TODO I have concens about indexing the commit info from latest revision, see #148
+ info = next(page.history)
body = dict(name=name,
content=page.data,
- message=page.info['message'],
- username=page.info['author'],
+ message=info['message'],
+ username=info['author'],
updated_on=entry['mtime'],
created_on=entry['ctime'])
search.index_wiki(name, body)
diff --git a/realms/modules/wiki/models.py b/realms/modules/wiki/models.py
index a73dbaa..aa276a3 100644
--- a/realms/modules/wiki/models.py
+++ b/realms/modules/wiki/models.py
@@ -89,17 +89,6 @@ class WikiPage(object):
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 = next(self.history)
- cache.set(cache_key, info)
- return info
-
@property
def history(self):
"""Get page history.
diff --git a/realms/modules/wiki/tests.py b/realms/modules/wiki/tests.py
index 4e9c8eb..565f4f1 100644
--- a/realms/modules/wiki/tests.py
+++ b/realms/modules/wiki/tests.py
@@ -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):
diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py
index a50df83..f7c474b 100644
--- a/realms/modules/wiki/views.py
+++ b/realms/modules/wiki/views.py
@@ -96,7 +96,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)
From eafff2ae439adb25e368b02c6f5e5bad1ec95a5c Mon Sep 17 00:00:00 2001
From: Chase Sterling
Date: Fri, 8 Jul 2016 21:30:20 -0400
Subject: [PATCH 06/24] Cause an error when trying to hook invalid HookMixin
method
---
realms/lib/hook.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/realms/lib/hook.py b/realms/lib/hook.py
index 61d2885..f4e012a 100644
--- a/realms/lib/hook.py
+++ b/realms/lib/hook.py
@@ -25,9 +25,12 @@ class HookMixinMeta(type):
def __new__(cls, name, bases, attrs):
super_new = super(HookMixinMeta, cls).__new__
+ hookable = []
for key, value in attrs.items():
if callable(value):
attrs[key] = hook_func(key, value)
+ hookable.append(key)
+ attrs['_hookable'] = hookable
return super_new(cls, name, bases, attrs)
@@ -37,9 +40,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 +53,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
From b3f6c311b33dfbed658f2fd9271066cc45b69be1 Mon Sep 17 00:00:00 2001
From: Chase Sterling
Date: Sat, 9 Jul 2016 15:50:07 -0400
Subject: [PATCH 07/24] Switch page history view to use jquery datatable
---
realms/modules/wiki/models.py | 14 ++++++++
realms/modules/wiki/views.py | 38 +++++++++++++--------
realms/templates/wiki/history.html | 55 +++++++++++-------------------
3 files changed, 57 insertions(+), 50 deletions(-)
diff --git a/realms/modules/wiki/models.py b/realms/modules/wiki/models.py
index aa276a3..3003978 100644
--- a/realms/modules/wiki/models.py
+++ b/realms/modules/wiki/models.py
@@ -142,6 +142,20 @@ class WikiPage(object):
finally:
cache.set(self._cache_key('history'), cached_revs)
+ @property
+ def history_cache(self):
+ """Get info about the history cache.
+
+ :return: tuple -- (cached items, cache complete?)
+ """
+ cache_complete = False
+ cached_revs = cache.get(self._cache_key('history')) or []
+ if cached_revs:
+ if cached_revs[-1] == 'TAIL':
+ del cached_revs[-1]
+ cache_complete = True
+ return len(cached_revs), cache_complete
+
@property
def partials(self):
data = self.data
diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py
index f7c474b..0dbc304 100644
--- a/realms/modules/wiki/views.py
+++ b/realms/modules/wiki/views.py
@@ -64,22 +64,32 @@ def revert():
def history(name):
if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
return current_app.login_manager.unauthorized()
- items_per_page = 15
- page = int(request.args.get('page', 1))
- start_index = (page - 1) * items_per_page
- hist = g.current_wiki.get_page(name).history
- # Grab one extra item to see if there is a next page
- items = list(itertools.islice(hist, start_index, start_index+items_per_page+1))
- more = False
- if len(items) > items_per_page:
- more = True
- items.pop()
- if page > 1 and not items:
- abort(404, 'Page is past end of history.')
+ return render_template('wiki/history.html', name=name)
+
+
+@blueprint.route("/_history_data/")
+def history_data(name):
+ """Ajax provider for paginated history data."""
+ if current_app.config.get('PRIVATE_WIKI') and current_user.is_anonymous():
+ return current_app.login_manager.unauthorized()
+ draw = int(request.args.get('draw', 0))
+ start = int(request.args.get('start', 0))
+ length = int(request.args.get('length', 10))
+ page = g.current_wiki.get_page(name)
+ items = list(itertools.islice(page.history, start, start + length))
for item in items:
item['gravatar'] = gravatar_url(item['author_email'])
- return render_template('wiki/history.html', name=name, history=items,
- pagination={'page': page, 'more': more})
+ 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
+ }
+
@blueprint.route("/_edit/")
diff --git a/realms/templates/wiki/history.html b/realms/templates/wiki/history.html
index c80ccd5..8595554 100644
--- a/realms/templates/wiki/history.html
+++ b/realms/templates/wiki/history.html
@@ -6,49 +6,14 @@
Compare Revisions
-