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

Bitbucket commits-noreply at bitbucket.org
Fri Jun 24 14:22:19 UTC 2011


3 new changesets in ckan:

http://bitbucket.org/okfn/ckan/changeset/160f995dc3d9/
changeset:   160f995dc3d9
branch:      feature-1094-authz
user:        thejimmyg
date:        2011-06-24 13:11:45
summary:     Half way refactored code
affected #:  9 files (2.2 KB)

--- a/ckan/config/middleware.py	Thu Jun 23 17:47:40 2011 +0100
+++ b/ckan/config/middleware.py	Fri Jun 24 12:11:45 2011 +0100
@@ -58,16 +58,16 @@
     # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
     #app = QueueLogMiddleware(app)
     
-    if asbool(full_stack):
-        # Handle Python exceptions
-        app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
+    #if asbool(full_stack):
+    #    # Handle Python exceptions
+    #    app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
 
-        # Display error documents for 401, 403, 404 status codes (and
-        # 500 when debug is disabled)
-        if asbool(config['debug']):
-            app = StatusCodeRedirect(app, [400, 404])
-        else:
-            app = StatusCodeRedirect(app, [400, 404, 500])
+    #    # Display error documents for 401, 403, 404 status codes (and
+    #    # 500 when debug is disabled)
+    #    if asbool(config['debug']):
+    #        app = StatusCodeRedirect(app, [400, 404])
+    #    else:
+    #        app = StatusCodeRedirect(app, [400, 404, 500])
     
     # Initialize repoze.who
     who_parser = WhoConfig(global_conf['here'])


--- a/ckan/controllers/revision.py	Thu Jun 23 17:47:40 2011 +0100
+++ b/ckan/controllers/revision.py	Fri Jun 24 12:11:45 2011 +0100
@@ -7,26 +7,55 @@
 from ckan.lib.helpers import Page
 import ckan.authz
 from ckan.lib.cache import proxy_cache, get_cache_expires
+from ckan.logic.action.get import revision_list
+
 cache_expires = get_cache_expires(sys.modules[__name__])
 
 class RevisionController(BaseController):
 
-    def __before__(self, action, **env):
-        BaseController.__before__(self, action, **env)
-        c.revision_change_state_allowed = (
-            c.user and
-            self.authorizer.is_authorized(c.user, model.Action.CHANGE_STATE,
-                model.Revision)
-            )
-        if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
-            abort(401, _('Not authorized to see this page'))
+    #def __before__(self, action, **env):
+    #    BaseController.__before__(self, action, **env)
+    #    c.revision_change_state_allowed = (
+    #        c.user and
+    #        self.authorizer.is_authorized(c.user, model.Action.CHANGE_STATE,
+    #            model.Revision)
+    #        )
+    #    if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
+    #        abort(401, _('Not authorized to see this page'))
 
     def index(self):
         return self.list()
 
     def list(self):
-        format = request.params.get('format', '')
-        if format == 'atom':
+        # Build context
+        context = {}
+        context['model'] = model
+        context['user'] = c.user
+        # Buld data_dict
+        try:
+            dayHorizon = int(request.params.get('days', 5))
+        except ValueError, TypeError:
+            dayHorizon = 5
+        try:
+            page = request.params.get('page', 1),
+        except ValueError, TypeError:
+            page = 1
+        data_dict = dict(
+            format = request.params.get('format', ''),
+            dayHorizon = dayHorizon,
+            page = page,
+        )
+        if data_dict['format'] == 'atom':
+            data_dict['maxresults'] = 200
+            data_dict['ourtimedelta'] = timedelta(days=-dayHorizon)
+            data_dict['since_when'] = datetime.now() + ourtimedelta
+        # Get the revision list (will fail if you don't have the correct permission)
+        # XXX This should return data, not a query
+        revision_records = revision_list(context, data_dict)
+        # If we have the query, we are allowed to make a change
+        # XXX This line should be deprecated, you won't get here otherwise
+        c.revision_change_state_allowed = True
+        if data_dict['format'] == 'atom':
             # Generate and return Atom 1.0 document.
             from webhelpers.feedgenerator import Atom1Feed
             feed = Atom1Feed(
@@ -35,21 +64,11 @@
                 description=_(u'Recent changes to the CKAN repository.'),
                 language=unicode(get_lang()),
             )
-            # TODO: make this configurable?
-            # we do not want the system to fall over!
-            maxresults = 200
-            try:
-                dayHorizon = int(request.params.get('days', 5))
-            except:
-                dayHorizon = 5
-            ourtimedelta = timedelta(days=-dayHorizon)
-            since_when = datetime.now() + ourtimedelta
-            revision_query = model.repo.history()
-            revision_query = revision_query.filter(
-                    model.Revision.timestamp>=since_when).filter(
-                    model.Revision.id!=None)
-            revision_query = revision_query.limit(maxresults)
-            for revision in revision_query:
+            # David Raznick to refactor to work more quickly and with less 
+            # code and move into the logic layer. The only but that should
+            # be here is the code that changes the data from the logic layer
+            # into the atom feed.
+            for revision in revision_records:
                 package_indications = []
                 revision_changes = model.repo.list_changes(revision)
                 resource_revisions = revision_changes[model.Resource]
@@ -105,10 +124,9 @@
             feed.content_type = 'application/atom+xml'
             return feed.writeString('utf-8')
         else:
-            query = model.Session.query(model.Revision)
             c.page = Page(
-                collection=query,
-                page=request.params.get('page', 1),
+                collection=revision_records,
+                page=data_dict['page'],
                 items_per_page=20
             )
             return render('revision/list.html')


--- a/ckan/lib/dictization/__init__.py	Thu Jun 23 17:47:40 2011 +0100
+++ b/ckan/lib/dictization/__init__.py	Fri Jun 24 12:11:45 2011 +0100
@@ -8,6 +8,22 @@
 # and saving dictized objects. If a specialised use is needed please do NOT extend
 # these functions.  Copy code from here as needed.
 
+def dictize(context, obj):
+    '''\
+    Get any model object and represent it as a dict
+    '''
+    result_dict = {}
+    model = context["model"]
+    session = model.Session
+    if isinstance(obj, sqlalchemy.engine.base.RowProxy):
+        fields = obj.keys()
+    else:
+        ModelClass = obj.__class__
+        table = class_mapper(ModelClass).mapped_table
+        fields = [field.name for field in table.c]
+    for name in fields:
+        result_dict[name] = getattr(obj, name)
+    return result_dict
 
 def table_dictize(obj, context):
     '''Get any model object and represent it as a dict'''


--- a/ckan/logic/action/get.py	Thu Jun 23 17:47:40 2011 +0100
+++ b/ckan/logic/action/get.py	Fri Jun 24 12:11:45 2011 +0100
@@ -5,7 +5,7 @@
                           IPackageController)
 import ckan.authz
 
-from ckan.lib.dictization import table_dictize
+from ckan.lib.dictization import table_dictize, dictize
 from ckan.lib.dictization.model_dictize import group_to_api1, group_to_api2
 from ckan.lib.dictization.model_dictize import (package_to_api1,
                                                 package_to_api2,
@@ -14,6 +14,26 @@
                                                 group_dictize)
 
 
+def revision_list(context, data_dict=None):
+    model = context["model"]
+    if data_dict is None:
+        # XXX Refactor calling function
+        raise Exception('JG Please use the new revision_list() function')
+        revs = model.Session.query(model.Revision).all()
+        return [rev.id for rev in revs]
+    query = model.Session.query(model.Revision)
+    if data_dict['format'] == 'atom':
+         query = query.filter_by(model.State.ACTIVE)
+         query = query.filter(
+                 model.Revision.timestamp>=since_when).filter(
+                 model.Revision.id!=None)
+         query = query.limit(maxresults)
+         return query.all()
+    else:
+         query.offset(20*data_dict['page']).limit(20)
+    # Return plain dictionaries as all logic layer functions should
+    return [dictize(context, i) for i in query.all()]
+
 def package_list(context):
     model = context["model"]
     user = context["user"]
@@ -52,11 +72,6 @@
         package_list.append(result_dict)
     return package_list
 
-def revision_list(context):
-
-    model = context["model"]
-    revs = model.Session.query(model.Revision).all()
-    return [rev.id for rev in revs]
 
 def package_revision_list(context):
     model = context["model"]


--- a/ckan/logic/action/update.py	Thu Jun 23 17:47:40 2011 +0100
+++ b/ckan/logic/action/update.py	Fri Jun 24 12:11:45 2011 +0100
@@ -179,9 +179,6 @@
     if pkg is None:
         raise NotFound(_('Package was not found.'))
 
-    import ckan.new_authz as new_authz
-    new_authz.is_authorized(context, 'edit', data_dict, id, 'package')
-
     check_access(context, 'edit', data_dict, id, 'package')
 
     data, errors = validate(data_dict, schema, context)


--- a/ckan/logic/schema.py	Thu Jun 23 17:47:40 2011 +0100
+++ b/ckan/logic/schema.py	Fri Jun 24 12:11:45 2011 +0100
@@ -18,7 +18,7 @@
                                    tag_name_validator,
                                    tag_string_convert,
                                    duplicate_extras_key,
-                                   ignore_not_admin,
+                                   ignore_no_package_delete_right,
                                    no_http,
                                    tag_not_uppercase)
 from formencode.validators import OneOf
@@ -72,7 +72,7 @@
         'notes': [ignore_missing, unicode],
         'url': [ignore_missing, unicode],#, URL(add_http=False)],
         'version': [ignore_missing, unicode],
-        'state': [ignore_not_admin, ignore_missing],
+        'state': [ignore_no_package_delete_right, ignore_missing],
         '__extras': [ignore],
         '__junk': [empty],
         'resources': default_resource_schema(),


--- a/ckan/logic/validators.py	Thu Jun 23 17:47:40 2011 +0100
+++ b/ckan/logic/validators.py	Fri Jun 24 12:11:45 2011 +0100
@@ -1,7 +1,8 @@
 import re
 from pylons.i18n import _, ungettext, N_, gettext
 from ckan.lib.navl.dictization_functions import Invalid, missing, unflatten
-from ckan.authz import Authorizer
+#from ckan.authz import Authorizer
+from ckan.logic.auth.helper import is_admin
 
 def package_id_not_changed(value, context):
 
@@ -153,8 +154,8 @@
         tag_length_validator(tag, context)
         tag_name_validator(tag, context)
 
-def ignore_not_admin(key, data, errors, context):
-
+def ignore_no_package_delete_right(key, data, errors, context):
+    
     model = context['model']
     user = context.get('user')
 
@@ -162,8 +163,9 @@
         return
 
     pkg = context.get('package')
-    if (user and pkg and 
-        Authorizer().is_authorized(user, model.Action.CHANGE_STATE, pkg)):
+    
+    if check_access(context, 'package_delete', {'id': context['id']}):
+
         return
 
     data.pop(key)


--- a/ckan/model/__init__.py	Thu Jun 23 17:47:40 2011 +0100
+++ b/ckan/model/__init__.py	Fri Jun 24 12:11:45 2011 +0100
@@ -85,7 +85,7 @@
         the database. If they are already there, this method does nothing.'''
         for username in (PSEUDO_USER__LOGGED_IN,
                          PSEUDO_USER__VISITOR):
-            if not User.by_name(username):
+            if not self.session.query(User.id).filter_by(name=username).first():
                 user = User(name=username)
                 Session.add(user)
         Session.flush() # so that these objects can be used
@@ -167,6 +167,8 @@
             'Database migration - only Postgresql engine supported (not %s).' %\
             meta.engine.name
         import migrate.versioning.api as mig
+        version = mig.version(self.migrate_repository)
+	print version
         self.setup_migration_version_control()
         mig.upgrade(self.metadata.bind, self.migrate_repository, version=version)
         self.init_const_data()


--- a/ckan/model/changeset.py	Thu Jun 23 17:47:40 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1682 +0,0 @@
-"""Subdomain model for distributed data version control.
-
-Changeset Use Cases
-===================
-
-   Commit working model
-       - generate changesets from working model.
-
-   Update working model
-       - adjust working model from registered changesets.
-
-   Add unseen changesets
-       - register changesets committed from a foreign working model.
-
-   Merge lines of development
-       - combine diverged lines of development into a new changeset.
-
-   Interrogate changeset model
-       - working
-       - heads
-       - diff
-       - log
-
-Highlighted Core
-================
-
-    Changeset domain object
-        - has an id uniquely determined by the content of the changeset
-        - may 'follow' other changesets in lines of development
-        - may 'close' one changeset whilst following another
-        - aggregates a list of changes to the working model
-    
-    Change domain object
-        - has a reference to an entity in the working model
-        - has a difference vector describing a change to such an entity
-
-    Change calculations:
-        - vector (the difference to an entity that is effected by a change)
-        - sequence (the effective set of changes for a list of sets of changes)
-        - intersection (the last common changeset of any two lines)
-        - sum (the effective list of changes for two non-conflicting sets of changes)
-        - reverse (the effective negation for a list of changes)
-        - range (the effective list of changes for part of a line)
-        - line (a contiguous series of changesets)
-        - reduce (deflates a list of changes in respect of any invariance)
-        - realign (adjusts one list of changes to follow another)
-        - resolve (decides conflicted values in diverging lines)
-        - merge (conflation of two potentially conflicting lines)
-
-
-    Other function objects
-
-"""
-from meta import *
-from vdm.sqlalchemy import StatefulObjectMixin
-from ckan.model.core import DomainObject, State
-from ckan.model import Session, Revision, Package
-from ckan.model import Resource, Tag, Group
-from ckan.model import setup_default_user_roles
-from ckan.lib.helpers import json
-import datetime
-import uuid
-
-
-#############################################################################
-#
-## Changeset exception classes.
-#
-
-class ChangesetException(Exception): pass
-
-class ConflictException(ChangesetException): pass
-
-class SequenceException(ChangesetException): pass
-
-class WorkingAtHeadException(ChangesetException): pass
-
-class ChangesSourceException(ChangesetException): pass
-
-class UncommittedChangesException(ChangesetException): pass
-
-class EmptyChangesetRegisterException(ChangesetException): pass
-
-class NoIntersectionException(ChangesetException): pass
-
-
-#############################################################################
-#
-## Changeset arithmetic classes.
-#
-
-class Merge(object):
-    """Creates changeset which closes one line and continues another."""
-
-    def __init__(self, closing, continuing):
-        self.closing = closing
-        self.continuing = continuing
-        self.range_sum = None
-        self.range1 = None
-        self.range2 = None
-        self.intersection = None
-
-    def is_conflicting(self):
-        sum = self.get_range_sum()
-        return sum.is_conflicting()
-
-    def create_mergeset(self, resolve_class=None):
-        # Identify closing and continuing changes.
-        sum = self.get_range_sum()
-        closing = sum.changes1
-        continuing = sum.changes2
-        # Resolve conflicts between diverging changes.
-        if resolve_class == None:
-            resolve_class = Resolve
-        resolve = resolve_class(closing, continuing)
-        resolving = resolve.calc_changes()
-        # Sum the closing and resolution changes to make the merging changes.
-        merging = Sum(closing, resolving).calc_changes()
-        # Realign merging's old values to avoid conflict with continuing's new values.
-        merging = Realign(continuing, merging).calc_changes()
-        # Reduce the merging changes.
-        merging = Reduce(merging).calc_changes()
-        # Assert there are no conflicts with continuing line.
-        try:
-            Sum(continuing, merging).detect_conflict()
-        except ConflictException, inst:
-            msg = "Merge in a non-sequitur for the continuing line: %s" % inst
-            raise ConflictException, msg
-        # Create a new changeset with these changes.
-        # Todo: Use message and author provided by the user doing the merge.
-        log_message = 'Merged changes from %s to %s.' % (
-            self.intersection.id, self.closing.id
-        )
-        author = 'system'
-        meta = {
-            'log_message': log_message,
-            'author': author,
-        }
-        register = register_classes['changeset']()
-        changeset = register.create_entity(
-            meta=meta,
-            closes_id=self.closing.id,
-            follows_id=self.continuing.id,
-            changes=merging
-        )
-        return changeset
- 
-    def get_range_sum(self):
-        if self.range_sum == None:
-            range1 = self.get_range1()
-            range2 = self.get_range2()
-            changes1 = range1.calc_changes()
-            changes2 = range2.calc_changes()
-            self.range_sum = Sum(changes1, changes2)
-        return self.range_sum
-
-    def get_range1(self):
-        if self.range1 == None:
-            self.range1 = self.create_merge_range(self.closing)
-        return self.range1
-
-    def get_range2(self):
-        if self.range2 == None:
-            self.range2 = self.create_merge_range(self.continuing)
-        return self.range2
-
-    def create_merge_range(self, stop):
-        start = self.get_intersection()
-        range = Range(start, stop)
-        # Drop the common ancestor.
-        range.pop_first()  
-        return range
-
-    def get_intersection(self):
-        if self.intersection == None:
-            changeset = Intersection(self.continuing, self.closing).find()
-            self.intersection = changeset
-        return self.intersection
-
-
-class Resolve(object):
-    """Identifies and decides between conflicting changes."""
-
-    def __init__(self, changes1, changes2):
-        self.changes1 = changes1
-        self.changes2 = changes2
-
-    def calc_changes(self):
-        resolution = []
-        resolution_uniqueness_violations = []
-        resolution_value_conflicts = []
-        # Todo: Push this data down to CKAN statements (somehow).
-        unique_aspects = ['/package at name']
-        # NB No need to check for violations with the total model: if branch1
-        # doesn't lead to violation, and branch2 doesn't lead to violation,
-        # and branch2 changes don't violate uniqueness in branch1, then merge
-        # shouldn't lead to violation. So there's no need to check further back
-        # than the common ancestor.
-        # Resolve any duplication of unique attribute values.
-        #  - e.g. names of packages must be unique
-        for aspect in unique_aspects:
-            unique_values = {}
-            ref, attr_name = aspect.split('@')
-            for change in self.changes2 + self.changes1:
-                if not change.ref.startswith(ref):
-                    continue
-                if change.new == None:
-                    continue
-                if attr_name not in change.new:
-                    continue
-                change_value = change.new[attr_name]
-                if change_value not in unique_values:
-                    # No other uses of this unique value detected so far.
-                    unique_values[change_value] = change.ref
-                elif unique_values[change_value] == change.ref:
-                    # It's the same value, but on the same entity.
-                    continue
-                else:
-                    # It's the same value, but on a different entity.
-                    msg = "Changes violate unique '%s' value constraint ('%s' used on both %s and %s)." % (
-                        attr_name, change_value, unique_values[change_value], change.ref
-                    )
-                    print msg
-                    entity_id = change.ref.split('/')[2]
-                    mangled_value = change_value + '-' + entity_id
-                    try:
-                        # Prefer the mangled value over the duplicate value.
-                        decided_value = self.decide_value(mangled_value, change_value)
-                    except ConflictException:
-                        msg = "Unable to resolve duplicate "
-                        msg += "%s '%s' " % (attr_name, change_value)
-                        duplicating_ref = unique_values[change_value] 
-                        msg += "(on %s and %s)." % (change.ref, duplicating_ref)
-                        raise ConflictException, msg
-                    if decided_value == change_value:
-                        raise ConflictException, msg
-                    print "Using value: %s" % decided_value
-                    vector = change.as_vector()
-                    vector.new[attr_name] = decided_value
-                    # Update the change directly, so if it is involved in any
-                    # value resolutions the new name will be carried forward.
-                    change.set_diff(vector.as_diff())
-                    resolution_uniqueness_violations.append(change)
-        # Resolve any conflicting entity attribute values.
-        for change1 in self.changes1:
-            ref = change1.ref
-            vector3 = None
-            for change2 in self.changes2:
-                vector1 = change1.as_vector()
-                vector2 = change2.as_vector()
-                old1 = vector1.old
-                old2 = vector2.old
-                new1 = vector1.new
-                new2 = vector2.new
-                if ref == change2.ref:
-                    if (new1 == None and new2 != None) or (new1 != None and new2 == None):
-                        print "Changes conflict about object lifetime: %s %s" % (change1, change2)
-                        # Prefer the continuing value over the closing value.
-                        new = self.decide_value(new2, new1)
-                        print "Using values: %s" % new
-                        vector3 = Vector(new1, new)
-                    elif new1 and new2:
-                        old3 = None
-                        new3 = None
-                        for name, value1 in new1.items():
-                            if name not in new2:
-                                break
-                            value2 = new2[name]
-                            if not vector1.is_equal(value1, value2):
-                                print "Changes conflict about new values of '%s' on %s: %s or %s" % (
-                                    name, ref, value1, value2
-                                )
-                                if old3 == None and new3 == None:
-                                    old3 = {}
-                                    new3 = {}
-                                # Prefer the continuing value over the closing value.
-                                value3 = self.decide_value(value2, value1)
-                                print "Using value: %s" % value3
-                                old3[name] = value1
-                                new3[name] = value3
-                        if old3 != None and new3 != None:
-                            vector3 = Vector(old3, new3)
-                    break    
-            if vector3:
-                diff = vector3.as_diff()
-                change3 = Change(ref=ref, diff=diff)
-                resolution_value_conflicts.append(change3)
-        changes3 = resolution_value_conflicts
-        for change in resolution_uniqueness_violations:
-            # Append any changes resolved only for uniqueness.
-            if change.ref not in [c.ref for c in changes3]:
-                changes3.append(change)
-        # NB Don't ever reduce here, because resolution changes are imposed on
-        # the closing range changes within a merge, so all values need carrying.
-        return changes3
-
-    def decide_value(self, preferred, alternative):
-        raise ConflictException, "Unable to resolve conflicted values '%s' and '%s'." % (preferred, alternative)
-
-
-class AutoResolve(Resolve):
-
-    def decide_value(self, preferred, alternative):
-        print "Auto-resolving conflicting values:"
-        print "1:  %s  <--- auto-selected" % preferred.encode('utf8')
-        print "2:  %s" % alternative.encode('utf8')
-        return preferred
-
-
-class AutoResolvePreferClosing(Resolve):
-
-    def decide_value(self, preferred, alternative):
-        print "Auto-resolving conflicting values:"
-        print "1:  %s" % preferred.encode('utf8')
-        print "2:  %s  <--- auto-selected" % alternative.encode('utf8')
-        return alternative
-
-
-class CliResolve(Resolve):
-    """Decides between conflicting values using command line intervention."""
-
-    def decide_value(self, preferred, alternative):
-        print "Please decide between these values:"
-        print "1:  %s" % preferred
-        print "2:  %s" % alternative
-        input = raw_input("Which value do you prefer? [1]: ")
-        if input == "2":
-            value = alternative
-        else:
-            value = preferred
-        return value
-
-
-class Realign(object):
-    """Adjust changes2 to follow changes1 without conflict."""
-
-    def __init__(self, changes1, changes2):
-        self.changes1 = changes1
-        self.changes2 = changes2
-
-    def calc_changes(self):
-        "Uses changes1's new values for changes2's old values."
-        for change2 in self.changes2:
-            ref = change2.ref
-            for change1 in self.changes1:
-                if change1.ref == ref:
-                    vector2 = change2.as_vector()
-                    vector1 = change1.as_vector()
-                    is_changed = False
-                    if vector2.old == None and vector1.new == None:
-                        pass
-                    elif vector2.old != None and vector1.new != None:
-                        for attr_name in vector2.new:
-                            if attr_name in vector1.old:
-                                intermediate_value = vector1.new[attr_name]
-                                vector2.old[attr_name] = intermediate_value
-                                is_changed = True
-                    else:
-                        vector2.old = vector1.new
-                        is_changed = True
-                    if is_changed:
-                        change2.set_diff(vector2.as_diff())
-        return self.changes2
-
-
-class Reduce(object):
-    """Reduce changes by eliminating any invariance."""
-
-    def __init__(self, changes):
-        self.changes = changes
-
-    def calc_changes(self):
-        reduction = []
-        for change in self.changes:
-            vector = change.as_vector()
-            # Reduce any invariant non-entities.
-            if vector.old == None and vector.new == None:
-                continue
-            # Reduce any invariant attribute values.
-            if vector.old and vector.new:
-                for attr_name,value_old in vector.old.items():
-                    if attr_name in vector.new:
-                        value_new = vector.new[attr_name]
-                        if vector.is_equal(value_old, value_new):
-                            del vector.old[attr_name]
-                            del vector.new[attr_name]
-            # Reduce any invariant entities.
-            if vector.old == {} and vector.new == {}:
-                continue
-            change.set_diff(vector.as_diff())
-            reduction.append(change)
-        return reduction
-
-
-class Line(object):
-    """Iterator steps back up the line towards its origin."""
-
-    def __init__(self, changeset):
-        self.changeset = changeset
-        self.register = register_classes['changeset']()
-
-    def next(self):
-        if self.changeset:
-            if self.changeset.follows_id:
-                self.changeset = self.register.get(self.changeset.follows_id)
-            else:
-                self.changeset = None
-        else:
-            raise Exception, "Can't go beyond the origin."
-        return self.changeset
-
-
-class Range(object):
-    """Continguous changesets along one line of development."""
-
-    def __init__(self, start, stop):
-        self.start = start
-        self.stop = stop
-        self.sequence = None
-        self.changesets = None
-
-    def is_broken(self):
-        try:
-            self.get_changesets()
-        except SequenceException:
-            return True
-        else:
-            return False
-
-    def calc_changes(self):
-        return self.get_sequence().calc_changes()
-
-    def pop_first(self):
-        return self.get_sequence().pop_first()
-
-    def get_sequence(self):
-        if self.sequence == None:
-            self.sequence = Sequence([])
-            for changeset in self.get_changesets():
-                self.sequence.append(changeset.changes)
-        return self.sequence
-
-    def get_changesets(self):
-        if self.changesets == None:
-            line = Line(self.stop)
-            self.changesets = [self.stop]
-            changeset = self.stop
-            while(changeset.id != self.start.id):
-                changeset = line.next()
-                if changeset == None:
-                    msg = "Changeset %s does not follow changeset %s." % (self.stop.id, self.start.id)
-                    raise SequenceException, msg
-                self.changesets.append(changeset)
-            self.changesets.reverse()
-        return self.changesets
-
-
-class Reverse(object):
-    """Simple negation of a list of changes."""
-
-    def __init__(self, changes):
-        self.changes = changes
-
-    def calc_changes(self):
-        changes = []
-        for change in self.changes:
-            vector = change.as_vector()
-            reverse = Vector(old=vector.new, new=vector.old)
-            diff = reverse.as_diff()
-            ref = change.ref
-            changes.append(Change(ref=ref, diff=diff))
-        return changes
-
-
-class Sum(object):
-    """Concatentations of two sets of changes."""
-
-    def __init__(self, changes1, changes2):
-        self.changes1 = changes1
-        self.changes2 = changes2
-
-    def is_conflicting(self):
-        try:
-            self.detect_conflict()
-        except ConflictException:
-            return True
-        else:
-            return False
-
-    def detect_conflict(self):
-        """Raises exception if a non-sequitur is detected."""
-        refs1 = {}
-        refs2 = {}
-        for change1 in self.changes1:
-            for change2 in self.changes2:
-                if change1.ref == change2.ref:
-                    vector1 = change1.as_vector()
-                    vector2 = change2.as_vector()
-                    old1 = vector1.old
-                    old2 = vector2.old
-                    new1 = vector1.new
-                    new2 = vector2.new
-                    if (new1 == None and old2 != None) or (new1 != None and old2 == None):
-                        msg = "Changes conflict about object lifetime on ref "
-                        msg += " '%s' when summing  %s  and  %s." % (change1.ref, change1, change2)
-                        raise ConflictException, msg
-                    elif new1 and old2:
-                        for name, value1 in new1.items():
-                            if name not in old2:
-                                continue
-                            value2 = old2[name]
-                            if not vector1.is_equal(value1, value2):
-                                msg = "Changes conflict about intermediate value of '%s' on %s: %s or %s" % (
-                                    name, change1.ref, value1, value2
-                                )
-                                raise ConflictException, msg
-
-    def calc_changes(self):
-        return Sequence([self.changes1, self.changes2]).calc_changes()
-
-        
-class Intersection(object):
-    """Intersection of two lines of development."""
-
-    def __init__(self, child1, child2):
-        self.child1 = child1
-        self.child2 = child2
-
-    def find(self):
-        # Alternates between stepping back through one line searching
-        # for each changeset in other line's stack and vice versa.
-        # Intersection is the first changeset discovered in both lines.
-        line1 = Line(self.child1)
-        line2 = Line(self.child2)
-        stack1 = [self.child1]
-        stack2 = [self.child2]
-        pointer1 = self.child1
-        pointer2 = self.child2
-        while (pointer1 or pointer2):
-            if pointer1:
-                for item2 in stack2:
-                    if pointer1.id == item2.id:
-                        return pointer1
-                pointer1 = line1.next()
-                if pointer1:
-                    stack1.append(pointer1)
-            if pointer2:
-                for item1 in stack1:
-                    if pointer2.id == item1.id:
-                        return pointer2
-                pointer2 = line2.next()
-                if pointer2:
-                    stack2.append(pointer2)
-        return None
-
-
-class Sequence(object):
-    """A list of lists of changes."""
-
-    def __init__(self, changeses):
-        self.changeses = changeses 
-
-    def calc_changes(self):
-        cache = {}
-        for changes in self.changeses:
-            for change in changes:
-                if change.ref not in cache:
-                    cache[change.ref] = Vector(change.old, change.new)
-                vector = cache[change.ref]
-                # Oldest old value...
-                if vector.old != None and change.old != None:
-                    for name, value in change.old.items():
-                        if name not in vector.old:
-                            vector.old[name] = value
-                # ...and newest new value.
-                if vector.new == None or change.new == None:
-                    vector.new = change.new
-                elif change.new != None:
-                    for name, value in change.new.items():
-                        vector.new[name] = value
-        changes = []
-        for ref, vector in cache.items():
-            diff = vector.as_diff()
-            change = Change(ref=ref, diff=diff)
-            changes.append(change)
-        return changes
-
-    def pop_first(self):
-        return self.changeses.pop(0)
-
-    def append(self, changes):
-        return self.changeses.append(changes)
-
-
-class Json(object):
-    """Dumps and loads JSON strings into Python objects."""
-
-    def dumps(cls, data):
-        try:
-            json_str = json.dumps(data, indent=2)
-        except Exception, inst:
-            msg = "Couldn't convert to JSON: %s" % data
-            raise Exception, "%s: %s" % (msg, inst)
-        return json_str
-
-    dumps = classmethod(dumps)
-
-    def loads(cls, json_str):
-        try:
-            data = json.loads(json_str)
-        except Exception, inst:
-            msg = "Couldn't parse JSON: %s" % json_str
-            raise Exception, "%s: %s" % (msg, inst)
-        return data
-
-    loads = classmethod(loads)
-
-
-class Vector(Json):
-    """Distance in "difference space"."""
-
-    def __init__(self, old=None, new=None):
-        """Initialises instance with old and new dicts of attribute values."""
-        self.old = old
-        self.new = new
-
-    def as_diff(self):
-        """Converts vector data to JSON string."""
-        data = {
-            'old': self.old,
-            'new': self.new,
-        } 
-        return unicode(self.dumps(data))
-
-    def is_equal(self, value1, value2):
-        """Compares vector values for equality."""
-        # Todo: Should list order differences be conflicts?
-        #   - why would the order (e.g. tags of a package) change?
-        if isinstance(value1, list):
-            value1.sort()
-        if isinstance(value2, list):
-            value2.sort()
-        if isinstance(value1, dict):
-            value1 = value1.items()
-            value1.sort()
-        if isinstance(value2, dict):
-            value2 = value2.items()
-            value2.sort()
-        return value1 == value2
-
-
-
-#############################################################################
-#
-## Changeset subdomain model objects and registers.
-#
-
-register_classes = {}
-
-class ChangesetSubdomainObject(DomainObject, Json):
-
-    pass
-
-
-class Changeset(ChangesetSubdomainObject):
-    """Models a list of changes made to a working model."""
-   
-    def get_meta(self):
-        return self.loads(self.meta or "{}")
-
-    def set_meta(self, meta_data):
-        self.meta = unicode(self.dumps(meta_data))
-
-    def apply(self, is_forced=False, report={}, moderator=None):
-        """Applies changeset to the working model as a single revision."""
-        meta = self.get_meta()
-        register = register_classes['changeset']()
-        Session.add(self) # Otherwise self.changes db lazy-load doesn't work.
-        changes = self.changes
-        revision_id = register.apply_changes(
-            changes=changes,
-            meta=meta,
-            report=report,
-            is_forced=is_forced,
-            moderator=moderator,
-        )
-        Session.add(self) # Otherwise revision_id isn't persisted.
-        self.revision_id = revision_id
-        Session.commit()
-        register.move_working(self.id)
-        return revision_id
-
-    def is_conflicting(self):
-        """Returns boolean value, true if model conflicts are detected."""
-        try:
-            self.detect_conflict()
-        except ConflictException:
-            return True
-        else:
-            return False
-
-    def detect_conflict(self):
-        """Checks changes for conflicts with the working model."""
-        for change in self.changes:
-            change.detect_conflict()
-
-    def as_dict(self):
-        """Presents domain data with basic data types."""
-        meta_data = self.get_meta()
-        changes_data = [c.as_dict() for c in self.changes]
-        changeset_data = {
-            'id': self.id,
-            'closes_id': self.closes_id,
-            'follows_id': self.follows_id,
-            'meta': meta_data,
-            'timestamp': self.timestamp.isoformat(),
-            'changes': changes_data,
-        }
-        return changeset_data
-
-
-class Change(ChangesetSubdomainObject):
-    """Models a change made to an entity in the working model."""
-
-    def get_mask_register(self):
-        return ChangemaskRegister()
-
-    def get_mask(self):
-        mask_register = self.get_mask_register()
-        return mask_register.get(self.ref, None)
-
-    def is_masked(self):
-        return bool(self.get_mask())
-
-    def apply(self, is_forced=False, moderator=None):
-        """Operates the change vector on the referenced model entity."""
-        if self.is_masked():
-            print "Warning: Screening change to '%s' (mask set for ref)." % self.ref
-            return
-        if not is_forced:
-            if moderator and not moderator.moderate_change_apply(self):
-                return
-            self.detect_conflict()
-        mask = self.get_mask()
-        register, key = self.deref()
-        vector = self.as_vector()
-        entity = register.get(key, None)
-        if vector.old == None:
-            # Create.
-            if entity != None:
-                msg = "Can't apply creating change, since entity already exists for ref: %s" % self.ref
-                raise Exception, msg
-            entity = register.create_entity(key)
-            register.patch(entity, vector)
-        elif vector.new == None:
-            # Delete.
-            if entity == None:
-                msg = "Can't apply deleting change, since entity not found for ref: %s" % self.ref
-                raise Exception, msg
-            # Mangle distinct values.
-            # Todo: Move this to the package register?
-            entity.name += str(uuid.uuid4())
-            entity.delete()
-            #entity.purge()
-        else:
-            # Update.
-            if entity == None:
-                msg = "Can't apply updating change, since entity not found for ref: %s" % self.ref
-                raise Exception, msg
-            entity = register.get(key)
-            register.patch(entity, vector)
-        return entity # keep in scope?
-
-    def detect_conflict(self):
-        """Checks for conflicts with the working model."""
-        register, key = self.deref()
-        register.detect_conflict(key, self.as_vector())
-
-    def deref(self):
-        """Returns the register and register key affected by the change."""
-        parts = self.ref.split('/')
-        register_type = parts[1]
-        register_key = parts[2]
-        if register_type in register_classes:
-            register_class = register_classes[register_type]
-            register = register_class()
-            return (register, register_key)
-        else:
-            raise Exception, "Can't deref '%s' with register map: %s" % (self.ref, register_classes)
-
-    def as_vector(self):
-        """Returns the pure vector of change, without any reference to an entity."""
-        if not hasattr(self, 'vector') or self.vector == None:
-            data = self.load_diff()
-            self.vector = Vector(data['old'], data['new'])
-        return self.vector
-
-    def load_diff(self):
-        """Parses the stored JSON diff string into Vector data."""
-        return self.loads(self.diff)
-
-    def set_diff(self, diff):
-        self.diff = diff
-        self.vector = None
-
-    def as_dict(self):
-        """Presents domain data with basic data types."""
-        change_data = {}
-        change_data['ref'] = self.ref
-        change_data['diff'] = self.load_diff()
-        return change_data
-
-    def get_old(self):
-        """Method implements Vector interface, for convenience."""
-        return self.as_vector().old
-
-    old = property(get_old)
-
-    def get_new(self):
-        """Method implements Vector interface, for convenience."""
-        return self.as_vector().new
-
-    new = property(get_new)
-
-
-class Changemask(ChangesetSubdomainObject):
-    """Screen working model from changes to the referenced entity"""
-
-    pass
-
-
-class ObjectRegister(object):
-    """Dictionary-like domain object register base class."""
-
-    object_type = None
-    key_attr = ''
-
-    def __init__(self):
-        assert self.object_type, "Missing domain object type on %s" % self
-        assert self.key_attr, "Missing key attribute name on %s" % self
-
-    def __getitem__(self, key, default=Exception):
-        return self.get(key, default=default)
-
-    def get(self, key, default=Exception, attr=None, state=State.ACTIVE):
-        """Finds a single entity in the register."""
-        # Todo: Implement a simple entity cache.
-        if attr == None:
-            attr = self.key_attr
-        kwds = {attr: key}
-        if issubclass(self.object_type, StatefulObjectMixin):
-            if attr == 'state':
-                msg = "Can't use 'state' attribute to key"
-                msg += " a stateful object register."
-                raise Exception, msg
-            kwds['state'] = state
-        q = Session.query(self.object_type).autoflush(False)
-        o = q.filter_by(**kwds).first()
-        if o:
-            return o
-        if default != Exception:
-            return default
-        else:
-            raise Exception, "%s not found: %s" % (self.object_type.__name__, key)
-
-    def _all(self):
-        """Finds all the entities in the register."""
-        return Session.query(self.object_type).all()
-
-    def __len__(self):
-        return len(self._all())
-
-    def __iter__(self):
-        return iter(self.keys())
-
-    def __contains__(self, key):
-        return key in self.keys()
-
-    def keys(self):
-        return [getattr(o, self.key_attr) for o in self._all()]
-
-    def items(self):
-        return [(getattr(o, self.key_attr), o) for o in self._all()]
-
-    def values(self):
-        return self._all()
-
-    def create_entity(self, *args, **kwds):
-        """Registers a new entity in the register."""
-        if args:
-            kwds[self.key_attr] = args[0]
-        if 'id' in kwds:
-            deleted_entity = self.get(kwds['id'], None,
-                attr='id', state=State.DELETED
-            )
-            if deleted_entity:
-                deleted_entity.state = State.ACTIVE
-                #Session.add(deleted_entity)
-                return deleted_entity
-        entity = self.object_type(**kwds)
-        return entity
-
-
-class TrackedObjectRegister(ObjectRegister):
-    """Abstract dictionary-like interface to changeset objects."""
-
-    distinct_attrs = []
-
-    def detect_conflict(self, key, vector):
-        """Checks whether the vector conflicts with the working model."""
-        entity = self.get(key, None)
-        if entity:
-            self.detect_prechange_divergence(entity, vector)
-        else:
-            if vector.old:
-                msg = "Entity '%s' not found for changeset with old values: %s" % (key, vector.old)
-                raise ConflictException, msg
-            self.detect_missing_values(vector)
-        self.detect_distinct_value_conflict(vector)
-
-    def detect_prechange_divergence(self, entity, vector):
-        """Checks vector old values against the current working model."""
-        if not vector.old:
-            return
-        entity_data = entity.as_dict()
-        for name in vector.old.keys():
-            entity_value = entity_data[name]
-            old_value = vector.old[name]
-            if not vector.is_equal(entity_value, old_value):
-                msg = u"Current '%s' value conflicts with old value of the change.\n" % name
-                msg += "current: %s\n" % entity_value
-                msg += "change old: %s\n" % old_value
-                raise ConflictException, msg.encode('utf8')
-
-    def detect_distinct_value_conflict(self, vector):
-        """Checks for unique value conflicts with existing entities."""
-        if vector.new == None:
-            # There aren't any new values.
-            return
-        for name in self.distinct_attrs:
-            if name not in vector.new:
-                # Not mentioned.
-                continue
-            existing_entity = self.get(vector.new[name], None, attr=name)
-            if existing_entity == None:
-                # Not already in use.
-                continue
-            msg = "Model already has an entity with '%s' equal to '%s': %s" % (
-                name, vector.new[name], existing_entity
-            )
-            raise ConflictException, msg.encode('utf8')
-
-    def detect_missing_values(self, vector):
-        """Checks for required values such as distinct values."""
-        for name in self.distinct_attrs:
-            if name in vector.new and vector.new[name]:
-                continue
-            msg = "Missing value '%s': '%s'." % (name, vector.new)
-            raise ConflictException, msg
-
-    def ref(self, entity):
-        """Returns path-like string that can reference given entity."""
-        return u'/%s/%s' % (
-            self.object_type.__name__.lower(),
-            getattr(entity, self.key_attr)
-        )
-
-    def diff(self, entity):
-        """Instantiates and returns a vector for the difference
-        between the current and previous versions of given entity."""
-        raise Exception, "Abstract method not implemented."
-
-    def patch(self, entity, vector):
-        """Updates given entity according to the given vector of change."""
-        for col in self.get_columns():
-            if col.name in vector.new:
-                value = vector.new[col.name]
-                type_name = col.type.__class__.__name__
-                value = self.convert_to_domain_value(value, type_name)
-                setattr(entity, col.name, value)
-
-    def get_columns(self):
-        """Returns the model of the entity attributes."""
-        from sqlalchemy import orm
-        table = orm.class_mapper(self.object_type).mapped_table
-        return table.c
-
-    def convert_to_domain_value(self, value, type_name):
-        """Returns a domain value for the given serialised value and type."""
-        if type_name in ['Unicode', 'UnicodeText']:
-            if value == None:
-                pass
-            else:
-                value = unicode(value)
-        elif type_name in ['DateTime']:
-            if value == None:
-                pass
-            else:
-                import datetime, re
-                value = datetime.datetime(*map(int, re.split('[^\d]', value)))
-        else:
-            raise Exception, "Unsupported type: %s" % type_name
-        return value
-
-
-class AbstractChangesetRegister(TrackedObjectRegister, Json):
-    """Dictionary-like interface to changeset objects."""
-
-    object_type = Changeset
-    key_attr = 'id'
-    NAMESPACE_CHANGESET = None
-
-    def create_entity(self, *args, **kwds):
-        """Instantiates a new Changeset object."""
-        if 'id' not in kwds:
-            kwds['id'] = self.determine_changeset_id(**kwds)
-        preexisting = self.get(kwds['id'], None)
-        if preexisting != None:
-            if 'revision_id' in kwds:
-                preexisting.revision_id = kwds['revision_id']
-            return preexisting
-        if 'meta' in kwds:
-            meta = kwds['meta']
-            if isinstance(meta, dict):
-                kwds['meta'] = unicode(self.dumps(meta))
-        return super(AbstractChangesetRegister, self).create_entity(*args, **kwds)
-
-    def determine_changeset_id(self, **kwds):
-        """Generates and returns a UUID from the changeset content."""
-        id_profile = []
-        closes_id = kwds.get('closes_id', None)
-        id_profile.append({'closes_id':'closes_id'}) # Separator.
-        id_profile.append(closes_id)
-        follows_id = kwds.get('follows_id', None)
-        id_profile.append({'follows_id':'follows_id'}) # Separator.
-        id_profile.append(follows_id)
-        changes = kwds.get('changes', [])
-        index = {}
-        for change in changes:
-            index[change.ref] = change
-        refs = index.keys()
-        refs.sort()
-        for ref in refs:
-            id_profile.append({'ref':ref}) # Separator.
-            change = index[ref]
-            id_profile.append({'old':'old'}) # Separator.
-            if change.old:
-                old_keys = change.old.keys()
-                old_keys.sort()
-                for key in old_keys:
-                    value = change.old[key]
-                    id_profile.append(key)
-                    id_profile.append(value)
-            id_profile.append({'new':'new'}) # Separator.
-            if change.new:
-                new_keys = change.new.keys()
-                new_keys.sort()
-                for key in new_keys:
-                    value = change.new[key]
-                    if isinstance(value, dict):
-                        value = value.items()
-                        value.sort()
-                    elif isinstance(value, list):
-                        value.sort()
-                    id_profile.append(key)
-                    id_profile.append(value)
-        id_profile = self.dumps(id_profile)
-        id_uuid = uuid.uuid5(self.NAMESPACE_CHANGESET, id_profile)
-        changeset_id = unicode(id_uuid)
-        return changeset_id
-        
-    def assert_status_code(self, ckan_service, operation_name, required_status=[200]):    
-        if ckan_service.last_status not in required_status:
-            raise Exception, "CKAN API operation '%s' returned status code %s (not one of %s): %s" % (operation_name, ckan_service.last_status, required_status, ckan_service.last_message)
-
-    def pull(self, source):
-        """Detects and retrieves unseen changesets from given source."""
-        api_location = source.split('/api')[0].strip('/') + '/api'
-        from ckanclient import CkanClient
-        ckan_service = CkanClient(base_location=api_location)
-        ckan_service.changeset_register_get()
-        self.assert_status_code(ckan_service, operation_name='Changeset Register Get')
-        foreign_ids = ckan_service.last_message
-        if foreign_ids == None:
-            msg = "Error pulling changes from: %s (CKAN service error: %s: %s)" % (source, ckan_service.last_url_error or "%s: %s" % (ckan_service.last_status, ckan_service.last_http_error), ckan_service.last_location)
-            raise ChangesSourceException, msg
-        local_ids = self.keys()
-        unseen_ids = []
-        for changeset_id in foreign_ids:
-            if changeset_id not in local_ids:
-                unseen_ids.append(changeset_id)
-        unseen_changesets = []
-        print "Unseen ids: ", unseen_ids
-        for unseen_id in unseen_ids:
-            unseen_data = ckan_service.changeset_entity_get(unseen_id)
-            self.assert_status_code(ckan_service, operation_name='Changeset Entity Get')
-            changeset_id = self.add_unseen(unseen_data)
-            if not changeset_id:
-                msg = "Error: Couldn't add incoming changeset: %s" % unseen_id
-                raise Exception, msg
-            if unseen_id != changeset_id:
-                msg = "Error: Changeset id mismatch: pulled '%s' but recorded '%s'." % (unseen_id, changeset_id)
-                raise Exception, msg
-        return unseen_ids
-
-    def update(self, target_id=None, report={}, moderator=None):
-        """Adjusts the working model to correspond with the target 
-        changeset, which defaults to the head of the working line."""
-        if not len(self):
-            raise EmptyChangesetRegisterException, "There are no changesets in the changeset register."
-        # Check there are no outstanding changes.
-        if self.is_outstanding_changes():
-            raise UncommittedChangesException, "There are outstanding changes in the working data."
-        # Get route from working to target.
-        working = self.get_working()
-        if not working:
-            raise Exception, "There is no working changeset."
-        head_ids = Heads().ids()
-        head_ids.reverse()
-        if not target_id:
-            # Infer a target from the list of heads.
-            # Todo: Infer cross-branch target when working changeset is closed by a mergeset.
-            if working.id in head_ids:
-                raise WorkingAtHeadException, "Nothing to update (working changeset is at the head of its line)."
-            else:
-                for head_id in head_ids:
-                    range = Range(working, self.get(head_id))
-                    if not range.is_broken():
-                        target_id = head_id
-                        break
-                if not target_id:
-                    raise Exception, "Can't find head changeset for the working line."
-        target = self.get(target_id)
-        route = Route(working, target)
-        range_back, range_forward = route.get_ranges()
-        if range_back == None and range_forward:
-            # Step through changesets to replicate history.
-            changesets = range_forward.get_changesets()[1:]
-            changesets_len = len(changesets)
-            print "There %s %s changeset%s..." % (
-                changesets_len != 1 and "are" or "is",
-                changesets_len, 
-                changesets_len != 1 and "s" or ""
-            )
-            range_forward.pop_first()
-            for changeset in range_forward.get_changesets()[1:]:
-                if moderator and moderator.moderate_changeset_apply(changeset):
-                    changeset.apply(report=report, moderator=moderator)
-                    print "Applied changeset '%s' OK." % changeset.id
-                    print ""
-                elif moderator:
-                    print "Not applying changeset '%s'. Stopping..." % changeset.id
-                    break
-                else:
-                    print "%s" % changeset.id
-                    changeset.apply(report=report)
-        elif range_back and range_forward == None:
-            print "Updating to a previous point on the line..."
-            range_back.pop_first()
-            changes = range_back.calc_changes()
-            reverse = Reverse(changes)
-            changes = reverse.calc_changes()
-            changes = Reduce(changes).calc_changes()
-            self.apply_jump_changes(changes, target_id, report=report, moderator=moderator)
-            # Todo: Make a better report.
-        elif range_back and range_forward:
-            print "Crossing branches..."
-            range_forward.pop_first()
-            changes_forward = range_forward.calc_changes()
-            range_back.pop_first()
-            changes = range_back.calc_changes()
-            reverse = Reverse(changes)
-            changes_back = reverse.calc_changes()
-            sum = Sum(changes_back, changes_forward)
-            changes = sum.calc_changes()
-            changes = Reduce(changes).calc_changes()
-            self.apply_jump_changes(changes, target_id, report=report, moderator=moderator)
-
-    def merge(self, closing_id, continuing_id, resolve_class=None):
-        """Creates a new changeset combining diverged lines of development."""
-        closing = self.get(closing_id)
-        continuing = self.get(continuing_id)
-        merge = Merge(closing=closing, continuing=continuing)
-        mergeset = merge.create_mergeset(resolve_class=resolve_class)
-        Session.add(mergeset)
-        Session.commit()
-        return mergeset
-
-    def commit(self):
-        """Creates a new changeset from changes made to the working model."""
-        raise Exception, "Abstract method not implemented."
-
-    def get_working(self):
-        """Returns the changeset last used to update the working model."""
-        return self.get(True, None, 'is_working')
-
-    def move_working(self, target_id):
-        """Switches the working changeset to the given target."""
-        target = self.get(target_id)
-        working = self.get_working()
-        if working:
-            working.is_working = False
-        target.is_working = True
-        Session.commit()
-
-    def add_unseen(self, changeset_data):
-        """Puts foreign changesets into the register."""
-        # Todo: Validate the data (dict with id str, meta dict, and changes list).
-        try:
-            changeset_id = unicode(changeset_data['id'])
-        except TypeError, exception:
-            msg = "%s: %s" % (str(exception), repr(changeset_data))
-            raise TypeError, msg
-        if changeset_id in self:
-            raise Exception, "Already have changeset with id '%s'." % changeset_id
-        closes_id = changeset_data.get('closes_id', None)
-        if closes_id:
-            closes_id = unicode(closes_id)
-        follows_id = changeset_data.get('follows_id', None)
-        if follows_id:
-            follows_id = unicode(follows_id)
-        meta = changeset_data['meta']
-        timestamp = self.convert_to_domain_value(changeset_data.get('timestamp', None), 'DateTime')
-        changes = []
-        changes_data = changeset_data['changes']
-        change_register = ChangeRegister()
-        # Create changes before changeset (changeset id depends on changes).
-        for change_data in changes_data:
-            ref = unicode(change_data['ref'])
-            diff_data = change_data['diff']
-            diff = unicode(self.dumps(diff_data))
-            change = change_register.create_entity(ref=ref, diff=diff)
-            changes.append(change)
-        changeset = self.create_entity(
-            id=changeset_id,
-            closes_id=closes_id,
-            follows_id=follows_id,
-            meta=meta, 
-            timestamp=timestamp,
-            changes=changes,
-        )
-        Session.add(changeset)
-        Session.commit()
-        return changeset.id
-
-
-class Route(object):
-
-    def __init__(self, start, stop):
-        self.start = start
-        self.stop = stop
-        self.back = None
-        self.forward = None
-        self.changes = None
-
-    def get_ranges(self):
-        if self.back == None and self.forward == None:
-            common = Intersection(self.start, self.stop)
-            ancestor = common.find()
-            if ancestor == None:
-                msg = "%s %s" % (self.start.id, self.stop.id)
-                raise NoIntersectionException, msg
-            if ancestor.id == self.start.id:
-                # Just go forward towards head.
-                self.forward = Range(self.start, self.stop)
-            elif ancestor.id == self.stop.id:
-                # Just go back towards root.
-                self.back = Range(self.stop, self.start)
-            else:
-                # Go back and then go forward.
-                self.back = Range(ancestor, self.start)
-                self.forward = Range(ancestor, self.stop)
-        return self.back, self.forward
-
-    def calc_changes(self):
-        if self.changes == None:
-            self.get_ranges()
-            changes_back = None
-            changes_forward = None
-            if self.back != None:
-                self.back.pop_first()
-                changes = self.back.calc_changes()
-                changes_back = Reverse(changes).calc_changes()
-            if self.forward != None:
-                self.forward.pop_first()
-                changes = self.forward.calc_changes()
-                changes_forward = changes
-            if changes_back != None and changes_forward != None:
-                self.changes = Sum(changes_back, changes_forward).calc_changes()
-            elif changes_back != None:
-                self.changes = changes_back
-            elif changes_forward != None:
-                self.changes = changes_forward
-        return self.changes
-
-
-register_classes['changeset'] = AbstractChangesetRegister
-
-
-class Heads(object):  # Rework as a list.
-    """Lists changesets which have no followers."""
-
-    def ids(self):
-        head_ids = []
-        followed_ids = {}
-        closed_ids = {}
-        changeset_ids = []
-        register = register_classes['changeset']()
-        changesets = register.values()
-        for changeset in changesets:
-            changeset_ids.append(changeset.id)
-            if changeset.closes_id:
-                closed_ids[changeset.closes_id] = changeset.id
-            if changeset.follows_id:
-                followed_ids[changeset.follows_id] = changeset.id
-        for id in changeset_ids:
-            if id not in followed_ids:
-                head_ids.append(id)
-        return head_ids
-
- 
-class ChangeRegister(TrackedObjectRegister):
-    """Dictionary-like interface to change objects."""
-
-    object_type = Change
-    key_attr = 'ref'
-
-# Todo: Extract (and put under test) the mask-setting and mask-getting routines.
-# Todo: Prevent user from easily not applying change and not applying mask.
-# Todo: Support mask-unsetting (necessarily with entity "catch up").
-# Todo: Support apply-conflict override (so some changes can be skipped).
-class ChangemaskRegister(TrackedObjectRegister):
-    """Dictionary-like interface to ignore objects."""
-
-    object_type = Changemask
-    key_attr = 'ref'
-
-
-#############################################################################
-#
-## Persistence model.
-#
-
-changeset_table = Table('changeset', metadata,
-        ## These are the "public" changeset attributes.
-        # 'id' - deterministic function of its content
-        Column('id', types.UnicodeText, primary_key=True),
-        # 'closes_id' - used by a mergesets to refer to its closed line
-        Column('closes_id', types.UnicodeText, nullable=True),
-        # 'follows_id' - refers to immediate ancestor of the changeset
-        Column('follows_id', types.UnicodeText, nullable=True),
-        # 'meta' - a JSON dict, optionally with author, log_message, etc.
-        Column('meta', types.UnicodeText, nullable=True),
-        # 'branch' - explicit name for a working line
-        Column('branch', types.UnicodeText, default=u'default', nullable=True),
-        # 'timestamp' - UTC time when changeset was constructed
-        Column('timestamp', DateTime, default=datetime.datetime.utcnow),
-        ## These are the "private" changeset attributes.
-        # 'is_working' - true if used for last update of working data
-        Column('is_working', types.Boolean, default=False),
-        # 'revision_id' - refers to constructing or applied revision
-        Column('revision_id', types.UnicodeText, ForeignKey('revision.id'), nullable=True),
-        # 'added_here' - UTC time when chaneset was added to local register
-        Column('added_here', DateTime, default=datetime.datetime.utcnow),
-)
-
-change_table = Table('change', metadata,
-        # 'ref' - type and unique identifier for tracked domain entity
-        Column('ref', types.UnicodeText, nullable=True),
-        # 'diff' - a JSON dict containing the change vector
-        Column('diff', types.UnicodeText, nullable=True),
-        # 'changeset_id' - changes are aggregated by changesets
-        Column('changeset_id', types.UnicodeText, ForeignKey('changeset.id')),
-)
-
-changemask_table = Table('changemask', metadata,
-        # 'ref' - type and unique identifier for masked domain entity
-        Column('ref', types.UnicodeText, primary_key=True),
-        # 'timestamp' - UTC time when mask was set
-        Column('timestamp', DateTime, default=datetime.datetime.utcnow),
-)
-
-mapper(Changeset, changeset_table, properties={
-    'changes':relation(Change, backref='changeset',
-        cascade='all, delete', #, delete-orphan',
-        ),
-    },
-    order_by=changeset_table.c.added_here,
-)
-
-mapper(Change, change_table, properties={
-    },
-    primary_key=[change_table.c.changeset_id, change_table.c.ref] 
-)
-
-mapper(Changemask, changemask_table, properties={
-    },
-    primary_key=[changemask_table.c.ref], 
-    order_by=changemask_table.c.timestamp,
-)
-
-
-#############################################################################
-#
-## Statements specific to the CKAN system.
-#
-
-# Todo: Robustness against buried revisions. Figure out what would happen if auto-commit running whilst moderated update is running? Might try to apply a changeset to a newly diverged working model. Argument in favour of not running automatic commits. :-) But also if a new revision is created from the Web interface, it won't be committed but its changes will never be committed if it is followed by revision created by applying changes during an update, and so we need to check for outstanding revisions before each 'apply_changes' (not just each 'update' because the duration of time needed for moderation or to apply a long series of changesets offers the possibility for burying a lost revision). Locking the model might help, but would need to be worked into all the forms. So basically the error would be trying to apply 'next' changeset to working model that has already changed.
-
-class ChangesetRegister(AbstractChangesetRegister):
-
-    NAMESPACE_CHANGESET = uuid.uuid5(uuid.NAMESPACE_OID, 'opendata')
-
-    def apply_jump_changes(self, changes, target_id, report={}, moderator=None):
-        """Applies changes to CKAN repository as a 'system jump' revision."""
-        log_message = u'Jumped to changeset %s' % target_id
-        author = u'system'
-        meta = {
-            'log_message': log_message,
-            'author': author,
-        }
-        revision_id = self.apply_changes(changes, meta=meta, report=report, moderator=moderator)
-        target = self.get(target_id)
-        target.revision_id = revision_id
-        Session.commit()
-        self.move_working(target_id)
-
-    def apply_changes(self, changes, meta={}, report={}, is_forced=False, moderator=None):
-        """Applies given changes to CKAN repository as a single revision."""
-        if not 'created' in report:
-            report['created'] = []
-        if not 'updated' in report:
-            report['updated'] = []
-        if not 'deleted' in report:
-            report['deleted'] = []
-        revision_register_class = register_classes['revision'] 
-        revision_register = revision_register_class()
-        revision = revision_register.create_entity()
-        revision.message = unicode(meta.get('log_message', ''))
-        revision.author = unicode(meta.get('author', ''))
-        # Apply deleting changes, then updating changes, then creating changes.
-        deleting = []
-        updating = []
-        creating = []
-        for change in changes:
-            if change.old == None and change.new != None:
-                creating.append(change)
-            elif change.old != None and change.new != None:
-                updating.append(change)
-            elif change.old != None and change.new == None:
-                deleting.append(change)
-        try:
-            for change in deleting:
-                entity = change.apply(is_forced=is_forced, moderator=moderator)
-                if entity:
-                    report['deleted'].append(entity)
-            Session.commit()
-            for change in updating:
-                entity = change.apply(is_forced=is_forced, moderator=moderator)
-                if entity:
-                    report['updated'].append(entity)
-            Session.commit()
-            created_entities = []  # Must configure access for new entities.
-            for change in creating:
-                entity = change.apply(is_forced=is_forced, moderator=moderator)
-                if entity:
-                    report['created'].append(entity)
-                    created_entities.append(entity)
-            Session.commit()
-        except Exception, inst:
-            try:
-                from ckan.model import repo as repository
-                repository.purge_revision(revision) # Commits and removes session.
-            except:
-                print "Error: Couldn't purge revision: %s" % revision
-            raise inst
-        revision_id = revision.id
-        # Setup access control for created entities.
-        for entity in created_entities:
-            setup_default_user_roles(entity, [])
-        # Todo: Teardown access control for deleted entities?
-        return revision_id
-
-    def commit(self):
-        """Constructs changesets from uncommitted CKAN repository revisions."""
-        uncommitted, head_revision = self.get_revisions_uncommitted_and_head()
-        working = self.get_working()
-        if working and not working.revision_id:
-            msg = "Working changeset '%s' has no revision id." % working.id
-            raise Exception, msg
-        if working and head_revision and working.revision_id != head_revision.id:
-            msg = "Working changeset points to revision '%s' (not head revision '%s')." % (working.revision_id, head_revision.id)
-            raise Exception, msg
-        uncommitted.reverse()
-        changeset_ids = []
-        follows_id = working and working.id or None
-        for revision in uncommitted:
-            changeset_id = self.construct_from_revision(revision, follows_id=follows_id)
-            changeset_ids.append(changeset_id)
-            self.move_working(changeset_id)
-            follows_id = changeset_id
-        return changeset_ids
-
-    def construct_from_revision(self, revision, follows_id=None):
-        """Creates changeset from given CKAN repository revision."""
-        meta = unicode(self.dumps({
-            'log_message': revision.message,
-            'author': revision.author,
-            'timestamp': revision.timestamp.isoformat(),
-        }))
-        if follows_id:
-            pass
-            # Todo: Detect if the new changes conflict with the line (it's a system error).
-            # Needed to protect against errors in diff generated by revision comparison.
-            # Todo: Calculation of current model state (diff origin to working):
-            #        - all values for a single attribute
-            #        - all values for a single entity
-            #        - all active entity refs
-            # Todo: Cache the change entity refs in the changeset to follow refs down a line.
-        # Create changes before changeset (changeset id depends on changes).
-        changes = []
-        for package in revision.packages:
-            change = self.construct_package_change(package, revision)
-            changes.append(change)
-        changeset = self.create_entity(
-            follows_id=follows_id,
-            meta=meta,
-            revision_id=revision.id,
-            changes=changes,
-        )
-        Session.add(changeset)
-        Session.commit()
-        return changeset.id
-
-    def construct_package_change(self, package, revision):
-        """Makes a changeset Change object from a CKAN package instance."""
-        packages = PackageRegister()
-        vector = packages.diff(package, revision)
-        ref = packages.ref(package)
-        diff = vector.as_diff()
-        change_register = ChangeRegister()
-        return change_register.create_entity(ref=ref, diff=diff)
-
-    def is_outstanding_changes(self):
-        """Checks for uncommitted revisions in CKAN repository."""
-        uncommitted, head_revision = self.get_revisions_uncommitted_and_head()
-        return len(uncommitted) > 0
-
-    def get_revisions_uncommitted_and_head(self):
-        """Finds uncommitted revisions in CKAN repository."""
-        import ckan.model
-        revisions =  ckan.model.repo.history()  # NB Youngest first.
-        uncommitted = []
-        head_revision = None
-        for revision in revisions:
-            changeset = self.get(revision.id, None, 'revision_id')
-            if changeset == None:
-                uncommitted.append(revision)
-            else:
-                head_revision = revision
-                # Assume contiguity of uncommitted revisions.
-                break 
-        return uncommitted, head_revision
-
-
-class PackageRegister(TrackedObjectRegister):
-    """Dictionary-like interface to package objects."""
-
-    object_type = Package
-    key_attr = 'id'
-    distinct_attrs = ['name']
-
-    def diff(self, entity, revision=None):
-        """Instantiates and returns a Vector for the difference
-        between the current and previous Package version."""
-        history = entity.all_revisions
-        age = len(history)
-        current_package_revision = None
-        previous_package_revision = None
-        for i in range(0, age):
-            package_revision = history[i]
-            if package_revision.revision.id == revision.id:
-                current_package_revision = package_revision
-                if i + 1 < age:
-                    previous_package_revision = history[i+1]
-                break
-        if not current_package_revision:
-            raise Exception, "Can't find package-revision for package: %s and revision: %s with package history: %s" % (entity, revision, history)
-        old_data = None  # Signifies object creation.
-        new_data = entity.as_dict()
-        del(new_data['revision_id'])
-        if previous_package_revision:
-            old_data = {}
-            for name in entity.revisioned_fields():
-                old_value = getattr(previous_package_revision, name)
-                new_value = getattr(current_package_revision, name)
-                if old_value == new_value:
-                    del(new_data[name])
-                else:
-                    old_data[name] = old_value
-                    new_data[name] = new_value
-        return Vector(old_data, new_data)
-
-    def patch(self, entity, vector):
-        """Updates Package according to the vector of change."""
-        super(PackageRegister, self).patch(entity, vector)
-        if 'tags' in vector.new:
-            register = TagRegister()
-            entity.tags = []
-            for tag_name in vector.new['tags']:
-                if tag_name in register:
-                    tag = register[tag_name]
-                else:
-                    tag = register.create_entity(name=tag_name)
-                entity.tags.append(tag)
-        if 'groups' in vector.new:
-            register = GroupRegister()
-            entity.groups = []
-            for group_name in vector.new['groups']:
-                if group_name in register:
-                    group = register[group_name]
-                else:
-                    # Todo: More about groups, not as simple as tags.
-                    group = register.create_entity(name=group_name)
-                entity.groups.append(group)
-        if 'license' in vector.new:
-            entity.license_id = vector.new['license']
-        if 'license_id' in vector.new:
-            entity.license_id = vector.new['license_id']
-        if 'extras' in vector.new:
-            entity.extras = vector.new['extras']
-        if 'resources' in vector.new:
-            entity.resources = []
-            for resource_data in vector.new['resources']:
-                package_resource = Resource(
-                    url=resource_data.get('url', u''),
-                    format=resource_data.get('format', u''),
-                    description=resource_data.get('description', u''),
-                    hash=resource_data.get('hash', u''),
-                )
-                #Session.add(package_resource)
-                entity.resources.append(package_resource) 
-
- 
-class RevisionRegister(ObjectRegister):
-    """Dictionary-like interface to revision objects."""
-
-    # Assume CKAN Revision class.
-    object_type = Revision
-    key_attr = 'id'
-
-    def create_entity(self, *args, **kwds):
-        """Creates new Revision instance with author and message."""
-        from ckan.model import repo
-        revision = repo.new_revision()
-        if 'author' in kwds:
-            revision.author = kwds['author']
-        if 'message' in kwds:
-            revision.message = kwds['message']
-        return revision
-
-
-class TagRegister(ObjectRegister):
-    """Dictionary-like interface to tag objects."""
-
-    # Assume CKAN Tag class.
-    object_type = Tag
-    key_attr = 'name'
-
-
-class GroupRegister(ObjectRegister):
-    """Dictionary-like interface to group objects."""
-
-    # Assume CKAN Group class.
-    object_type = Group
-    key_attr = 'name'
-
-
-register_classes['changeset'] = ChangesetRegister
-register_classes['revision'] = RevisionRegister
-register_classes['package'] = PackageRegister
-


http://bitbucket.org/okfn/ckan/changeset/bd811b87178b/
changeset:   bd811b87178b
branch:      feature-1094-authz
user:        thejimmyg
date:        2011-06-24 13:48:24
summary:     merged with more half finished changes
affected #:  9 files (4.5 KB)

--- a/ckan/authz.py	Fri Jun 24 12:11:45 2011 +0100
+++ b/ckan/authz.py	Fri Jun 24 12:48:24 2011 +0100
@@ -157,11 +157,7 @@
         return True
 
         user = model.User.by_name(username, autoflush=False)
-        if user:
-            q = model.Session.query(model.SystemRole)
-            q = q.autoflush(False)
-            q = q.filter_by(role=model.Role.ADMIN, user=user)
-            return q.count() > 0
+        return user.sysadmin
 
     @classmethod
     def get_admins(cls, domain_obj):


--- a/ckan/lib/create_test_data.py	Fri Jun 24 12:11:45 2011 +0100
+++ b/ckan/lib/create_test_data.py	Fri Jun 24 12:48:24 2011 +0100
@@ -401,36 +401,38 @@
         model.setup_default_user_roles(roger, [russianfan])
         model.add_user_to_role(visitor, model.Role.ADMIN, roger)
         testsysadmin = model.User.by_name(u'testsysadmin')
-        model.add_user_to_role(testsysadmin, model.Role.ADMIN, model.System())
+        testsysadmin.sysadmin = True
+        model.Session.add(testsysadmin)
+        #model.add_user_to_role(testsysadmin, model.Role.ADMIN, model.System())
 
         model.repo.commit_and_remove()
 
-        if commit_changesets:
-            from ckan.model.changeset import ChangesetRegister
-            changeset_ids = ChangesetRegister().commit()
+#        if commit_changesets:
+#            from ckan.model.changeset import ChangesetRegister
+#            changeset_ids = ChangesetRegister().commit()
 
         # Create a couple of authorization groups
-        for ag_name in [u'anauthzgroup', u'anotherauthzgroup']:
-            ag=model.AuthorizationGroup.by_name(ag_name) 
-            if not ag: #may already exist, if not create
-                ag=model.AuthorizationGroup(name=ag_name)
-                model.Session.add(ag)
+        #for ag_name in [u'anauthzgroup', u'anotherauthzgroup']:
+        #    ag=model.AuthorizationGroup.by_name(ag_name) 
+        #    if not ag: #may already exist, if not create
+        #        ag=model.AuthorizationGroup(name=ag_name)
+        #        model.Session.add(ag)
 
-        model.repo.commit_and_remove()
+        #model.repo.commit_and_remove()
 
         # and give them a range of roles on various things
-        ag = model.AuthorizationGroup.by_name(u'anauthzgroup')
-        aag = model.AuthorizationGroup.by_name(u'anotherauthzgroup')
-        pkg = model.Package.by_name(u'warandpeace')
-        g = model.Group.by_name('david')
+        #ag = model.AuthorizationGroup.by_name(u'anauthzgroup')
+        #aag = model.AuthorizationGroup.by_name(u'anotherauthzgroup')
+        #pkg = model.Package.by_name(u'warandpeace')
+        #g = model.Group.by_name('david')
+#
+#        model.add_authorization_group_to_role(ag, u'editor', model.System())
+#        model.add_authorization_group_to_role(ag, u'reader', pkg)
+#        model.add_authorization_group_to_role(ag, u'admin', aag)
+#        model.add_authorization_group_to_role(aag, u'editor', ag)
+#        model.add_authorization_group_to_role(ag, u'editor', g)
 
-        model.add_authorization_group_to_role(ag, u'editor', model.System())
-        model.add_authorization_group_to_role(ag, u'reader', pkg)
-        model.add_authorization_group_to_role(ag, u'admin', aag)
-        model.add_authorization_group_to_role(aag, u'editor', ag)
-        model.add_authorization_group_to_role(ag, u'editor', g)
-
-        model.repo.commit_and_remove()
+#        model.repo.commit_and_remove()
 
 
 


--- a/ckan/lib/helpers.py	Fri Jun 24 12:11:45 2011 +0100
+++ b/ckan/lib/helpers.py	Fri Jun 24 12:48:24 2011 +0100
@@ -144,6 +144,8 @@
     return config.get('search.facets.%s.title' % name, name.capitalize())
 
 def am_authorized(c, action, domain_object=None):
+    # XXX This needs refactoring everywhere else
+    return True
     from ckan.authz import Authorizer
     if domain_object is None:
         from ckan import model


--- a/ckan/logic/action/get.py	Fri Jun 24 12:11:45 2011 +0100
+++ b/ckan/logic/action/get.py	Fri Jun 24 12:48:24 2011 +0100
@@ -32,7 +32,27 @@
     else:
          query.offset(20*data_dict['page']).limit(20)
     # Return plain dictionaries as all logic layer functions should
-    return [dictize(context, i) for i in query.all()]
+    return query
+    #results = [dictize(context, i) for i in query.all()]
+
+
+    #    <py:if test="revision.has_key('package_id')">
+    #      <a href="${h.url_for(controller='package', action='read', id=revsion.package_name)}">${revsion.package_name}</a>
+    #    </py:if>
+    #    <py:if test="revision.has_key('group_id')">
+    #      <a href="${h.url_for(controller='group', action='read', id=revision.group_name)}">${revision.group_name}</a>
+    #    </py:if>
+
+    ## Now add data for any groups or packages in the revision
+    #for result in results:
+    #    # XXX this misses off changes to things that aren't the key entities
+    #    package_records = model.Session.query(model.PackageRevision).filter_by(revision_id=result['id']).all()
+    #    if package_records:
+    #        result['package_name'] = pacakge_records[0].name
+    #    group_records = model.Session.query(model.GroupRevision).filter_by(revision_id=result['id']).all()
+    #    if group_records:
+    #        result['group_name'] = group_records[0].name
+    #return results
 
 def package_list(context):
     model = context["model"]


--- a/ckan/model/__init__.py	Fri Jun 24 12:11:45 2011 +0100
+++ b/ckan/model/__init__.py	Fri Jun 24 12:48:24 2011 +0100
@@ -95,7 +95,7 @@
     def init_configuration_data(self):
         '''Default configuration, for when CKAN is first used out of the box.
         This state may be subsequently configured by the user.'''
-        init_authz_configuration_data()
+        #init_authz_configuration_data()
         if Session.query(Revision).count() == 0:
             rev = Revision()
             rev.author = 'system'
@@ -109,7 +109,6 @@
         has shortcuts.
         '''
         self.metadata.create_all(bind=self.metadata.bind)    
-        return
         self.init_const_data()
         self.init_configuration_data()
 


--- a/ckan/model/authz.py	Fri Jun 24 12:11:45 2011 +0100
+++ b/ckan/model/authz.py	Fri Jun 24 12:48:24 2011 +0100
@@ -8,7 +8,6 @@
 from group import Group
 from types import make_uuid
 from user import User
-from core import System
 #from authorization_group import AuthorizationGroup, authorization_group_table
 
 PSEUDO_USER__LOGGED_IN = u'logged_in'
@@ -122,20 +121,9 @@
             return '<%s group="%s" role="%s" context="%s">' % \
                 (self.__class__.__name__, self.authorized_group.name, self.role, self.context)
         else:
-            assert False, "UserObjectRole is neither for an authzgroup or for a user" 
+            assert False, "AuthorizationOverride is neither for an authzgroup or for a user" 
             
 
-
-    @classmethod
-    def get_object_role_class(cls, domain_obj):
-        protected_object = protected_objects.get(domain_obj.__class__, None)
-        if protected_object is None:
-            # TODO: make into an authz exception
-            msg = '%s is not a protected object, i.e. a subject of authorization' % domain_obj
-            raise Exception(msg)
-        else:
-            return protected_object
-
     @classmethod
     def user_has_role(cls, user, role, domain_obj):
         assert isinstance(user, User), user
@@ -143,32 +131,15 @@
         return q.count() == 1
         
     @classmethod
-    def authorization_group_has_role(cls, authorized_group, role, domain_obj):
-        assert isinstance(authorized_group, AuthorizationGroup), authorized_group
-        q = cls._authorized_group_query(authorized_group, role, domain_obj)
-        return q.count() == 1
-        
-    @classmethod
     def _user_query(cls, user, role, domain_obj):
-        q = Session.query(cls).filter_by(role=role)
+        q = Session.query(cls).filter_by(
+            role=role,
+            object_id=domain_obj.id,
+            user_id=user.id)
         # some protected objects are not "contextual"
-        if cls.name is not None:
-            # e.g. filter_by(package=domain_obj)
-            q = q.filter_by(**dict({cls.name: domain_obj}))
-        q = q.filter_by(user=user)
         return q
     
     @classmethod
-    def _authorized_group_query(cls, authorized_group, role, domain_obj):
-        q = Session.query(cls).filter_by(role=role)
-        # some protected objects are not "contextual"
-        if cls.name is not None:
-            # e.g. filter_by(package=domain_obj)
-            q = q.filter_by(**dict({cls.name: domain_obj}))
-        q = q.filter_by(authorized_group=authorized_group)
-        return q
-
-    @classmethod
     def add_user_to_role(cls, user, role, domain_obj):
         '''NB: Leaves the caller to commit the change. If called twice without a
         commit, will add the role to the database twice. Since some other
@@ -180,24 +151,8 @@
         # that won't work if the transaction hasn't been committed yet, which allows a role to be added twice (you can do this from the interface)
         if cls.user_has_role(user, role, domain_obj):
             return
-        objectrole = cls(role=role, user=user)
-        if cls.name is not None:
-            setattr(objectrole, cls.name, domain_obj)
-        Session.add(objectrole)
-         
-    @classmethod
-    def add_authorization_group_to_role(cls, authorization_group, role, domain_obj):
-        '''NB: Leaves the caller to commit the change. If called twice without a
-        commit, will add the role to the database twice. Since some other
-        functions count the number of occurrences, that leaves a fairly obvious
-        bug. But adding a commit here seems to break various tests.
-        So don't call this twice without committing, I guess...
-        '''
-        if cls.authorization_group_has_role(authorization_group, role, domain_obj):
-            return
-        objectrole = cls(role=role, authorized_group=authorization_group)
-        if cls.name is not None:
-            setattr(objectrole, cls.name, domain_obj)
+        objectrole = cls(role=role, user=user, object_id=domain_obj.id, 
+                         object_type=domain_obj.__class__.__name__.lower())
         Session.add(objectrole)
 
     @classmethod
@@ -208,50 +163,28 @@
         Session.commit()
         Session.remove()
 
-    @classmethod
-    def remove_authorization_group_from_role(cls, authorization_group, role, domain_obj):
-        q = cls._authorized_group_query(authorization_group, role, domain_obj)
-        for ago_role in q.all():
-            Session.delete(ago_role)
-        Session.commit()
-        Session.remove()
-
-
-
-
 ## ======================================
 ## Helpers
 
 
 def user_has_role(user, role, domain_obj):
-    objectrole = UserObjectRole.get_object_role_class(domain_obj)
-    return objectrole.user_has_role(user, role, domain_obj)
+    return AuthorizationOverride.user_has_role(user, role, domain_obj)
 
 def add_user_to_role(user, role, domain_obj):
-    objectrole = UserObjectRole.get_object_role_class(domain_obj)
-    objectrole.add_user_to_role(user, role, domain_obj)
+    AuthorizationOverride.add_user_to_role(user, role, domain_obj)
 
 def remove_user_from_role(user, role, domain_obj):
-    objectrole = UserObjectRole.get_object_role_class(domain_obj)
-    objectrole.remove_user_from_role(user, role, domain_obj)
+    AuthorizationOverride.remove_user_from_role(user, role, domain_obj)
 
     
 def authorization_group_has_role(authorization_group, role, domain_obj):
-    objectrole = UserObjectRole.get_object_role_class(domain_obj)
-    return objectrole.authorization_group_has_role(authorization_group, role, domain_obj)
+    return AuthorizationOverride.authorization_group_has_role(authorization_group, role, domain_obj)
         
 def add_authorization_group_to_role(authorization_group, role, domain_obj):
-    objectrole = UserObjectRole.get_object_role_class(domain_obj)
-    objectrole.add_authorization_group_to_role(authorization_group, role, domain_obj)
+    AuthorizationOverride.add_authorization_group_to_role(authorization_group, role, domain_obj)
 
 def remove_authorization_group_from_role(authorization_group, role, domain_obj):
-    objectrole = UserObjectRole.get_object_role_class(domain_obj)
-    objectrole.remove_authorization_group_from_role(authorization_group, role, domain_obj)
-    
-def init_authz_configuration_data():
-    setup_default_user_roles(System())
-    Session.commit()
-    Session.remove()
+    AuthorizationOverride.remove_authorization_group_from_role(authorization_group, role, domain_obj)
     
 def init_authz_const_data():
     '''Setup all default role-actions.
@@ -324,7 +257,6 @@
     'Package': {"visitor": ["editor"], "logged_in": ["editor"]},
     'Group': {"visitor": ["reader"], "logged_in": ["reader"]},
     'System': {"visitor": ["anon_editor"], "logged_in": ["editor"]},
-    'AuthorizationGroup': {"visitor": ["reader"], "logged_in": ["reader"]},
     }
 
 global _default_user_roles_cache
@@ -360,7 +292,7 @@
     @param admins - a list of User objects
     NB: leaves caller to commit change.
     '''
-    assert isinstance(domain_object, (Package, Group, System, AuthorizationGroup)), domain_object
+    assert isinstance(domain_object, (Package, Group)), domain_object
     assert isinstance(admins, list)
     user_roles_ = get_default_user_roles(domain_object)
     setup_user_roles(domain_object,
@@ -393,6 +325,11 @@
                 cascade='all, delete, delete-orphan'
             )
         ),
+        'package': orm.relation(Package,
+            backref=orm.backref('roles'),
+            primaryjoin=authorization_override_table.c.object_id == package_table.c.id,
+            foreign_keys=[authorization_override_table.c.object_id],
+            )
     },
     order_by=[authorization_override_table.c.id],
 )


--- a/ckan/model/core.py	Fri Jun 24 12:11:45 2011 +0100
+++ b/ckan/model/core.py	Fri Jun 24 12:48:24 2011 +0100
@@ -8,19 +8,6 @@
 revision_table = vdm.sqlalchemy.make_revision_table(metadata)
 revision_table.append_column(Column('approved_timestamp', DateTime))
 
-class System(DomainObject):
-    
-    name = 'system'
-    
-    def __unicode__(self):
-        return u'<%s>' % self.__class__.__name__
-    
-    def purge(self):
-        pass
-        
-    @classmethod
-    def by_name(cls, name): 
-        return System()        
 
 # VDM-specific domain objects
 State = vdm.sqlalchemy.State


--- a/ckan/new_authz.py	Fri Jun 24 12:11:45 2011 +0100
+++ b/ckan/new_authz.py	Fri Jun 24 12:48:24 2011 +0100
@@ -56,7 +56,6 @@
     if _auth_functions:
         return _auth_functions.get(action)
     # Otherwise look in all the plugins to resolve all possible
-    global _auth_functions
     # First get the default ones in the ckan/logic/auth directory
     # Rather than writing them out in full will use __import__
     # to load anything from ckan.auth that looks like it might


--- a/ckan/templates/_util.html	Fri Jun 24 12:11:45 2011 +0100
+++ b/ckan/templates/_util.html	Fri Jun 24 12:48:24 2011 +0100
@@ -365,12 +365,6 @@
       <td>${h.render_datetime(revision.timestamp)}</td><td>${h.linked_user(revision.author)}</td><td>
-        <py:for each="pkg in revision.packages">
-          <a href="${h.url_for(controller='package', action='read', id=pkg.name)}">${pkg.name}</a>
-        </py:for>
-        <py:for each="grp in revision.groups">
-          <a href="${h.url_for(controller='group', action='read', id=grp.name)}">${grp.name}</a>
-        </py:for></td><td>${revision.message}</td></tr>


http://bitbucket.org/okfn/ckan/changeset/8711fb18db6c/
changeset:   8711fb18db6c
branch:      feature-1094-authz
user:        thejimmyg
date:        2011-06-24 16:22:03
summary:     Direction established, code not working
affected #:  11 files (4.4 KB)

--- a/ckan/controllers/api.py	Fri Jun 24 12:48:24 2011 +0100
+++ b/ckan/controllers/api.py	Fri Jun 24 15:22:03 2011 +0100
@@ -36,7 +36,15 @@
 
     def __call__(self, environ, start_response):
         self._identify_user()
-        if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
+        check = check_access(
+            dict(
+                model=model, 
+                user=c.user,
+            ),
+            'site_read',
+            {},
+        )      
+        if not check.success:
             response_msg = self._finish(403, _('Not authorized to see this page'))
             # Call start_response manually instead of the parent __call__
             # because we want to end the request instead of continuing.


--- a/ckan/controllers/package.py	Fri Jun 24 12:48:24 2011 +0100
+++ b/ckan/controllers/package.py	Fri Jun 24 15:22:03 2011 +0100
@@ -33,6 +33,8 @@
 import ckan.rating
 import ckan.misc
 
+from ckan.logic import check_access
+
 log = logging.getLogger('ckan.controllers')
 
 def search_url(params):
@@ -87,8 +89,14 @@
         ## This is messy as auths take domain object not data_dict
         pkg = context.get('package') or c.pkg
         if pkg:
-            c.auth_for_change_state = Authorizer().am_authorized(
-                c, model.Action.CHANGE_STATE, pkg)
+            check = check_access(
+                context,
+                'package_delete',
+                dict(
+                    id=pkg.id,
+                ),
+            )
+            c.auth_for_change_state = check.success
 
     ## end hooks
 
@@ -96,8 +104,6 @@
     extensions = PluginImplementations(IPackageController)
 
     def search(self):        
-        if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
-            abort(401, _('Not authorized to see this page'))
         q = c.q = request.params.get('q') # unicode format (decoded from utf8)
         c.open_only = request.params.get('open_only')
         c.downloadable_only = request.params.get('downloadable_only')
@@ -111,7 +117,26 @@
 
         # most search operations should reset the page counter:
         params_nopage = [(k, v) for k,v in request.params.items() if k != 'page']
-        
+        check = check_access(
+            dict(
+                model=model, 
+                user=c.user,
+            ),
+            'search',
+            dict(
+                q=q,
+                open_only=c.open_only,
+                downloadable_only=c.downloadable_only,
+                page=page,
+                limit=limit,
+                # @@@ We should also add the other params but how to get
+                #     get them out isn't immediately clear
+            ),
+        )      
+        if not check.success:
+            abort(401, check['msg'])
+
+
         def drill_down_url(**by):
             params = list(params_nopage)
             params.extend(by.items())
@@ -216,7 +241,13 @@
                 break
 
         PackageSaver().render_package(c.pkg_dict, context)
-        return render('package/read.html')
+        return render(
+            'package/read.html', 
+            extra_vars=dict(
+                context=context, 
+                check_access=check_access,
+            ) 
+        )
 
     def comments(self, id):
         context = {'model': model, 'session': model.Session,
@@ -352,9 +383,9 @@
 
         c.pkg = context.get("package")
 
-        am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, c.pkg)
-        if not am_authz:
-            abort(401, _('User %r not authorized to edit %s') % (c.user, id))
+        check = check_access(context, 'package_edit', {'id': id})
+        if not check.success:
+            abort(401, check.msg)
 
         errors = errors or {}
         vars = {'data': data, 'errors': errors, 'error_summary': error_summary}


--- a/ckan/controllers/revision.py	Fri Jun 24 12:48:24 2011 +0100
+++ b/ckan/controllers/revision.py	Fri Jun 24 15:22:03 2011 +0100
@@ -8,21 +8,14 @@
 import ckan.authz
 from ckan.lib.cache import proxy_cache, get_cache_expires
 from ckan.logic.action.get import revision_list
+from ckan.logic import NotAuthorized
+from ckan.logic import check_access
+
 
 cache_expires = get_cache_expires(sys.modules[__name__])
 
 class RevisionController(BaseController):
 
-    #def __before__(self, action, **env):
-    #    BaseController.__before__(self, action, **env)
-    #    c.revision_change_state_allowed = (
-    #        c.user and
-    #        self.authorizer.is_authorized(c.user, model.Action.CHANGE_STATE,
-    #            model.Revision)
-    #        )
-    #    if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
-    #        abort(401, _('Not authorized to see this page'))
-
     def index(self):
         return self.list()
 
@@ -51,7 +44,10 @@
             data_dict['since_when'] = datetime.now() + ourtimedelta
         # Get the revision list (will fail if you don't have the correct permission)
         # XXX This should return data, not a query
-        revision_records = revision_list(context, data_dict)
+        try:
+            revision_records = revision_list(context, data_dict)
+        except NotAuthorized:
+            abort(401, _('Not authorized to see this page'))
         # If we have the query, we are allowed to make a change
         # XXX This line should be deprecated, you won't get here otherwise
         c.revision_change_state_allowed = True
@@ -132,8 +128,14 @@
             return render('revision/list.html')
 
     def read(self, id=None):
+        context = {}
+        context['model'] = model
+        context['user'] = c.user
         if id is None:
             abort(404)
+        check = check_access(context, "revision_show", {})
+        if not check.success:
+            abort(401, _('Not authorized to see this page'))
         c.revision = model.Session.query(model.Revision).get(id)
         if c.revision is None:
             abort(404)
@@ -147,6 +149,13 @@
         return render('revision/read.html')
 
     def diff(self, id=None):
+        context = {}
+        context['model'] = model
+        context['user'] = c.user
+        check = check_access(context, "revision_diff", {})
+        if not check.success:
+            abort(401, _('Not authorized to see this page'))
+
         if 'diff' not in request.params or 'oldid' not in request.params:
             abort(400)
         c.revision_from = model.Session.query(model.Revision).get(
@@ -169,6 +178,20 @@
         return render('revision/diff.html')
 
     def edit(self, id=None):
+        context = {}
+        context['model'] = model
+        context['user'] = c.user
+        if action=='delete':
+            check = check_access(context, "revision_delete", {})
+            if not check.success:
+                abort(401, _(check['msg']))
+        elif action=='undelete':
+            check = check_access(context, "revision_undelete", {})
+            if not check.success:
+                abort(401, _(check['msg']))
+        else:
+            abort(404)
+
         if id is None:
             abort(404)
         revision = model.Session.query(model.Revision).get(id)


--- a/ckan/controllers/tag.py	Fri Jun 24 12:48:24 2011 +0100
+++ b/ckan/controllers/tag.py	Fri Jun 24 15:22:03 2011 +0100
@@ -13,7 +13,15 @@
 
     def __before__(self, action, **env):
         BaseController.__before__(self, action, **env)
-        if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System):
+        check = check_access(
+            dict(
+                model=model, 
+                user=c.user,
+            ),
+            'site_read',
+            {},
+        )      
+        if not check.success:
             abort(401, _('Not authorized to see this page'))
 
     def index(self):


--- a/ckan/logic/__init__.py	Fri Jun 24 12:48:24 2011 +0100
+++ b/ckan/logic/__init__.py	Fri Jun 24 15:22:03 2011 +0100
@@ -4,6 +4,18 @@
 
 from ckan.lib.navl.dictization_functions import flatten_dict
 
+class AttributeDict(dict):
+    def __getattr__(self, name):
+        try:
+            return self[name]
+        except KeyError:
+            raise AttributeError('No such attribute %r'%name)
+
+    def __setattr__(self, name, value):
+        raise AttributeError(
+            'You cannot set attributes of this object directly'
+        )
+
 class ActionError(Exception):
     def __init__(self, extra_msg=None):
         self.extra_msg = extra_msg
@@ -87,10 +99,10 @@
         logic_authorization = new_authz.is_authorized(context, action, data_dict, object_id, object_type)
         if not logic_authorization['success']:
             if not new_authz.check_overridden(context, action, object_id, object_type):
-                raise NotAuthorized(logic_authorization['msg'])
+                return AttributeDict(logic_authorization)
     elif not user:
         log.debug("No valid API key provided.")
-        raise NotAuthorized()
+        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 Jun 24 12:48:24 2011 +0100
+++ b/ckan/logic/action/delete.py	Fri Jun 24 15:22:03 2011 +0100
@@ -4,6 +4,12 @@
 from ckan.plugins import PluginImplementations, IGroupController, IPackageController
 
 
+def revision_undelete(context, data_dict):
+    raise Exception('David Raznick to implement')
+
+def revision_delete(context, data_dict):
+    raise Exception('David Raznick to implement')
+
 def package_delete(context):
 
     model = context['model']


--- a/ckan/logic/action/get.py	Fri Jun 24 12:48:24 2011 +0100
+++ b/ckan/logic/action/get.py	Fri Jun 24 15:22:03 2011 +0100
@@ -15,6 +15,7 @@
 
 
 def revision_list(context, data_dict=None):
+    check_access(context, "revision_list", data_dict)
     model = context["model"]
     if data_dict is None:
         # XXX Refactor calling function
@@ -92,6 +93,14 @@
         package_list.append(result_dict)
     return package_list
 
+def search(context, data_dict):
+    raise Exception('David Raznick to implement')
+
+def revision_diff(context, data_dict):
+    raise Exception('David Raznick to implement')
+
+def group_revision_list(context, data_dict):
+    raise Exception('David Raznick to implement')
 
 def package_revision_list(context):
     model = context["model"]
@@ -211,7 +220,7 @@
     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']


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/logic/auth/delete.py	Fri Jun 24 15:22:03 2011 +0100
@@ -0,0 +1,23 @@
+
+def package_delete(context, data_dict):
+    """
+    "./ckan/logic/validators.py" 
+    Replaced ignore_not_admi()
+    if (user and pkg and 
+        Authorizer().is_authorized(user, model.Action.CHANGE_STATE, pkg)):
+
+    """
+    return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
+
+def package_relationship_delete(context, data_dict):
+    return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
+
+def group_delete(context, data_dict):
+    return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
+
+def revision_undelete(context, data_dict):
+    return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
+
+def revision_delete(context, data_dict):
+    return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
+


--- a/ckan/logic/auth/get.py	Fri Jun 24 12:48:24 2011 +0100
+++ b/ckan/logic/auth/get.py	Fri Jun 24 15:22:03 2011 +0100
@@ -1,3 +1,18 @@
+def site_read(context, data_dict):
+    """\
+    This function should be deprecated. It is only here because we couldn't
+    get hold of Friedrich to ask what it was for.
+
+    ./ckan/controllers/api.py
+    """
+    return {'success': True}
+
+def search(context, data_dict):
+    """\
+    Everyone can search by default
+    """
+    return {'success': True}
+
 def package_list(context, data_dict):
     return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
 
@@ -5,6 +20,17 @@
     return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
 
 def revision_list(context, data_dict):
+    """\
+    from controller/revision __before__
+    if not self.authorizer.am_authorized(c, model.Action.SITE_READ, model.System): abort
+    -> In our new model everyone can read the revison list
+    """
+    return {'success': True}
+
+def revision_diff(context, data_dict):
+    return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
+
+def group_revision_list(context, data_dict):
     return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
 
 def package_revision_list(context, data_dict):


--- a/ckan/logic/auth/update.py	Fri Jun 24 12:48:24 2011 +0100
+++ b/ckan/logic/auth/update.py	Fri Jun 24 15:22:03 2011 +0100
@@ -1,8 +1,10 @@
+from pylons.i18n import _
+
 def make_latest_pending_package_active(context, data_dict):
     return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
 
 def package_update(context, data_dict):
-    return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}
+    return {'success': False, 'msg': _('User %r not authorized to edit %s') % (contect['user'], data_dict['id'])}
 
 def package_relationship_update(context, data_dict):
     return {'success': False, 'msg': 'Not implemented yet in the auth refactor'}


--- a/ckan/templates/_util.html	Fri Jun 24 12:48:24 2011 +0100
+++ b/ckan/templates/_util.html	Fri Jun 24 15:22:03 2011 +0100
@@ -365,6 +365,12 @@
       <td>${h.render_datetime(revision.timestamp)}</td><td>${h.linked_user(revision.author)}</td><td>
+        <py:for each="pkg in revision.packages">
+          <a href="${h.url_for(controller='package', action='read', id=pkg.name)}">${pkg.name}</a>
+        </py:for>
+        <py:for each="grp in revision.groups">
+          <a href="${h.url_for(controller='group', action='read', id=grp.name)}">${grp.name}</a>
+        </py:for></td><td>${revision.message}</td></tr>

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