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)
 
--- a/wikked/wiki.py	Thu Jan 03 07:56:27 2013 -0800
+++ b/wikked/wiki.py	Thu Jan 03 08:00:24 2013 -0800
@@ -275,6 +275,9 @@
     def pageExists(self, url):
         return self.fs.pageExists(url)
 
+    def getHistory(self):
+        return self.scm.getHistory();
+
     def _passthrough(self, content):
         return content