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

Bitbucket commits-noreply at bitbucket.org
Mon Aug 8 17:23:51 UTC 2011


3 new changesets in ckan:

http://bitbucket.org/okfn/ckan/changeset/5e662137c341/
changeset:   5e662137c341
branch:      feature-1253-authz-refactor
user:        amercader
date:        2011-08-08 16:48:28
summary:     [logic,authz] Fix auth for delete actions. Package ids are now stored in data_dict rather than context
affected #:  5 files (418 bytes)

--- a/ckan/controllers/api.py	Fri Aug 05 18:17:13 2011 +0100
+++ b/ckan/controllers/api.py	Mon Aug 08 15:48:28 2011 +0100
@@ -337,8 +337,10 @@
             action_map[('package', type)] = delete.package_relationship_delete
 
         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}
+
         log.debug('delete %s/%s/%s/%s' % (register, id, subregister, id2))
 
         action = action_map.get((register, subregister)) 
@@ -349,7 +351,7 @@
                 gettext('Cannot delete entity of this type: %s %s') %\
                 (register, subregister or ''))
         try:
-            response_data = action(context)
+            response_data = action(context, data_dict)
             return self._finish_ok(response_data)
         except NotAuthorized:
             return self._finish_not_authz()


--- a/ckan/logic/__init__.py	Fri Aug 05 18:17:13 2011 +0100
+++ b/ckan/logic/__init__.py	Mon Aug 08 15:48:28 2011 +0100
@@ -92,10 +92,9 @@
     model = context['model']
     user = context.get('user')
 
-    log.debug('check access - user %r' % user)
-    
-    if action and data_dict:
-
+    log.debug('check access - user %r, action %s' % (user,action))
+       
+    if action:
         #if action != model.Action.READ and user in (model.PSEUDO_USER__VISITOR, ''):
         #    # TODO Check the API key is valid at some point too!
         #    log.debug('Valid API key needed to make changes')
@@ -105,13 +104,12 @@
             msg = logic_authorization.get('msg','')
             raise NotAuthorized(msg)
     #TODO: Is this really necessary?
-    '''
     elif not user:
         msg = _('No valid API key provided.')
         log.debug(msg)
         raise NotAuthorized(msg)       
         #return AttributeDict(success=False, msg='No valid API key provided.')
-    '''
+
     log.debug('Access OK.')
     return True
     #return AttributeDict(success=True)


--- a/ckan/logic/action/delete.py	Fri Aug 05 18:17:13 2011 +0100
+++ b/ckan/logic/action/delete.py	Mon Aug 08 15:48:28 2011 +0100
@@ -6,18 +6,18 @@
 from ckan.plugins import PluginImplementations, IGroupController, IPackageController
 
 
-def package_delete(context):
+def package_delete(context, data_dict):
 
     model = context['model']
     user = context['user']
-    id = context["id"]
+    id = data_dict['id']
 
     entity = model.Package.get(id)
 
     if entity is None:
         raise NotFound
 
-    check_access_new('package_delete',context)
+    check_access_new('package_delete',context, data_dict)
 
     rev = model.repo.new_revision()
     rev.author = user
@@ -29,13 +29,13 @@
     model.repo.commit()
 
 
-def package_relationship_delete(context):
+def package_relationship_delete(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']
 
     pkg1 = model.Package.get(id)
     pkg2 = model.Package.get(id2)
@@ -44,7 +44,7 @@
     if not pkg2:
         return NotFound('Second package named in address was not found.')
 
-    check_access_new('package_relationship_delete', context)
+    check_access_new('package_relationship_delete', context, data_dict)
 
     existing_rels = pkg1.get_relationships_with(pkg2, rel)
     if not existing_rels:
@@ -54,7 +54,7 @@
     revisioned_details = 'Package Relationship: %s %s %s' % (id, rel, id2)
 
     context['relationship'] = relationship
-    check_access_new('relationship_delete', context)
+    check_access_new('relationship_delete', context, data_dict)
 
     rev = model.repo.new_revision()
     rev.author = user
@@ -63,20 +63,20 @@
     relationship.delete()
     model.repo.commit()
 
-def group_delete(context):
+def group_delete(context, data_dict):
 
     model = context['model']
     user = context['user']
-    id = context["id"]
+    id = data_dict['id']
 
     group = model.Group.get(id)
-    context["group"] = group
+    context['group'] = group
     if group is None:
         raise NotFound('Group was not found.')
 
     revisioned_details = 'Group: %s' % group.name
 
-    check_access_new('group_delete', context)
+    check_access_new('group_delete', context, data_dict)
 
     rev = model.repo.new_revision()
     rev.author = user


--- a/ckan/logic/auth/delete.py	Fri Aug 05 18:17:13 2011 +0100
+++ b/ckan/logic/auth/delete.py	Mon Aug 08 15:48:28 2011 +0100
@@ -7,13 +7,18 @@
 def package_delete(context, data_dict):
     model = context['model']
     user = context['user']
-    id = context['id']
-    pkg = model.Package.get(id)
+    if not 'package' in context:
+        id = data_dict.get('id',None)
+        package = model.Package.get(id)
+        if not package:
+            raise NotFound
+    else:
+        package = context['package']
 
     #TODO: model.Action.CHANGE_STATE or model.Action.PURGE?
-    authorized = check_access(pkg, model.Action.PURGE, context)
+    authorized = check_access(package, model.Action.PURGE, context)
     if not authorized:
-        return {'success': False, 'msg': _('User %s not authorized to delete package %s') % (str(user),id)}
+        return {'success': False, 'msg': _('User %s not authorized to delete package %s') % (str(user),package.id)}
     else:
         return {'success': True}
 
@@ -25,22 +30,26 @@
     user = context['user']
     relationship = context['relationship']
 
-    authorized = check_access(realtionship, model.Action.PURGE, context)
+    authorized = check_access(relationship, model.Action.PURGE, context)
     if not authorized:
-        return {'success': False, 'msg': _('User %s not authorized to delete relationship %s') % (str(user),id)}
+        return {'success': False, 'msg': _('User %s not authorized to delete relationship %s') % (str(user),relationship.id)}
     else:
         return {'success': True}
 
 def group_delete(context, data_dict):
     model = context['model']
     user = context['user']
-    #group = context['group']
-    id = context['id']
-    pkg = model.Group.get(id)
+    if not 'group' in context:
+        id = data_dict.get('id',None)
+        group = model.Group.get(id)
+        if not group:
+            raise NotFound
+    else:
+        group = context['group']
 
     authorized = check_access(group, model.Action.PURGE, context)
     if not authorized:
-        return {'success': False, 'msg': _('User %s not authorized to delete group %s') % (str(user),id)}
+        return {'success': False, 'msg': _('User %s not authorized to delete group %s') % (str(user),group.id)}
     else:
         return {'success': True}
 


--- a/ckan/new_authz.py	Fri Aug 05 18:17:13 2011 +0100
+++ b/ckan/new_authz.py	Mon Aug 08 15:48:28 2011 +0100
@@ -24,7 +24,7 @@
     # Rather than writing them out in full will use __import__
     # to load anything from ckan.auth that looks like it might
     # be an authorisation function
-    for auth_module_name in ['get', 'create', 'update']:
+    for auth_module_name in ['get', 'create', 'update','delete']:
         module_path = 'ckan.logic.auth.'+auth_module_name
         try:
             module = __import__(module_path)
@@ -37,6 +37,7 @@
         for k, v in module.__dict__.items():
             if not k.startswith('_'):
                 _auth_functions[k] = v
+
     # Then overwrite them with any specific ones in the plugins:
     resolved_auth_function_plugins = {}
     fetched_auth_functions = {}


http://bitbucket.org/okfn/ckan/changeset/1f70a265f98d/
changeset:   1f70a265f98d
branch:      feature-1253-authz-refactor
user:        amercader
date:        2011-08-08 16:56:56
summary:     [merge] from default
affected #:  38 files (1.6 MB)

--- a/.hgtags	Mon Aug 08 15:48:28 2011 +0100
+++ b/.hgtags	Mon Aug 08 15:56:56 2011 +0100
@@ -32,3 +32,4 @@
 d83e20d807a6d5587ae5adef49e31fe48c906f9a ckan-1.3.3
 3a59aa5b63d06dde77424e3313433a1fb9eb1215 ckan-1.4
 a394ca150daac6977114ce5d8c911f7d9896d1cd ckan-1.4.1
+445917818a652787cae2457f2d8d871fadfc39bb ckan-1.4.2


--- a/CHANGELOG.txt	Mon Aug 08 15:48:28 2011 +0100
+++ b/CHANGELOG.txt	Mon Aug 08 15:56:56 2011 +0100
@@ -1,19 +1,21 @@
 CKAN CHANGELOG
 ++++++++++++++
 
-v1.4.2 2011-XX-XX
+v1.4.2 2011-08-05
 =================
 Major:
   * Packages revisions can be marked as 'moderated' (#1141)
   * Password reset facility (#1186/#1198)
 
 Minor:
-  * Viewing of a package at any revision (#1141)
+  * Viewing of a package at any revision (#1236)
   * API POSTs can be of Content-Type "application/json" as alternative to existing "application/x-www-form-urlencoded" (#1206)
   * Caching of static files (#1223)
 
 Bug fixes:
   * When you removed last row of resource table, you could't add it again - since 1.0 (#1215)
+  * Adding a tag to package that had it previously didn't work - since 1.4.1 in UI and 1.4.0 in API (#1239)
+  * Search index was not updated if you added a package to a group - since 1.1 (#1140)
   * Exception if you had any Groups and migrated between CKAN v1.0.2 to v1.2 (migration 29) - since v1.0.2 (#1205)
   * API Package edit requests returned the Package in a different format to usual - since 1.4 (#1214)
   * API error responses were not all JSON format and didn't have correct Content-Type (#1214)


--- a/ckan/controllers/api.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/controllers/api.py	Mon Aug 08 15:56:56 2011 +0100
@@ -133,6 +133,9 @@
     
     def action(self, logic_function):
         function = get_action(logic_function)
+        if not function:
+            return self._finish_bad_request(
+                gettext('Action name not known: %s') % str(logic_function))
         
         context = {'model': model, 'session': model.Session, 'user': c.user}
         model.Session()._context = context


--- a/ckan/controllers/home.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/controllers/home.py	Mon Aug 08 15:56:56 2011 +0100
@@ -49,11 +49,10 @@
         c.fields = []
         c.package_count = query.count
         c.latest_packages = current_package_list_with_resources({'model': model,
-                                                                'user': c.user,
-                                                                'limit': 5},
-                                                                 {})      
+                                                                 'user': c.user},
+                                                                 {'limit': 5})      
         return render('home/index.html', cache_key=cache_key,
-                cache_expire=cache_expires)
+                      cache_expire=cache_expires)
 
     def license(self):
         return render('home/license.html', cache_expire=cache_expires)


--- a/ckan/controllers/user.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/controllers/user.py	Mon Aug 08 15:56:56 2011 +0100
@@ -141,13 +141,18 @@
             error_summary = e.error_summary
             return self.new(data_dict, errors, error_summary)
 
-    def edit(self, id, data=None, errors=None, error_summary=None):
+    def edit(self, id=None, data=None, errors=None, error_summary=None):
         context = {'model': model, 'session': model.Session,
                    'user': c.user or c.author,
                    'preview': 'preview' in request.params,
                    'save': 'save' in request.params,
                    'schema': self._edit_form_to_db_schema(),
                    }
+        if id is None:
+            if c.userobj:
+                id = c.userobj.id
+            else:
+                abort(400, _('No user specified'))
         data_dict = {'id': id}
 
         if (context['save'] or context['preview']) and not data:
@@ -167,6 +172,8 @@
 
         except NotAuthorized:
             abort(401, _('Unauthorized to edit user %s') % '')
+        except NotFound, e:
+            abort(404, _('User not found'))
 
         user_obj = context.get('user_obj')
         


--- a/ckan/i18n/ckan.pot	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/i18n/ckan.pot	Mon Aug 08 15:56:56 2011 +0100
@@ -6,9 +6,9 @@
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: ckan 1.4.1\n"
+"Project-Id-Version: ckan 1.4.2\n"
 "Report-Msgid-Bugs-To: EMAIL at ADDRESS\n"
-"POT-Creation-Date: 2011-06-27 10:54+0100\n"
+"POT-Creation-Date: 2011-08-05 11:21+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
 "Language-Team: LANGUAGE <LL at li.org>\n"
@@ -17,87 +17,95 @@
 "Content-Transfer-Encoding: 8bit\n"
 "Generated-By: Babel 0.9.5\n"
 
-#: ckan/controllers/api.py:40 ckan/controllers/authorization_group.py:19
-#: ckan/controllers/group.py:50 ckan/controllers/home.py:23
-#: ckan/controllers/package.py:95 ckan/controllers/revision.py:22
+#: ckan/controllers/api.py:36 ckan/controllers/authorization_group.py:19
+#: ckan/controllers/group.py:50 ckan/controllers/home.py:24
+#: ckan/controllers/package.py:100 ckan/controllers/revision.py:22
 #: ckan/controllers/tag.py:17 ckan/controllers/user.py:22
 #: ckan/controllers/user.py:53 ckan/controllers/user.py:79
 msgid "Not authorized to see this page"
 msgstr ""
 
-#: ckan/controllers/api.py:96
+#: ckan/controllers/api.py:94
 msgid "Access denied"
 msgstr ""
 
-#: ckan/controllers/api.py:102
+#: ckan/controllers/api.py:100
 msgid "Not found"
 msgstr ""
 
-#: ckan/controllers/api.py:144
+#: ckan/controllers/api.py:108
+msgid "Bad request"
+msgstr ""
+
+#: ckan/controllers/api.py:150
 #, python-format
 msgid "Cannot list entity of this type: %s"
 msgstr ""
 
-#: ckan/controllers/api.py:175
+#: ckan/controllers/api.py:180
 #, python-format
 msgid "Cannot read entity of this type: %s"
 msgstr ""
 
-#: ckan/controllers/api.py:209 ckan/controllers/api.py:255
+#: ckan/controllers/api.py:214 ckan/controllers/api.py:261
 #, python-format
 msgid "JSON Error: %s"
 msgstr ""
 
-#: ckan/controllers/api.py:216
+#: ckan/controllers/api.py:221
 #, python-format
 msgid "Cannot create new entity of this type: %s %s"
 msgstr ""
 
-#: ckan/controllers/api.py:232 ckan/controllers/api.py:277
+#: ckan/controllers/api.py:238 ckan/controllers/api.py:283
 #: ckan/controllers/group.py:161 ckan/controllers/group.py:179
-#: ckan/controllers/package.py:370 ckan/controllers/package.py:397
+#: ckan/controllers/package.py:437 ckan/controllers/package.py:469
 msgid "Integrity Error"
 msgstr ""
 
-#: ckan/controllers/api.py:261
+#: ckan/controllers/api.py:267
 #, python-format
 msgid "Cannot update entity of this type: %s"
 msgstr ""
 
-#: ckan/controllers/api.py:298
+#: ckan/controllers/api.py:304
 #, python-format
 msgid "Cannot delete entity of this type: %s %s"
 msgstr ""
 
-#: ckan/controllers/api.py:321
+#: ckan/controllers/api.py:327
+msgid "No revision specified"
+msgstr ""
+
+#: ckan/controllers/api.py:331
 #, python-format
 msgid "There is no revision with id: %s"
 msgstr ""
 
-#: ckan/controllers/api.py:332
+#: ckan/controllers/api.py:341
 msgid "Missing search term ('since_id=UUID' or 'since_time=TIMESTAMP')"
 msgstr ""
 
-#: ckan/controllers/api.py:340
+#: ckan/controllers/api.py:349
 #, python-format
 msgid "Could not read parameters: %r"
 msgstr ""
 
-#: ckan/controllers/api.py:377
+#: ckan/controllers/api.py:386
 #, python-format
 msgid "Bad search option: %s"
 msgstr ""
 
-#: ckan/controllers/api.py:380
+#: ckan/controllers/api.py:389
 #, python-format
 msgid "Unknown register: %s"
 msgstr ""
 
-#: ckan/controllers/api.py:388
+#: ckan/controllers/api.py:397
 msgid "Malformed qjson value"
 msgstr ""
 
-#: ckan/controllers/api.py:397 ckan/lib/base.py:159
+#: ckan/controllers/api.py:406 ckan/lib/base.py:172
 msgid "Request params must be in form of a json encoded dictionary."
 msgstr ""
 
@@ -123,7 +131,7 @@
 msgstr ""
 
 #: ckan/controllers/authorization_group.py:155 ckan/controllers/group.py:194
-#: ckan/controllers/package.py:438
+#: ckan/controllers/package.py:510
 #, python-format
 msgid "User %r not authorized to edit %s authorizations"
 msgstr ""
@@ -134,21 +142,21 @@
 msgid "Unauthorized to read group %s"
 msgstr ""
 
-#: ckan/controllers/group.py:140 ckan/controllers/package.py:341
-#: ckan/controllers/package_formalchemy.py:97
+#: ckan/controllers/group.py:140 ckan/controllers/package.py:357
+#: ckan/controllers/package_formalchemy.py:102
 #, python-format
 msgid "User %r not authorized to edit %s"
 msgstr ""
 
 #: ckan/controllers/group.py:159 ckan/controllers/group.py:177
-#: ckan/controllers/package.py:178 ckan/controllers/package.py:214
-#: ckan/controllers/package.py:249 ckan/controllers/package.py:335
-#: ckan/controllers/package.py:368 ckan/controllers/package.py:395
-#: ckan/controllers/package.py:432
+#: ckan/controllers/package.py:196 ckan/controllers/package.py:232
+#: ckan/controllers/package.py:264 ckan/controllers/package.py:351
+#: ckan/controllers/package.py:381 ckan/controllers/package.py:435
+#: ckan/controllers/package.py:467 ckan/controllers/package.py:504
 msgid "Package not found"
 msgstr ""
 
-#: ckan/controllers/group.py:412 ckan/controllers/package.py:242
+#: ckan/controllers/group.py:412 ckan/controllers/package.py:257
 msgid "Select two revisions before doing the comparison."
 msgstr ""
 
@@ -160,46 +168,46 @@
 msgid "Recent changes to CKAN Group: "
 msgstr ""
 
-#: ckan/controllers/group.py:450 ckan/controllers/package.py:276
+#: ckan/controllers/group.py:450 ckan/controllers/package.py:291
 msgid "Log message: "
 msgstr ""
 
-#: ckan/controllers/home.py:73
+#: ckan/controllers/home.py:74
 msgid "Invalid language specified"
 msgstr ""
 
-#: ckan/controllers/home.py:74
+#: ckan/controllers/home.py:75
 msgid "Language has been set to: English"
 msgstr ""
 
-#: ckan/controllers/home.py:76
+#: ckan/controllers/home.py:77
 msgid "No language given!"
 msgstr ""
 
-#: ckan/controllers/package.py:201 ckan/controllers/package.py:222
-#: ckan/controllers/package.py:333 ckan/controllers/package.py:366
-#: ckan/controllers/package.py:393
+#: ckan/controllers/package.py:198 ckan/controllers/package.py:234
+#: ckan/controllers/package.py:349 ckan/controllers/package.py:379
+#: ckan/controllers/package.py:433 ckan/controllers/package.py:465
 #, python-format
 msgid "Unauthorized to read package %s"
 msgstr ""
 
-#: ckan/controllers/package.py:255
+#: ckan/controllers/package.py:270
 msgid "CKAN Package Revision History"
 msgstr ""
 
-#: ckan/controllers/package.py:257
+#: ckan/controllers/package.py:272
 msgid "Recent changes to CKAN Package: "
 msgstr ""
 
-#: ckan/controllers/package.py:301 ckan/controllers/package_formalchemy.py:21
+#: ckan/controllers/package.py:316 ckan/controllers/package_formalchemy.py:23
 msgid "Unauthorized to create a package"
 msgstr ""
 
-#: ckan/controllers/package.py:659
+#: ckan/controllers/package.py:731
 msgid "Package Not Found"
 msgstr ""
 
-#: ckan/controllers/package.py:666
+#: ckan/controllers/package.py:738
 msgid "Rating value invalid"
 msgstr ""
 
@@ -263,11 +271,38 @@
 msgid "Your account has been updated."
 msgstr ""
 
-#: ckan/controllers/user.py:201
+#: ckan/controllers/user.py:197
+#, python-format
+msgid "\"%s\" matched several users"
+msgstr ""
+
+#: ckan/controllers/user.py:200
+#, python-format
+msgid "No such user: %s"
+msgstr ""
+
+#: ckan/controllers/user.py:204
+msgid "Please check your inbox for a reset code."
+msgstr ""
+
+#: ckan/controllers/user.py:207
+#, python-format
+msgid "Could not send reset link: %s"
+msgstr ""
+
+#: ckan/controllers/user.py:216
+msgid "Invalid reset key. Please try again."
+msgstr ""
+
+#: ckan/controllers/user.py:224
+msgid "Your password has been reset."
+msgstr ""
+
+#: ckan/controllers/user.py:244
 msgid "Your password must be 4 characters or longer."
 msgstr ""
 
-#: ckan/controllers/user.py:203
+#: ckan/controllers/user.py:246
 msgid "The passwords you entered do not match."
 msgstr ""
 
@@ -357,8 +392,8 @@
 msgstr ""
 
 #: ckan/forms/group.py:64 ckan/forms/package.py:102 ckan/forms/package.py:112
-#: ckan/logic/action/update.py:29 ckan/logic/action/update.py:31
-#: ckan/logic/action/update.py:41 ckan/logic/action/update.py:43
+#: ckan/logic/action/update.py:35 ckan/logic/action/update.py:37
+#: ckan/logic/action/update.py:47 ckan/logic/action/update.py:49
 #: ckan/templates/group/new_group_form.html:41
 #: ckan/templates/package/new_package_form.html:154
 msgid "Extras"
@@ -499,7 +534,7 @@
 msgstr ""
 
 #: ckan/forms/package.py:96 ckan/forms/package.py:111
-#: ckan/logic/action/update.py:27 ckan/templates/package/new_package_form.html:67
+#: ckan/logic/action/update.py:33 ckan/templates/package/new_package_form.html:67
 #: ckan/templates/package/read_core.html:60
 msgid "Resources"
 msgstr ""
@@ -516,8 +551,8 @@
 msgid "Detail"
 msgstr ""
 
-#: ckan/forms/package.py:110 ckan/templates/_util.html:97
-#: ckan/templates/_util.html:110 ckan/templates/group/new_group_form.html:23
+#: ckan/forms/package.py:110 ckan/templates/_util.html:147
+#: ckan/templates/_util.html:160 ckan/templates/group/new_group_form.html:23
 #: ckan/templates/package/new_package_form.html:19
 msgid "Title"
 msgstr ""
@@ -530,7 +565,7 @@
 msgid "URL"
 msgstr ""
 
-#: ckan/forms/package.py:111 ckan/templates/_util.html:282
+#: ckan/forms/package.py:111 ckan/templates/_util.html:332
 #: ckan/templates/group/history.html:35 ckan/templates/package/history.html:41
 #: ckan/templates/package/new_package_form.html:123
 msgid "Author"
@@ -572,20 +607,59 @@
 msgid "Key blank"
 msgstr ""
 
-#: ckan/lib/base.py:152
+#: ckan/lib/base.py:154
 #, python-format
 msgid "Could not find the POST data: %r : %s"
 msgstr ""
 
-#: ckan/lib/package_saver.py:40
+#: ckan/lib/base.py:162
+#, python-format
+msgid "Could not extract request body data: %s"
+msgstr ""
+
+#: ckan/lib/base.py:167
+msgid "No request body data"
+msgstr ""
+
+#: ckan/lib/mailer.py:21
+#, python-format
+msgid "Dear %s,"
+msgstr ""
+
+#: ckan/lib/mailer.py:34
+#, python-format
+msgid "%s <%s>"
+msgstr ""
+
+#: ckan/lib/mailer.py:57
+msgid "No recipient email address available!"
+msgstr ""
+
+#: ckan/lib/mailer.py:62
+#, python-format
+msgid ""
+"You have requested your password on %(site_title)s to be reset.\n"
+"\n"
+"Please click the following link to confirm this request:\n"
+"\n"
+"   %(reset_link)s\n"
+msgstr ""
+
+#: ckan/lib/mailer.py:94 ckan/templates/user/login.html:26
+#: ckan/templates/user/perform_reset.html:6
+#: ckan/templates/user/perform_reset.html:18
+msgid "Reset your password"
+msgstr ""
+
+#: ckan/lib/package_saver.py:44
 msgid "Cannot render package description"
 msgstr ""
 
-#: ckan/lib/package_saver.py:44
+#: ckan/lib/package_saver.py:49
 msgid "No web page given"
 msgstr ""
 
-#: ckan/lib/package_saver.py:141 ckan/logic/validators.py:20
+#: ckan/lib/package_saver.py:151 ckan/logic/validators.py:20
 msgid "No links are allowed in the log_message."
 msgstr ""
 
@@ -595,39 +669,38 @@
 msgstr ""
 
 #: ckan/logic/validators.py:30 ckan/logic/validators.py:56
-#: ckan/logic/action/update.py:86
+#: ckan/logic/action/update.py:161
 msgid "Package was not found."
 msgstr ""
 
-#: ckan/logic/validators.py:41 ckan/logic/action/create.py:182
+#: ckan/logic/validators.py:41 ckan/logic/action/create.py:185
 #, python-format
 msgid "Package with name %r does not exist."
 msgstr ""
 
-#: ckan/logic/action/create.py:56 ckan/logic/action/create.py:143
-#: ckan/logic/action/update.py:103 ckan/logic/action/update.py:189
+#: ckan/logic/action/create.py:56 ckan/logic/action/create.py:146
 #, python-format
 msgid "REST API: Create object %s"
 msgstr ""
 
-#: ckan/logic/action/create.py:118
+#: ckan/logic/action/create.py:121
 #, python-format
 msgid "REST API: Create package relationship: %s %s %s"
 msgstr ""
 
-#: ckan/logic/action/create.py:169
+#: ckan/logic/action/create.py:172
 msgid "You must supply a package id or name (parameter \"package\")."
 msgstr ""
 
-#: ckan/logic/action/create.py:171
+#: ckan/logic/action/create.py:174
 msgid "You must supply a rating (parameter \"rating\")."
 msgstr ""
 
-#: ckan/logic/action/create.py:176
+#: ckan/logic/action/create.py:179
 msgid "Rating must be an integer value."
 msgstr ""
 
-#: ckan/logic/action/create.py:180
+#: ckan/logic/action/create.py:183
 #, python-format
 msgid "Rating must be between %i and %i."
 msgstr ""
@@ -642,19 +715,24 @@
 msgid "REST API: Delete %s"
 msgstr ""
 
-#: ckan/logic/action/update.py:27
+#: ckan/logic/action/update.py:33
 msgid "Package resource(s) incomplete"
 msgstr ""
 
-#: ckan/logic/action/update.py:29 ckan/logic/action/update.py:41
+#: ckan/logic/action/update.py:35 ckan/logic/action/update.py:47
 msgid "Missing Value"
 msgstr ""
 
-#: ckan/logic/action/update.py:64
+#: ckan/logic/action/update.py:70
 msgid "Group was not found."
 msgstr ""
 
-#: ckan/logic/action/update.py:123
+#: ckan/logic/action/update.py:180 ckan/logic/action/update.py:264
+#, python-format
+msgid "REST API: Update object %s"
+msgstr ""
+
+#: ckan/logic/action/update.py:200
 #, python-format
 msgid "REST API: Update package relationship: %s %s %s"
 msgstr ""
@@ -705,93 +783,96 @@
 msgstr ""
 
 #: ckan/templates/_util.html:65 ckan/templates/_util.html:70
+#: ckan/templates/_util.html:115 ckan/templates/_util.html:120
 msgid "This package satisfies the Open Definition."
 msgstr ""
 
-#: ckan/templates/_util.html:66 ckan/templates/package/read.html:75
+#: ckan/templates/_util.html:66 ckan/templates/_util.html:116
+#: ckan/templates/package/read.html:75
 msgid "[Open Data]"
 msgstr ""
 
-#: ckan/templates/_util.html:71 ckan/templates/package/read.html:79
+#: ckan/templates/_util.html:71 ckan/templates/_util.html:121
+#: ckan/templates/package/read.html:79
 msgid "[Open Content]"
 msgstr ""
 
-#: ckan/templates/_util.html:78
+#: ckan/templates/_util.html:78 ckan/templates/_util.html:128
 msgid "Not Openly Licensed"
 msgstr ""
 
-#: ckan/templates/_util.html:97
+#: ckan/templates/_util.html:147
 msgid "Number of packages"
 msgstr ""
 
-#: ckan/templates/_util.html:97 ckan/templates/group/new_group_form.html:27
+#: ckan/templates/_util.html:147 ckan/templates/group/new_group_form.html:27
 #: ckan/templates/package/new_package_form.html:73
 #: ckan/templates/package/new_package_form.html:92
 #: ckan/templates/package/read_core.html:36
 msgid "Description"
 msgstr ""
 
-#: ckan/templates/_util.html:110
+#: ckan/templates/_util.html:160
 msgid "Number of members"
 msgstr ""
 
-#: ckan/templates/_util.html:130
+#: ckan/templates/_util.html:180
 msgid "View package resources"
 msgstr ""
 
-#: ckan/templates/_util.html:130
+#: ckan/templates/_util.html:180
 msgid "DOWNLOAD"
 msgstr ""
 
-#: ckan/templates/_util.html:133
+#: ckan/templates/_util.html:183
 msgid "No downloadable resources."
 msgstr ""
 
-#: ckan/templates/_util.html:151
+#: ckan/templates/_util.html:201
 msgid "no ratings yet"
 msgstr ""
 
-#: ckan/templates/_util.html:152
+#: ckan/templates/_util.html:202
 msgid ""
 "–\n"
 "    rate it now"
 msgstr ""
 
-#: ckan/templates/_util.html:170 ckan/templates/_util.html:240
+#: ckan/templates/_util.html:220 ckan/templates/_util.html:290
 msgid "User"
 msgstr ""
 
-#: ckan/templates/_util.html:205 ckan/templates/_util.html:261
+#: ckan/templates/_util.html:255 ckan/templates/_util.html:311
 msgid "User Group"
 msgstr ""
 
-#: ckan/templates/_util.html:282 ckan/templates/group/history.html:35
+#: ckan/templates/_util.html:332 ckan/templates/group/history.html:35
 #: ckan/templates/package/history.html:41 ckan/templates/revision/read.html:5
 msgid "Revision"
 msgstr ""
 
-#: ckan/templates/_util.html:282 ckan/templates/group/history.html:35
+#: ckan/templates/_util.html:332 ckan/templates/group/history.html:35
 #: ckan/templates/package/history.html:41
 msgid "Timestamp"
 msgstr ""
 
-#: ckan/templates/_util.html:282
+#: ckan/templates/_util.html:332
 msgid "Entity"
 msgstr ""
 
-#: ckan/templates/_util.html:282 ckan/templates/group/history.html:35
+#: ckan/templates/_util.html:332 ckan/templates/group/history.html:35
 #: ckan/templates/package/history.html:41
 msgid "Log Message"
 msgstr ""
 
-#: ckan/templates/_util.html:306 ckan/templates/group/new_group_form.html:49
+#: ckan/templates/_util.html:356 ckan/templates/group/new_group_form.html:49
 #: ckan/templates/package/form_extra_fields.html:22
 #: ckan/templates/package/new_package_form.html:162
 #: ckan/templates/revision/read.html:20
 msgid "Delete"
 msgstr ""
 
-#: ckan/templates/_util.html:309 ckan/templates/revision/read.html:23
+#: ckan/templates/_util.html:359 ckan/templates/revision/read.html:23
 msgid "Undelete"
 msgstr ""
 
@@ -807,8 +888,8 @@
 msgid "Logout"
 msgstr ""
 
-#: ckan/templates/layout_base.html:80 ckan/templates/user/login.html:34
-#: ckan/templates/user/login.html:47
+#: ckan/templates/layout_base.html:80 ckan/templates/user/login.html:35
+#: ckan/templates/user/login.html:48
 msgid "Login"
 msgstr ""
 
@@ -896,7 +977,7 @@
 msgid "Contact Us"
 msgstr ""
 
-#: ckan/templates/layout_base.html:210 ckan/templates/user/login.html:26
+#: ckan/templates/layout_base.html:210 ckan/templates/user/login.html:27
 msgid "Privacy Policy"
 msgstr ""
 
@@ -955,7 +1036,7 @@
 #: ckan/templates/group/authz.html:16 ckan/templates/group/authz.html:34
 #: ckan/templates/group/edit_form.html:23 ckan/templates/package/authz.html:16
 #: ckan/templates/package/authz.html:34 ckan/templates/package/edit_form.html:29
-#: ckan/templates/user/edit.html:45
+#: ckan/templates/user/edit.html:45 ckan/templates/user/perform_reset.html:27
 msgid "Save"
 msgstr ""
 
@@ -2031,12 +2112,13 @@
 msgid "Change your password"
 msgstr ""
 
-#: ckan/templates/user/edit.html:32 ckan/templates/user/login.html:43
-#: ckan/templates/user/register.html:37
+#: ckan/templates/user/edit.html:32 ckan/templates/user/login.html:44
+#: ckan/templates/user/perform_reset.html:19 ckan/templates/user/register.html:37
 msgid "Password:"
 msgstr ""
 
-#: ckan/templates/user/edit.html:35 ckan/templates/user/register.html:40
+#: ckan/templates/user/edit.html:35 ckan/templates/user/perform_reset.html:22
+#: ckan/templates/user/register.html:40
 msgid "Password (repeat):"
 msgstr ""
 
@@ -2077,23 +2159,27 @@
 msgid "Join CKAN to contribute packages under your own name."
 msgstr ""
 
-#: ckan/templates/user/login.html:31
+#: ckan/templates/user/login.html:32
 msgid "Login - User"
 msgstr ""
 
-#: ckan/templates/user/login.html:40 ckan/templates/user/register.html:28
+#: ckan/templates/user/login.html:41 ckan/templates/user/register.html:28
 msgid "Login:"
 msgstr ""
 
-#: ckan/templates/user/login.html:54
+#: ckan/templates/user/login.html:49
+msgid "Forgot your password?"
+msgstr ""
+
+#: ckan/templates/user/login.html:56
 msgid "Login using Open ID"
 msgstr ""
 
-#: ckan/templates/user/login.html:56
+#: ckan/templates/user/login.html:58
 msgid "Please click your account provider:"
 msgstr ""
 
-#: ckan/templates/user/login.html:64
+#: ckan/templates/user/login.html:66
 msgid ""
 "OpenID is service that allows you to log-on to many different websites using "
 "a single identity.\n"
@@ -2101,11 +2187,11 @@
 "account]."
 msgstr ""
 
-#: ckan/templates/user/login.html:71
+#: ckan/templates/user/login.html:73
 msgid "Don't have an OpenID?"
 msgstr ""
 
-#: ckan/templates/user/login.html:72
+#: ckan/templates/user/login.html:74
 msgid ""
 "OpenID is service that allows you to log-on to many different websites\n"
 "        using a single identity. Find out [1:more\n"
@@ -2204,3 +2290,17 @@
 msgid "Sign up"
 msgstr ""
 
+#: ckan/templates/user/request_reset.html:6
+#: ckan/templates/user/request_reset.html:22
+#: ckan/templates/user/request_reset.html:28
+msgid "Reset password"
+msgstr ""
+
+#: ckan/templates/user/request_reset.html:13
+msgid "Request a password reset"
+msgstr ""
+
+#: ckan/templates/user/request_reset.html:23
+msgid "User name:"
+msgstr ""
+


--- a/ckan/lib/base.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/lib/base.py	Mon Aug 08 15:56:56 2011 +0100
@@ -99,6 +99,13 @@
         if c.user:
             c.user = c.user.decode('utf8')
             c.userobj = model.User.by_name(c.user)
+            if c.userobj is None:
+                # This occurs when you are logged in with openid, clean db
+                # and then restart i.e. only really for testers. There is no
+                # user object, so even though repoze thinks you are logged in
+                # and your cookie has ckan_display_name, we need to force user
+                # to login again to get the User object.
+                c.user = None
         else:
             c.userobj = self._get_user_for_apikey()
             if c.userobj is not None:


--- a/ckan/lib/dictization/model_save.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/lib/dictization/model_save.py	Mon Aug 08 15:56:56 2011 +0100
@@ -139,12 +139,19 @@
     tag_package_tag = dict((package_tag.tag, package_tag) 
                             for package_tag in
                             package.package_tag_all)
+    
+    tag_package_tag_inactive = dict(
+        [ (tag,pt) for tag,pt in tag_package_tag.items() if
+            pt.state in ['deleted', 'pending-deleted'] ]
+        )
 
     tags = set()
     for tag_dict in tag_dicts:
         obj = table_dict_save(tag_dict, model.Tag, context)
         tags.add(obj)
 
+    # 3 cases
+    # case 1: currently active but not in new list
     for tag in set(tag_package_tag.keys()) - tags:
         package_tag = tag_package_tag[tag]
         if pending and package_tag.state <> 'deleted':
@@ -152,12 +159,19 @@
         else:
             package_tag.state = 'deleted'
 
+    # in new list but never used before
     for tag in tags - set(tag_package_tag.keys()):
         state = 'pending' if pending else 'active'
         package_tag_obj = model.PackageTag(package, tag, state)
         session.add(package_tag_obj)
         tag_package_tag[tag] = package_tag_obj
 
+    # in new list and already used but in deleted state
+    for tag in tags.intersection(set(tag_package_tag_inactive.keys())):
+        state = 'pending' if pending else 'active'
+        package_tag = tag_package_tag[tag]
+        package_tag.state = state
+
     package.package_tag_all[:] = tag_package_tag.values()
 
 def package_group_list_save(group_dicts, package, context):


--- a/ckan/lib/helpers.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/lib/helpers.py	Mon Aug 08 15:56:56 2011 +0100
@@ -5,7 +5,9 @@
 Consists of functions to typically be used within templates, but also
 available to Controllers. This module is available to templates as 'h'.
 """
-from datetime import datetime
+import datetime
+import re
+
 from webhelpers.html import escape, HTML, literal, url_escape
 from webhelpers.html.tools import mail_to
 from webhelpers.html.tags import *
@@ -28,8 +30,7 @@
     import json
 except ImportError:
     import simplejson as json
-
-ISO_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.%f'
+    
 
 class Message(object):
     """A message returned by ``Flash.pop_messages()``.
@@ -210,11 +211,11 @@
     '''
     from ckan import model
     date_format = '%Y-%m-%d %H:%M'
-    if isinstance(datetime_, datetime):
+    if isinstance(datetime_, datetime.datetime):
         return datetime_.strftime(date_format)
     elif isinstance(datetime_, basestring):
         try:
-            datetime_ = model.strptimestamp(datetime_)
+            datetime_ = date_str_to_datetime(datetime_)
         except TypeError:
             return ''
         except ValueError:
@@ -223,8 +224,20 @@
     else:
         return ''
 
-def date_str_to_datetime(date_str, format=ISO_DATE_FORMAT):
-    return datetime.strptime(date_str, format)
+def datetime_to_date_str(datetime_):
+    '''Takes a datetime.datetime object and returns a string of it
+    in ISO format.
+    '''
+    return datetime_.isoformat()
 
-def time_ago_in_words_from_str(date_str, format=ISO_DATE_FORMAT, granularity='month'):
-    return date.time_ago_in_words(datetime.strptime(date_str, format), granularity=granularity)
+def date_str_to_datetime(date_str):
+    '''Takes an ISO format timestamp and returns the equivalent
+    datetime.datetime object.
+    '''
+    # Doing this split is more accepting of input variations than doing
+    # a strptime. Also avoids problem with Python 2.5 not having %f.
+    return datetime.datetime(*map(int, re.split('[^\d]', date_str)))
+
+def time_ago_in_words_from_str(date_str, granularity='month'):
+    return date.time_ago_in_words(date_str_to_datetime(date_str), granularity=granularity)
+


--- a/ckan/lib/search/worker.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/lib/search/worker.py	Mon Aug 08 15:56:56 2011 +0100
@@ -1,7 +1,9 @@
 import logging
 
+import ckan.model as model
 from ckan.model import DomainObjectOperation
 from ckan.plugins import SingletonPlugin, implements, IDomainObjectModification
+from ckan.lib.dictization.model_dictize import package_to_api1
 from common import SearchError
 
 log = logging.getLogger(__name__)
@@ -31,10 +33,11 @@
     implements(IDomainObjectModification, inherit=True)
 
     def notify(self, entity, operation):
-        
-        if hasattr(entity, 'as_dict') and operation != DomainObjectOperation.deleted:
+
+        if operation != DomainObjectOperation.deleted:
             dispatch_by_operation(entity.__class__.__name__, 
-                                  entity.as_dict(), operation)
+                                  package_to_api1(entity, {'model': model}),
+                                  operation)
         elif operation == DomainObjectOperation.deleted:
             dispatch_by_operation(entity.__class__.__name__, 
                                   {'id': entity.id}, operation)


--- a/ckan/logic/action/get.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/logic/action/get.py	Mon Aug 08 15:56:56 2011 +0100
@@ -624,7 +624,10 @@
         package_dict['resources'] = []
     license_id = package_dict['license_id']
     if license_id:
-        isopen = model.Package.get_license_register()[license_id].isopen()
+        try:
+            isopen = model.Package.get_license_register()[license_id].isopen()
+        except KeyError:
+            isopen = False
         package_dict['isopen'] = isopen
     else:
         package_dict['isopen'] = False


--- a/ckan/logic/action/update.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/logic/action/update.py	Mon Aug 08 15:56:56 2011 +0100
@@ -8,6 +8,7 @@
 from ckan.logic import check_access_new, check_access
 
 from ckan.lib.base import _
+from vdm.sqlalchemy.base import SQLAlchemySession
 from ckan.lib.dictization.model_dictize import (package_dictize,
                                                 package_to_api1,
                                                 package_to_api2,
@@ -84,7 +85,9 @@
     latest_rev.current = True
     if latest_rev.state in ('pending-deleted', 'deleted'):
         latest_rev.state = 'deleted'
+        latest_rev.continuity.state = 'deleted'
     else:
+        latest_rev.continuity.state = 'active'
         latest_rev.state = 'active'
 
     session.add(latest_rev)
@@ -104,6 +107,7 @@
 
     model = context['model']
     session = model.Session
+    SQLAlchemySession.setattr(session, 'revisioning_disabled', True)
     id = data_dict["id"]
     pkg = model.Package.get(id)
 


--- a/ckan/model/__init__.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/model/__init__.py	Mon Aug 08 15:56:56 2011 +0100
@@ -23,6 +23,7 @@
 from changeset import Changeset, Change, Changemask
 import ckan.migration
 from ckan.lib.helpers import OrderedDict
+from vdm.sqlalchemy.base import SQLAlchemySession
 
 # set up in init_model after metadata is bound
 version_table = None
@@ -180,6 +181,63 @@
         metadata.reflect()
         return bool(metadata.tables)
 
+    def purge_revision(self, revision, leave_record=False):
+        '''Purge all changes associated with a revision.
+
+        @param leave_record: if True leave revision in existence but change message
+            to "PURGED: {date-time-of-purge}". If false delete revision object as
+            well.
+
+        Summary of the Algorithm
+        ------------------------
+
+        1. list all RevisionObjects affected by this revision
+        2. check continuity objects and cascade on everything else ?
+            1. crudely get all object revisions associated with this
+            2. then check whether this is the only revision and delete the
+            continuity object
+
+            3. ALTERNATIVELY delete all associated object revisions then do a
+            select on continutity to check which have zero associated revisions
+            (should only be these ...)
+        '''
+        to_purge = []
+        SQLAlchemySession.setattr(self.session, 'revisioning_disabled', True)
+        self.session.autoflush = False
+        for o in self.versioned_objects:
+            revobj = o.__revision_class__
+            items = self.session.query(revobj).filter_by(revision=revision).all()
+            for item in items:
+                continuity = item.continuity
+
+                if continuity.revision == revision: # need to change continuity
+                    trevobjs = self.session.query(revobj).join('revision').  filter(
+                            revobj.continuity==continuity
+                            ).order_by(Revision.timestamp.desc()).all()
+                    if len(trevobjs) == 0:
+                        raise Exception('Should have at least one revision.')
+                    if len(trevobjs) == 1:
+                        to_purge.append(continuity)
+                    else:
+                        self.revert(continuity, trevobjs[1])
+                        for num, obj in enumerate(trevobjs):
+                            if num == 0:
+                                continue
+                            if 'pending' not in obj.state:
+                                obj.current = True
+                                self.session.add(obj)
+                                break
+                # now delete revision object
+                self.session.delete(item)
+            for cont in to_purge:
+                self.session.delete(cont)
+        if leave_record:
+            import datetime
+            revision.message = u'PURGED: %s' % datetime.datetime.now()
+        else:
+            self.session.delete(revision)
+        self.commit_and_remove()
+
 
 repo = Repository(metadata, Session,
         versioned_objects=[Package, PackageTag, Resource, ResourceGroup, PackageExtra, PackageGroup, Group]
@@ -226,7 +284,7 @@
                      and 7 (see datetime constructor).
     raises ValueError if any of the numbers are out of range.
     '''
-    
+    # TODO: METHOD DEPRECATED - use ckan.lib.helpers.date_str_to_datetime
     import datetime, re
     return datetime.datetime(*map(int, re.split('[^\d]', s)))
 
@@ -234,6 +292,7 @@
     '''Takes a datetime.datetime and returns it as an ISO string. For
     a pretty printed string, use ckan.lib.helpers.render_datetime.
     '''
+    # TODO: METHOD DEPRECATED - use ckan.lib.helpers.datetime_to_date_str
     return t.isoformat()
 
 def revision_as_dict(revision, include_packages=True, include_groups=True,ref_package_by='name'):


--- a/ckan/model/group.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/model/group.py	Mon Aug 08 15:56:56 2011 +0100
@@ -38,7 +38,8 @@
 class PackageGroup(vdm.sqlalchemy.RevisionedObjectMixin,
         vdm.sqlalchemy.StatefulObjectMixin,
         DomainObject):
-    pass
+    def related_packages(self):
+        return [self.package]
 
 class Group(vdm.sqlalchemy.RevisionedObjectMixin,
             vdm.sqlalchemy.StatefulObjectMixin,
@@ -154,6 +155,8 @@
 PackageGroupRevision = vdm.sqlalchemy.create_object_version(mapper, PackageGroup,
         package_group_revision_table)
 
+PackageGroupRevision.related_packages = lambda self: [self.continuity.package]
+
 
 from vdm.sqlalchemy.base import add_stateful_versioned_m2m 
 #vdm.sqlalchemy.add_stateful_versioned_m2m(Package, PackageGroup, 'groups', 'group',


--- a/ckan/model/modification.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/model/modification.py	Mon Aug 08 15:56:56 2011 +0100
@@ -52,12 +52,10 @@
                     related_packages = obj.related_packages()
                 except AttributeError:
                     continue
-                if 'pending' in obj.state:
-                    continue
                 # this is needed to sort out vdm bug where pkg.as_dict does not
                 # work when the package is deleted.
                 for package in related_packages:
-                    if package not in deleted | new:
+                    if package and package not in deleted | new:
                         changed_pkgs.add(package)
         for obj in changed_pkgs:
             self.notify(obj, DomainObjectOperation.changed)


--- a/ckan/model/package_extra.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/model/package_extra.py	Mon Aug 08 15:56:56 2011 +0100
@@ -51,6 +51,8 @@
 PackageExtraRevision= vdm.sqlalchemy.create_object_version(mapper, PackageExtra,
         extra_revision_table)
 
+PackageExtraRevision.related_packages = lambda self: [self.continuity.package]
+
 def _create_extra(key, value):
     return PackageExtra(key=unicode(key), value=value)
 


--- a/ckan/model/package_mapping.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/model/package_mapping.py	Mon Aug 08 15:56:56 2011 +0100
@@ -33,4 +33,9 @@
 PackageRevision = vdm.sqlalchemy.create_object_version(mapper, Package,
         package_revision_table)
 
+def related_packages(self):
+    return [self.continuity]
 
+PackageRevision.related_packages = related_packages
+
+


--- a/ckan/model/resource.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/model/resource.py	Mon Aug 08 15:56:56 2011 +0100
@@ -203,6 +203,9 @@
 ResourceGroupRevision = vdm.sqlalchemy.create_object_version(
     mapper, ResourceGroup, resource_group_revision_table)
 
+ResourceGroupRevision.related_packages = lambda self: [self.continuity.package]
+ResourceRevision.related_packages = lambda self: [self.continuity.resouce_group.package]
+
 import vdm.sqlalchemy.stateful
 # TODO: move this into vdm
 def add_stateful_m21(object_to_alter, m21_property_name,


--- a/ckan/model/tag.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/model/tag.py	Mon Aug 08 15:56:56 2011 +0100
@@ -132,6 +132,7 @@
 PackageTagRevision = vdm.sqlalchemy.create_object_version(mapper, PackageTag,
         package_tag_revision_table)
 
+PackageTagRevision.related_packages = lambda self: [self.continuity.package]
 
 from vdm.sqlalchemy.base import add_stateful_versioned_m2m 
 vdm.sqlalchemy.add_stateful_versioned_m2m(Package, PackageTag, 'tags', 'tag',


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/public/scripts/formatautocomplete.js	Mon Aug 08 15:56:56 2011 +0100
@@ -0,0 +1,37 @@
+(function($){
+    var url = "";
+
+    function extractDataAttributes(){
+        var el = $(this);
+        $.each(this.attributes, function(){
+            // get the autocomplete API URL
+            if(this.name === 'data-format-autocomplete-url'){
+                url = this.value;
+            }
+        });
+    }
+
+    function autoCompleteList(request, response){
+        var requestData = {'incomplete': request.term};
+
+        $.ajax({
+            url: url,
+            data: requestData,
+            dataType: 'jsonp',
+            type: 'get',
+            jsonpCallback: 'callback',
+            success: function(json){
+                var formats = [];
+                $.each(json["ResultSet"]["Result"], function(){
+                    formats.push(this["Format"]);
+                });
+                response(formats);
+            },
+        });
+    }
+
+    $(document).ready(function(){
+        $('.format-autocomplete').focus(extractDataAttributes)
+            .autocomplete({source: autoCompleteList});
+    });
+})(jQuery);


--- a/ckan/templates/package/edit.html	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/templates/package/edit.html	Mon Aug 08 15:56:56 2011 +0100
@@ -17,6 +17,9 @@
     <!-- Tagcomplete --><script type="text/javascript" src="${g.site_url}/scripts/tagcomplete.js"></script><link rel="stylesheet" href="${g.site_url}/css/tagcomplete.css" /> 
+
+    <!-- Format field autocomplete --> 
+    <script type="text/javascript" src="${g.site_url}/scripts/formatautocomplete.js"></script></py:def><div py:match="content" class="package">


--- a/ckan/templates/package/new_package_form.html	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/templates/package/new_package_form.html	Mon Aug 08 15:56:56 2011 +0100
@@ -78,8 +78,11 @@
       <py:for each="num, res in enumerate(data.get('resources', []) + [{}])"><tr><py:for each="col in c.resource_columns">
-        <td class="resource-${col}">
-          <input name="resources__${num}__${col}" type="text" value="${res.get(col, '')}" class="${'medium-width' if col=='description' else 'short'}" />
+        <td py:choose="" class="resource-${col}">
+          <input py:when="col == 'format'" name="resources__${num}__${col}" 
+                 type="text" value="${res.get(col, '')}" class="format-autocomplete short"
+                 data-format-autocomplete-url="/api/2/util/resource/format_autocomplete" />
+          <input py:otherwise="" name="resources__${num}__${col}" type="text" value="${res.get(col, '')}" class="${'medium-width' if col=='description' else 'short'}" /></td></py:for><td class="resource-id"><input name="resources__${num}__id" type="hidden" value="${res.get('id', '')}" /></td>


--- a/ckan/tests/functional/api/model/test_package.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/tests/functional/api/model/test_package.py	Mon Aug 08 15:56:56 2011 +0100
@@ -419,6 +419,37 @@
         # - title
         assert len(package.extras) == 1, package.extras
 
+    def test_entity_update_readd_tag(self):
+        name = self.package_fixture_data['name']
+        old_fixture_data = {
+            'name': name,
+            'tags': ['tag1', 'tag2']
+        }
+        new_fixture_data = {
+            'name': name,
+            'tags': ['tag1']
+        }
+        self.create_package_roles_revision(old_fixture_data)
+        offset = self.package_offset(name)
+        params = '%s=1' % self.dumps(new_fixture_data)
+        res = self.app.post(offset, params=params, status=self.STATUS_200_OK,
+                            extra_environ=self.extra_environ)
+
+        # Check the returned package is as expected
+        pkg = self.loads(res.body)
+        assert_equal(pkg['name'], new_fixture_data['name'])
+        assert_equal(pkg['tags'], ['tag1'])
+
+        package = self.get_package_by_name(new_fixture_data['name'])
+        assert len(package.tags) == 1, package.tags
+
+        # now reinstate the tag
+        params = '%s=1' % self.dumps(old_fixture_data)
+        res = self.app.post(offset, params=params, status=self.STATUS_200_OK,
+                            extra_environ=self.extra_environ)
+        pkg = self.loads(res.body)
+        assert_equal(pkg['tags'], ['tag1', 'tag2'])
+
     def test_entity_update_conflict(self):
         package1_name = self.package_fixture_data['name']
         package1_data = {'name': package1_name}


--- a/ckan/tests/functional/api/test_action.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/tests/functional/api/test_action.py	Mon Aug 08 15:56:56 2011 +0100
@@ -1,5 +1,6 @@
 import json
 from pprint import pprint, pformat
+from nose.tools import assert_equal
 
 from ckan.lib.create_test_data import CreateTestData
 import ckan.model as model
@@ -146,6 +147,27 @@
         assert 'apikey' in result
         assert 'reset_key' in result
 
+    def test_05_user_show_edits(self):
+        postparams = '%s=1' % json.dumps({'id':'tester'})
+        res = self.app.post('/api/action/user_show', params=postparams)
+        res_obj = json.loads(res.body)
+        assert res_obj['help'] == 'Shows user details'
+        assert res_obj['success'] == True
+        result = res_obj['result']
+        assert result['name'] == 'tester'
+        assert_equal(result['about'], None)
+        assert result['number_of_edits'] >= 1
+        edit = result['activity'][-1] # first edit chronologically
+        assert_equal(edit['author'], 'tester')
+        assert 'timestamp' in edit
+        assert_equal(edit['state'], 'active')
+        assert_equal(edit['approved_timestamp'], None)
+        assert_equal(set(edit['groups']), set(('roger', 'david')))
+        assert_equal(edit['state'], 'active')
+        assert edit['message'].startswith('Creating test data.')
+        assert_equal(set(edit['packages']), set(('warandpeace', 'annakarenina')))
+        assert 'id' in edit
+
     def test_06_tag_list(self):
         postparams = '%s=1' % json.dumps({})
         res = self.app.post('/api/action/tag_list', params=postparams)
@@ -185,6 +207,26 @@
         assert 'packages' in result and len(result['packages']) == 3
         assert [package['name'] for package in result['packages']].sort() == ['annakarenina', 'warandpeace', 'moo'].sort()
 
+    def test_07_tag_show_unknown_license(self):
+        # create a tagged package which has an invalid license
+        CreateTestData.create_arbitrary([{
+            'name': u'tag_test',
+            'tags': u'tolstoy',
+            'license': 'never_heard_of_it',
+            }])
+        postparams = '%s=1' % json.dumps({'id':'tolstoy'})
+        res = self.app.post('/api/action/tag_show', params=postparams)
+        res_obj = json.loads(res.body)
+        assert res_obj['success'] == True
+        result = res_obj['result']
+        for pkg in result['packages']:
+            if pkg['name'] == 'tag_test':
+                break
+        else:
+            assert 0, 'tag_test not among packages'
+        assert_equal(pkg['license_id'], 'never_heard_of_it')
+        assert_equal(pkg['isopen'], False)
+
     def test_08_user_create_not_authorized(self):
         postparams = '%s=1' % json.dumps({'name':'test_create_from_action_api', 'password':'testpass'})
         res = self.app.post('/api/action/user_create', params=postparams,
@@ -424,3 +466,11 @@
         assert res_obj['result'][0]['name'] == 'joeadmin'
         assert 'id','fullname' in res_obj['result'][0]
 
+    def test_17_bad_action(self):
+        #Empty query
+        postparams = '%s=1' % json.dumps({})
+        res = self.app.post('/api/action/bad_action_name', params=postparams,
+                            status=400)
+        res_obj = json.loads(res.body)
+        assert_equal(res_obj, u'Bad request - Action name not known: bad_action_name')
+


--- a/ckan/tests/functional/test_package.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/tests/functional/test_package.py	Mon Aug 08 15:56:56 2011 +0100
@@ -1646,7 +1646,8 @@
         self.assert_not_equal(hash_7, hash_6)
 
     def test_etags_in_response(self):
-        c.user = 'test user'
+        c.user = 'annafan'
+        c.userobj = model.User.by_name(u'annafan')
         res = self.app.get('/package/annakarenina',
                            extra_environ={'REMOTE_USER':c.user})
         anna_hash = str(PackageController._pkg_cache_key(self.anna))


--- a/ckan/tests/functional/test_user.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/tests/functional/test_user.py	Mon Aug 08 15:56:56 2011 +0100
@@ -62,6 +62,10 @@
         offset = '/user/'
         res = self.app.get(offset, status=302)
 
+    def test_user_read_me_without_id(self):
+        offset = '/user/me'
+        res = self.app.get(offset, status=302)
+
     def test_user_read_without_id_but_logged_in(self):
         user = model.User.by_name(u'annafan')
         offset = '/user/'
@@ -412,6 +416,30 @@
         main_res = self.main_div(res)
         assert new_about in main_res, main_res
 
+    def test_user_edit_no_user(self):
+        offset = url_for(controller='user', action='edit', id=None)
+        res = self.app.get(offset, status=400)
+        assert 'No user specified' in res, res
+
+    def test_user_edit_unknown_user(self):
+        offset = url_for(controller='user', action='edit', id='unknown_person')
+        res = self.app.get(offset, status=404)
+        assert 'User not found' in res, res
+
+    def test_user_edit_not_logged_in(self):
+        # create user
+        username = 'testedit'
+        about = u'Test About'
+        user = model.User.by_name(unicode(username))
+        if not user:
+            model.Session.add(model.User(name=unicode(username), about=about,
+                                         password='letmein'))
+            model.repo.commit_and_remove()
+            user = model.User.by_name(unicode(username))
+
+        offset = url_for(controller='user', action='edit', id=username)
+        res = self.app.get(offset, status=302)
+
     def test_edit_spammer(self):
         # create user
         username = 'testeditspam'


--- a/ckan/tests/lib/test_helpers.py	Mon Aug 08 15:48:28 2011 +0100
+++ b/ckan/tests/lib/test_helpers.py	Mon Aug 08 15:56:56 2011 +0100
@@ -1,5 +1,7 @@
 # -*- coding: utf-8 -*-
 import time
+import datetime
+from nose.tools import assert_equal
 
 from ckan.tests import *
 from ckan.lib import helpers as h
@@ -16,4 +18,35 @@
         
     def test_extract_markdown(self):
         assert "Data exposed" in h.markdown_extract(WITH_HTML)
-        assert "collects information" in h.markdown_extract(WITH_UNICODE)
\ No newline at end of file
+        assert "collects information" in h.markdown_extract(WITH_UNICODE)
+
+    def test_render_datetime(self):
+        res = h.render_datetime(datetime.datetime(2008, 4, 13, 20, 40, 20, 123456))
+        assert_equal(res, '2008-04-13 20:40')
+
+    def test_render_datetime_but_from_string(self):
+        res = h.render_datetime('2008-04-13T20:40:20.123456')
+        assert_equal(res, '2008-04-13 20:40')
+
+    def test_render_datetime_blank(self):
+        res = h.render_datetime(None)
+        assert_equal(res, '')
+
+    def test_datetime_to_date_str(self):
+        res = h.datetime_to_date_str(datetime.datetime(2008, 4, 13, 20, 40, 20, 123456))
+        assert_equal(res, '2008-04-13T20:40:20.123456')
+
+    def test_date_str_to_datetime(self):
+        res = h.date_str_to_datetime('2008-04-13T20:40:20.123456')
+        assert_equal(res, datetime.datetime(2008, 4, 13, 20, 40, 20, 123456))
+
+    def test_date_str_to_datetime_without_microseconds(self):
+        # This occurs in ckan.net timestamps - not sure how they appeared
+        res = h.date_str_to_datetime('2008-04-13T20:40:20')
+        assert_equal(res, datetime.datetime(2008, 4, 13, 20, 40, 20))
+
+    def test_time_ago_in_words_from_str(self):
+        two_months_ago = datetime.datetime.now() - datetime.timedelta(days=65)
+        two_months_ago_str = h.datetime_to_date_str(two_months_ago)
+        res = h.time_ago_in_words_from_str(two_months_ago_str)
+        assert_equal(res, '2 months')


--- a/doc/api.rst	Mon Aug 08 15:48:28 2011 +0100
+++ b/doc/api.rst	Mon Aug 08 15:56:56 2011 +0100
@@ -535,3 +535,16 @@
 ::
 
     {"ResultSet": {"Result": [{"Name": "russian"}]}}
+
+Similarly, there is an autocomplete API for the resource format field
+which is available at:
+
+::
+
+    /api/2/util/resource/format_autocomplete?incomplete=cs
+
+This returns:
+
+::
+
+    {"ResultSet": {"Result": [{"Format": "csv"}]}}


Binary file doc/images/virtualbox4-newvm.png has changed


Binary file doc/images/virtualbox5-vmtype.png has changed


Binary file doc/images/virtualbox6-vmloc.png has changed


Binary file doc/images/virtualbox7-startvm.png has changed


Binary file doc/images/virtualbox8-firstrun.png has changed


Binary file doc/images/virtualbox9-iso.png has changed


--- a/doc/install-from-package.rst	Mon Aug 08 15:48:28 2011 +0100
+++ b/doc/install-from-package.rst	Mon Aug 08 15:56:56 2011 +0100
@@ -4,9 +4,9 @@
 
 This section describes how to install CKAN from packages. This is the recommended and by far the easiest way to install CKAN.
 
-Package install requires you to use 64-bit Ubuntu 10.04: either locally, through a virtual machine or Amazon EC2. Your options are as follows:
+Package install requires you to use Ubuntu 10.04: either locally, through a virtual machine or Amazon EC2. Your options are as follows:
 
-* Using 64-bit Ubuntu 10.04 directly. 
+* Using Ubuntu 10.04 directly.
 * :ref:`using-virtualbox`. This is suitable if you want to host your CKAN instance on a machine running any other OS. 
 * :ref:`using-amazon`. This is suitable if you want to host your CKAN instance in the cloud, on a ready-made Ubuntu OS.
 
@@ -17,7 +17,7 @@
 Prepare your System
 --------------------
 
-CKAN runs on 64-bit Ubuntu 10.04. If you are already using Ubuntu 10.04, you can continue straight to :ref:`run-package-installer`.
+CKAN runs on Ubuntu 10.04. If you are already using Ubuntu 10.04, you can continue straight to :ref:`run-package-installer`.
 
 However, if you're not, you can either use VirtualBox to set up an Ubuntu VM on Windows, Linux, Macintosh and Solaris. Alternatively, you can use an Amazon EC2 instance.
 
@@ -36,7 +36,7 @@
 Then download the installation files. 
 
 * `Download the VirtualBox installer <http://www.virtualbox.org/wiki/Downloads>`_.
-* `Download the Ubuntu image <http://www.ubuntu.com/download/ubuntu/download>`_ - make sure you choose Ubuntu 10.04 and 64-bit.
+* `Download the Ubuntu image <http://www.ubuntu.com/download/ubuntu/download>`_ - make sure you choose Ubuntu 10.04.
 
 Install VirtualBox
 ******************
@@ -59,19 +59,16 @@
 Go to Applications and open VirtualBox, then click New:
 
 .. image:: images/virtualbox4-newvm.png
-   :width: 807px
    :alt: The VirtualBox installer - the New Virtual Machine Wizard
 
-Give your VM a name - we'll call ours ``ubuntu_ckan``. Under **OS Type**, choose **Linux** and **Ubuntu 64-bit**.
+Give your VM a name - we'll call ours ``ubuntu_ckan``. Under **OS Type**, choose **Linux** and **Ubuntu (32 or 64-bit)**.
 
 .. image:: images/virtualbox5-vmtype.png
-   :width: 807px
    :alt: The VirtualBox installer - choosing your operating system
 
 Leave the memory size as 512MB, and choose **Create new hard disk**. This will open a new wizard:
 
 .. image:: images/virtualbox6-vmloc.png
-   :width: 807px
    :alt: The VirtualBox installer - creating a new hard disk
 
 You can leave the defaults unchanged here too - click **Continue**, and then **Done**, and **Done** again, to create a new VM. 
@@ -79,19 +76,16 @@
 Next, choose your VM from the left-hand menu, and click **Start**:
 
 .. image:: images/virtualbox7-startvm.png
-   :width: 807px
    :alt: Starting your new VM
 
 This will open the First Run Wizard:
 
 .. image:: images/virtualbox8-firstrun.png
-   :width: 807px
    :alt: The VirtualBox First Run Wizard
 
 After clicking **Continue**, you'll see **Select Installation Media**. This is where we need to tell our VM to boot from Ubuntu. Click on the file icon, and find your Ubuntu ``.iso`` file: 
 
 .. image:: images/virtualbox9-iso.png
-   :width: 807px
    :alt: When you get to Select Installation Media, choose your Ubuntu .iso file
 
 Click **Done**, wait for a few seconds, and you will see your Ubuntu VM booting. 


--- a/doc/install-from-source.rst	Mon Aug 08 15:48:28 2011 +0100
+++ b/doc/install-from-source.rst	Mon Aug 08 15:56:56 2011 +0100
@@ -4,7 +4,7 @@
 
 This section describes how to install CKAN from source. This removes the requirement for Ubuntu 10.04 that exists with :doc:`install-from-package`.
 
-.. warning:: This option is more complex than :doc:`install-from-package`, so we suggest only doing it this way if you plan to work on CKAN core, or have no access to Ubuntu 10.04 via any of the suggested methods. 
+.. warning:: This option is more complex than :doc:`install-from-package`, so f you prefer simplicity we suggest installing from package. 
 
 For support during installation, please contact `the ckan-dev mailing list <http://lists.okfn.org/mailman/listinfo/ckan-dev>`_. 
 


--- a/doc/paster.rst	Mon Aug 08 15:48:28 2011 +0100
+++ b/doc/paster.rst	Mon Aug 08 15:56:56 2011 +0100
@@ -99,16 +99,38 @@
 db: Manage databases
 --------------------
 
-Lets you initialise, upgrade, and dump database files in various formats. 
+Lets you initialise, upgrade, and dump the CKAN database. 
 
-For example, to initialise the CKAN database, creating the tables that CKAN uses (note that you don't need to do this during setup if you have run ``create-test-data``)::
+Initialisation
+~~~~~~~~~~~~~~
+
+Before you can run CKAN for the first time, you need to run "db init" to create the tables in the database and the default authorization settings::
 
  paster --plugin=ckan db init --config=/etc/ckan/std/std.ini
 
-When you upgrade CKAN software by any method *other* than the package update described in :doc:`upgrade`, before you restart it, you should run 'db upgrade', to migrate the database tables if necessary::
+If you forget to do this then CKAN won't serve requests and you will see errors such as this in the logs::
+
+ ProgrammingError: (ProgrammingError) relation "user" does not exist
+
+Cleaning
+~~~~~~~~
+
+You can delete everything in the CKAN database, including the tables, to start from scratch::
+
+ paster --plugin=ckan db clean --config=/etc/ckan/std/std.ini
+
+The next logical step from this point is to do a "db init" step before starting CKAN again.
+
+Upgrade migration
+~~~~~~~~~~~~~~~~~
+
+When you upgrade CKAN software by any method *other* than the package update described in :doc:`upgrade`, before you restart it, you should run 'db upgrade', which will do any necessary migrations to the database tables::
 
  paster --plugin=ckan db upgrade --config=/etc/ckan/std/std.ini
 
+Creating dump files
+~~~~~~~~~~~~~~~~~~~
+
 For information on using ``db`` to create dumpfiles, see :doc:`database_dumps`.
 
 


http://bitbucket.org/okfn/ckan/changeset/69396d5d1eae/
changeset:   69396d5d1eae
branch:      feature-1253-authz-refactor
user:        amercader
date:        2011-08-08 19:23:29
summary:     [auth,tests] Update auth test for package listing
affected #:  1 file (226 bytes)

--- a/ckan/tests/functional/test_authz.py	Mon Aug 08 15:56:56 2011 +0100
+++ b/ckan/tests/functional/test_authz.py	Mon Aug 08 18:23:29 2011 +0100
@@ -351,11 +351,8 @@
     
     def test_list(self):
         # NB there is no listing of package in wui interface any more
-        self._test_can('list', [self.testsysadmin, self.pkggroupadmin], ['xx', 'rx', 'wx', 'rr', 'wr', 'ww'], interfaces=['rest'])
-        self._test_can('list', self.mrloggedin, ['rx', 'wx', 'rr', 'wr', 'ww'], interfaces=['rest'])
-        self._test_can('list', self.visitor, ['rr', 'wr', 'ww'], interfaces=['rest'])
-        self._test_cant('list', self.mrloggedin, ['xx'], interfaces=['rest'])
-        self._test_cant('list', self.visitor, ['xx', 'rx', 'wx'], interfaces=['rest'])
+        # NB under the new model all active packages are always visible in listings by default
+        self._test_can('list', [self.testsysadmin, self.pkggroupadmin, self.mrloggedin, self.visitor], ['xx', 'rx', 'wx', 'rr', 'wr', 'ww'], interfaces=['rest'])
 
     def test_admin_edit_deleted(self):
         self._test_can('edit', self.pkggroupadmin, ['xx', 'rx', 'wx', 'rr', 'wr', 'ww', 'deleted'])

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