Commits

David Larlet committed 81b112d

Initial commit of the project

Comments (0)

Files changed (27)

+====================
+django-roa changelog
+====================
+
+Version 0.1, 12 December 2008:
+------------------------------
+
+Initial release.
+Copyright (c) 2008-2009, David Larlet
+All rights reserved.
+
+Redistribution of this application is not permitted.
+===============
+django-roa todo
+===============
+
+Priority 1:
+-----------
+
+* Handle ForeignKey relations (top priority, required for admin auth)
+* Finish remoteauth application
+* Improve documentation
+* Write more specs
+* Support XML serialization
+
+
+Priority 2:
+-----------
+
+* Handle Q filters
+* Handle ManyToMany relations
+* Improve test server for production
+* Cascading changes/deletions
+* Improve debugging

django_roa/__init__.py

+# Depends on settings for flexibility
+from django.conf import settings
+
+from django_roa.db.models import RemoteModel, DjangoModel
+from django_roa.db.managers import RemoteManager, DjangoManager
+
+Model = getattr(settings, "ROA_MODELS", False) and RemoteModel or DjangoModel
+Manager = getattr(settings, "ROA_MODELS", False) and RemoteManager or DjangoManager

django_roa/db/__init__.py

Empty file added.

django_roa/db/managers.py

+from django.db.models.manager import Manager as DjangoManager
+
+from django_roa.db.query import RemoteQuerySet
+
+
+class RemoteManager(DjangoManager):
+    """
+    Manager which access remote resources.
+    """
+    use_for_related_fields = True
+    
+    def get_query_set(self):
+        """
+        Returns a QuerySet which access remote resources.
+        """
+        return RemoteQuerySet(self.model)

django_roa/db/models.py

+import sys
+from django.conf import settings
+from django.db import models, connection, transaction
+from django.db.models import signals
+from django.db.models.fields import AutoField
+from django.core import serializers
+
+from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
+from django.db.models.fields import AutoField
+from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
+from django.db.models.query import delete_objects, Q, CollectedObjects
+from django.db.models.options import Options
+from django.db.models import signals, Manager
+from django.db.models.loading import register_models, get_model
+from django.db.models.base import ModelBase, subclass_exception
+
+from restclient import Resource
+from django_roa.db.managers import RemoteManager
+
+class ResourceAsMetaModelBase(ModelBase):
+    """
+    Deal with the new Meta ``resource_url`` attribute in __new__.
+    """
+    def __new__(cls, name, bases, attrs):
+        super_new = super(ModelBase, cls).__new__
+        parents = [b for b in bases if isinstance(b, ModelBase)]
+        if not parents:
+            # If this isn't a subclass of Model, don't do anything special.
+            return super_new(cls, name, bases, attrs)
+
+        # Create the class.
+        module = attrs.pop('__module__')
+        new_class = super_new(cls, name, bases, {'__module__': module})
+        attr_meta = attrs.pop('Meta', None)
+        abstract = getattr(attr_meta, 'abstract', False)
+        if not attr_meta:
+            meta = getattr(new_class, 'Meta', None)
+        else:
+            meta = attr_meta
+        base_meta = getattr(new_class, '_meta', None)
+
+        if getattr(meta, 'app_label', None) is None:
+            # Figure out the app_label by looking one level up.
+            # For 'django.contrib.sites.models', this would be 'sites'.
+            model_module = sys.modules[new_class.__module__]
+            kwargs = {"app_label": model_module.__name__.split('.')[-2]}
+        else:
+            kwargs = {}
+
+        # Custom Meta, replace:
+        # new_class.add_to_class('_meta', Options(meta, **kwargs))
+        resource_url = None
+        options = Options(meta, **kwargs)
+        if hasattr(options, 'meta') and options.meta is not None:
+            resource_url = options.meta.__dict__['resource_url']
+            resource_url_id = options.meta.__dict__['resource_url_id']
+            del options.meta.__dict__['resource_url']
+            del options.meta.__dict__['resource_url_id']
+        new_class.add_to_class('_meta', options)
+        if resource_url is not None:
+            setattr(new_class._meta, 'resource_url', resource_url)
+            setattr(new_class._meta, 'resource_url_id', resource_url_id)
+        # /Custom Meta
+
+        if not abstract:
+            new_class.add_to_class('DoesNotExist',
+                    subclass_exception('DoesNotExist', ObjectDoesNotExist, module))
+            new_class.add_to_class('MultipleObjectsReturned',
+                    subclass_exception('MultipleObjectsReturned', MultipleObjectsReturned, module))
+            if base_meta and not base_meta.abstract:
+                # Non-abstract child classes inherit some attributes from their
+                # non-abstract parent (unless an ABC comes before it in the
+                # method resolution order).
+                if not hasattr(meta, 'ordering'):
+                    new_class._meta.ordering = base_meta.ordering
+                if not hasattr(meta, 'get_latest_by'):
+                    new_class._meta.get_latest_by = base_meta.get_latest_by
+
+        # Custom default manager (sometimes Django instanciate a classic one?)
+        #if getattr(new_class, '_default_manager', None):
+        #    new_class._default_manager = None
+        new_class._default_manager = RemoteManager()
+        new_class._default_manager.contribute_to_class(new_class, name)
+        # /Custom default manager
+
+        # Bail out early if we have already created this class.
+        m = get_model(new_class._meta.app_label, name, False)
+        if m is not None:
+            return m
+
+        # Add all attributes to the class.
+        for obj_name, obj in attrs.items():
+            new_class.add_to_class(obj_name, obj)
+
+        # Do the appropriate setup for any model parents.
+        o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields
+                if isinstance(f, OneToOneField)])
+        for base in parents:
+            if not hasattr(base, '_meta'):
+                # Things without _meta aren't functional models, so they're
+                # uninteresting parents.
+                continue
+
+            # All the fields of any type declared on this model
+            new_fields = new_class._meta.local_fields + \
+                         new_class._meta.local_many_to_many + \
+                         new_class._meta.virtual_fields
+            field_names = set([f.name for f in new_fields])
+
+            if not base._meta.abstract:
+                # Concrete classes...
+                if base in o2o_map:
+                    field = o2o_map[base]
+                    field.primary_key = True
+                    new_class._meta.setup_pk(field)
+                else:
+                    attr_name = '%s_ptr' % base._meta.module_name
+                    field = OneToOneField(base, name=attr_name,
+                            auto_created=True, parent_link=True)
+                    new_class.add_to_class(attr_name, field)
+                new_class._meta.parents[base] = field
+
+            else:
+                # .. and abstract ones.
+
+                # Check for clashes between locally declared fields and those
+                # on the ABC.
+                parent_fields = base._meta.local_fields + base._meta.local_many_to_many
+                for field in parent_fields:
+                    if field.name in field_names:
+                        raise FieldError('Local field %r in class %r clashes '\
+                                         'with field of similar name from '\
+                                         'abstract base class %r' % \
+                                            (field.name, name, base.__name__))
+                    new_class.add_to_class(field.name, copy.deepcopy(field))
+
+                # Pass any non-abstract parent classes onto child.
+                new_class._meta.parents.update(base._meta.parents)
+
+            # Inherit managers from the abstract base classes.
+            base_managers = base._meta.abstract_managers
+            base_managers.sort()
+            for _, mgr_name, manager in base_managers:
+                val = getattr(new_class, mgr_name, None)
+                if not val or val is manager:
+                    new_manager = manager._copy_to_model(new_class)
+                    new_class.add_to_class(mgr_name, new_manager)
+
+            # Inherit virtual fields (like GenericForeignKey) from the parent class
+            for field in base._meta.virtual_fields:
+                if base._meta.abstract and field.name in field_names:
+                    raise FieldError('Local field %r in class %r clashes '\
+                                     'with field of similar name from '\
+                                     'abstract base class %r' % \
+                                        (field.name, name, base.__name__))
+                new_class.add_to_class(field.name, copy.deepcopy(field))
+
+        if abstract:
+            # Abstract base models can't be instantiated and don't appear in
+            # the list of models for an app. We do the final setup for them a
+            # little differently from normal models.
+            attr_meta.abstract = False
+            new_class.Meta = attr_meta
+            return new_class
+
+        new_class._prepare()
+        register_models(new_class._meta.app_label, new_class)
+
+        # Because of the way imports happen (recursively), we may or may not be
+        # the first time this model tries to register with the framework. There
+        # should only be one class for each model, so we always return the
+        # registered version.
+        return get_model(new_class._meta.app_label, name, False)
+
+
+class RemoteModel(models.Model):
+    """
+    Model which access remote resources.
+    """
+    __metaclass__ = ResourceAsMetaModelBase
+    
+    def save_base(self, raw=False, cls=None, force_insert=False,
+            force_update=False):
+        assert not (force_insert and force_update)
+        if not cls:
+            cls = self.__class__
+            meta = self._meta
+            signal = True
+            signals.pre_save.send(sender=self.__class__, instance=self, raw=raw)
+        else:
+            meta = cls._meta
+            signal = False
+        
+        # If we are in a raw save, save the object exactly as presented.
+        # That means that we don't try to be smart about saving attributes
+        # that might have come from the parent class - we just save the
+        # attributes we have been given to the class we have been given.
+        if not raw:
+            for parent, field in meta.parents.items():
+                # At this point, parent's primary key field may be unknown
+                # (for example, from administration form which doesn't fill
+                # this field). If so, fill it.
+                if getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
+                    setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
+                
+                #self.save_base(raw, parent)
+                setattr(self, field.attname, self._get_pk_val(parent._meta))
+
+        non_pks = [f for f in meta.local_fields if not f.primary_key]
+
+        args = dict((field.name, field.value_to_string(self)) for field in meta.local_fields)
+        fk_args = dict((field.get_attname(), getattr(self, field.name).id) \
+                    for field in meta.local_fields \
+                        if isinstance(field, models.ForeignKey) \
+                            and field.name != 'remotemodel_ptr')
+        args.update(fk_args)
+        pk_val = self._get_pk_val(meta)
+        pk_set = pk_val is not None
+
+        if force_update or pk_set and not self.id is None:
+            resource = Resource(meta.resource_url_id % {'id': self.id})
+            response = resource.put(**args)
+        else:
+            resource = Resource(meta.resource_url)
+            response = resource.post(**args)
+        
+        result = serializers.deserialize(getattr(settings, "ROA_FORMAT", 'json'), response).next()
+
+        try:
+            result_id = int(result.object.remotemodel_ptr_id)
+        except AttributeError: # FK
+            result_id = int(result.object.id)
+        self.id = self.remotemodel_ptr_id = result_id
+        self = result.object
+
+    save_base.alters_data = True
+
+    def delete(self):
+        assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
+
+        # TODO: Find all the objects that need to be deleted.
+        resource = Resource(self._meta.resource_url_id % {'id': self.id})
+        response = resource.delete()
+
+    delete.alters_data = True
+
+
+class DjangoModel(models.Model):
+    """
+    Model which allows ``resource_url*`` as Meta attributes.
+    """
+    __metaclass__ = ResourceAsMetaModelBase
+    
+

django_roa/db/query.py

+from django.conf import settings
+from django.db.models import query
+from django.core import serializers
+
+from restclient.rest import Resource, ResourceNotFound
+
+
+class Query(object):
+    def __init__(self):
+        self.count = False
+        self.order_by = []
+        self.filters = {}
+        self.excludes = {}
+        self.filterable = True
+        self.limit_start = None
+        self.limit_stop = None
+        self.where = False
+    
+    def can_filter(self):
+        return self.filterable
+    
+    def clone(self):
+        return self
+    
+    def get_count(self):
+        self.count = True
+    
+    def clear_ordering(self):
+        self.order_by = []
+    
+    def filter(self, *args, **kwargs):
+        self.filters.update(kwargs)
+
+    def exclude(self, *args, **kwargs):
+        self.excludes.update(kwargs)
+    
+    def set_limits(self, start=None, stop=None):
+        self.limit_start = start
+        self.limit_stop = stop
+        self.filterable = False
+    
+    @property
+    def parameters(self):
+        parameters = {}
+        # Counting
+        if self.count:
+            parameters['count'] = True
+        
+        # Filtering
+        for k, v in self.filters.iteritems():
+            parameters['filter_%s' % k] = v
+        for k, v in self.excludes.iteritems():
+            parameters['exclude_%s' % k] = v
+        
+        # Ordering
+        if self.order_by:
+            parameters['order_by'] = ','.join(self.order_by)
+        
+        # Slicing
+        if self.limit_start:
+            parameters['limit_start'] = self.limit_start
+        if self.limit_stop:
+            parameters['limit_stop'] = self.limit_stop
+        
+        # Format
+        parameters['format'] = getattr(settings, "ROA_FORMAT", 'json')
+        
+        #print parameters
+        return parameters
+
+
+class RemoteQuerySet(query.QuerySet):
+    """
+    QuerySet which access remote resources.
+    """
+    def __init__(self, model=None, query=None):
+        self.model = model
+        self.query = query or Query()
+        self._result_cache = None
+        self._iter = None
+        self._sticky_filter = False
+        
+        self.params = {}
+    
+    ########################
+    # PYTHON MAGIC METHODS #
+    ########################
+
+    def __repr__(self):
+        if not self.query.limit_start and not self.query.limit_stop:
+            data = list(self[:query.REPR_OUTPUT_SIZE + 1])
+            if len(data) > query.REPR_OUTPUT_SIZE:
+                data[-1] = "...(remaining elements truncated)..."
+        else:
+            data = list(self)
+        return repr(data)
+
+    ####################################
+    # METHODS THAT DO RESOURCE QUERIES #
+    ####################################
+
+    def iterator(self):
+        """
+        An iterator over the results from applying this QuerySet to the
+        remote web service.
+        """
+        try:
+            resource = Resource(self.model._meta.resource_url)
+        except AttributeError:
+            raise Exception, self.model._meta.__repr__()
+
+        try:
+            response = resource.get(**self.query.parameters)
+        except ResourceNotFound:
+            return
+
+        # TODO: find a better way to do this
+        response = response.replace('auth.user', 'remoteauth.remoteuser')
+        response = response.replace('auth.message', 'remoteauth.remotemessage')
+
+        for res in serializers.deserialize(getattr(settings, "ROA_FORMAT", 'json'), response):
+            obj = res.object
+            obj.id = obj.remotemodel_ptr_id
+            yield obj
+        
+    def count(self):
+        """
+        Returns the number of records as an integer.
+
+        If the QuerySet is already fully cached this simply returns the length
+        of the cached results set to avoid multiple remote calls.
+        """
+        if self._result_cache is not None and not self._iter:
+            return len(self._result_cache)
+
+        try:
+            # We must force iteration otherwise admin paginator does not work
+            return len(self.iterator())
+        except TypeError:
+            # object of type 'generator' has no len()
+            self.query.get_count()
+            
+            resource = Resource(self.model._meta.resource_url)
+            
+            try:
+                response = resource.get(**self.query.parameters)
+            except ResourceNotFound:
+                return 0
+            
+            return int(response)
+
+    def latest(self, field_name=None):
+        """
+        Returns the latest object, according to the model's 'get_latest_by'
+        option or optional given field_name.
+        """
+        latest_by = field_name or self.model._meta.get_latest_by
+        assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model"
+        
+        self.query.order_by.append('-%s' % latest_by)
+        return self.iterator().next()
+
+    def delete(self):
+        """
+        Deletes the records in the current QuerySet.
+        """
+        assert self.query.can_filter(), \
+                "Cannot use 'limit' or 'offset' with delete."
+
+        del_query = self._clone()
+
+        # Disable non-supported fields.
+        del_query.query.select_related = False
+        del_query.query.clear_ordering()
+
+        for obj in del_query:
+            obj.delete()
+
+        # Clear the result cache, in case this QuerySet gets reused.
+        self._result_cache = None
+    delete.alters_data = True
+
+    ##################################################################
+    # PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET #
+    ##################################################################
+
+    def filter(self, *args, **kwargs):
+        """
+        Returns a filtered QuerySet instance.
+        """
+        if args or kwargs:
+            assert self.query.can_filter(), \
+                    "Cannot filter a query once a slice has been taken."
+
+        clone = self._clone()
+        clone.query.filter(*args, **kwargs)
+        return clone
+
+    def exclude(self, *args, **kwargs):
+        """
+        Returns a filtered QuerySet instance.
+        """
+        if args or kwargs:
+            assert self.query.can_filter(), \
+                    "Cannot filter a query once a slice has been taken."
+
+        clone = self._clone()
+        clone.query.exclude(*args, **kwargs)
+        return clone
+
+    def order_by(self, *field_names):
+        """
+        Returns a QuerySet instance with the ordering changed.
+        """
+        assert self.query.can_filter(), \
+                "Cannot reorder a query once a slice has been taken."
+                
+        for field_name in field_names:
+            self.query.order_by.append(field_name)
+        return self._clone()

django_roa/models.py

+# Otherwise Django do not consider this app for tests

django_roa/remoteauth/__init__.py

Empty file added.

django_roa/remoteauth/backends.py

+from django.conf import settings
+from django.contrib.auth.backends import ModelBackend
+from django.core.exceptions import ImproperlyConfigured
+from django.db.models import get_model
+
+from django_roa.remoteauth.models import RemoteUser
+
+class RemoteUserModelBackend(ModelBackend):
+    def authenticate(self, username=None, password=None):
+        try:
+            user = RemoteUser.objects.get(username=username)
+            if user.check_password(password):
+                return user
+        except RemoteUser.DoesNotExist:
+            return None
+
+    def get_user(self, user_id):
+        try:
+            return RemoteUser.objects.get(pk=user_id)
+        except RemoteUser.DoesNotExist:
+            return None

django_roa/remoteauth/models.py

+import datetime
+
+from django.contrib.auth.models import Group, Permission, get_hexdigest, check_password
+from django.utils.translation import ugettext_lazy as _
+from django.db import models
+
+from django_roa import Model, Manager
+
+
+class RemoteUserManager(Manager):
+    def create_user(self, username, email, password=None):
+        "Creates and saves a User with the given username, e-mail and password."
+        now = datetime.datetime.now()
+        user = self.model(None, None, username, '', '', email.strip().lower(), 'placeholder', False, True, False, now, now)
+        if password:
+            user.set_password(password)
+        else:
+            user.set_unusable_password()
+        user.save()
+        return user
+
+    def create_superuser(self, username, email, password):
+        u = self.create_user(username, email, password)
+        u.is_staff = True
+        u.is_active = True
+        u.is_superuser = True
+        u.save()
+
+    def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'):
+        "Generates a random password with the given length and given allowed_chars"
+        # Note that default value of allowed_chars does not have "I" or letters
+        # that look like it -- just to avoid confusion.
+        from random import choice
+        return ''.join([choice(allowed_chars) for i in range(length)])
+
+
+class RemoteUser(Model):
+    """Users within the Django authentication system are represented by this model.
+
+    Username and password are required. Other fields are optional.
+    """
+    username = models.CharField(max_length=30)
+    first_name = models.CharField(_('first name'), max_length=30, blank=True)
+    last_name = models.CharField(_('last name'), max_length=30, blank=True)
+    email = models.EmailField(_('e-mail address'), blank=True)
+    password = models.CharField(_('password'), max_length=128, help_text=_("Use '[algo]$[salt]$[hexdigest]' or use the <a href=\"password/\">change password form</a>."))
+    is_staff = models.BooleanField(_('staff status'), default=False, help_text=_("Designates whether the user can log into this admin site."))
+    is_active = models.BooleanField(_('active'), default=True, help_text=_("Designates whether this user should be treated as active. Unselect this instead of deleting accounts."))
+    is_superuser = models.BooleanField(_('superuser status'), default=False, help_text=_("Designates that this user has all permissions without explicitly assigning them."))
+    last_login = models.DateTimeField(_('last login'), default=datetime.datetime.now)
+    date_joined = models.DateTimeField(_('date joined'), default=datetime.datetime.now)
+    groups = models.ManyToManyField(Group, verbose_name=_('groups'), blank=True,
+        help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."))
+    user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True)
+    objects = RemoteUserManager()
+
+    class Meta:
+        resource_url = 'http://127.0.0.1:8081/auth/user/'
+        resource_url_id = resource_url + '%(id)s/'
+
+    def __unicode__(self):
+        return self.username
+
+    def get_absolute_url(self):
+        return "/users/%s/" % urllib.quote(smart_str(self.username))
+
+    def is_anonymous(self):
+        "Always returns False. This is a way of comparing User objects to anonymous users."
+        return False
+
+    def is_authenticated(self):
+        """Always return True. This is a way to tell if the user has been authenticated in templates.
+        """
+        return True
+
+    def get_full_name(self):
+        "Returns the first_name plus the last_name, with a space in between."
+        full_name = u'%s %s' % (self.first_name, self.last_name)
+        return full_name.strip()
+
+    def set_password(self, raw_password):
+        import random
+        algo = 'sha1'
+        salt = get_hexdigest(algo, str(random.random()), str(random.random()))[:5]
+        hsh = get_hexdigest(algo, salt, raw_password)
+        self.password = '%s$%s$%s' % (algo, salt, hsh)
+
+    def check_password(self, raw_password):
+        """
+        Returns a boolean of whether the raw_password was correct. Handles
+        encryption formats behind the scenes.
+        """
+        # Backwards-compatibility check. Older passwords won't include the
+        # algorithm or salt.
+        if '$' not in self.password:
+            is_correct = (self.password == get_hexdigest('md5', '', raw_password))
+            if is_correct:
+                # Convert the password to the new, more secure format.
+                self.set_password(raw_password)
+                self.save()
+            return is_correct
+        return check_password(raw_password, self.password)
+
+    def set_unusable_password(self):
+        # Sets a value that will never be a valid hash
+        self.password = UNUSABLE_PASSWORD
+
+    def has_usable_password(self):
+        return self.password != UNUSABLE_PASSWORD
+
+    def get_group_permissions(self):
+        """
+        Returns a list of permission strings that this user has through
+        his/her groups. This method queries all available auth backends.
+        """
+        permissions = set()
+        for backend in auth.get_backends():
+            if hasattr(backend, "get_group_permissions"):
+                permissions.update(backend.get_group_permissions(self))
+        return permissions
+
+    def get_all_permissions(self):
+        permissions = set()
+        for backend in auth.get_backends():
+            if hasattr(backend, "get_all_permissions"):
+                permissions.update(backend.get_all_permissions(self))
+        return permissions
+
+    def has_perm(self, perm):
+        """
+        Returns True if the user has the specified permission. This method
+        queries all available auth backends, but returns immediately if any
+        backend returns True. Thus, a user who has permission from a single
+        auth backend is assumed to have permission in general.
+        """
+        # Inactive users have no permissions.
+        if not self.is_active:
+            return False
+
+        # Superusers have all permissions.
+        if self.is_superuser:
+            return True
+
+        # Otherwise we need to check the backends.
+        for backend in auth.get_backends():
+            if hasattr(backend, "has_perm"):
+                if backend.has_perm(self, perm):
+                    return True
+        return False
+
+    def has_perms(self, perm_list):
+        """Returns True if the user has each of the specified permissions."""
+        for perm in perm_list:
+            if not self.has_perm(perm):
+                return False
+        return True
+
+    def has_module_perms(self, app_label):
+        """
+        Returns True if the user has any permissions in the given app
+        label. Uses pretty much the same logic as has_perm, above.
+        """
+        if not self.is_active:
+            return False
+
+        if self.is_superuser:
+            return True
+
+        for backend in auth.get_backends():
+            if hasattr(backend, "has_module_perms"):
+                if backend.has_module_perms(self, app_label):
+                    return True
+        return False
+
+    def get_and_delete_messages(self):
+        messages = []
+        for m in self.message_set.all():
+            messages.append(m.message)
+            m.delete()
+        return messages
+    
+    def email_user(self, subject, message, from_email=None):
+        "Sends an e-mail to this User."
+        from django.core.mail import send_mail
+        send_mail(subject, message, from_email, [self.email])
+
+    def get_profile(self):
+        """
+        Returns site-specific profile for this user. Raises
+        SiteProfileNotAvailable if this site does not allow profiles.
+        """
+        if not hasattr(self, '_profile_cache'):
+            from django.conf import settings
+            if not getattr(settings, 'AUTH_PROFILE_MODULE', False):
+                raise SiteProfileNotAvailable
+            try:
+                app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
+                model = models.get_model(app_label, model_name)
+                self._profile_cache = model._default_manager.get(user__id__exact=self.id)
+                self._profile_cache.user = self
+            except (ImportError, ImproperlyConfigured):
+                raise SiteProfileNotAvailable
+        return self._profile_cache
+
+
+class RemoteMessage(Model):
+    """
+    The message system is a lightweight way to queue messages for given
+    users. A message is associated with a User instance (so it is only
+    applicable for registered users). There's no concept of expiration or
+    timestamps. Messages are created by the Django admin after successful
+    actions. For example, "The poll Foo was created successfully." is a
+    message.
+    """
+    user = models.ForeignKey(RemoteUser)
+    message = models.TextField(_('message'))
+    objects = Manager()
+
+    class Meta:
+        resource_url = 'http://127.0.0.1:8081/auth/message/'
+        resource_url_id = resource_url + '%(id)s/'
+
+    def __unicode__(self):
+        return self.message

django_roa/tests.py

+r"""
+==========
+Django ROA
+==========
+
+Tests
+=====
+
+How to run tests
+----------------
+
+You need to launch ``test_projects/django_roa_server`` project on port 8081 in 
+order to test this suite with this command::
+
+    $ python manage.py runserver 8081
+
+Then you can go to ``test_projects/django_roa_client`` and run this command::
+
+    $ python manage.py test
+
+It should return no error and you will be able to see logs from the test
+server which confirm that it works as expected: remote requests are done.
+
+Initialization
+--------------
+
+First of all, we verify that remote classes are called::
+
+    >>> from django_roa_client.models import RemotePage
+    >>> RemotePage.objects.__class__
+    <class 'django_roa.db.managers.RemoteManager'>
+    >>> RemotePage.__class__
+    <class 'django_roa.db.models.ResourceAsMetaModelBase'>
+
+API
+---
+
+Now, let's create, update, retrieve and delete a simple object::
+
+    >>> page = RemotePage.objects.create(title='A first remote page')
+    >>> page
+    <RemotePage: A first remote page (1)>
+    >>> page.title = 'Another title'
+    >>> page.save()
+    >>> page = RemotePage.objects.get(title='Another title') # from cache (TODO!)
+    >>> page.title
+    u'Another title'
+    >>> pages = RemotePage.objects.all()
+    >>> pages
+    [<RemotePage: Another title (1)>]
+    >>> pages.count() # do not hit the remote web service (from cache)
+    1
+    >>> page.delete()
+    >>> RemotePage.objects.all()
+    []
+    >>> RemotePage.objects.count() # hit the remote web service
+    0
+
+Get or create::
+
+    >>> page2 = RemotePage.objects.create(title='A second remote page')
+    >>> page3 = RemotePage.objects.create(title='A third remote page')
+
+    >>> RemotePage.objects.get_or_create(title='A second remote page')
+    (<RemotePage: A second remote page (1)>, False)
+    >>> page4, created = RemotePage.objects.get_or_create(title='A fourth remote page')
+    >>> created
+    True
+
+Latest::
+
+    >>> RemotePage.objects.latest('id')
+    <RemotePage: A fourth remote page (3)>
+    >>> RemotePage.objects.latest('title')
+    <RemotePage: A third remote page (2)>
+
+Filtering::
+
+    >>> RemotePage.objects.exclude(id=2)
+    [<RemotePage: A second remote page (1)>, <RemotePage: A fourth remote page (3)>]
+
+    >>> RemotePage.objects.filter(title__iexact='a FOURTH remote page')
+    [<RemotePage: A fourth remote page (3)>]
+    >>> RemotePage.objects.filter(title__contains='second')
+    [<RemotePage: A second remote page (1)>]
+
+Ordering::
+
+    >>> RemotePage.objects.order_by('title')
+    [<RemotePage: A fourth remote page (3)>, <RemotePage: A second remote page (1)>, <RemotePage: A third remote page (2)>]
+    >>> page5 = RemotePage.objects.create(title='A fourth remote page')
+    >>> RemotePage.objects.order_by('-title', '-id')
+    [<RemotePage: A third remote page (2)>, <RemotePage: A second remote page (1)>, <RemotePage: A fourth remote page (4)>, <RemotePage: A fourth remote page (3)>]
+
+Slicing::
+
+    >>> RemotePage.objects.all()[1:3]
+    [<RemotePage: A third remote page (2)>, <RemotePage: A fourth remote page (3)>]
+    >>> RemotePage.objects.all()[0]
+    <RemotePage: A second remote page (1)>
+
+Combined::
+
+    >>> page6 = RemotePage.objects.create(title='A fool remote page')
+    >>> RemotePage.objects.exclude(title__contains='fool').order_by('title', '-id')[:2]
+    [<RemotePage: A fourth remote page (4)>, <RemotePage: A fourth remote page (3)>]
+
+
+Users
+-----
+
+Remote users are defined in ``django_roa.remoteauth`` application::
+
+    >>> from django_roa.remoteauth.models import RemoteUser, RemoteMessage
+    >>> RemoteUser.objects.all()
+    [<RemoteUser: david>]
+    >>> alice = RemoteUser.objects.create_user(username="alice", password="secret", email="alice@example.com")
+    >>> alice.is_superuser
+    False
+    >>> RemoteUser.objects.all()
+    [<RemoteUser: david>, <RemoteUser: alice>]
+    >>> alice.id
+    2
+    >>> RemoteMessage.objects.all()
+    []
+    >>> message = RemoteMessage.objects.create(user=alice, message=u"Test message")
+    >>> message.message
+    u'Test message'
+    >>> message.user
+    <RemoteUser: alice>
+    >>> RemoteMessage.objects.all()
+    [<RemoteMessage: Test message>]
+    >>> #alice.remotemessage_set.all()
+
+
+Clean up
+--------
+::
+
+    >>> RemotePage.objects.all().delete()
+    >>> RemoteUser.objects.exclude(username="david").delete()
+"""

docs/overview.rst

+==========
+Django ROA
+==========
+
+Overview
+========
+
+Django ROA is a Django application which allows you to deal with a REST API to
+interact with your data in a true Resource Oriented Architecture style. It is
+possible to map your models on the API and to create/retrieve/update/delete
+objects as you've always done with Django's models.
+
+You can easily switch from local storage of data to remote one given a unique
+setting. That's very useful if you need to develop locally.
+
+Python 2.4 or more and Django 1.O or more are required.
+
+
+Installation
+============
+
+There are a few steps:
+
+    * add ``django_roa`` and ``django_roa.remoteauth`` to your 
+      ``INSTALLED_APPS`` setting::
+      
+        INSTALLED_APPS = (
+            'django_roa',
+            'django_roa.remoteauth',
+            etc
+        )
+    
+    * add ``RemoteUserModelBackend`` to your ``AUTHENTICATION_BACKENDS``
+      setting::
+      
+        AUTHENTICATION_BACKENDS = (
+            'django_roa.remoteauth.backends.RemoteUserModelBackend',
+        )
+    
+    * add ``ROA_MODELS = True`` in your settings.
+
+
+Basic use
+=========
+
+In order to use remote access with your models, there are 3 steps:
+
+    * inherit from ``django_roa.Model`` for your models
+    * add a custom default manager ``django_roa.Manager`` or inherit from it
+      for your managers
+    * define ``resource_url`` and ``resource_url_id`` Meta's variables in your
+      models to access your remote resource in a RESTful way. Use ``%(id)s``
+      pattern to define ``resource_url_id``, for instance::
+      
+          resource_url_id = 'http://example.com/foo/%(id)s/'
+
+You can take a look at what have been done in 
+``test_projects.django_roa_client/server`` for examples of use.
+
+
+How does it works
+=================
+
+Each time a request should be passed to the database, an HTTP request is done
+to the remote server with the rigth method (GET, POST, PUT or DELETE) given
+the ``resource_url*`` specified in model's ``Meta``.
+
+
+How to run tests
+================
+
+You need to launch ``test_projects/django_roa_server`` project on port 8081 in 
+order to test this suite with this command::
+
+    $ python manage.py runserver 8081
+
+Then you can go to ``test_projects/django_roa_client`` and run this command::
+
+    $ python manage.py test
+
+It should return no error and you will be able to see logs from the test
+server which confirm that it works as expected: remote requests are done.

docs/specifications.rst

+=========================
+Django ROA specifications
+=========================
+
+Client
+======
+
+Model
+-----
+
+Available methods and limitations:
+
+    * save(): Creates or updates the object
+    * delete(): Deletes the object, do not delete in cascade
+
+
+Manager
+-------
+
+Available methods and limitations:
+
+    * all(): Returns all the elements of a QuerySet
+    * get_or_create(): Get or create an object
+    * delete(): Deletes the records in the current QuerySet
+    * filter(): Returns a filtered QuerySet, do not handle Q objects
+    * exclude(): Returns a filtered QuerySet, do not handle Q objects
+    * order_by(): Returns an ordered QuerySet
+    * count(): Returns the number of elements of a QuerySet
+    * latest(): Returns the latest element of a QuerySet
+    * [start:end]: Returns a sliced QuerySet, useful for pagination
+
+
+Server
+======
+
+URLs
+----
+
+Required URLs and limitations:
+
+    * /{resource}/: used for retrieving lists (GET) or creating objects (POST)
+    * /{resource}/{id}/: used for retrieving an object (GET), updating it 
+      (PUT) or deleting (DELETE) it
+
+Note: URL id is required but you can choose a totally different URL scheme.
+
+
+Parameters
+----------
+
+Optionnal parameters:
+
+    * format: json, xml will be supported as soon as possible
+    * count: returns the number of elements
+    * filter_* and exclude_*: * is the string used by Django to filter/exclude
+    * order_by: order the results given this field
+    * limit_start and limit_stop: slice the results given those integers
+
+Note: take a look at tests and test_projects to see all actual possibilities.

templates/admin/index.html

+{% extends "admin/base_site.html" %}
+{% load i18n %}
+
+{% block extrastyle %}<link rel="stylesheet" type="text/css" href="{% load adminmedia %}{% admin_media_prefix %}css/dashboard.css" />{% endblock %}
+
+{% block coltype %}colMS{% endblock %}
+
+{% block bodyclass %}dashboard{% endblock %}
+
+{% block breadcrumbs %}{% endblock %}
+
+{% block content %}
+<div id="content-main">
+
+{% if app_list %}
+    {% for app in app_list %}
+        <div class="module">
+        <table summary="{% blocktrans with app.name as name %}Models available in the {{ name }} application.{% endblocktrans %}">
+        <caption><a href="{{ app.app_url }}" class="section">{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}</a></caption>
+        {% for model in app.models %}
+            <tr>
+            {% if model.perms.change %}
+                <th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
+            {% else %}
+                <th scope="row">{{ model.name }}</th>
+            {% endif %}
+
+            {% if model.perms.add %}
+                <td><a href="{{ model.admin_url }}add/" class="addlink">{% trans 'Add' %}</a></td>
+            {% else %}
+                <td>&nbsp;</td>
+            {% endif %}
+
+            {% if model.perms.change %}
+                <td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
+            {% else %}
+                <td>&nbsp;</td>
+            {% endif %}
+            </tr>
+        {% endfor %}
+        </table>
+        </div>
+    {% endfor %}
+{% else %}
+    <p>{% trans "You don't have permission to edit anything." %}</p>
+{% endif %}
+</div>
+{% endblock %}
+
+{% block sidebar %}
+<div id="content-related">
+    <div class="module" id="recent-actions-module">
+        <h2>{% trans 'Recent Actions' %}</h2>
+        <h3>{% trans 'My Actions' %}</h3>
+        <p>Removed for remote access (for now).</p>
+    </div>
+</div>
+{% endblock %}

test_projects/django_roa_client/__init__.py

Empty file added.

test_projects/django_roa_client/manage.py

+import sys, os
+sys.path = [os.path.join(os.getcwd(), '../../'), '../../../../lib/django_src'] + sys.path
+
+from django.core.management import execute_manager
+
+try:
+    import settings # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+    sys.exit(1)
+
+if __name__ == "__main__":
+    execute_manager(settings)

test_projects/django_roa_client/models.py

+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from django_roa import Model, Manager
+
+class RemotePage(Model):
+    title = models.CharField(max_length=50, blank=True, null=True)
+    
+    objects = Manager()
+    
+    class Meta:
+        resource_url = 'http://127.0.0.1:8081/django_roa_server/remotepage/'
+        resource_url_id = resource_url + '%(id)s/'
+    
+    def __unicode__(self):
+        return u'%s (%s)' % (self.title, self.id)
+
+
+from django.contrib import admin
+admin.site.register(RemotePage)

test_projects/django_roa_client/settings.py

+import os
+ROOT_PATH = os.path.dirname(__file__)
+
+TEMPLATE_DEBUG = DEBUG = True
+MANAGERS = ADMINS = ()
+DATABASE_ENGINE = 'sqlite3'
+DATABASE_NAME = os.path.join(ROOT_PATH, 'testdb.sqlite')
+
+TIME_ZONE = 'America/Chicago'
+LANGUAGE_CODE = 'en-us'
+SITE_ID = 1
+USE_I18N = True
+MEDIA_ROOT = ''
+MEDIA_URL = ''
+ADMIN_MEDIA_PREFIX = '/media/'
+SECRET_KEY = '2+@4vnr#v8e273^+a)g$8%dre^dwcn#d&n#8+l6jk7r#$p&3zk'
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.load_template_source',
+    'django.template.loaders.app_directories.load_template_source',
+)
+TEMPLATE_CONTEXT_PROCESSORS = (
+    "django.core.context_processors.auth",
+    "django.core.context_processors.debug",
+    "django.core.context_processors.i18n",
+    "django.core.context_processors.request",
+)
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+)
+ROOT_URLCONF = 'urls'
+TEMPLATE_DIRS = (os.path.join(ROOT_PATH, '../templates'),)
+INSTALLED_APPS = (
+    'django_roa',
+    'django_roa.remoteauth',
+    'django_roa_client',
+    'django.contrib.admin',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+)
+AUTHENTICATION_BACKENDS = (
+    'django_roa.remoteauth.backends.RemoteUserModelBackend',
+)
+
+ROA_MODELS = True   # set to False if you'd like to develop/test locally
+ROA_FORMAT = 'json' # json (default) or xml (still in development)

test_projects/django_roa_client/urls.py

+from django.conf.urls.defaults import *
+from django.contrib import admin
+
+admin.autodiscover()
+
+def fake_home(request):
+    from django.http import HttpResponse
+    return HttpResponse('Home' * 50)
+
+urlpatterns = patterns('',
+    url(r'^admin/(.*)', admin.site.root),
+    url(r'^$', fake_home),
+)

test_projects/django_roa_server/__init__.py

Empty file added.

test_projects/django_roa_server/manage.py

+import sys, os
+sys.path = [os.path.join(os.getcwd(), '../../'), '../../../../lib/django_src'] + sys.path
+
+from django.core.management import execute_manager
+
+try:
+    import settings # Assumed to be in the same directory.
+    
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+    sys.exit(1)
+
+if __name__ == "__main__":
+    execute_manager(settings)

test_projects/django_roa_server/models.py

+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+class RemotePage(models.Model):
+    title = models.CharField(max_length=50, blank=True, null=True)
+    
+    def __unicode__(self):
+        return u'%s (%s)' % (self.title, self.id)
+

test_projects/django_roa_server/settings.py

+import os
+ROOT_PATH = os.path.dirname(__file__)
+
+TEMPLATE_DEBUG = DEBUG = True
+MANAGERS = ADMINS = ()
+DATABASE_ENGINE = 'sqlite3'
+DATABASE_NAME = os.path.join(ROOT_PATH, 'testdb.sqlite')
+
+TIME_ZONE = 'America/Chicago'
+LANGUAGE_CODE = 'en-us'
+SITE_ID = 1
+USE_I18N = True
+MEDIA_ROOT = ''
+MEDIA_URL = ''
+ADMIN_MEDIA_PREFIX = '/media/'
+SECRET_KEY = '2+@4vnr#v8e273^+a)g$8%dre^dwcn#d&n#8+l6jk7r#$p&3zk'
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.load_template_source',
+    'django.template.loaders.app_directories.load_template_source',
+)
+TEMPLATE_CONTEXT_PROCESSORS = (
+    "django.core.context_processors.auth",
+    "django.core.context_processors.debug",
+    "django.core.context_processors.i18n",
+    "django.core.context_processors.request",
+)
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+)
+ROOT_URLCONF = 'urls'
+TEMPLATE_DIRS = (os.path.join(ROOT_PATH, 'templates'),)
+INSTALLED_APPS = (
+    'django_roa',
+    'django_roa_server',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    #'django.contrib.sites',
+    #'django.contrib.admin',
+)
+APPEND_SLASH = False

test_projects/django_roa_server/urls.py

+from django.conf import settings
+from django.conf.urls.defaults import *
+
+from django_roa_server.views import MethodDispatcher
+
+
+urlpatterns = patterns('',
+    (r'^(?P<app_label>[_\w]+)/(?P<model_name>[_\w]+)/?(?P<object_id>[\d]+)?/?$', MethodDispatcher()),
+)
+
+#urlpatterns = patterns('django_roa_server.views',
+#    (r'^([^/]+)/([^/]+)/?$', 'resource'),
+#    (r'^([^/]+)/([^/]+)/(.+)/?$', 'resource_id'),
+#)

test_projects/django_roa_server/views.py

+import logging
+from sets import Set
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.exceptions import ObjectDoesNotExist
+from django.core import serializers
+from django.db import models
+from django.http import Http404, HttpResponse, HttpResponseNotAllowed
+from django.shortcuts import get_object_or_404, _get_queryset
+
+_MIMETYPE = {
+    'json': 'application/json',
+    'xml': 'application/xml'
+}
+
+# create logger
+logger = logging.getLogger("django_roa_server log")
+logger.setLevel(logging.DEBUG)
+# create console handler and set level to debug
+ch = logging.StreamHandler()
+ch.setLevel(logging.DEBUG)
+ch.setFormatter(logging.Formatter("%(name)s - %(message)s"))
+# add ch to logger
+logger.addHandler(ch)
+
+
+def serialize(f):
+    """
+    Decorator to serialize responses.
+    """
+    def wrapped(self, request, *args, **kwargs):
+        format = request.GET.get('format', 'json')
+        mimetype = _MIMETYPE.get(format, 'text/plain')
+        try:
+            result = f(self, request, *args, **kwargs)
+        except ObjectDoesNotExist:
+            response = HttpResponse('ERROR', mimetype=mimetype)
+            response.status_code = 404
+            return response
+        
+        # count
+        try:
+            response = HttpResponse(int(result), mimetype=mimetype)
+            return response
+        except TypeError:
+            pass
+        
+        if result:
+            # serialization
+            response = serializers.serialize(format, result)
+            response = response.replace('_server', '_client')
+            response = HttpResponse(response, mimetype=mimetype)
+            return response
+        return HttpResponse('OK', mimetype=mimetype)
+    return wrapped
+
+
+class MethodDispatcher(object):
+    
+    def __call__(self, request, app_label, model_name, object_id):
+        """
+        Dispatch the request given the method and object_id argument.
+        """
+        model = models.get_model(app_label, model_name)
+        method = request.method
+        logger.debug(u"Request: %s %s %s" % (method, model.__name__, object_id))
+        if object_id is None:
+            if method == 'GET':
+                return self.index(request, model)
+            elif method == 'POST':
+                return self.add(request, model)
+        else:
+            object = get_object_or_404(model, id=object_id)
+            if method == 'GET':
+                return self.retrieve(request, model, object)
+            elif method == 'PUT':
+                return self.modify(request, model, object)
+            elif method == 'DELETE':
+                return self.delete(request, model, object)
+    
+    ######################
+    ## Resource methods ##
+    ######################    
+    @serialize
+    def index(self, request, model):
+        """
+        Returns a list of objects given request args.
+        """
+        # Initialization
+        queryset = _get_queryset(model)
+        
+        # Filtering
+        filters, excludes = {}, {}
+        for k, v in request.GET.iteritems():
+            if k.startswith('filter_'):
+                filters[k[7:]] = v
+            if k.startswith('exclude_'):
+                excludes[k[8:]] = v
+        queryset = queryset.filter(*filters.items()).exclude(*excludes.items())
+        
+        # Ordering
+        if 'order_by' in request.GET:
+            order_bys = request.GET['order_by'].replace('remotemodel_ptr', 'id').split(',')
+            queryset = queryset.order_by(*order_bys)
+        
+        # Counting
+        if 'count' in request.GET:
+            counter = queryset.count()
+            logger.debug(u'Count: %s objects' % counter)
+            return counter
+        
+        # Slicing
+        limit_start = int(request.GET.get('limit_start', 0))
+        limit_stop = request.GET.get('limit_stop', False) and int(request.GET['limit_stop']) or None
+        queryset = queryset[limit_start:limit_stop]
+        
+        obj_list = list(queryset)
+        if not obj_list:
+            raise Http404('No %s matches the given query.' % queryset.model._meta.object_name)
+        logger.debug(u'Objects: %s retrieved' % obj_list)
+        return obj_list
+
+    @serialize
+    def add(self, request, model):
+        """
+        Creates a new object given request args, returned as a list.
+        """
+        data = request.REQUEST.copy()
+        keys = []
+        for dict_ in data.dicts:
+            keys += dict_.keys()
+        values = dict([(f.name, data.get(f.name, None)) \
+                            for f in model._meta.fields \
+                                if data.get(f.name) != 'None'])
+        for key in keys:
+            if key.endswith('_id') and key not in values:
+                values[str(key)] = int(data[key])
+                del values[key[:-3]]
+        
+        object = model.objects.create(**values)
+        response = [object]
+        logger.debug(u'Object "%s" created' % object)
+        return response
+
+    ####################
+    ## Object methods ##
+    ####################
+    @serialize
+    def retrieve(self, request, model, object):
+        """
+        Returns an object as a list.
+        """
+        response = [object]
+        logger.debug(u'Object "%s" retrieved' % object)
+        return response
+    
+    @serialize
+    def delete(self, request, model, object):
+        """
+        Deletes an object.
+        """
+        object.delete()
+        logger.debug(u'Object "%s" deleted, remains %s' % (object, model.objects.all()))
+    
+    @serialize
+    def modify(self, request, model, object):
+        """
+        Modifies an object given request args, returned as a list.
+        """
+        data = request.REQUEST.copy()
+        keys = []
+        for dict_ in data.dicts:
+            keys += dict_.keys()
+        keys = Set(keys).intersection(Set([f.name for f in model._meta.fields]))
+        for k in keys:
+            setattr(object, k, data[k])
+        object.save()
+        response = [object]
+        logger.debug(u'Object "%s" modified' % object)
+        return response