Mercurial > wikked
changeset 19:884eb6c8edf0
Added "wiki history" special page:
- Added ability to get the whole repo history from the `scm`.
- Added ability to get pages changed in a revision.
- Added "wiki history" page (not complete).
- Better formatting for revision dates.
Fixed bugs with revision and page diff views:
- Using IDs instead of hashes if possible
- Using query parameters instead of path components.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Thu, 03 Jan 2013 08:00:24 -0800 |
parents | 67c150d5ed53 |
children | f34fc906ded9 |
files | wikked/resources/hg_log.style wikked/scm.py wikked/static/js/wikked/handlebars.js wikked/static/js/wikked/models.js wikked/static/js/wikked/views.js wikked/static/tpl/history-page.html wikked/static/tpl/special-changes.html wikked/static/tpl/special-pages.html wikked/views.py wikked/wiki.py |
diffstat | 10 files changed, 251 insertions(+), 81 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/resources/hg_log.style Thu Jan 03 08:00:24 2013 -0800 @@ -0,0 +1,14 @@ +start_file_adds = '' +file_add = 'A {file_add}\n' +end_file_adds = '' + +start_file_dels = '' +file_del = 'R {file_del}\n' +end_file_dels = '' + +start_file_mods = '' +file_mod = 'M {file_mod}\n' +end_file_mods = '' + +changeset = "{rev} {node} [{author}] {date|localdate}\n{desc}\n---\n{file_adds}{file_mods}{file_dels}$$$\n" +
--- a/wikked/scm.py Thu Jan 03 07:56:27 2013 -0800 +++ b/wikked/scm.py Thu Jan 03 08:00:24 2013 -0800 @@ -9,6 +9,13 @@ STATE_COMMITTED = 0 STATE_MODIFIED = 1 STATE_NEW = 2 +STATE_NAMES = ['committed', 'modified', 'new'] + +ACTION_ADD = 0 +ACTION_DELETE = 1 +ACTION_EDIT = 2 +ACTION_NAMES = ['add', 'delete', 'edit'] + class SourceControl(object): def __init__(self, root, logger=None): @@ -20,7 +27,7 @@ def getSpecialDirs(self): raise NotImplementedError() - def getHistory(self, path): + def getHistory(self, path=None): raise NotImplementedError() def getState(self, path): @@ -39,12 +46,13 @@ raise NotImplementedError() -class PageRevision(object): +class Revision(object): def __init__(self, rev_id=-1): self.rev_id = rev_id self.author = None self.timestamp = 0 self.description = None + self.files = [] @property def is_local(self): @@ -69,28 +77,34 @@ self._run('add', ignore_path) self._run('commit', ignore_path, '-m', 'Created .hgignore.') + self.log_style = os.path.join(os.path.dirname(__file__), 'resources', 'hg_log.style') + self.actions = { + 'A': ACTION_ADD, + 'R': ACTION_DELETE, + 'M': ACTION_EDIT + } + def getSpecialDirs(self): specials = [ '.hg', '.hgignore', '.hgtags' ] return [ os.path.join(self.root, d) for d in specials ] - def getHistory(self, path): - st_out = self._run('status', path) - if len(st_out) > 0 and st_out[0] == '?': - return [ PageRevision() ] + def getHistory(self, path=None): + if path is not None: + st_out = self._run('status', path) + if len(st_out) > 0 and st_out[0] == '?': + return [ Revision() ] + + log_args = [] + if path is not None: + log_args.append(path) + log_args += ['--style', self.log_style] + log_out = self._run('log', *log_args) revisions = [] - log_out = self._run('log', path, '--template', '{rev} {node} [{author}] {date|localdate} {desc}\n') - for line in log_out.splitlines(): - m = re.match(r'(\d+) ([0-9a-f]+) \[([^\]]+)\] ([^ ]+) (.*)', line) - if m is None: - raise Exception('Error parsing history from Mercurial, got: ' + line) - rev = PageRevision() - rev.rev_id = int(m.group(1)) - rev.rev_hash = m.group(2) - rev.author = m.group(3) - rev.timestamp = float(m.group(4)) - rev.description = m.group(5) - revisions.append(rev) + for group in log_out.split("$$$\n"): + if group == '': + continue + revisions.append(self._parseRevision(group)) return revisions def getState(self, path): @@ -129,7 +143,6 @@ # Create a temp file with the commit message. f, temp = tempfile.mkstemp() with os.fdopen(f, 'w') as fd: - self.logger.debug("Saving message: " + op_meta['message']) fd.write(op_meta['message']) # Commit and clean up the temp file. @@ -147,6 +160,35 @@ else: self._run('revert', '-a', '-C') + def _parseRevision(self, group): + lines = group.split("\n") + + m = re.match(r'(\d+) ([0-9a-f]+) \[([^\]]+)\] ([^ ]+)', lines[0]) + if m is None: + raise Exception('Error parsing history from Mercurial, got: ' + lines[0]) + + rev = Revision() + rev.rev_id = int(m.group(1)) + rev.rev_hash = m.group(2) + rev.author = m.group(3) + rev.timestamp = float(m.group(4)) + + i = 1 + rev.description = '' + while lines[i] != '---': + if i > 1: + rev.description += "\n" + rev.description += lines[i] + i += 1 + + rev.files = [] + for j in range(i + 1, len(lines)): + if lines[j] == '': + continue + rev.files.append({ 'path': lines[j][2:], 'action': self.actions[lines[j][0]] }) + + return rev + def _run(self, cmd, *args, **kwargs): exe = [ self.hg ] if 'norepo' not in kwargs or not kwargs['norepo']:
--- a/wikked/static/js/wikked/handlebars.js Thu Jan 03 07:56:27 2013 -0800 +++ b/wikked/static/js/wikked/handlebars.js Thu Jan 03 08:00:24 2013 -0800 @@ -44,5 +44,10 @@ } return options.inverse(this); }); + + Handlebars.registerHelper('date', function(timestamp) { + var date = new Date(timestamp * 1000); + return date.toDateString(); + }); });
--- a/wikked/static/js/wikked/models.js Thu Jan 03 07:56:27 2013 -0800 +++ b/wikked/static/js/wikked/models.js Thu Jan 03 08:00:24 2013 -0800 @@ -233,8 +233,6 @@ }); var PageRevisionModel = exports.PageRevisionModel = MasterPageModel.extend({ - urlRoot: '/api/revision/', - idAttribute: 'path_and_rev', action: 'revision', defaults: function() { return { @@ -242,19 +240,18 @@ rev: "tip" }; }, + url: function() { + return '/api/revision/' + this.get('path') + '?rev=' + this.get('rev'); + }, initialize: function() { PageRevisionModel.__super__.initialize.apply(this, arguments); - this.on('change:path', function(model, path) { - model._onChangePathOrRev(path, model.get('rev')); + this.on('change:rev', function(model, rev) { + model._onChangeRev(rev); }); - this.on('change:rev', function(model, rev) { - model._onChangePathOrRev(model.get('path'), rev); - }); - this._onChangePathOrRev(this.get('path'), this.get('rev')); + this._onChangeRev(this.get('rev')); return this; }, - _onChangePathOrRev: function(path, rev) { - this.set('path_and_rev', path + '/' + rev); + _onChangeRev: function(rev) { this.set('disp_rev', rev); if (rev.match(/[a-f0-9]{40}/)) { this.set('disp_rev', rev.substring(0, 8)); @@ -263,8 +260,6 @@ }); var PageDiffModel = exports.PageDiffModel = MasterPageModel.extend({ - urlRoot: '/api/diff/', - idAttribute: 'path_and_revs', action: 'diff', defaults: function() { return { @@ -273,29 +268,32 @@ rev2: "" }; }, + url: function() { + var apiUrl = '/api/diff/' + this.get('path') + '?rev1=' + this.get('rev1'); + if (this.get('rev2')) { + apiUrl += '&rev2=' + this.get('rev2'); + } + return apiUrl; + }, initialize: function() { PageDiffModel.__super__.initialize.apply(this, arguments); - this.on('change:path', function(model, path) { - model._onChangePathOrRevs(path, model.get('rev')); - }); this.on('change:rev1', function(model, rev1) { - model._onChangePathOrRevs(model.get('path'), rev1, model.get('rev2')); + model._onChangeRev1(rev1); }); this.on('change:rev2', function(model, rev2) { - model._onChangePathOrRevs(model.get('path'), model.get('rev1'), rev2); + model._onChangeRev2(rev2); }); - this._onChangePathOrRevs(this.get('path'), this.get('rev1'), this.get('rev2')); + this._onChangeRev1(this.get('rev1')); + this._onChangeRev2(this.get('rev2')); return this; }, - _onChangePathOrRevs: function(path, rev1, rev2) { - this.set('path_and_revs', path + '/' + rev1 + '/' + rev2); - if (!rev2) { - this.set('path_and_revs', path + '/' + rev1); - } + _onChangeRev1: function(rev1) { this.set('disp_rev1', rev1); if (rev1 !== undefined && rev1.match(/[a-f0-9]{40}/)) { this.set('disp_rev1', rev1.substring(0, 8)); } + }, + _onChangeRev2: function(rev2) { this.set('disp_rev2', rev2); if (rev2 !== undefined && rev2.match(/[a-f0-9]{40}/)) { this.set('disp_rev2', rev2.substring(0, 8)); @@ -339,11 +337,31 @@ var GenericSpecialPageModel = exports.GenericSpecialPageModel = MasterPageModel.extend({ action: 'special', - urlRoot: '/api/special', - idAttribute: 'page', initialize: function() { GenericSpecialPageModel.__super__.initialize.apply(this, arguments); this.footer.clearExtraUrls(); + }, + titleMap: { + orphans: 'Orphaned Pages', + changes: 'Wiki History' + }, + title: function() { + var key = this.get('page'); + if (key in this.titleMap) { + return this.titleMap[key]; + } + return 'Unknown'; + }, + urlMap: { + orphans: '/api/orphans', + changes: '/api/history' + }, + url: function() { + var key = this.get('page'); + if (key in this.urlMap) { + return this.urlMap[key]; + } + return false; } });
--- a/wikked/static/js/wikked/views.js Thu Jan 03 07:56:27 2013 -0800 +++ b/wikked/static/js/wikked/views.js Thu Jan 03 08:00:24 2013 -0800 @@ -20,7 +20,7 @@ PageView.__super__.initialize.apply(this, arguments); if (this.model !== undefined) { var $view = this; - this.model.on("change", function() { $view.render(); }); + this.model.on("change", function() { $view._onModelChange(); }); } return this; }, @@ -47,6 +47,9 @@ title = formatter.call(this, title); } document.title = title; + }, + _onModelChange: function() { + this.render(); } }); _.extend(PageView, Backbone.Events); @@ -279,6 +282,31 @@ _createNavigation: function(model) { model.set('show_root_link', true); return new SpecialNavigationView({ model: model }); + }, + _onModelChange: function() { + var history = this.model.get('history'); + for (var i = 0; i < history.length; ++i) { + var rev = history[i]; + rev.changes = []; + for (var j = 0; j < rev.pages.length; ++j) { + var page = rev.pages[j]; + switch (page.action) { + case 'edit': + rev.changes.push({ is_edit: true, url: page.url }); + break; + case 'add': + rev.changes.push({ is_add: true, url: page.url }); + break; + case 'delete': + rev.changes.push({ is_delete: true, url: page.url }); + break; + } + rev.pages[j] = page; + } + history[i] = rev; + } + this.model.set('history', history); + this.render(); } });
--- a/wikked/static/tpl/history-page.html Thu Jan 03 07:56:27 2013 -0800 +++ b/wikked/static/tpl/history-page.html Thu Jan 03 08:00:24 2013 -0800 @@ -16,14 +16,14 @@ <tbody> {{#eachr history}} <tr> - <td><a href="{{../url_rev}}/{{rev_hash}}">{{index}}</a></td> + <td><a href="{{../url_rev}}/{{rev_id}}">{{rev_id}}</a></td> <td>{{timestamp}}</td> <td>{{author}}</td> <td>{{description}}</td> <td> - <input type="radio" name="rev1" value="{{rev_hash}}" {{#ifeq @index to=0 }}checked="true" {{/ifeq}}/> - <input type="radio" name="rev2" value="{{rev_hash}}" {{#ifeq @index to=1 }}checked="true" {{/ifeq}}/> - <small><a href="{{../url_diffc}}/{{rev_hash}}">with previous</a></small> + <input type="radio" name="rev1" value="{{rev_id}}" {{#ifeq @index to=0 }}checked="true" {{/ifeq}}/> + <input type="radio" name="rev2" value="{{rev_id}}" {{#ifeq @index to=1 }}checked="true" {{/ifeq}}/> + <small><a href="{{../url_diffc}}/{{rev_id}}">with previous</a></small> </td> </tr> {{/eachr}}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/static/tpl/special-changes.html Thu Jan 03 08:00:24 2013 -0800 @@ -0,0 +1,40 @@ +<article class="row"> + <div class="page special span12"> + <h1>Wiki History</h1> + <p>Here are the recent changes on this wiki.</p> + <form> + <table class="table table-hover"> + <thead> + <tr> + <th>Rev.</th> + <th>Date</th> + <th>Author</th> + <th>Changes</th> + <th>Comment</th> + <th><button id="diff-revs" class="btn btn-primary">Show Diff.</button></th> + </tr> + </thead> + <tbody> + {{#eachr history}} + <tr> + <td>{{rev_id}}</td> + <td>{{date timestamp}}</td> + <td>{{author}}</td> + <td> + {{#each changes}} + <a href="/#/read/{{url}}">{{url}}</a> + {{#if is_edit}}(edit) {{/if}} + {{#if is_add}}(added) {{/if}} + {{#if is_delete}}(deleted) {{/if}} + <br/> + {{/each}} + </td> + <td>{{description}}</td> + <td></td> + </tr> + {{/eachr}} + </tbody> + </table> + </form> + </div> +</article>
--- a/wikked/static/tpl/special-pages.html Thu Jan 03 07:56:27 2013 -0800 +++ b/wikked/static/tpl/special-pages.html Thu Jan 03 08:00:24 2013 -0800 @@ -1,6 +1,8 @@ <article class="row"> <div class="page special span12"> <h1>Special Pages</h1> + <h3><a href="/#/special/changes">Recent Changes</a></h3> + <p>See all changes in the wiki.</p> <h3><a href="/#/special/orphans">Orphaned Pages</a></h3> <p>Lists pages in the wiki that have no links to them.</p> </div>
--- a/wikked/views.py Thu Jan 03 07:56:27 2013 -0800 +++ b/wikked/views.py Thu Jan 03 08:00:24 2013 -0800 @@ -30,6 +30,30 @@ abort(404) +def get_history_data(history): + hist_data = [] + for i, rev in enumerate(reversed(history)): + rev_data = { + 'index': i + 1, + 'rev_id': rev.rev_id, + 'rev_hash': rev.rev_hash, + 'author': rev.author, + 'timestamp': rev.timestamp, + 'description': rev.description, + 'pages': [] + } + for f in rev.files: + f_info = wiki.fs.getPageInfo(f['path']) + if f_info is None: + continue + rev_data['pages'].append({ + 'url': f_info['url'], + 'action': scm.ACTION_NAMES[f['action']] + }) + hist_data.append(rev_data) + return hist_data + + def make_auth_response(data): if current_user.is_authenticated(): data['auth'] = { @@ -80,8 +104,11 @@ return make_auth_response(result) -@app.route('/api/revision/<path:url>/<rev>') -def api_read_page_rev(url, rev): +@app.route('/api/revision/<path:url>') +def api_read_page_rev(url): + rev = request.args.get('rev') + if rev is None: + abort(400) page = get_page_or_404(url) page_rev = page.getRevision(rev) meta = dict(page.all_meta, rev=rev) @@ -89,13 +116,12 @@ return make_auth_response(result) -@app.route('/api/diff/<path:url>/<rev>') -def api_diff_page_change(url, rev): - return api_diff_page_revs(url, rev, None) - - -@app.route('/api/diff/<path:url>/<rev1>/<rev2>') -def api_diff_page_revs(url, rev1, rev2): +@app.route('/api/diff/<path:url>') +def api_diff_page(url): + rev1 = request.args.get('rev1') + rev2 = request.args.get('rev2') + if rev1 is None: + abort(400) page = get_page_or_404(url) diff = page.getDiff(rev1, rev2) if 'raw' not in request.args: @@ -114,13 +140,11 @@ def api_get_state(url): page = get_page_or_404(url) state = page.getState() - if state == scm.STATE_NEW: - result = 'new' - elif state == scm.STATE_MODIFIED: - result = 'modified' - elif state == scm.STATE_COMMITTED: - result = 'committed' - return make_auth_response({ 'path': url, 'meta': page.all_meta, 'state': result }) + return make_auth_response({ + 'path': url, + 'meta': page.all_meta, + 'state': scm.STATE_NAMES[state] + }) @app.route('/api/outlinks/<path:url>') @@ -204,12 +228,7 @@ pass -@app.route('/api/history') -def api_site_history(): - pass - - -@app.route('/api/special/orphans') +@app.route('/api/orphans') def api_special_orphans(): orphans = [] for page in wiki.getPages(): @@ -219,20 +238,19 @@ return make_auth_response(result) +@app.route('/api/history') +def api_site_history(): + history = wiki.getHistory() + hist_data = get_history_data(history) + result = { 'history': hist_data } + return make_auth_response(result) + + @app.route('/api/history/<path:url>') def api_page_history(url): page = get_page_or_404(url) history = page.getHistory() - hist_data = [] - for i, rev in enumerate(reversed(history)): - hist_data.append({ - 'index': i + 1, - 'rev_id': rev.rev_id, - 'rev_hash': rev.rev_hash, - 'author': rev.author, - 'timestamp': rev.timestamp, - 'description': rev.description - }) + hist_data = get_history_data(history) result = { 'url': url, 'meta': page.all_meta, 'history': hist_data } return make_auth_response(result)