[ckan-changes] commit/ckan: 2 new changesets

Bitbucket commits-noreply at bitbucket.org
Tue Jul 26 16:57:58 UTC 2011


2 new changesets in ckan:

http://bitbucket.org/okfn/ckan/changeset/fc484aab9280/
changeset:   fc484aab9280
branch:      enh-1236-view-package-revision
user:        dread
date:        2011-07-26 18:54:50
summary:     [branch] close.
affected #:  0 files (0 bytes)

http://bitbucket.org/okfn/ckan/changeset/53854d348d93/
changeset:   53854d348d93
user:        dread
date:        2011-07-26 18:55:11
summary:     [merge] from enh-1236-view-package-revision.
affected #:  10 files (10.5 KB)

--- a/ckan/controllers/package.py	Wed Jul 20 18:26:36 2011 +0100
+++ b/ckan/controllers/package.py	Tue Jul 26 17:55:11 2011 +0100
@@ -176,14 +176,24 @@
                    'user': c.user or c.author, 'extras_as_string': True,
                    'schema': self._form_to_db_schema(),
                    'id': id}
+
+        # interpret @<revision_id> or @<date> suffix
         split = id.split('@')
         if len(split) == 2:
-            context['id'], revision = split
-            try:
-                date = datetime.datetime(*map(int, re.split('[^\d]', revision)))
-                context['revision_date'] = date
-            except ValueError:
-                context['revision_id'] = revision
+            context['id'], revision_ref = split
+            if model.is_id(revision_ref):
+                context['revision_id'] = revision_ref
+            else:
+                try:
+                    date = model.strptimestamp(revision_ref)
+                    context['revision_date'] = date
+                except TypeError, e:
+                    abort(400, _('Invalid revision format: %r') % e.args)
+                except ValueError, e:
+                    abort(400, _('Invalid revision format: %r') % e.args)
+        elif len(split) > 2:
+            abort(400, _('Invalid revision format: %r') % 'Too many "@" symbols')
+            
         #check if package exists
         try:
             c.pkg_dict = get.package_show(context)


--- a/ckan/lib/dictization/model_dictize.py	Wed Jul 20 18:26:36 2011 +0100
+++ b/ckan/lib/dictization/model_dictize.py	Tue Jul 26 17:55:11 2011 +0100
@@ -74,7 +74,11 @@
     return resource
 
 def _execute_with_revision(q, rev_table, context):
+    '''
+    Raises NotFound if the context['revision_id'] does not exist.
+    Returns [] if there are no results.
 
+    '''
     model = context['model']
     meta = model.meta
     session = model.Session
@@ -83,8 +87,11 @@
     pending = context.get('pending')
 
     if revision_id:
-        revision_date = session.query(context['model'].Revision).filter_by(
-            id=revision_id).one().timestamp
+        revision = session.query(context['model'].Revision).filter_by(
+            id=revision_id).first()
+        if not revision:
+            raise NotFound
+        revision_date = revision.timestamp
     
     if revision_date:
         q = q.where(rev_table.c.revision_timestamp <= revision_date)


--- a/ckan/lib/helpers.py	Wed Jul 20 18:26:36 2011 +0100
+++ b/ckan/lib/helpers.py	Tue Jul 26 17:55:11 2011 +0100
@@ -16,6 +16,7 @@
 from routes import url_for, redirect_to
 from alphabet_paginate import AlphaPage
 from lxml.html import fromstring
+import datetime
 from ckan.i18n import get_available_locales
 
 try:
@@ -202,10 +203,22 @@
 
 
 def render_datetime(datetime_):
-    '''Render a datetime object as a string in a reasonable way (Y-m-d H:m).
+    '''Render a datetime object or timestamp string as a pretty string
+    (Y-m-d H:m).
+    If timestamp is badly formatted, then a blank string is returned.
     '''
-    if datetime_:
-        return datetime_.strftime('%Y-%m-%d %H:%M')
+    from ckan import model
+    date_format = '%Y-%m-%d %H:%M'
+    if isinstance(datetime_, datetime.datetime):
+        return datetime_.strftime(date_format)
+    elif isinstance(datetime_, basestring):
+        try:
+            datetime_ = model.strptimestamp(datetime_)
+        except TypeError:
+            return ''
+        except ValueError:
+            return ''
+        return datetime_.strftime(date_format)
     else:
         return ''
 


--- a/ckan/lib/package_saver.py	Wed Jul 20 18:26:36 2011 +0100
+++ b/ckan/lib/package_saver.py	Tue Jul 26 17:55:11 2011 +0100
@@ -62,6 +62,11 @@
             if isinstance(v, (list, tuple)):
                 v = ", ".join(map(unicode, v))
             c.pkg_extras.append((k, v))
+        if context.get('revision_id') or context.get('revision_date'):
+            # request was for a specific revision id or date
+            c.pkg_revision_id = c.pkg_dict[u'revision_id']
+            c.pkg_revision_timestamp = c.pkg_dict[u'revision_timestamp']
+            c.pkg_revision_not_latest = c.pkg_dict[u'revision_id'] != c.pkg.revision.id
 
     @classmethod
     def _preview_pkg(cls, fs, log_message=None, author=None, client=None):


--- a/ckan/model/__init__.py	Wed Jul 20 18:26:36 2011 +0100
+++ b/ckan/model/__init__.py	Tue Jul 26 17:55:11 2011 +0100
@@ -220,10 +220,20 @@
 Revision.user = property(_get_revision_user)
 
 def strptimestamp(s):
+    '''Convert a string of an ISO date into a datetime.datetime object.
+    
+    raises TypeError if the number of numbers in the string is not between 3
+                     and 7 (see datetime constructor).
+    raises ValueError if any of the numbers are out of range.
+    '''
+    
     import datetime, re
     return datetime.datetime(*map(int, re.split('[^\d]', s)))
 
 def strftimestamp(t):
+    '''Takes a datetime.datetime and returns it as an ISO string. For
+    a pretty printed string, use ckan.lib.helpers.render_datetime.
+    '''
     return t.isoformat()
 
 def revision_as_dict(revision, include_packages=True, ref_package_by='name'):
@@ -237,3 +247,8 @@
         revision_dict['packages'] = [getattr(pkg, ref_package_by) \
                                      for pkg in revision.packages]
     return revision_dict
+
+def is_id(id_string):
+    '''Tells the client if the string looks like a revision id or not'''
+    import re
+    return bool(re.match('^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', id_string))


--- a/ckan/public/css/ckan.css	Wed Jul 20 18:26:36 2011 +0100
+++ b/ckan/public/css/ckan.css	Tue Jul 26 17:55:11 2011 +0100
@@ -957,6 +957,20 @@
   float: none;
 }
 
+#revision.widget-container
+{
+  background: #f9f2ce;
+  color: #333;
+  margin: 0 0 1em 0;
+  padding: 10px;
+	border: 1px solid #ebd897;
+	border-left: none;
+	border-top: none;
+	border-radius: 0.5em;
+	-moz-border-radius: 0.5em;
+	-webkit-border-radius: 0.5em;
+}
+
 /* ===================== */
 /* = User Listing      = */
 /* ===================== */


--- a/ckan/templates/package/history.html	Wed Jul 20 18:26:36 2011 +0100
+++ b/ckan/templates/package/history.html	Tue Jul 26 17:55:11 2011 +0100
@@ -38,7 +38,7 @@
 
       <table><tr>
-          <th></th><th>Revision</th><th>Timestamp</th><th>Author</th><th>Log Message</th>
+          <th></th><th>Revision ID</th><th>Package with timestamp</th><th>Author</th><th>Log Message</th></tr><py:for each="index, rev in enumerate([rev for rev, obj_revs in c.pkg_revisions])"><tr>
@@ -47,9 +47,10 @@
               ${h.radio("selected2", rev.id, checked=(index == len(c.pkg_revisions)-1))}
             </td><td>
-              <a href="${h.url_for(controller='revision',action='read',id=rev.id)}">${rev.id}</a>
+              <a href="${h.url_for(controller='revision',action='read',id=rev.id)}" title="${rev.id}">${rev.id[:4]}…</a></td>
-            <td>${rev.timestamp}</td>
+            <td>
+              <a href="${h.url_for(controller='package',action='read',id='%s@%s' % (c.pkg.name, rev.timestamp))}" title="${'Read package as of %s' % rev.timestamp}">${h.render_datetime(rev.timestamp)}</a></td><td>${h.linked_user(rev.author)}</td><td>${rev.message}</td></tr> 


--- a/ckan/templates/package/read.html	Wed Jul 20 18:26:36 2011 +0100
+++ b/ckan/templates/package/read.html	Tue Jul 26 17:55:11 2011 +0100
@@ -93,6 +93,13 @@
   </py:match><div py:match="content">
+    <py:if test="c.pkg_revision_id">
+      <div id="revision" class="widget-container">
+            <p py:if="c.pkg_revision_not_latest">This is an old revision of this package, as edited <!--!by ${h.linked_user(rev.author)}-->at ${h.render_datetime(c.pkg_revision_timestamp)}. It may differ significantly from the <a href="${url(controller='package', action='read', id=c.pkg.name)}">current revision</a>.</p>
+            <p py:if="not c.pkg_revision_not_latest">This is the current revision of this package, as edited <!--!by ${h.linked_user(rev.author)}-->at ${h.render_datetime(c.pkg_revision_timestamp)}.</p>
+      </div>
+    </py:if>
+
     <xi:include href="read_core.html" /></div>
 


--- a/ckan/tests/functional/test_package.py	Wed Jul 20 18:26:36 2011 +0100
+++ b/ckan/tests/functional/test_package.py	Tue Jul 26 17:55:11 2011 +0100
@@ -1,4 +1,5 @@
 import cgi
+import datetime
 
 from paste.fixture import AppError
 from pylons import config
@@ -427,6 +428,178 @@
         assert plugin.calls['read'] == 1, plugin.calls
         plugins.unload(plugin)
 
+
+class TestReadAtRevision(FunctionalTestCase, HtmlCheckMethods):
+
+    @classmethod
+    def setup_class(cls):
+        cls.before = datetime.datetime(2010, 1, 1)
+        cls.date1 = datetime.datetime(2011, 1, 1)
+        cls.date2 = datetime.datetime(2011, 1, 2)
+        cls.date3 = datetime.datetime(2011, 1, 3)
+        cls.today = datetime.datetime.now()
+        cls.pkg_name = u'testpkg'
+        
+        # create package
+        rev = model.repo.new_revision()
+        rev.timestamp = cls.date1
+        pkg = model.Package(name=cls.pkg_name, title=u'title1')
+        model.Session.add(pkg)
+        model.setup_default_user_roles(pkg)
+        model.repo.commit_and_remove()
+
+        # edit package
+        rev = model.repo.new_revision()
+        rev.timestamp = cls.date2
+        pkg = model.Package.by_name(cls.pkg_name)
+        pkg.title = u'title2'
+        pkg.add_tag_by_name(u'tag2')
+        pkg.extras = {'key2': u'value2'}
+        model.repo.commit_and_remove()
+
+        # edit package again
+        rev = model.repo.new_revision()
+        rev.timestamp = cls.date3
+        pkg = model.Package.by_name(cls.pkg_name)
+        pkg.title = u'title3'
+        pkg.add_tag_by_name(u'tag3')
+        pkg.extras['key2'] = u'value3'
+        model.repo.commit_and_remove()
+
+        cls.offset = url_for(controller='package',
+                             action='read',
+                             id=cls.pkg_name)
+        pkg = model.Package.by_name(cls.pkg_name)
+        cls.revision_ids = [rev[0].id for rev in pkg.all_related_revisions[::-1]]
+                        # revision order is reversed to be chronological
+
+    @classmethod
+    def teardown_class(cls):
+        model.repo.rebuild_db()
+
+    def test_read_normally(self):
+        res = self.app.get(self.offset, status=200)
+        pkg_html = self.named_div('package', res)
+        side_html = self.named_div('primary', res)
+        print 'PKG', pkg_html
+        assert 'title3' in pkg_html
+        assert 'key2' in pkg_html
+        assert 'value3' in pkg_html
+        print 'SIDE', side_html
+        assert 'tag3' in side_html
+        assert 'tag2' in side_html
+
+    def test_read_date1(self):
+        offset = self.offset + self.date1.strftime('@%Y-%m-%d')
+        res = self.app.get(offset, status=200)
+        pkg_html = self.named_div('package', res)
+        side_html = self.named_div('primary', res)
+        print 'PKG', pkg_html
+        assert 'title1' in pkg_html
+        assert 'key2' not in pkg_html
+        assert 'value3' not in pkg_html
+        print 'SIDE', side_html
+        assert 'tag3' not in side_html
+        assert 'tag2' not in side_html
+
+    def test_read_date2(self):
+        date2_plus_3h = self.date2 + datetime.timedelta(hours=3)
+        offset = self.offset + date2_plus_3h.strftime('@%Y-%m-%d')
+        res = self.app.get(offset, status=200)
+        pkg_html = self.named_div('package', res)
+        side_html = self.named_div('primary', res)
+        print 'PKG', pkg_html
+        assert 'title2' in pkg_html
+        assert 'key2' in pkg_html
+        assert 'value2' in pkg_html
+        print 'SIDE', side_html
+        assert 'tag3' not in side_html
+        assert 'tag2' in side_html
+
+    def test_read_date3(self):
+        offset = self.offset + self.date3.strftime('@%Y-%m-%d-%H-%M')
+        res = self.app.get(offset, status=200)
+        pkg_html = self.named_div('package', res)
+        side_html = self.named_div('primary', res)
+        print 'PKG', pkg_html
+        assert 'title3' in pkg_html
+        assert 'key2' in pkg_html
+        assert 'value3' in pkg_html
+        print 'SIDE', side_html
+        assert 'tag3' in side_html
+        assert 'tag2' in side_html
+
+    def test_read_date_before_created(self):
+        offset = self.offset + self.before.strftime('@%Y-%m-%d')
+        res = self.app.get(offset, status=404)
+
+    def test_read_date_invalid(self):
+        res = self.app.get(self.offset + self.date3.strftime('@%Y-%m'),
+                           status=400)
+        res = self.app.get(self.offset + self.date3.strftime('@%Y'),
+                           status=400)
+        res = self.app.get(self.offset + self.date3.strftime('@%Y@%m'),
+                           status=400)
+
+    def test_read_revision1(self):
+        offset = self.offset + '@%s' % self.revision_ids[0]
+        res = self.app.get(offset, status=200)
+        main_html = self.main_div(res)
+        pkg_html = self.named_div('package', res)
+        side_html = self.named_div('primary', res)
+        print 'MAIN', main_html
+        assert 'This is an old revision of this package' in main_html
+        assert 'at 2011-01-01 00:00' in main_html
+        self.check_named_element(main_html, 'a', 'href="/package/%s"' % self.pkg_name)
+        print 'PKG', pkg_html
+        assert 'title1' in pkg_html
+        assert 'key2' not in pkg_html
+        assert 'value3' not in pkg_html
+        print 'SIDE', side_html
+        assert 'tag3' not in side_html
+        assert 'tag2' not in side_html
+
+    def test_read_revision2(self):
+        offset = self.offset + '@%s' % self.revision_ids[1]
+        res = self.app.get(offset, status=200)
+        main_html = self.main_div(res)
+        pkg_html = self.named_div('package', res)
+        side_html = self.named_div('primary', res)
+        print 'MAIN', main_html
+        assert 'This is an old revision of this package' in main_html
+        assert 'at 2011-01-02 00:00' in main_html
+        self.check_named_element(main_html, 'a', 'href="/package/%s"' % self.pkg_name)
+        print 'PKG', pkg_html
+        assert 'title2' in pkg_html
+        assert 'key2' in pkg_html
+        assert 'value2' in pkg_html
+        print 'SIDE', side_html
+        assert 'tag3' not in side_html
+        assert 'tag2' in side_html
+
+    def test_read_revision3(self):
+        offset = self.offset + '@%s' % self.revision_ids[2]
+        res = self.app.get(offset, status=200)
+        main_html = self.main_div(res)
+        pkg_html = self.named_div('package', res)
+        side_html = self.named_div('primary', res)
+        print 'MAIN', main_html
+        assert 'This is an old revision of this package' not in main_html
+        assert 'This is the current revision of this package' in main_html
+        assert 'at 2011-01-03 00:00' in main_html
+        self.check_named_element(main_html, 'a', 'href="/package/%s"' % self.pkg_name)
+        print 'PKG', pkg_html
+        assert 'title3' in pkg_html
+        assert 'key2' in pkg_html
+        assert 'value3' in pkg_html
+        print 'SIDE', side_html
+        assert 'tag3' in side_html
+        assert 'tag2' in side_html
+
+    def test_read_bad_revision(self):
+        # this revision doesn't exist in the db
+        offset = self.offset + '@ccab6798-1f4b-4a22-bcf5-462703aa4594'
+        res = self.app.get(offset, status=404)
         
 class TestEdit(TestPackageForm):
     editpkg_name = u'editpkgtest'
@@ -1277,36 +1450,39 @@
 
 class TestRevisions(TestPackageBase):
     @classmethod
-    def setup_class(self):
+    def setup_class(cls):
         model.Session.remove()
         model.repo.init_db()
-        self.name = u'revisiontest1'
+        cls.name = u'revisiontest1'
 
         # create pkg
-        self.notes = [u'Written by Puccini', u'Written by Rossini', u'Not written at all', u'Written again', u'Written off']
+        cls.notes = [u'Written by Puccini', u'Written by Rossini', u'Not written at all', u'Written again', u'Written off']
         rev = model.repo.new_revision()
-        self.pkg1 = model.Package(name=self.name)
-        self.pkg1.notes = self.notes[0]
-        model.Session.add(self.pkg1)
-        model.setup_default_user_roles(self.pkg1)
+        cls.pkg1 = model.Package(name=cls.name)
+        cls.pkg1.notes = cls.notes[0]
+        model.Session.add(cls.pkg1)
+        model.setup_default_user_roles(cls.pkg1)
         model.repo.commit_and_remove()
 
         # edit pkg
         for i in range(5)[1:]:
             rev = model.repo.new_revision()
-            pkg1 = model.Package.by_name(self.name)
-            pkg1.notes = self.notes[i]
+            pkg1 = model.Package.by_name(cls.name)
+            pkg1.notes = cls.notes[i]
             model.repo.commit_and_remove()
 
-        self.pkg1 = model.Package.by_name(self.name)        
+        cls.pkg1 = model.Package.by_name(cls.name)        
+        cls.revision_ids = [rev[0].id for rev in cls.pkg1.all_related_revisions]
+                           # revision ids are newest first
+        cls.revision_timestamps = [rev[0].timestamp for rev in cls.pkg1.all_related_revisions]
+        cls.offset = url_for(controller='package', action='history', id=cls.pkg1.name)
 
     @classmethod
-    def teardown_class(self):
+    def teardown_class(cls):
         model.repo.rebuild_db()
     
     def test_0_read_history(self):
-        offset = url_for(controller='package', action='history', id=self.pkg1.name)
-        res = self.app.get(offset)
+        res = self.app.get(self.offset)
         main_res = self.main_div(res)
         assert self.pkg1.name in main_res, main_res
         assert 'radio' in main_res, main_res
@@ -1318,8 +1494,7 @@
         assert last_radio_checked_html in main_res, '%s %s' % (last_radio_checked_html, main_res)
 
     def test_1_do_diff(self):
-        offset = url_for(controller='package', action='history', id=self.pkg1.name)
-        res = self.app.get(offset)
+        res = self.app.get(self.offset)
         form = res.forms['package-revisions']
         res = form.submit()
         res = res.follow()
@@ -1330,13 +1505,26 @@
         assert '<tr><td>notes</td><td><pre>- Written by Puccini\n+ Written off</pre></td></tr>' in main_res, main_res
 
     def test_2_atom_feed(self):
-        offset = url_for(controller='package', action='history', id=self.pkg1.name)
-        offset = "%s?format=atom" % offset
+        offset = "%s?format=atom" % self.offset
         res = self.app.get(offset)
         assert '<feed' in res, res
         assert 'xmlns="http://www.w3.org/2005/Atom"' in res, res
         assert '</feed>' in res, res
 
+    def test_3_history_revision_link(self):
+        res = self.app.get(self.offset)
+        res = res.click('%s' % self.revision_ids[2][:4])
+        main_res = self.main_div(res)
+        assert 'Revision: %s' % self.revision_ids[2] in main_res
+
+    def test_4_history_revision_package_link(self):
+        res = self.app.get(self.offset)
+        url = str(self.revision_timestamps[1])[-6:]
+        res = res.click(href=url)
+        main_html = self.main_div(res)
+        assert 'This is an old revision of this package' in main_html
+        assert 'at %s' % str(self.revision_timestamps[1])[:6] in main_html
+
    
 class TestMarkdownHtmlWhitelist(TestPackageForm):
 


--- a/setup.py	Wed Jul 20 18:26:36 2011 +0100
+++ b/setup.py	Tue Jul 26 17:55:11 2011 +0100
@@ -65,6 +65,9 @@
     rights = ckan.lib.authztool:RightsCommand
     roles = ckan.lib.authztool:RolesCommand
     
+    [console_scripts]
+    ckan-admin = bin.ckan_admin:Command
+
     [paste.paster_create_template]
     ckanext=ckan.pastertemplates:CkanextTemplate

Repository URL: https://bitbucket.org/okfn/ckan/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.




More information about the ckan-changes mailing list