[ckan-changes] commit/ckan: dread: [logic]: Bulk of work done.
Bitbucket
commits-noreply at bitbucket.org
Fri Jul 22 16:15:11 UTC 2011
1 new changeset in ckan:
http://bitbucket.org/okfn/ckan/changeset/e0508e02d03d/
changeset: e0508e02d03d
branch: enh-1236-view-package-revision
user: dread
date: 2011-07-22 18:13:40
summary: [logic]: Bulk of work done.
affected #: 10 files (10.5 KB)
--- a/ckan/controllers/package.py Wed Jul 20 18:26:36 2011 +0100
+++ b/ckan/controllers/package.py Fri Jul 22 17:13:40 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 Fri Jul 22 17:13:40 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 Fri Jul 22 17:13:40 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 Fri Jul 22 17:13:40 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 Fri Jul 22 17:13:40 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 Fri Jul 22 17:13:40 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 Fri Jul 22 17:13:40 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 Fri Jul 22 17:13:40 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 Fri Jul 22 17:13:40 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 Fri Jul 22 17:13:40 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