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

Bitbucket commits-noreply at bitbucket.org
Wed Jul 27 16:20:01 UTC 2011


3 new changesets in ckan:

http://bitbucket.org/okfn/ckan/changeset/87d4b23bbc6f/
changeset:   87d4b23bbc6f
branch:      feature-1141-moderated-edits-ajax
user:        John Glover
date:        2011-07-27 18:03:47
summary:     [logic] Bug fix: authenticate update_resource request against the resource's package
affected #:  1 file (268 bytes)

--- a/ckan/logic/action/update.py	Tue Jul 26 11:52:40 2011 +0100
+++ b/ckan/logic/action/update.py	Wed Jul 27 17:03:47 2011 +0100
@@ -163,6 +163,7 @@
 
 def resource_update(data_dict, context):
     model = context['model']
+    session = context['session']
     user = context['user']
     id = context["id"]
     schema = context.get('schema') or default_update_resource_schema()
@@ -171,13 +172,20 @@
     resource = model.Resource.get(id)
     context["resource"] = resource
 
-    if resource is None:
+    if not resource:
         raise NotFound(_('Resource was not found.'))
     context["id"] = resource.id
 
-    # TODO: check_access needs to be called against the package 
-    #       rather than the resource
-    check_access(resource, model.Action.EDIT, context)
+    # TODO: can check_access be used against a resource?
+    query = session.query(model.Package
+    ).join(model.ResourceGroup
+    ).join(model.Resource
+    ).filter(model.ResourceGroup.id == resource.resource_group_id)
+    pkg = query.first()
+    if not pkg:
+        raise NotFound(_('No package found for this resource, cannot check auth.'))
+
+    check_access(pkg, model.Action.EDIT, context)
 
     data, errors = validate(data_dict, schema, context)
 


http://bitbucket.org/okfn/ckan/changeset/488244fbb556/
changeset:   488244fbb556
branch:      feature-1141-moderated-edits-ajax
user:        John Glover
date:        2011-07-27 18:11:20
summary:     [merge] default
affected #:  24 files (19.7 KB)

--- a/ckan/config/routing.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/config/routing.py	Wed Jul 27 17:11:20 2011 +0100
@@ -104,6 +104,8 @@
     
     map.connect('/api/rest', controller='api', action='index')
 
+    map.connect('/api/action/{logic_function}', controller='api', action='action')
+
     map.connect('/api/rest/{register}', controller='api', action='list',
         requirements=dict(register=register_list_str),
         conditions=dict(method=['GET'])


--- a/ckan/controllers/api.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/controllers/api.py	Wed Jul 27 17:11:20 2011 +0100
@@ -11,6 +11,7 @@
 from ckan.plugins import PluginImplementations, IGroupController
 from ckan.lib.munge import munge_title_to_name
 from ckan.lib.navl.dictization_functions import DataError
+from ckan.logic import get_action
 import ckan.logic.action.get as get 
 import ckan.logic.action.create as create
 import ckan.logic.action.update as update
@@ -30,6 +31,8 @@
     }
 class ApiController(BaseController):
 
+    _actions = {}
+
     def __call__(self, environ, start_response):
         self._identify_user()
         if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
@@ -127,10 +130,45 @@
         response_data = {}
         response_data['version'] = ver or '1'
         return self._finish_ok(response_data) 
+    
+    def action(self, logic_function):
+        function = get_action(logic_function)
+        
+        context = {'model': model, 'session': model.Session, 'user': c.user}
+        model.Session()._context = context
+        return_dict = {'help': function.__doc__}
+
+        try:
+            request_data = self._get_request_data()
+        except ValueError, inst:
+
+            return self._finish_bad_request(
+                gettext('JSON Error: %s') % str(inst))
+        try:
+            result = function(context, request_data)
+            return_dict['success'] = True
+            return_dict['result'] = result
+        except DataError:
+            log.error('Format incorrect: %s' % request_data)
+            #TODO make better error message
+            return self._finish(400, _(u'Integrity Error') % request_data)
+        except NotAuthorized:
+            return_dict['error'] = {'__type': 'Authorization Error',
+                                    'message': _('Access denied')}
+            return_dict['success'] = False
+            return self._finish(403, return_dict, content_type='json')
+        except ValidationError, e:
+            error_dict = e.error_dict 
+            error_dict['__type'] = 'Validtion Error'
+            return_dict['error'] = error_dict
+            return_dict['success'] = False
+            log.error('Validation error: %r' % str(e.error_dict))
+            return self._finish(409, return_dict, content_type='json')
+        return self._finish_ok(return_dict)
 
     def list(self, ver=None, register=None, subregister=None, id=None):
         context = {'model': model, 'session': model.Session,
-                   'user': c.user, 'id': id, 'api_version': ver}
+                   'user': c.user, 'api_version': ver}
         log.debug('listing: %s' % context)
         action_map = {
             'revision': get.revision_list,
@@ -149,7 +187,7 @@
             return self._finish_bad_request(
                 gettext('Cannot list entity of this type: %s') % register)
         try:
-            return self._finish_ok(action(context))
+            return self._finish_ok(action(context, {'id': id}))
         except NotFound, e:
             extra_msg = e.extra_msg
             return self._finish_not_found(extra_msg)
@@ -166,8 +204,9 @@
         }
 
         context = {'model': model, 'session': model.Session, 'user': c.user,
-                   'id': id, 'id2': id2, 'rel': subregister,
                    'api_version': ver}
+        data_dict = {'id': id, 'id2': id2, 'rel': subregister}
+
         for type in model.PackageRelationship.get_all_types():
             action_map[('package', type)] = get.package_relationships_list
         log.debug('show: %s' % context)
@@ -180,7 +219,7 @@
                 gettext('Cannot read entity of this type: %s') % register)
         try:
             
-            return self._finish_ok(action(context))
+            return self._finish_ok(action(context, data_dict))
         except NotFound, e:
             extra_msg = e.extra_msg
             return self._finish_not_found(extra_msg)
@@ -193,22 +232,22 @@
     def create(self, ver=None, register=None, subregister=None, id=None, id2=None):
 
         action_map = {
-            ('package', 'relationships'): create.package_relationship_create,
-             'group': create.group_create_rest,
-             'package': create.package_create_rest,
-             'rating': create.rating_create,
+            ('package', 'relationships'): get_action('package_relationship_create'),
+             'group': get_action('group_create_rest'),
+             'package': get_action('package_create_rest'),
+             'rating': get_action('rating_create'),
         }
 
         for type in model.PackageRelationship.get_all_types():
             action_map[('package', type)] = create.package_relationship_create
 
-
         context = {'model': model, 'session': model.Session, 'user': c.user,
-                   'id': id, 'id2': id2, 'rel': subregister,
                    'api_version': ver}
         log.debug('create: %s' % (context))
         try:
             request_data = self._get_request_data()
+            data_dict = {'id': id, 'id2': id2, 'rel': subregister}
+            data_dict.update(request_data)
         except ValueError, inst:
             return self._finish_bad_request(
                 gettext('JSON Error: %s') % str(inst))
@@ -221,10 +260,10 @@
                 gettext('Cannot create new entity of this type: %s %s') % \
                 (register, subregister))
         try:
-            response_data = action(request_data, context)
+            response_data = action(context, data_dict)
             location = None
-            if "id" in context:
-                location = str('%s/%s' % (request.path, context.get("id")))
+            if "id" in data_dict:
+                location = str('%s/%s' % (request.path, data_dict.get("id")))
             return self._finish_ok(response_data,
                                    resource_location=location)
         except NotAuthorized:
@@ -243,19 +282,20 @@
     def update(self, ver=None, register=None, subregister=None, id=None, id2=None):
 
         action_map = {
-            ('package', 'relationships'): update.package_relationship_update,
-             'package': update.package_update_rest,
-             'group': update.group_update_rest,
+            ('package', 'relationships'): get_action('package_relationship_update'),
+             'package': get_action('package_update_rest'),
+             'group': get_action('group_update_rest'),
         }
         for type in model.PackageRelationship.get_all_types():
             action_map[('package', type)] = update.package_relationship_update
 
         context = {'model': model, 'session': model.Session, 'user': c.user,
-                   'id': id, 'id2': id2, 'rel': subregister,
-                   'api_version': ver}
+                   'api_version': ver, 'id': id}
         log.debug('update: %s' % (context))
         try:
             request_data = self._get_request_data()
+            data_dict = {'id': id, 'id2': id2, 'rel': subregister}
+            data_dict.update(request_data)
         except ValueError, inst:
             return self._finish_bad_request(
                 gettext('JSON Error: %s') % str(inst))
@@ -267,7 +307,7 @@
                 gettext('Cannot update entity of this type: %s') % \
                     register.encode('utf-8'))
         try:
-            response_data = action(request_data, context)
+            response_data = action(context, data_dict)
             return self._finish_ok(response_data)
         except NotAuthorized:
             return self._finish_not_authz()
@@ -510,3 +550,4 @@
             resultSet["ResultSet"]["Result"].append(result)
         return self._finish_ok(resultSet)
 
+


--- a/ckan/controllers/group.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/controllers/group.py	Wed Jul 27 17:11:20 2011 +0100
@@ -115,13 +115,14 @@
                    'user': c.user or c.author, 'extras_as_string': True,
                    'save': 'save' in request.params,
                    'schema': self._form_to_db_schema(),
-                   'id': id}
+                   }
+        data_dict = {'id': id}
 
         if context['save'] and not data:
             return self._save_edit(id, context)
 
         try:
-            old_data = get.group_show(context)
+            old_data = get.group_show(context, data_dict)
             c.grouptitle = old_data.get('title')
             c.groupname = old_data.get('name')
             schema = self._db_to_form_schema()
@@ -151,7 +152,7 @@
             data_dict = clean_dict(unflatten(
                 tuplize_dict(parse_params(request.params))))
             context['message'] = data_dict.get('log_message', '')
-            group = create.group_create(data_dict, context)
+            group = create.group_create(context, data_dict)
             h.redirect_to(controller='group', action='read', id=group['name'])
         except NotAuthorized:
             abort(401, _('Unauthorized to read group %s') % '')
@@ -169,7 +170,8 @@
             data_dict = clean_dict(unflatten(
                 tuplize_dict(parse_params(request.params))))
             context['message'] = data_dict.get('log_message', '')
-            group = update.group_update(data_dict, context)
+            data_dict['id'] = id
+            group = update.group_update(context, data_dict)
             h.redirect_to(controller='group', action='read', id=group['name'])
         except NotAuthorized:
             abort(401, _('Unauthorized to read group %s') % id)


--- a/ckan/controllers/home.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/controllers/home.py	Wed Jul 27 17:11:20 2011 +0100
@@ -50,7 +50,8 @@
         c.package_count = query.count
         c.latest_packages = current_package_list_with_resources({'model': model,
                                                                 'user': c.user,
-                                                                'limit': 5})      
+                                                                'limit': 5},
+                                                                 {})      
         return render('home/index.html', cache_key=cache_key,
                 cache_expire=cache_expires)
 


--- a/ckan/controllers/package.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/controllers/package.py	Wed Jul 27 17:11:20 2011 +0100
@@ -16,6 +16,7 @@
 import ckan.logic.action.create as create
 import ckan.logic.action.update as update
 import ckan.logic.action.get as get
+from ckan.logic import get_action
 from ckan.logic.schema import package_form_schema
 from ckan.lib.base import request, c, BaseController, model, abort, h, g, render
 from ckan.lib.base import etag_cache, response, redirect, gettext
@@ -77,9 +78,9 @@
             log.info('incorrect form fields posted')
             raise DataError(data_dict)
 
-    def _setup_template_variables(self, context):
-        c.groups = get.group_list_availible(context)
-        c.groups_authz = get.group_list_authz(context)
+    def _setup_template_variables(self, context, data_dict):
+        c.groups = get.group_list_availible(context, data_dict)
+        c.groups_authz = get.group_list_authz(context, data_dict)
         c.licences = [('', '')] + model.Package.get_license_options()
         c.is_sysadmin = Authorizer().is_sysadmin(c.user)
         c.resource_columns = model.Resource.get_columns()
@@ -174,19 +175,29 @@
     def read(self, id):
         context = {'model': model, 'session': model.Session,
                    'user': c.user or c.author, 'extras_as_string': True,
-                   'schema': self._form_to_db_schema(),
-                   'id': id}
+                   'schema': self._form_to_db_schema()}
+        data_dict = {'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
+            data_dict['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)
+            c.pkg_dict = get.package_show(context, data_dict)
             c.pkg = context['package']
         except NotFound:
             abort(404, _('Package not found'))
@@ -217,12 +228,11 @@
     def comments(self, id):
         context = {'model': model, 'session': model.Session,
                    'user': c.user or c.author, 'extras_as_string': True,
-                   'schema': self._form_to_db_schema(),
-                   'id': id}
+                   'schema': self._form_to_db_schema()}
 
         #check if package exists
         try:
-            c.pkg_dict = get.package_show(context)
+            c.pkg_dict = get.package_show(context, {'id':id})
             c.pkg = context['package']
         except NotFound:
             abort(404, _('Package not found'))
@@ -319,7 +329,7 @@
         error_summary = error_summary or {}
         vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
 
-        self._setup_template_variables(context)
+        self._setup_template_variables(context, {'id': id})
         c.form = render(self.package_form, extra_vars=vars)
         return render('package/new.html')
 
@@ -329,14 +339,14 @@
                    'user': c.user or c.author, 'extras_as_string': True,
                    'preview': 'preview' in request.params,
                    'save': 'save' in request.params,
-                   'id': id, 'moderated': config.get('moderated'),
+                   'moderated': config.get('moderated'),
                    'pending': True,
                    'schema': self._form_to_db_schema()}
 
         if (context['save'] or context['preview']) and not data:
             return self._save_edit(id, context)
         try:
-            old_data = get.package_show(context)
+            old_data = get.package_show(context, {'id':id})
             schema = self._db_to_form_schema()
             if schema:
                 old_data, errors = validate(old_data, schema)
@@ -355,19 +365,19 @@
         errors = errors or {}
         vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
 
-        self._setup_template_variables(context)
+        self._setup_template_variables(context, {'id':'id'})
         c.form = render(self.package_form, extra_vars=vars)
         return render('package/edit.html')
 
     def read_ajax(self, id, revision=None):
         context = {'model': model, 'session': model.Session,
                    'user': c.user or c.author,
-                   'id': id, 'extras_as_string': True,
+                   'extras_as_string': True,
                    'schema': self._form_to_db_schema(),
                    'revision_id': revision}
 
         try:
-            data = get.package_show(context)
+            data = get.package_show(context, {'id': id})
             schema = self._db_to_form_schema()
             if schema:
                 data, errors = validate(data, schema)
@@ -385,9 +395,6 @@
 
     def history_ajax(self, id):
 
-        context = {'model': model, 'session': model.Session,
-                   'user': c.user or c.author,
-                   'id': id, 'extras_as_string': True}
         pkg = model.Package.get(id)
         data = []
         approved = False
@@ -414,7 +421,7 @@
                 tuplize_dict(parse_params(request.POST))))
             self._check_data_dict(data_dict)
             context['message'] = data_dict.get('log_message', '')
-            pkg = create.package_create(data_dict, context)
+            pkg = get_action('package_create')(context, data_dict)
 
             if context['preview']:
                 PackageSaver().render_package(pkg, context)
@@ -444,9 +451,10 @@
             context['message'] = data_dict.get('log_message', '')
             if not context['moderated']:
                 context['pending'] = False
-            pkg = update.package_update(data_dict, context)
+            data_dict['id'] = id
+            pkg = get_action('package_update')(context, data_dict)
             if request.params.get('save', '') == 'Approve':
-                update.make_latest_pending_package_active(context)
+                update.make_latest_pending_package_active(context, data_dict)
             c.pkg = context['package']
             c.pkg_dict = pkg
 


--- a/ckan/lib/dictization/model_dictize.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/lib/dictization/model_dictize.py	Wed Jul 27 17:11:20 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/dictization/model_save.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/lib/dictization/model_save.py	Wed Jul 27 17:11:20 2011 +0100
@@ -106,7 +106,7 @@
         extra = old_extras[key]
         if extra.state == 'deleted':
             continue
-        state = 'pending-deleted' if context.get('pending') else 'delete'
+        state = 'pending-deleted' if context.get('pending') else 'deleted'
         extra.state = state
 
 def group_extras_save(extras_dicts, context):
@@ -127,7 +127,6 @@
     return result_dict
 
 def package_tag_list_save(tag_dicts, package, context):
-
     
     allow_partial_update = context.get("allow_partial_update", False)
     if not tag_dicts and allow_partial_update:


--- a/ckan/lib/helpers.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/lib/helpers.py	Wed Jul 27 17:11:20 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 27 17:03:47 2011 +0100
+++ b/ckan/lib/package_saver.py	Wed Jul 27 17:11:20 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/logic/__init__.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/logic/__init__.py	Wed Jul 27 17:11:20 2011 +0100
@@ -1,6 +1,8 @@
 import logging
 import ckan.authz
 from ckan.lib.navl.dictization_functions import flatten_dict
+from ckan.plugins import PluginImplementations
+from ckan.plugins.interfaces import IActions
 
 class ActionError(Exception):
     def __init__(self, extra_msg=None):
@@ -90,4 +92,42 @@
         log.debug("No valid API key provided.")
         raise NotAuthorized
     log.debug("Access OK.")
-    return True                
+    return True             
+
+_actions = {}
+
+def get_action(action):
+    if _actions:
+        return _actions.get(action)
+    # Otherwise look in all the plugins to resolve all possible
+    # First get the default ones in the ckan/logic/action directory
+    # Rather than writing them out in full will use __import__
+    # to load anything from ckan.logic.action that looks like it might
+    # be an action 
+    for action_module_name in ['get', 'create', 'update']:
+        module_path = 'ckan.logic.action.'+action_module_name
+        module = __import__(module_path)
+        for part in module_path.split('.')[1:]:
+            module = getattr(module, part)
+        for k, v in module.__dict__.items():
+            if not k.startswith('_'):
+                _actions[k] = v
+    # Then overwrite them with any specific ones in the plugins:
+    resolved_action_plugins = {}
+    fetched_actions = {}
+    for plugin in PluginImplementations(IActions):
+        for name, auth_function in plugin.get_actions().items():
+            if name in resolved_action_plugins:
+                raise Exception(
+                    'The action %r is already implemented in %r' % (
+                        name,
+                        resolved_action_plugins[name]
+                    )
+                )
+            log.debug('Auth function %r was inserted', plugin.name)
+            resolved_action_plugins[name] = plugin.name
+            fetched_actions[name] = auth_function
+    # Use the updated ones in preference to the originals.
+    _actions.update(fetched_actions)
+    return _actions.get(action)
+


--- a/ckan/logic/action/create.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/logic/action/create.py	Wed Jul 27 17:11:20 2011 +0100
@@ -30,16 +30,17 @@
                                       check_group_auth)
 log = logging.getLogger(__name__)
 
-def package_create(data_dict, context):
+def package_create(context, data_dict):
 
     model = context['model']
     user = context['user']
     preview = context.get('preview', False)
     schema = context.get('schema') or default_create_package_schema()
     model.Session.remove()
+    model.Session()._context = context
 
     check_access(model.System(), model.Action.PACKAGE_CREATE, context)
-    check_group_auth(data_dict, context)
+    check_group_auth(context, data_dict)
 
     data, errors = validate(data_dict, schema, context)
 
@@ -76,7 +77,26 @@
     else:
         return data
 
-def resource_create(data_dict, context):
+def package_create_validate(context, data_dict):
+    model = context['model']
+    user = context['user']
+    preview = context.get('preview', False)
+    schema = context.get('schema') or default_create_package_schema()
+    model.Session.remove()
+    model.Session()._context = context
+
+    check_access(model.System(), model.Action.PACKAGE_CREATE, context)
+    check_group_auth(context, data_dict)
+
+    data, errors = validate(data_dict, schema, context)
+
+    if errors:
+        model.Session.rollback()
+        raise ValidationError(errors, package_error_summary(errors))
+    else:
+        return data
+
+def resource_create(context, data_dict):
     model = context['model']
     user = context['user']
 
@@ -84,14 +104,13 @@
                             default_resource_schema(),
                             context)
 
-
-def package_relationship_create(data_dict, context):
+def package_relationship_create(context, data_dict):
 
     model = context['model']
     user = context['user']
-    id = context["id"]
-    id2 = context["id2"]
-    rel_type = context["rel"]
+    id = data_dict["id"]
+    id2 = data_dict["id2"]
+    rel_type = data_dict["rel"]
     api = context.get('api_version') or '1'
     ref_package_by = 'id' if api == '2' else 'name'
 
@@ -124,7 +143,7 @@
     relationship_dicts = rel.as_dict(ref_package_by=ref_package_by)
     return relationship_dicts
 
-def group_create(data_dict, context):
+def group_create(context, data_dict):
     model = context['model']
     user = context['user']
     schema = context.get('schema') or default_group_schema()
@@ -160,7 +179,7 @@
     log.debug('Created object %s' % str(group.name))
     return group_dictize(group, context)
 
-def rating_create(data_dict, context):
+def rating_create(context, data_dict):
 
     model = context['model']
     user = context.get("user") 
@@ -197,12 +216,12 @@
 
 ## Modifications for rest api
 
-def package_create_rest(data_dict, context):
+def package_create_rest(context, data_dict):
 
     api = context.get('api_version') or '1'
 
     dictized_package = package_api_to_dict(data_dict, context)
-    dictized_after = package_create(dictized_package, context) 
+    dictized_after = package_create(context, dictized_package) 
 
     pkg = context['package']
 
@@ -211,14 +230,16 @@
     else:
         package_dict = package_to_api2(pkg, context)
 
+    data_dict['id'] = pkg.id
+
     return package_dict
 
-def group_create_rest(data_dict, context):
+def group_create_rest(context, data_dict):
 
     api = context.get('api_version') or '1'
 
     dictized_group = group_api_to_dict(data_dict, context)
-    dictized_after = group_create(dictized_group, context) 
+    dictized_after = group_create(context, dictized_group) 
 
     group = context['group']
 
@@ -227,5 +248,7 @@
     else:
         group_dict = group_to_api2(group, context)
 
+    data_dict['id'] = group.id
+
     return group_dict
 


--- a/ckan/logic/action/get.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/logic/action/get.py	Wed Jul 27 17:11:20 2011 +0100
@@ -15,10 +15,11 @@
                                                 group_dictize)
 
 
-def package_list(context):
+def package_list(context, data_dict):
+    '''Lists the package by name'''
     model = context["model"]
     user = context["user"]
-    api = context["api_version"]
+    api = context.get("api_version", '1')
     ref_package_by = 'id' if api == '2' else 'name'
 
     query = Session.query(model.PackageRevision)
@@ -27,10 +28,10 @@
     packages = query.all()
     return [getattr(p, ref_package_by) for p in packages]
 
-def current_package_list_with_resources(context):
+def current_package_list_with_resources(context, data_dict):
     model = context["model"]
     user = context["user"]
-    limit = context.get("limit")
+    limit = data_dict.get("limit")
 
     q = Session.query(model.PackageRevision)
     q = q.filter(model.PackageRevision.state=='active')
@@ -63,15 +64,15 @@
         package_list.append(result_dict)
     return package_list
 
-def revision_list(context):
+def revision_list(context, data_dict):
 
     model = context["model"]
     revs = model.Session.query(model.Revision).all()
     return [rev.id for rev in revs]
 
-def package_revision_list(context):
+def package_revision_list(context, data_dict):
     model = context["model"]
-    id = context["id"]
+    id = data_dict["id"]
     pkg = model.Package.get(id)
     if pkg is None:
         raise NotFound
@@ -83,7 +84,7 @@
                                                      include_packages=False))
     return revision_dicts
 
-def group_list(context):
+def group_list(context, data_dict):
     model = context["model"]
     user = context["user"]
     api = context.get('api_version') or '1'
@@ -93,7 +94,7 @@
     groups = query.all() 
     return [getattr(p, ref_group_by) for p in groups]
 
-def group_list_authz(context):
+def group_list_authz(context, data_dict):
     model = context['model']
     user = context['user']
     pkg = context.get('package')
@@ -102,7 +103,7 @@
     groups = set(query.all())
     return dict((group.id, group.name) for group in groups)
 
-def group_list_availible(context):
+def group_list_availible(context, data_dict):
     model = context['model']
     user = context['user']
     pkg = context.get('package')
@@ -115,30 +116,30 @@
 
     return [(group.id, group.name) for group in groups]
 
-def licence_list(context):
+def licence_list(context, data_dict):
     model = context["model"]
     license_register = model.Package.get_license_register()
     licenses = license_register.values()
     licences = [l.as_dict() for l in licenses]
     return licences
 
-def tag_list(context):
+def tag_list(context, data_dict):
     model = context["model"]
     tags = model.Session.query(model.Tag).all() #TODO
     tag_list = [tag.name for tag in tags]
     return tag_list
 
-def package_relationships_list(context):
+def package_relationships_list(context, data_dict):
 
     ##TODO needs to work with dictization layer
     model = context['model']
     user = context['user']
-    id = context["id"]
-    id2 = context.get("id2")
-    rel = context.get("rel")
     api = context.get('api_version') or '1'
+
+    id = data_dict["id"]
+    id2 = data_dict.get("id2")
+    rel = data_dict.get("rel")
     ref_package_by = 'id' if api == '2' else 'name';
-
     pkg1 = model.Package.get(id)
     pkg2 = None
     if not pkg1:
@@ -164,11 +165,11 @@
 
     return relationship_dicts
 
-def package_show(context):
+def package_show(context, data_dict):
 
     model = context['model']
     api = context.get('api_version') or '1'
-    id = context['id']
+    id = data_dict['id']
 
     pkg = model.Package.get(id)
 
@@ -186,10 +187,10 @@
     return package_dict
 
 
-def revision_show(context):
+def revision_show(context, data_dict):
     model = context['model']
     api = context.get('api_version') or '1'
-    id = context['id']
+    id = data_dict['id']
     ref_package_by = 'id' if api == '2' else 'name'
 
     rev = model.Session.query(model.Revision).get(id)
@@ -199,9 +200,9 @@
                                       ref_package_by=ref_package_by)
     return rev_dict
 
-def group_show(context):
+def group_show(context, data_dict):
     model = context['model']
-    id = context['id']
+    id = data_dict['id']
     api = context.get('api_version') or '1'
 
 
@@ -220,10 +221,10 @@
     return group_dict
 
 
-def tag_show(context):
+def tag_show(context, data_dict):
     model = context['model']
     api = context.get('api_version') or '1'
-    id = context['id']
+    id = data_dict['id']
     ref_package_by = 'id' if api == '2' else 'name'
     obj = model.Tag.get(id) #TODO tags
     if obj is None:
@@ -233,12 +234,11 @@
     return package_list 
 
 
-def package_show_rest(context):
+def package_show_rest(context, data_dict):
 
-    package_show(context)
+    package_show(context, data_dict)
 
     api = context.get('api_version') or '1'
-
     pkg = context['package']
 
     if api == '1':
@@ -248,9 +248,9 @@
 
     return package_dict
 
-def group_show_rest(context):
+def group_show_rest(context, data_dict):
 
-    group_show(context)
+    group_show(context, data_dict)
     api = context.get('api_version') or '1'
     group = context['group']
 


--- a/ckan/logic/action/update.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/logic/action/update.py	Wed Jul 27 17:11:20 2011 +0100
@@ -66,7 +66,7 @@
             error_summary[_(prettify(key))] = error[0]
     return error_summary
 
-def check_group_auth(data_dict, context):
+def check_group_auth(context, data_dict):
     model = context['model']
     pkg = context.get("package")
 
@@ -120,11 +120,11 @@
         context['latest_revision_date'] = latest_rev.revision_timestamp
         context['latest_revision'] = latest_rev.revision_id
 
-def make_latest_pending_package_active(context):
+def make_latest_pending_package_active(context, data_dict):
 
     model = context['model']
     session = model.Session
-    id = context["id"]
+    id = data_dict["id"]
     pkg = model.Package.get(id)
 
     check_access(pkg, model.Action.EDIT, context)
@@ -161,7 +161,7 @@
     session.remove()        
 
 
-def resource_update(data_dict, context):
+def resource_update(context, data_dict):
     model = context['model']
     session = context['session']
     user = context['user']
@@ -205,26 +205,28 @@
     return resource_dictize(resource, context)
 
 
-def package_update(data_dict, context):
+def package_update(context, data_dict):
     model = context['model']
     user = context['user']
-    id = context["id"]
+    
+    id = data_dict["id"]
     preview = context.get('preview', False)
     schema = context.get('schema') or default_update_package_schema()
     model.Session.remove()
+    model.Session()._context = context
 
     pkg = model.Package.get(id)
     context["package"] = pkg
 
     if pkg is None:
         raise NotFound(_('Package was not found.'))
-    context["id"] = pkg.id
+    data_dict["id"] = pkg.id
 
     check_access(pkg, model.Action.EDIT, context)
 
     data, errors = validate(data_dict, schema, context)
 
-    check_group_auth(data, context)
+    check_group_auth(context, data)
 
     if errors:
         model.Session.rollback()
@@ -247,6 +249,31 @@
         return package_dictize(pkg, context)
     return data
 
+def package_update_validate(context, data_dict):
+    model = context['model']
+    user = context['user']
+    
+    id = data_dict["id"]
+    preview = context.get('preview', False)
+    schema = context.get('schema') or default_update_package_schema()
+    model.Session.remove()
+    model.Session()._context = context
+
+    pkg = model.Package.get(id)
+    context["package"] = pkg
+
+    if pkg is None:
+        raise NotFound(_('Package was not found.'))
+    data_dict["id"] = pkg.id
+
+    check_access(pkg, model.Action.EDIT, context)
+    data, errors = validate(data_dict, schema, context)
+
+    if errors:
+        model.Session.rollback()
+        raise ValidationError(errors, package_error_summary(errors))
+    return data
+
 
 def _update_package_relationship(relationship, comment, context):
     model = context['model']
@@ -264,13 +291,13 @@
                                     ref_package_by=ref_package_by)
     return rel_dict
 
-def package_relationship_update(data_dict, context):
+def package_relationship_update(context, data_dict):
 
     model = context['model']
     user = context['user']
-    id = context["id"]
-    id2 = context["id2"]
-    rel = context["rel"]
+    id = data_dict["id"]
+    id2 = data_dict["id2"]
+    rel = data_dict["rel"]
     api = context.get('api_version') or '1'
     ref_package_by = 'id' if api == '2' else 'name'
 
@@ -295,12 +322,12 @@
     comment = data_dict.get('comment', u'')
     return _update_package_relationship(entity, comment, context)
 
-def group_update(data_dict, context):
+def group_update(context, data_dict):
 
     model = context['model']
     user = context['user']
     schema = context.get('schema') or default_update_group_schema()
-    id = context['id']
+    id = data_dict['id']
 
     group = model.Group.get(id)
     context["group"] = group
@@ -335,16 +362,28 @@
 
 ## Modifications for rest api
 
-def package_update_rest(data_dict, context):
+def package_update_rest(context, data_dict):
 
     model = context['model']
-    id = context["id"]
+    id = data_dict.get("id")
+    request_id = context['id']
     api = context.get('api_version') or '1'
-    pkg = model.Package.get(id)
+    pkg = model.Package.get(request_id)
+
+    if not pkg:
+        raise NotFound
+
+    if id and id != pkg.id:
+        pkg_from_data = model.Package.get(id)
+        if pkg_from_data != pkg:
+            error_dict = {id:('Cannot change value of key from %s to %s. '
+                'This key is read-only') % (pkg.id, id)}
+            raise ValidationError(error_dict)
+
     context["package"] = pkg
     context["allow_partial_update"] = True
     dictized_package = package_api_to_dict(data_dict, context)
-    dictized_after = package_update(dictized_package, context)
+    dictized_after = package_update(context, dictized_package)
 
     pkg = context['package']
 
@@ -355,16 +394,16 @@
 
     return package_dict
 
-def group_update_rest(data_dict, context):
+def group_update_rest(context, data_dict):
 
     model = context['model']
-    id = context["id"]
+    id = data_dict["id"]
     api = context.get('api_version') or '1'
     group = model.Group.get(id)
     context["group"] = group
     context["allow_partial_update"] = True
     dictized_package = group_api_to_dict(data_dict, context)
-    dictized_after = group_update(dictized_package, context)
+    dictized_after = group_update(context, dictized_package)
 
     group = context['group']
 


--- a/ckan/model/__init__.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/model/__init__.py	Wed Jul 27 17:11:20 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/model/meta.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/model/meta.py	Wed Jul 27 17:11:20 2011 +0100
@@ -1,6 +1,7 @@
 import datetime
 """SQLAlchemy Metadata and Session object"""
 from sqlalchemy import MetaData, __version__ as sqav
+from sqlalchemy.orm import class_mapper
 from sqlalchemy.orm import scoped_session, sessionmaker
 import sqlalchemy.orm as orm
 from sqlalchemy.orm.session import SessionExtension
@@ -37,42 +38,42 @@
             revision = session.revision
         except AttributeError:
             return
-
         new = obj_cache['new']
         changed = obj_cache['changed']
         deleted = obj_cache['deleted']
-
         for obj in new | changed | deleted:
-            
             if not hasattr(obj, '__revision_class__'):
                 continue
-
             revision_cls = obj.__revision_class__
-
+            revision_table = class_mapper(revision_cls).mapped_table
             ## when a normal active transaction happens
             if 'pending' not in obj.state:
-                revision.approved_timestamp = datetime.datetime.now()
-                old = session.query(revision_cls).filter_by(
-                    current='1',
-                    id = obj.id
-                ).first()
-                if old:
-                    old.current = '0'
-                    session.add(old)
+                ### this is asql statement as we do not want it in object cache
+                session.execute(
+                    revision_table.update().where(
+                        and_(revision_table.c.id == obj.id,
+                             revision_table.c.current == '1')
+                    ).values(current='0')
+                )
 
             q = session.query(revision_cls)
             q = q.filter_by(expired_timestamp=datetime.datetime(9999, 12, 31), id=obj.id)
             results = q.all()
-
             for rev_obj in results:
+                values = {}
                 if rev_obj.revision_id == revision.id:
-                    rev_obj.revision_timestamp = revision.timestamp
+                    values['revision_timestamp'] = revision.timestamp
                     if 'pending' not in obj.state:
-                        rev_obj.current = '1'
+                        values['current'] = '1'
                 else:
-                    rev_obj.expired_id = revision.id
-                    rev_obj.expired_timestamp = revision.timestamp
-                session.add(rev_obj)
+                    values['expired_id'] = revision.id
+                    values['expired_timestamp'] = revision.timestamp
+                session.execute(
+                    revision_table.update().where(
+                        and_(revision_table.c.id == rev_obj.id,
+                             revision_table.c.revision_id == rev_obj.revision_id)
+                    ).values(**values)
+                )
 
     def after_commit(self, session):
         if hasattr(session, '_object_cache'):


--- a/ckan/plugins/interfaces.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/plugins/interfaces.py	Wed Jul 27 17:11:20 2011 +0100
@@ -10,7 +10,8 @@
     'IMiddleware',
     'IDomainObjectModification', 'IGroupController', 
     'IPackageController', 'IPluginObserver',
-    'IConfigurable', 'IConfigurer', 'IAuthorizer'
+    'IConfigurable', 'IConfigurer', 'IAuthorizer',
+    'IActions'
 ]
 
 from inspect import isclass
@@ -300,4 +301,12 @@
         other Authorizers to run; True will shortcircuit and return.
         """
         
-
+class IActions(Interface):
+    """
+    Allow adding of actions to the logic layer.
+    """
+    def get_actions(self):
+        """
+        Should return a dict, the keys being the name of the logic 
+        function and the values being the functions themselves.
+        """


--- a/ckan/public/css/ckan.css	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/public/css/ckan.css	Wed Jul 27 17:11:20 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 27 17:03:47 2011 +0100
+++ b/ckan/templates/package/history.html	Wed Jul 27 17:11:20 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 27 17:03:47 2011 +0100
+++ b/ckan/templates/package/read.html	Wed Jul 27 17:11:20 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/api/model/test_package.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/tests/functional/api/model/test_package.py	Wed Jul 27 17:11:20 2011 +0100
@@ -48,6 +48,7 @@
 
         # Check the value of the Location header.
         location = res.header('Location')
+        
         assert offset in location
         res = self.app.get(location, status=self.STATUS_200_OK)
         # Check the database record.


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/tests/functional/api/test_action.py	Wed Jul 27 17:11:20 2011 +0100
@@ -0,0 +1,72 @@
+from ckan.lib.create_test_data import CreateTestData
+import ckan.model as model
+from ckan.tests import WsgiAppCase
+import json
+from pprint import pprint, pformat
+
+class TestAction(WsgiAppCase):
+
+    @classmethod
+    def setup_class(self):
+        CreateTestData.create()
+
+    @classmethod
+    def teardown_class(self):
+        model.repo.rebuild_db()
+
+    def test_01_package_list(self):
+        postparams = '%s=1' % json.dumps({})
+        res = self.app.post('/api/action/package_list', params=postparams)
+        assert json.loads(res.body) == {"help": "Lists the package by name",
+                                        "success": True,
+                                        "result": ["annakarenina", "warandpeace"]}
+
+    def test_02_create_update_package(self):
+
+        package = {
+            'author': None,
+            'author_email': None,
+            'extras': [{'key': u'original media','value': u'"book"'}],
+            'license_id': u'other-open',
+            'maintainer': None,
+            'maintainer_email': None,
+            'name': u'annakareninanew',
+            'notes': u'Some test now',
+            'resources': [{'alt_url': u'alt123',
+                           'description': u'Full text.',
+                           'extras': {u'alt_url': u'alt123', u'size': u'123'},
+                           'format': u'plain text',
+                           'hash': u'abc123',
+                           'position': 0,
+                           'url': u'http://www.annakarenina.com/download/'},
+                          {'alt_url': u'alt345',
+                           'description': u'Index of the novel',
+                           'extras': {u'alt_url': u'alt345', u'size': u'345'},
+                           'format': u'json',
+                           'hash': u'def456',
+                           'position': 1,
+                           'url': u'http://www.annakarenina.com/index.json'}],
+            'tags': [{'name': u'russian'}, {'name': u'tolstoy'}],
+            'title': u'A Novel By Tolstoy',
+            'url': u'http://www.annakarenina.com',
+            'version': u'0.7a'
+        }
+
+        wee = json.dumps(package)
+        postparams = '%s=1' % json.dumps(package)
+        res = self.app.post('/api/action/package_create', params=postparams,
+                            extra_environ={'Authorization': 'tester'})
+        package_created = json.loads(res.body)['result']
+        print package_created
+        package_created['name'] = 'moo'
+        postparams = '%s=1' % json.dumps(package_created)
+        res = self.app.post('/api/action/package_update', params=postparams,
+                            extra_environ={'Authorization': 'tester'})
+
+        package_updated = json.loads(res.body)['result']
+        package_updated.pop('revision_id')
+        package_updated.pop('revision_timestamp')
+        package_created.pop('revision_id')
+        package_created.pop('revision_timestamp')
+        assert package_updated == package_created#, (pformat(json.loads(res.body)), pformat(package_created['result']))
+


--- a/ckan/tests/functional/test_package.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/tests/functional/test_package.py	Wed Jul 27 17:11:20 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/ckan/tests/lib/test_dictization.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/ckan/tests/lib/test_dictization.py	Wed Jul 27 17:11:20 2011 +0100
@@ -516,9 +516,8 @@
         anna1 = model.Session.query(model.Package).filter_by(name='annakarenina_changed2').one()
         context = {"model": model,
                    "session": model.Session,
-                   'user': 'testsysadmin',
-                   "id": anna1.id}
-        make_latest_pending_package_active(context)
+                   'user': 'testsysadmin'}
+        make_latest_pending_package_active(context, {'id': anna1.id})
 
         pkgrevisions = model.Session.query(model.PackageRevision).filter_by(id=anna1.id).all()
         sorted_packages = sorted(pkgrevisions, key=lambda x:x.revision_timestamp)[::-1]


--- a/setup.py	Wed Jul 27 17:03:47 2011 +0100
+++ b/setup.py	Wed Jul 27 17:11:20 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
 


http://bitbucket.org/okfn/ckan/changeset/5beecbb1a01a/
changeset:   5beecbb1a01a
user:        John Glover
date:        2011-07-27 18:15:34
summary:     [merge] moderated edits branch
affected #:  9 files (3.4 KB)

--- a/ckan/controllers/package.py	Wed Jul 27 11:33:48 2011 +0100
+++ b/ckan/controllers/package.py	Wed Jul 27 17:15:34 2011 +0100
@@ -506,6 +506,7 @@
         pkg = model.Package.get(id)
         if pkg is None:
             abort(404, gettext('Package not found'))
+        c.pkg = pkg # needed to add in the tab bar to the top of the auth page
         c.pkgname = pkg.name
         c.pkgtitle = pkg.title
 


--- a/ckan/controllers/tag.py	Wed Jul 27 11:33:48 2011 +0100
+++ b/ckan/controllers/tag.py	Wed Jul 27 17:15:34 2011 +0100
@@ -49,9 +49,6 @@
     def read(self, id):
         query = model.Session.query(model.Tag)
         query = query.filter(model.Tag.name==id)
-        query = query.options(eagerload_all('package_tags.package'))
-        query = query.options(eagerload_all('package_tags.package.package_tags.tag'))
-        query = query.options(eagerload_all('package_tags.package.resource_groups_all.resources_all'))
         c.tag = query.first()
         if c.tag is None:
             abort(404)


--- a/ckan/lib/dictization/model_save.py	Wed Jul 27 11:33:48 2011 +0100
+++ b/ckan/lib/dictization/model_save.py	Wed Jul 27 17:15:34 2011 +0100
@@ -5,17 +5,15 @@
 ##package saving
 
 def resource_dict_save(res_dict, context):
-
     model = context["model"]
     session = context["session"]
 
-    obj = None
-
+    # try to get resource object directly from context, then by ID
+    # if not found, create a new resource object
     id = res_dict.get("id")
-    
-    if id:
+    obj = context.get("resource")
+    if (not obj) and id:
         obj = session.query(model.Resource).get(id)
-
     if not obj:
         obj = model.Resource()
 
@@ -30,14 +28,17 @@
         if key in fields:
             setattr(obj, key, value)
         else:
+            # resources save extras directly onto the object, instead
+            # of in a separate extras field like packages and groups
             obj.extras[key] = value
 
     if context.get('pending'):
         if session.is_modified(obj, include_collections=False):
-            obj.state = 'pending'
+            obj.state = u'pending'
+    else:
+        obj.state = u'active'
 
     session.add(obj)
-
     return obj
 
 def package_resource_list_save(res_dicts, package, context):


--- a/ckan/logic/action/get.py	Wed Jul 27 11:33:48 2011 +0100
+++ b/ckan/logic/action/get.py	Wed Jul 27 17:15:34 2011 +0100
@@ -1,5 +1,6 @@
 from sqlalchemy.sql import select
 from ckan.logic import NotFound, check_access
+from ckan.model import Session
 from ckan.plugins import (PluginImplementations,
                           IGroupController,
                           IPackageController)
@@ -21,7 +22,9 @@
     api = context.get("api_version", '1')
     ref_package_by = 'id' if api == '2' else 'name'
 
-    query = ckan.authz.Authorizer().authorized_query(user, model.Package)
+    query = Session.query(model.PackageRevision)
+    query = query.filter(model.PackageRevision.state=='active')
+    query = query.filter(model.PackageRevision.current==True)
     packages = query.all()
     return [getattr(p, ref_package_by) for p in packages]
 
@@ -30,7 +33,7 @@
     user = context["user"]
     limit = data_dict.get("limit")
 
-    q = ckan.authz.Authorizer().authorized_query(user, model.PackageRevision)
+    q = Session.query(model.PackageRevision)
     q = q.filter(model.PackageRevision.state=='active')
     q = q.filter(model.PackageRevision.current==True)
 
@@ -50,8 +53,12 @@
         result_dict["resources"] = resource_list_dictize(result, context)
         license_id = result_dict['license_id']
         if license_id:
-            isopen = model.Package.get_license_register()[license_id].isopen()
-            result_dict['isopen'] = isopen
+            try:
+                isopen = model.Package.get_license_register()[license_id].isopen()
+                result_dict['isopen'] = isopen
+            except KeyError:
+                # TODO: create a log message this error?
+                result_dict['isopen'] = False 
         else:
             result_dict['isopen'] = False
         package_list.append(result_dict)


--- a/ckan/logic/action/update.py	Wed Jul 27 11:33:48 2011 +0100
+++ b/ckan/logic/action/update.py	Wed Jul 27 17:15:34 2011 +0100
@@ -9,15 +9,18 @@
 from ckan.lib.dictization.model_dictize import (package_dictize,
                                                 package_to_api1,
                                                 package_to_api2,
+                                                resource_dictize,
                                                 group_dictize,
                                                 group_to_api1,
                                                 group_to_api2)
 from ckan.lib.dictization.model_save import (group_api_to_dict,
                                              package_api_to_dict,
                                              group_dict_save,
-                                             package_dict_save)
+                                             package_dict_save,
+                                             resource_dict_save)
 from ckan.logic.schema import (default_update_group_schema,
-                               default_update_package_schema)
+                               default_update_package_schema,
+                               default_update_resource_schema)
 from ckan.lib.navl.dictization_functions import validate
 log = logging.getLogger(__name__)
 
@@ -39,6 +42,18 @@
             error_summary[_(prettify(key))] = error[0]
     return error_summary
 
+def resource_error_summary(error_dict):
+
+    error_summary = {}
+    for key, error in error_dict.iteritems():
+        if key == 'extras':
+            error_summary[_('Extras')] = _('Missing Value')
+        elif key == 'extras_validation':
+            error_summary[_('Extras')] = error[0]
+        else:
+            error_summary[_(prettify(key))] = error[0]
+    return error_summary
+
 def group_error_summary(error_dict):
 
     error_summary = {}
@@ -146,6 +161,50 @@
     session.remove()        
 
 
+def resource_update(context, data_dict):
+    model = context['model']
+    session = context['session']
+    user = context['user']
+    id = context["id"]
+    schema = context.get('schema') or default_update_resource_schema()
+    model.Session.remove()
+
+    resource = model.Resource.get(id)
+    context["resource"] = resource
+
+    if not resource:
+        raise NotFound(_('Resource was not found.'))
+    context["id"] = resource.id
+
+    # TODO: can check_access be used against a resource?
+    query = session.query(model.Package
+    ).join(model.ResourceGroup
+    ).join(model.Resource
+    ).filter(model.ResourceGroup.id == resource.resource_group_id)
+    pkg = query.first()
+    if not pkg:
+        raise NotFound(_('No package found for this resource, cannot check auth.'))
+
+    check_access(pkg, model.Action.EDIT, context)
+
+    data, errors = validate(data_dict, schema, context)
+
+    if errors:
+        model.Session.rollback()
+        raise ValidationError(errors, resource_error_summary(errors))
+
+    rev = model.repo.new_revision()
+    rev.author = user
+    if 'message' in context:
+        rev.message = context['message']
+    else:
+        rev.message = _(u'REST API: Update object %s') % data.get("name")
+
+    resource = resource_dict_save(data, context)
+    model.repo.commit()        
+    return resource_dictize(resource, context)
+
+
 def package_update(context, data_dict):
     model = context['model']
     user = context['user']


--- a/ckan/logic/schema.py	Wed Jul 27 11:33:48 2011 +0100
+++ b/ckan/logic/schema.py	Wed Jul 27 17:15:34 2011 +0100
@@ -44,6 +44,10 @@
 
     return schema
 
+def default_update_resource_schema():
+    schema = default_resource_schema()
+    return schema
+
 def default_tags_schema():
 
     schema = {


--- a/ckan/model/resource.py	Wed Jul 27 11:33:48 2011 +0100
+++ b/ckan/model/resource.py	Wed Jul 27 17:15:34 2011 +0100
@@ -81,6 +81,15 @@
         if self.resource_group and not core_columns_only:
             _dict["package_id"] = self.resource_group.package_id
         return _dict
+
+    @classmethod
+    def get(cls, reference):
+        '''Returns a resource object referenced by its id.'''
+        query = Session.query(ResourceRevision).filter(ResourceRevision.id==reference)
+        query = query.filter(and_(
+            ResourceRevision.state == u'active', ResourceRevision.current == True
+        ))
+        return query.first()
         
     @classmethod
     def get_columns(cls, extra_columns=True):


--- a/ckan/model/tag.py	Wed Jul 27 11:33:48 2011 +0100
+++ b/ckan/model/tag.py	Wed Jul 27 17:15:34 2011 +0100
@@ -1,4 +1,5 @@
 from sqlalchemy.orm import eagerload_all
+from sqlalchemy import and_
 import vdm.sqlalchemy
 
 from types import make_uuid
@@ -60,15 +61,21 @@
     @classmethod
     def all(cls):
         q = Session.query(cls)
-        q = q.distinct().join(cls.package_tags)
-        q = q.filter(PackageTag.state == 'active')
+        q = q.distinct().join(PackageTagRevision)
+        q = q.filter(and_(
+            PackageTagRevision.state == 'active', PackageTagRevision.current == True
+        ))
         return q
 
     @property
     def packages_ordered(self):
-        ## make sure packages are active
-        packages = [package for package in self.packages 
-                    if package.state == State.ACTIVE]
+        q = Session.query(Package)
+        q = q.join(PackageTagRevision)
+        q = q.filter(PackageTagRevision.tag_id == self.id)
+        q = q.filter(and_(
+            PackageTagRevision.state == 'active', PackageTagRevision.current == True
+        ))
+        packages = [p for p in q]
         ourcmp = lambda pkg1, pkg2: cmp(pkg1.name, pkg2.name)
         return sorted(packages, cmp=ourcmp)
 


--- a/ckan/templates/layout_base.html	Wed Jul 27 11:33:48 2011 +0100
+++ b/ckan/templates/layout_base.html	Wed Jul 27 17:15:34 2011 +0100
@@ -30,7 +30,7 @@
   <![endif]--><script type="text/javascript" src="${g.site_url}/language.js"></script>
-  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
+  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js"></script><script type="text/javascript" src="http://assets.okfn.org/ext/jquery.cookie/jquery.cookie.min.js"></script><script type="text/javascript" src="http://assets.okfn.org/ext/jquery.placeholder/jquery.placeholder.js"></script><script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.11/jquery-ui.min.js"></script>

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