david / django-roa (http://welldev.org/)

Turn your models into remote resources that you can access through Django's ORM. ROA stands for Resource Oriented Architecture.

Clone this repository (size: 559.8 KB): HTTPS / SSH
$ hg clone http://code.welldev.org/django-roa

Changed (Δ50.1 KB):

raw changeset »

CHANGELOG (8 lines added, 0 lines removed)

LICENSE (4 lines added, 0 lines removed)

TODO (22 lines added, 0 lines removed)

django_roa/__init__.py (8 lines added, 0 lines removed)

django_roa/db/__init__.py (null-size change)

django_roa/db/managers.py (16 lines added, 0 lines removed)

django_roa/db/models.py (254 lines added, 0 lines removed)

django_roa/db/query.py (220 lines added, 0 lines removed)

django_roa/models.py (1 lines added, 0 lines removed)

django_roa/remoteauth/__init__.py (null-size change)

django_roa/remoteauth/backends.py (21 lines added, 0 lines removed)

django_roa/remoteauth/models.py (224 lines added, 0 lines removed)

django_roa/tests.py (142 lines added, 0 lines removed)

docs/overview.rst (82 lines added, 0 lines removed)

docs/specifications.rst (59 lines added, 0 lines removed)

templates/admin/index.html (58 lines added, 0 lines removed)

test_projects/django_roa_client/__init__.py (null-size change)

test_projects/django_roa_client/manage.py (14 lines added, 0 lines removed)

test_projects/django_roa_client/models.py (20 lines added, 0 lines removed)

test_projects/django_roa_client/settings.py (47 lines added, 0 lines removed)

test_projects/django_roa_client/urls.py (13 lines added, 0 lines removed)

test_projects/django_roa_server/__init__.py (null-size change)

test_projects/django_roa_server/manage.py (15 lines added, 0 lines removed)

test_projects/django_roa_server/models.py (9 lines added, 0 lines removed)

test_projects/django_roa_server/settings.py (43 lines added, 0 lines removed)

test_projects/django_roa_server/urls.py (14 lines added, 0 lines removed)

test_projects/django_roa_server/views.py (181 lines added, 0 lines removed)

Up to file-list CHANGELOG:

1
====================
2
django-roa changelog
3
====================
4
5
Version 0.1, 12 December 2008:
6
------------------------------
7
8
Initial release.

Up to file-list LICENSE:

1
Copyright (c) 2008-2009, David Larlet
2
All rights reserved.
3
4
Redistribution of this application is not permitted.

Up to file-list TODO:

1
===============
2
django-roa todo
3
===============
4
5
Priority 1:
6
-----------
7
8
* Handle ForeignKey relations (top priority, required for admin auth)
9
* Finish remoteauth application
10
* Improve documentation
11
* Write more specs
12
* Support XML serialization
13
14
15
Priority 2:
16
-----------
17
18
* Handle Q filters
19
* Handle ManyToMany relations
20
* Improve test server for production
21
* Cascading changes/deletions
22
* Improve debugging

Up to file-list django_roa/__init__.py:

1
# Depends on settings for flexibility
2
from django.conf import settings
3
4
from django_roa.db.models import RemoteModel, DjangoModel
5
from django_roa.db.managers import RemoteManager, DjangoManager
6
7
Model = getattr(settings, "ROA_MODELS", False) and RemoteModel or DjangoModel
8
Manager = getattr(settings, "ROA_MODELS", False) and RemoteManager or DjangoManager

Up to file-list django_roa/db/managers.py:

1
from django.db.models.manager import Manager as DjangoManager
2
3
from django_roa.db.query import RemoteQuerySet
4
5
6
class RemoteManager(DjangoManager):
7
    """
8
    Manager which access remote resources.
9
    """
10
    use_for_related_fields = True
11
    
12
    def get_query_set(self):
13
        """
14
        Returns a QuerySet which access remote resources.
15
        """
16
        return RemoteQuerySet(self.model)

Up to file-list django_roa/db/models.py:

1
import sys
2
from django.conf import settings
3
from django.db import models, connection, transaction
4
from django.db.models import signals
5
from django.db.models.fields import AutoField
6
from django.core import serializers
7
8
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
9
from django.db.models.fields import AutoField
10
from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
11
from django.db.models.query import delete_objects, Q, CollectedObjects
12
from django.db.models.options import Options
13
from django.db.models import signals, Manager
14
from django.db.models.loading import register_models, get_model
15
from django.db.models.base import ModelBase, subclass_exception
16
17
from restclient import Resource
18
from django_roa.db.managers import RemoteManager
19
20
class ResourceAsMetaModelBase(ModelBase):
21
    """
22
    Deal with the new Meta ``resource_url`` attribute in __new__.
23
    """
24
    def __new__(cls, name, bases, attrs):
25
        super_new = super(ModelBase, cls).__new__
26
        parents = [b for b in bases if isinstance(b, ModelBase)]
27
        if not parents:
28
            # If this isn't a subclass of Model, don't do anything special.
29
            return super_new(cls, name, bases, attrs)
30
31
        # Create the class.
32
        module = attrs.pop('__module__')
33
        new_class = super_new(cls, name, bases, {'__module__': module})
34
        attr_meta = attrs.pop('Meta', None)
35
        abstract = getattr(attr_meta, 'abstract', False)
36
        if not attr_meta:
37
            meta = getattr(new_class, 'Meta', None)
38
        else:
39
            meta = attr_meta
40
        base_meta = getattr(new_class, '_meta', None)
41
42
        if getattr(meta, 'app_label', None) is None:
43
            # Figure out the app_label by looking one level up.
44
            # For 'django.contrib.sites.models', this would be 'sites'.
45
            model_module = sys.modules[new_class.__module__]
46
            kwargs = {"app_label": model_module.__name__.split('.')[-2]}
47
        else:
48
            kwargs = {}
49
50
        # Custom Meta, replace:
51
        # new_class.add_to_class('_meta', Options(meta, **kwargs))
52
        resource_url = None
53
        options = Options(meta, **kwargs)
54
        if hasattr(options, 'meta') and options.meta is not None:
55
            resource_url = options.meta.__dict__['resource_url']
56
            resource_url_id = options.meta.__dict__['resource_url_id']
57
            del options.meta.__dict__['resource_url']
58
            del options.meta.__dict__['resource_url_id']
59
        new_class.add_to_class('_meta', options)
60
        if resource_url is not None:
61
            setattr(new_class._meta, 'resource_url', resource_url)
62
            setattr(new_class._meta, 'resource_url_id', resource_url_id)
63
        # /Custom Meta
64
65
        if not abstract:
66
            new_class.add_to_class('DoesNotExist',
67
                    subclass_exception('DoesNotExist', ObjectDoesNotExist, module))
68
            new_class.add_to_class('MultipleObjectsReturned',
69
                    subclass_exception('MultipleObjectsReturned', MultipleObjectsReturned, module))
70
            if base_meta and not base_meta.abstract:
71
                # Non-abstract child classes inherit some attributes from their
72
                # non-abstract parent (unless an ABC comes before it in the
73
                # method resolution order).
74
                if not hasattr(meta, 'ordering'):
75
                    new_class._meta.ordering = base_meta.ordering
76
                if not hasattr(meta, 'get_latest_by'):
77
                    new_class._meta.get_latest_by = base_meta.get_latest_by
78
79
        # Custom default manager (sometimes Django instanciate a classic one?)
80
        #if getattr(new_class, '_default_manager', None):
81
        #    new_class._default_manager = None
82
        new_class._default_manager = RemoteManager()
83
        new_class._default_manager.contribute_to_class(new_class, name)
84
        # /Custom default manager
85
86
        # Bail out early if we have already created this class.
87
        m = get_model(new_class._meta.app_label, name, False)
88
        if m is not None:
89
            return m
90
91
        # Add all attributes to the class.
92
        for obj_name, obj in attrs.items():
93
            new_class.add_to_class(obj_name, obj)
94
95
        # Do the appropriate setup for any model parents.
96
        o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields
97
                if isinstance(f, OneToOneField)])
98
        for base in parents:
99
            if not hasattr(base, '_meta'):
100
                # Things without _meta aren't functional models, so they're
101
                # uninteresting parents.
102
                continue
103
104
            # All the fields of any type declared on this model
105
            new_fields = new_class._meta.local_fields + \
106
                         new_class._meta.local_many_to_many + \
107
                         new_class._meta.virtual_fields
108
            field_names = set([f.name for f in new_fields])
109
110
            if not base._meta.abstract:
111
                # Concrete classes...
112
                if base in o2o_map:
113
                    field = o2o_map[base]
114
                    field.primary_key = True
115
                    new_class._meta.setup_pk(field)
116
                else:
117
                    attr_name = '%s_ptr' % base._meta.module_name
118
                    field = OneToOneField(base, name=attr_name,
119
                            auto_created=True, parent_link=True)
120
                    new_class.add_to_class(attr_name, field)
121
                new_class._meta.parents[base] = field
122
123
            else:
124
                # .. and abstract ones.
125
126
                # Check for clashes between locally declared fields and those
127
                # on the ABC.
128
                parent_fields = base._meta.local_fields + base._meta.local_many_to_many
129
                for field in parent_fields:
130
                    if field.name in field_names:
131
                        raise FieldError('Local field %r in class %r clashes '\
132
                                         'with field of similar name from '\
133
                                         'abstract base class %r' % \
134
                                            (field.name, name, base.__name__))
135
                    new_class.add_to_class(field.name, copy.deepcopy(field))
136
137
                # Pass any non-abstract parent classes onto child.
138
                new_class._meta.parents.update(base._meta.parents)
139
140
            # Inherit managers from the abstract base classes.
141
            base_managers = base._meta.abstract_managers
142
            base_managers.sort()
143
            for _, mgr_name, manager in base_managers:
144
                val = getattr(new_class, mgr_name, None)
145
                if not val or val is manager:
146
                    new_manager = manager._copy_to_model(new_class)
147
                    new_class.add_to_class(mgr_name, new_manager)
148
149
            # Inherit virtual fields (like GenericForeignKey) from the parent class
150
            for field in base._meta.virtual_fields:
151
                if base._meta.abstract and field.name in field_names:
152
                    raise FieldError('Local field %r in class %r clashes '\
153
                                     'with field of similar name from '\
154
                                     'abstract base class %r' % \
155
                                        (field.name, name, base.__name__))
156
                new_class.add_to_class(field.name, copy.deepcopy(field))
157
158
        if abstract:
159
            # Abstract base models can't be instantiated and don't appear in
160
            # the list of models for an app. We do the final setup for them a
161
            # little differently from normal models.
162
            attr_meta.abstract = False
163
            new_class.Meta = attr_meta
164
            return new_class
165
166
        new_class._prepare()
167
        register_models(new_class._meta.app_label, new_class)
168
169
        # Because of the way imports happen (recursively), we may or may not be
170
        # the first time this model tries to register with the framework. There
171
        # should only be one class for each model, so we always return the
172
        # registered version.
173
        return get_model(new_class._meta.app_label, name, False)
174
175
176
class RemoteModel(models.Model):
177
    """
178
    Model which access remote resources.
179
    """
180
    __metaclass__ = ResourceAsMetaModelBase
181
    
182
    def save_base(self, raw=False, cls=None, force_insert=False,
183
            force_update=False):
184
        assert not (force_insert and force_update)
185
        if not cls:
186
            cls = self.__class__
187
            meta = self._meta
188
            signal = True
189
            signals.pre_save.send(sender=self.__class__, instance=self, raw=raw)
190
        else:
191
            meta = cls._meta
192
            signal = False
193
        
194
        # If we are in a raw save, save the object exactly as presented.
195
        # That means that we don't try to be smart about saving attributes
196
        # that might have come from the parent class - we just save the
197
        # attributes we have been given to the class we have been given.
198
        if not raw:
199
            for parent, field in meta.parents.items():
200
                # At this point, parent's primary key field may be unknown
201
                # (for example, from administration form which doesn't fill
202
                # this field). If so, fill it.
203
                if getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
204
                    setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
205
                
206
                #self.save_base(raw, parent)
207
                setattr(self, field.attname, self._get_pk_val(parent._meta))
208
209
        non_pks = [f for f in meta.local_fields if not f.primary_key]
210
211
        args = dict((field.name, field.value_to_string(self)) for field in meta.local_fields)
212
        fk_args = dict((field.get_attname(), getattr(self, field.name).id) \
213
                    for field in meta.local_fields \
214
                        if isinstance(field, models.ForeignKey) \
215
                            and field.name != 'remotemodel_ptr')
216
        args.update(fk_args)
217
        pk_val = self._get_pk_val(meta)
218
        pk_set = pk_val is not None
219
220
        if force_update or pk_set and not self.id is None:
221
            resource = Resource(meta.resource_url_id % {'id': self.id})
222
            response = resource.put(**args)
223
        else:
224
            resource = Resource(meta.resource_url)
225
            response = resource.post(**args)
226
        
227
        result = serializers.deserialize(getattr(settings, "ROA_FORMAT", 'json'), response).next()
228
229
        try:
230
            result_id = int(result.object.remotemodel_ptr_id)
231
        except AttributeError: # FK
232
            result_id = int(result.object.id)
233
        self.id = self.remotemodel_ptr_id = result_id
234
        self = result.object
235
236
    save_base.alters_data = True
237
238
    def delete(self):
239
        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)
240
241
        # TODO: Find all the objects that need to be deleted.
242
        resource = Resource(self._meta.resource_url_id % {'id': self.id})
243
        response = resource.delete()
244
245
    delete.alters_data = True
246
247
248
class DjangoModel(models.Model):
249
    """
250
    Model which allows ``resource_url*`` as Meta attributes.
251
    """
252
    __metaclass__ = ResourceAsMetaModelBase
253
    
254

Up to file-list django_roa/db/query.py:

1
from django.conf import settings
2
from django.db.models import query
3
from django.core import serializers
4
5
from restclient.rest import Resource, ResourceNotFound
6
7
8
class Query(object):
9
    def __init__(self):
10
        self.count = False
11
        self.order_by = []
12
        self.filters = {}
13
        self.excludes = {}
14
        self.filterable = True
15
        self.limit_start = None
16
        self.limit_stop = None
17
        self.where = False
18
    
19
    def can_filter(self):
20
        return self.filterable
21
    
22
    def clone(self):
23
        return self
24
    
25
    def get_count(self):
26
        self.count = True
27
    
28
    def clear_ordering(self):
29
        self.order_by = []
30
    
31
    def filter(self, *args, **kwargs):
32
        self.filters.update(kwargs)
33
34
    def exclude(self, *args, **kwargs):
35
        self.excludes.update(kwargs)
36
    
37
    def set_limits(self, start=None, stop=None):
38
        self.limit_start = start
39
        self.limit_stop = stop
40
        self.filterable = False
41
    
42
    @property
43
    def parameters(self):
44
        parameters = {}
45
        # Counting
46
        if self.count:
47
            parameters['count'] = True
48
        
49
        # Filtering
50
        for k, v in self.filters.iteritems():
51
            parameters['filter_%s' % k] = v
52
        for k, v in self.excludes.iteritems():
53
            parameters['exclude_%s' % k] = v
54
        
55
        # Ordering
56
        if self.order_by:
57
            parameters['order_by'] = ','.join(self.order_by)
58
        
59
        # Slicing
60
        if self.limit_start:
61
            parameters['limit_start'] = self.limit_start
62
        if self.limit_stop:
63
            parameters['limit_stop'] = self.limit_stop
64
        
65
        # Format
66
        parameters['format'] = getattr(settings, "ROA_FORMAT", 'json')
67
        
68
        #print parameters
69
        return parameters
70
71
72
class RemoteQuerySet(query.QuerySet):
73
    """
74
    QuerySet which access remote resources.
75
    """
76
    def __init__(self, model=None, query=None):
77
        self.model = model
78
        self.query = query or Query()
79
        self._result_cache = None
80
        self._iter = None
81
        self._sticky_filter = False
82
        
83
        self.params = {}
84
    
85
    ########################
86
    # PYTHON MAGIC METHODS #
87
    ########################
88
89
    def __repr__(self):
90
        if not self.query.limit_start and not self.query.limit_stop:
91
            data = list(self[:query.REPR_OUTPUT_SIZE + 1])
92
            if len(data) > query.REPR_OUTPUT_SIZE:
93
                data[-1] = "...(remaining elements truncated)..."
94
        else:
95
            data = list(self)
96
        return repr(data)
97
98
    ####################################
99
    # METHODS THAT DO RESOURCE QUERIES #
100
    ####################################
101
102
    def iterator(self):
103
        """
104
        An iterator over the results from applying this QuerySet to the
105
        remote web service.
106
        """
107
        try:
108
            resource = Resource(self.model._meta.resource_url)
109
        except AttributeError:
110
            raise Exception, self.model._meta.__repr__()
111
112
        try:
113
            response = resource.get(**self.query.parameters)
114
        except ResourceNotFound:
115
            return
116
117
        # TODO: find a better way to do this
118
        response = response.replace('auth.user', 'remoteauth.remoteuser')
119
        response = response.replace('auth.message', 'remoteauth.remotemessage')
120
121
        for res in serializers.deserialize(getattr(settings, "ROA_FORMAT", 'json'), response):
122
            obj = res.object
123
            obj.id = obj.remotemodel_ptr_id
124
            yield obj
125
        
126
    def count(self):
127
        """
128
        Returns the number of records as an integer.
129
130
        If the QuerySet is already fully cached this simply returns the length
131
        of the cached results set to avoid multiple remote calls.
132
        """
133
        if self._result_cache is not None and not self._iter:
134
            return len(self._result_cache)
135
136
        try:
137
            # We must force iteration otherwise admin paginator does not work
138
            return len(self.iterator())
139
        except TypeError:
140
            # object of type 'generator' has no len()
141
            self.query.get_count()
142
            
143
            resource = Resource(self.model._meta.resource_url)
144
            
145
            try:
146
                response = resource.get(**self.query.parameters)
147
            except ResourceNotFound:
148
                return 0
149
            
150
            return int(response)
151
152
    def latest(self, field_name=None):
153
        """
154
        Returns the latest object, according to the model's 'get_latest_by'
155
        option or optional given field_name.
156
        """
157
        latest_by = field_name or self.model._meta.get_latest_by
158
        assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model"
159
        
160
        self.query.order_by.append('-%s' % latest_by)
161
        return self.iterator().next()
162
163
    def delete(self):
164
        """
165
        Deletes the records in the current QuerySet.
166
        """
167
        assert self.query.can_filter(), \
168
                "Cannot use 'limit' or 'offset' with delete."
169
170
        del_query = self._clone()
171
172
        # Disable non-supported fields.
173
        del_query.query.select_related = False
174
        del_query.query.clear_ordering()
175
176
        for obj in del_query:
177
            obj.delete()
178
179
        # Clear the result cache, in case this QuerySet gets reused.
180
        self._result_cache = None
181
    delete.alters_data = True
182
183
    ##################################################################
184
    # PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET #
185
    ##################################################################
186
187
    def filter(self, *args, **kwargs):
188
        """
189
        Returns a filtered QuerySet instance.
190
        """
191
        if args or kwargs:
192
            assert self.query.can_filter(), \
193
                    "Cannot filter a query once a slice has been taken."
194
195
        clone = self._clone()
196
        clone.query.filter(*args, **kwargs)
197
        return clone
198
199
    def exclude(self, *args, **kwargs):
200
        """
201
        Returns a filtered QuerySet instance.
202
        """
203
        if args or kwargs:
204
            assert self.query.can_filter(), \
205
                    "Cannot filter a query once a slice has been taken."
206
207
        clone = self._clone()
208
        clone.query.exclude(*args, **kwargs)
209
        return clone
210
211
    def order_by(self, *field_names):
212
        """
213
        Returns a QuerySet instance with the ordering changed.
214
        """
215
        assert self.query.can_filter(), \
216
                "Cannot reorder a query once a slice has been taken."
217
                
218
        for field_name in field_names:
219
            self.query.order_by.append(field_name)
220
        return self._clone()

Up to file-list django_roa/models.py:

1
# Otherwise Django do not consider this app for tests

Up to file-list django_roa/remoteauth/backends.py:

1
from django.conf import settings
2
from django.contrib.auth.backends import ModelBackend
3
from django.core.exceptions import ImproperlyConfigured
4
from django.db.models import get_model
5
6
from django_roa.remoteauth.models import RemoteUser
7
8
class RemoteUserModelBackend(ModelBackend):
9
    def authenticate(self, username=None, password=None):
10
        try:
11
            user = RemoteUser.objects.get(username=username)
12
            if user.check_password(password):
13
                return user
14
        except RemoteUser.DoesNotExist:
15
            return None
16
17
    def get_user(self, user_id):
18
        try:
19
            return RemoteUser.objects.get(pk=user_id)
20
        except RemoteUser.DoesNotExist:
21
            return None

Up to file-list django_roa/remoteauth/models.py:

1
import datetime
2
3
from django.contrib.auth.models import Group, Permission, get_hexdigest, check_password
4
from django.utils.translation import ugettext_lazy as _
5
from django.db import models
6
7
from django_roa import Model, Manager
8
9
10
class RemoteUserManager(Manager):
11
    def create_user(self, username, email, password=None):
12
        "Creates and saves a User with the given username, e-mail and password."
13
        now = datetime.datetime.now()
14
        user = self.model(None, None, username, '', '', email.strip().lower(), 'placeholder', False, True, False, now, now)
15
        if password:
16
            user.set_password(password)
17
        else:
18
            user.set_unusable_password()
19
        user.save()
20
        return user
21
22
    def create_superuser(self, username, email, password):
23
        u = self.create_user(username, email, password)
24
        u.is_staff = True
25
        u.is_active = True
26
        u.is_superuser = True
27
        u.save()
28
29
    def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'):
30
        "Generates a random password with the given length and given allowed_chars"
31
        # Note that default value of allowed_chars does not have "I" or letters
32
        # that look like it -- just to avoid confusion.
33
        from random import choice
34
        return ''.join([choice(allowed_chars) for i in range(length)])
35
36
37
class RemoteUser(Model):
38
    """Users within the Django authentication system are represented by this model.
39
40
    Username and password are required. Other fields are optional.
41
    """
42
    username = models.CharField(max_length=30)
43
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
44
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
45
    email = models.EmailField(_('e-mail address'), blank=True)
46
    password = models.CharField(_('password'), max_length=128, help_text=_("Use '[algo]$[salt]$[hexdigest]' or use the <a href=\"password/\">change password form</a>."))
47
    is_staff = models.BooleanField(_('staff status'), default=False, help_text=_("Designates whether the user can log into this admin site."))
48
    is_active = models.BooleanField(_('active'), default=True, help_text=_("Designates whether this user should be treated as active. Unselect this instead of deleting accounts."))
49
    is_superuser = models.BooleanField(_('superuser status'), default=False, help_text=_("Designates that this user has all permissions without explicitly assigning them."))
50
    last_login = models.DateTimeField(_('last login'), default=datetime.datetime.now)
51
    date_joined = models.DateTimeField(_('date joined'), default=datetime.datetime.now)
52
    groups = models.ManyToManyField(Group, verbose_name=_('groups'), blank=True,
53
        help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."))
54
    user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True)
55
    objects = RemoteUserManager()
56
57
    class Meta:
58
        resource_url = 'http://127.0.0.1:8081/auth/user/'
59
        resource_url_id = resource_url + '%(id)s/'
60
61
    def __unicode__(self):
62
        return self.username
63
64
    def get_absolute_url(self):
65
        return "/users/%s/" % urllib.quote(smart_str(self.username))
66
67
    def is_anonymous(self):
68
        "Always returns False. This is a way of comparing User objects to anonymous users."
69
        return False
70
71
    def is_authenticated(self):
72
        """Always return True. This is a way to tell if the user has been authenticated in templates.
73
        """
74
        return True
75
76
    def get_full_name(self):
77
        "Returns the first_name plus the last_name, with a space in between."
78
        full_name = u'%s %s' % (self.first_name, self.last_name)
79
        return full_name.strip()
80
81
    def set_password(self, raw_password):
82
        import random
83
        algo = 'sha1'
84
        salt = get_hexdigest(algo, str(random.random()), str(random.random()))[:5]
85
        hsh = get_hexdigest(algo, salt, raw_password)
86
        self.password = '%s$%s$%s' % (algo, salt, hsh)
87
88
    def check_password(self, raw_password):
89
        """
90
        Returns a boolean of whether the raw_password was correct. Handles
91
        encryption formats behind the scenes.
92
        """
93
        # Backwards-compatibility check. Older passwords won't include the
94
        # algorithm or salt.
95
        if '$' not in self.password:
96
            is_correct = (self.password == get_hexdigest('md5', '', raw_password))
97
            if is_correct:
98
                # Convert the password to the new, more secure format.
99
                self.set_password(raw_password)
100
                self.save()
101
            return is_correct
102
        return check_password(raw_password, self.password)
103
104
    def set_unusable_password(self):
105
        # Sets a value that will never be a valid hash
106
        self.password = UNUSABLE_PASSWORD
107
108
    def has_usable_password(self):
109
        return self.password != UNUSABLE_PASSWORD
110
111
    def get_group_permissions(self):
112
        """
113
        Returns a list of permission strings that this user has through
114
        his/her groups. This method queries all available auth backends.
115
        """
116
        permissions = set()
117
        for backend in auth.get_backends():
118
            if hasattr(backend, "get_group_permissions"):
119
                permissions.update(backend.get_group_permissions(self))
120
        return permissions
121
122
    def get_all_permissions(self):
123
        permissions = set()
124
        for backend in auth.get_backends():
125
            if hasattr(backend, "get_all_permissions"):
126
                permissions.update(backend.get_all_permissions(self))
127
        return permissions
128
129
    def has_perm(self, perm):
130
        """
131
        Returns True if the user has the specified permission. This method
132
        queries all available auth backends, but returns immediately if any
133
        backend returns True. Thus, a user who has permission from a single
134
        auth backend is assumed to have permission in general.
135
        """
136
        # Inactive users have no permissions.
137
        if not self.is_active:
138
            return False
139
140
        # Superusers have all permissions.
141
        if self.is_superuser:
142
            return True
143
144
        # Otherwise we need to check the backends.
145
        for backend in auth.get_backends():
146
            if hasattr(backend, "has_perm"):
147
                if backend.has_perm(self, perm):
148
                    return True
149
        return False
150
151
    def has_perms(self, perm_list):
152
        """Returns True if the user has each of the specified permissions."""
153
        for perm in perm_list:
154
            if not self.has_perm(perm):
155
                return False
156
        return True
157
158
    def has_module_perms(self, app_label):
159
        """
160
        Returns True if the user has any permissions in the given app
161
        label. Uses pretty much the same logic as has_perm, above.
162
        """
163
        if not self.is_active:
164
            return False
165
166
        if self.is_superuser:
167
            return True
168
169
        for backend in auth.get_backends():
170
            if hasattr(backend, "has_module_perms"):
171
                if backend.has_module_perms(self, app_label):
172
                    return True
173
        return False
174
175
    def get_and_delete_messages(self):
176
        messages = []
177
        for m in self.message_set.all():
178
            messages.append(m.message)
179
            m.delete()
180
        return messages
181
    
182
    def email_user(self, subject, message, from_email=None):
183
        "Sends an e-mail to this User."
184
        from django.core.mail import send_mail
185
        send_mail(subject, message, from_email, [self.email])
186
187
    def get_profile(self):
188
        """
189
        Returns site-specific profile for this user. Raises
190
        SiteProfileNotAvailable if this site does not allow profiles.
191
        """
192
        if not hasattr(self, '_profile_cache'):
193
            from django.conf import settings
194
            if not getattr(settings, 'AUTH_PROFILE_MODULE', False):
195
                raise SiteProfileNotAvailable
196
            try:
197
                app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
198
                model = models.get_model(app_label, model_name)
199
                self._profile_cache = model._default_manager.get(user__id__exact=self.id)
200
                self._profile_cache.user = self
201
            except (ImportError, ImproperlyConfigured):
202
                raise SiteProfileNotAvailable
203
        return self._profile_cache
204
205
206
class RemoteMessage(Model):
207
    """
208
    The message system is a lightweight way to queue messages for given
209
    users. A message is associated with a User instance (so it is only
210
    applicable for registered users). There's no concept of expiration or
211
    timestamps. Messages are created by the Django admin after successful
212
    actions. For example, "The poll Foo was created successfully." is a
213
    message.
214
    """
215
    user = models.ForeignKey(RemoteUser)
216
    message = models.TextField(_('message'))
217
    objects = Manager()
218
219
    class Meta:
220
        resource_url = 'http://127.0.0.1:8081/auth/message/'
221
        resource_url_id = resource_url + '%(id)s/'
222
223
    def __unicode__(self):
224
        return self.message

Up to file-list django_roa/tests.py:

1
r"""
2
==========
3
Django ROA
4
==========
5
6
Tests
7
=====
8
9
How to run tests
10
----------------
11
12
You need to launch ``test_projects/django_roa_server`` project on port 8081 in 
13
order to test this suite with this command::
14
15
    $ python manage.py runserver 8081
16
17
Then you can go to ``test_projects/django_roa_client`` and run this command::
18
19
    $ python manage.py test
20
21
It should return no error and you will be able to see logs from the test
22
server which confirm that it works as expected: remote requests are done.
23
24
Initialization
25
--------------
26
27
First of all, we verify that remote classes are called::
28
29
    >>> from django_roa_client.models import RemotePage
30
    >>> RemotePage.objects.__class__
31
    <class 'django_roa.db.managers.RemoteManager'>
32
    >>> RemotePage.__class__
33
    <class 'django_roa.db.models.ResourceAsMetaModelBase'>
34
35
API
36
---
37
38
Now, let's create, update, retrieve and delete a simple object::
39
40
    >>> page = RemotePage.objects.create(title='A first remote page')
41
    >>> page
42
    <RemotePage: A first remote page (1)>
43
    >>> page.title = 'Another title'
44
    >>> page.save()
45
    >>> page = RemotePage.objects.get(title='Another title') # from cache (TODO!)
46
    >>> page.title
47
    u'Another title'
48
    >>> pages = RemotePage.objects.all()
49
    >>> pages
50
    [<RemotePage: Another title (1)>]
51
    >>> pages.count() # do not hit the remote web service (from cache)
52
    1
53
    >>> page.delete()
54
    >>> RemotePage.objects.all()
55
    []
56
    >>> RemotePage.objects.count() # hit the remote web service
57
    0
58
59
Get or create::
60
61
    >>> page2 = RemotePage.objects.create(title='A second remote page')
62
    >>> page3 = RemotePage.objects.create(title='A third remote page')
63
64
    >>> RemotePage.objects.get_or_create(title='A second remote page')
65
    (<RemotePage: A second remote page (1)>, False)
66
    >>> page4, created = RemotePage.objects.get_or_create(title='A fourth remote page')
67
    >>> created
68
    True
69
70
Latest::
71
72
    >>> RemotePage.objects.latest('id')
73
    <RemotePage: A fourth remote page (3)>
74
    >>> RemotePage.objects.latest('title')
75
    <RemotePage: A third remote page (2)>
76
77
Filtering::
78
79
    >>> RemotePage.objects.exclude(id=2)
80
    [<RemotePage: A second remote page (1)>, <RemotePage: A fourth remote page (3)>]
81
82
    >>> RemotePage.objects.filter(title__iexact='a FOURTH remote page')
83
    [<RemotePage: A fourth remote page (3)>]
84
    >>> RemotePage.objects.filter(title__contains='second')
85
    [<RemotePage: A second remote page (1)>]
86
87
Ordering::
88
89
    >>> RemotePage.objects.order_by('title')
90
    [<RemotePage: A fourth remote page (3)>, <RemotePage: A second remote page (1)>, <RemotePage: A third remote page (2)>]
91
    >>> page5 = RemotePage.objects.create(title='A fourth remote page')
92
    >>> RemotePage.objects.order_by('-title', '-id')
93
    [<RemotePage: A third remote page (2)>, <RemotePage: A second remote page (1)>, <RemotePage: A fourth remote page (4)>, <RemotePage: A fourth remote page (3)>]
94
95
Slicing::
96
97
    >>> RemotePage.objects.all()[1:3]
98
    [<RemotePage: A third remote page (2)>, <RemotePage: A fourth remote page (3)>]
99
    >>> RemotePage.objects.all()[0]
100
    <RemotePage: A second remote page (1)>
101
102
Combined::
103
104
    >>> page6 = RemotePage.objects.create(title='A fool remote page')
105
    >>> RemotePage.objects.exclude(title__contains='fool').order_by('title', '-id')[:2]
106
    [<RemotePage: A fourth remote page (4)>, <RemotePage: A fourth remote page (3)>]
107
108
109
Users
110
-----
111
112
Remote users are defined in ``django_roa.remoteauth`` application::
113
114
    >>> from django_roa.remoteauth.models import RemoteUser, RemoteMessage
115
    >>> RemoteUser.objects.all()
116
    [<RemoteUser: david>]
117
    >>> alice = RemoteUser.objects.create_user(username="alice", password="secret", email="alice@example.com")
118
    >>> alice.is_superuser
119
    False
120
    >>> RemoteUser.objects.all()
121
    [<RemoteUser: david>, <RemoteUser: alice>]
122
    >>> alice.id
123
    2
124
    >>> RemoteMessage.objects.all()
125
    []
126
    >>> message = RemoteMessage.objects.create(user=alice, message=u"Test message")
127
    >>> message.message
128
    u'Test message'
129
    >>> message.user
130
    <RemoteUser: alice>
131
    >>> RemoteMessage.objects.all()
132
    [<RemoteMessage: Test message>]
133
    >>> #alice.remotemessage_set.all()
134
135
136
Clean up
137
--------
138
::
139
140
    >>> RemotePage.objects.all().delete()
141
    >>> RemoteUser.objects.exclude(username="david").delete()
142
"""

Up to file-list docs/overview.rst:

1
==========
2
Django ROA
3
==========
4
5
Overview
6
========
7
8
Django ROA is a Django application which allows you to deal with a REST API to
9
interact with your data in a true Resource Oriented Architecture style. It is
10
possible to map your models on the API and to create/retrieve/update/delete
11
objects as you've always done with Django's models.
12
13
You can easily switch from local storage of data to remote one given a unique
14
setting. That's very useful if you need to develop locally.
15
16
Python 2.4 or more and Django 1.O or more are required.
17
18
19
Installation
20
============
21
22
There are a few steps:
23
24
    * add ``django_roa`` and ``django_roa.remoteauth`` to your 
25
      ``INSTALLED_APPS`` setting::
26
      
27
        INSTALLED_APPS = (
28
            'django_roa',
29
            'django_roa.remoteauth',
30
            etc
31
        )
32
    
33
    * add ``RemoteUserModelBackend`` to your ``AUTHENTICATION_BACKENDS``
34
      setting::
35
      
36
        AUTHENTICATION_BACKENDS = (
37
            'django_roa.remoteauth.backends.RemoteUserModelBackend',
38
        )
39
    
40
    * add ``ROA_MODELS = True`` in your settings.
41
42
43
Basic use
44
=========
45
46
In order to use remote access with your models, there are 3 steps:
47
48
    * inherit from ``django_roa.Model`` for your models
49
    * add a custom default manager ``django_roa.Manager`` or inherit from it
50
      for your managers
51
    * define ``resource_url`` and ``resource_url_id`` Meta's variables in your
52
      models to access your remote resource in a RESTful way. Use ``%(id)s``
53
      pattern to define ``resource_url_id``, for instance::
54
      
55
          resource_url_id = 'http://example.com/foo/%(id)s/'
56
57
You can take a look at what have been done in 
58
``test_projects.django_roa_client/server`` for examples of use.
59
60
61
How does it works
62
=================
63
64
Each time a request should be passed to the database, an HTTP request is done
65
to the remote server with the rigth method (GET, POST, PUT or DELETE) given
66
the ``resource_url*`` specified in model's ``Meta``.
67
68
69
How to run tests
70
================
71
72
You need to launch ``test_projects/django_roa_server`` project on port 8081 in 
73
order to test this suite with this command::
74
75
    $ python manage.py runserver 8081
76
77
Then you can go to ``test_projects/django_roa_client`` and run this command::
78
79
    $ python manage.py test
80
81
It should return no error and you will be able to see logs from the test
82
server which confirm that it works as expected: remote requests are done.

Up to file-list docs/specifications.rst:

1
=========================
2
Django ROA specifications
3
=========================
4
5
Client
6
======
7
8
Model
9
-----
10
11
Available methods and limitations:
12
13
    * save(): Creates or updates the object
14
    * delete(): Deletes the object, do not delete in cascade
15
16
17
Manager
18
-------
19
20
Available methods and limitations:
21
22
    * all(): Returns all the elements of a QuerySet
23
    * get_or_create(): Get or create an object
24
    * delete(): Deletes the records in the current QuerySet
25
    * filter(): Returns a filtered QuerySet, do not handle Q objects
26
    * exclude(): Returns a filtered QuerySet, do not handle Q objects
27
    * order_by(): Returns an ordered QuerySet
28
    * count(): Returns the number of elements of a QuerySet
29
    * latest(): Returns the latest element of a QuerySet
30
    * [start:end]: Returns a sliced QuerySet, useful for pagination
31
32
33
Server
34
======
35
36
URLs
37
----
38
39
Required URLs and limitations:
40
41
    * /{resource}/: used for retrieving lists (GET) or creating objects (POST)
42
    * /{resource}/{id}/: used for retrieving an object (GET), updating it 
43
      (PUT) or deleting (DELETE) it
44
45
Note: URL id is required but you can choose a totally different URL scheme.
46
47
48
Parameters
49
----------
50
51
Optionnal parameters:
52
53
    * format: json, xml will be supported as soon as possible
54
    * count: returns the number of elements
55
    * filter_* and exclude_*: * is the string used by Django to filter/exclude
56
    * order_by: order the results given this field
57
    * limit_start and limit_stop: slice the results given those integers
58
59
Note: take a look at tests and test_projects to see all actual possibilities.

Up to file-list templates/admin/index.html:

1
{% extends "admin/base_site.html" %}
2
{% load i18n %}
3
4
{% block extrastyle %}<link rel="stylesheet" type="text/css" href="{% load adminmedia %}{% admin_media_prefix %}css/dashboard.css" />{% endblock %}
5
6
{% block coltype %}colMS{% endblock %}
7
8
{% block bodyclass %}dashboard{% endblock %}
9
10
{% block breadcrumbs %}{% endblock %}
11
12
{% block content %}
13
<div id="content-main">
14
15
{% if app_list %}
16
    {% for app in app_list %}
17
        <div class="module">
18
        <table summary="{% blocktrans with app.name as name %}Models available in the {{ name }} application.{% endblocktrans %}">
19
        <caption><a href="{{ app.app_url }}" class="section">{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}</a></caption>
20
        {% for model in app.models %}
21
            <tr>
22
            {% if model.perms.change %}
23
                <th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
24
            {% else %}
25
                <th scope="row">{{ model.name }}</th>
26
            {% endif %}
27
28
            {% if model.perms.add %}
29
                <td><a href="{{ model.admin_url }}add/" class="addlink">{% trans 'Add' %}</a></td>
30
            {% else %}
31
                <td> </td>
32
            {% endif %}
33
34
            {% if model.perms.change %}
35
                <td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
36
            {% else %}
37
                <td> </td>
38
            {% endif %}
39
            </tr>
40
        {% endfor %}
41
        </table>
42
        </div>
43
    {% endfor %}
44
{% else %}
45
    <p>{% trans "You don't have permission to edit anything." %}</p>
46
{% endif %}
47
</div>
48
{% endblock %}
49
50
{% block sidebar %}
51
<div id="content-related">
52
    <div class="module" id="recent-actions-module">
53
        <h2>{% trans 'Recent Actions' %}</h2>
54
        <h3>{% trans 'My Actions' %}</h3>
55
        <p>Removed for remote access (for now).</p>
56
    </div>
57
</div>
58
{% endblock %}

Up to file-list test_projects/django_roa_client/manage.py:

1
import sys, os
2
sys.path = [os.path.join(os.getcwd(), '../../'), '../../../../lib/django_src'] + sys.path
3
4
from django.core.management import execute_manager
5
6
try:
7
    import settings # Assumed to be in the same directory.
8
except ImportError:
9
    import sys
10
    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__)
11
    sys.exit(1)
12
13
if __name__ == "__main__":
14
    execute_manager(settings)

Up to file-list test_projects/django_roa_client/models.py:

1
from django.db import models
2
from django.utils.translation import gettext_lazy as _
3
4
from django_roa import Model, Manager
5
6
class RemotePage(Model):
7
    title = models.CharField(max_length=50, blank=True, null=True)
8
    
9
    objects = Manager()
10
    
11
    class Meta:
12
        resource_url = 'http://127.0.0.1:8081/django_roa_server/remotepage/'
13
        resource_url_id = resource_url + '%(id)s/'
14
    
15
    def __unicode__(self):
16
        return u'%s (%s)' % (self.title, self.id)
17
18
19
from django.contrib import admin
20
admin.site.register(RemotePage)

Up to file-list test_projects/django_roa_client/settings.py:

1
import os
2
ROOT_PATH = os.path.dirname(__file__)
3
4
TEMPLATE_DEBUG = DEBUG = True
5
MANAGERS = ADMINS = ()
6
DATABASE_ENGINE = 'sqlite3'
7
DATABASE_NAME = os.path.join(ROOT_PATH, 'testdb.sqlite')
8
9
TIME_ZONE = 'America/Chicago'
10
LANGUAGE_CODE = 'en-us'
11
SITE_ID = 1
12
USE_I18N = True
13
MEDIA_ROOT = ''
14
MEDIA_URL = ''
15
ADMIN_MEDIA_PREFIX = '/media/'
16
SECRET_KEY = '2+@4vnr#v8e273^+a)g$8%dre^dwcn#d&n#8+l6jk7r#$p&3zk'
17
TEMPLATE_LOADERS = (
18
    'django.template.loaders.filesystem.load_template_source',
19
    'django.template.loaders.app_directories.load_template_source',
20
)
21
TEMPLATE_CONTEXT_PROCESSORS = (
22
    "django.core.context_processors.auth",
23
    "django.core.context_processors.debug",
24
    "django.core.context_processors.i18n",
25
    "django.core.context_processors.request",
26
)
27
MIDDLEWARE_CLASSES = (
28
    'django.middleware.common.CommonMiddleware',
29
    'django.contrib.sessions.middleware.SessionMiddleware',
30
    'django.contrib.auth.middleware.AuthenticationMiddleware',
31
)
32
ROOT_URLCONF = 'urls'
33
TEMPLATE_DIRS = (os.path.join(ROOT_PATH, '../templates'),)
34
INSTALLED_APPS = (
35
    'django_roa',
36
    'django_roa.remoteauth',
37
    'django_roa_client',
38
    'django.contrib.admin',
39
    'django.contrib.contenttypes',
40
    'django.contrib.sessions',
41
)
42
AUTHENTICATION_BACKENDS = (
43
    'django_roa.remoteauth.backends.RemoteUserModelBackend',
44
)
45
46
ROA_MODELS = True   # set to False if you'd like to develop/test locally
47
ROA_FORMAT = 'json' # json (default) or xml (still in development)

Up to file-list test_projects/django_roa_client/urls.py:

1
from django.conf.urls.defaults import *
2
from django.contrib import admin
3
4
admin.autodiscover()
5
6
def fake_home(request):
7
    from django.http import HttpResponse
8
    return HttpResponse('Home' * 50)
9
10
urlpatterns = patterns('',
11
    url(r'^admin/(.*)', admin.site.root),
12
    url(r'^$', fake_home),
13
)

Up to file-list test_projects/django_roa_server/manage.py:

1
import sys, os
2
sys.path = [os.path.join(os.getcwd(), '../../'), '../../../../lib/django_src'] + sys.path
3
4
from django.core.management import execute_manager
5
6
try:
7
    import settings # Assumed to be in the same directory.
8
    
9
except ImportError:
10
    import sys
11
    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__)
12
    sys.exit(1)
13
14
if __name__ == "__main__":
15
    execute_manager(settings)

Up to file-list test_projects/django_roa_server/models.py:

1
from django.db import models
2
from django.utils.translation import gettext_lazy as _
3
4
class RemotePage(models.Model):
5
    title = models.CharField(max_length=50, blank=True, null=True)
6
    
7
    def __unicode__(self):
8
        return u'%s (%s)' % (self.title, self.id)
9

Up to file-list test_projects/django_roa_server/settings.py:

1
import os
2
ROOT_PATH = os.path.dirname(__file__)
3
4
TEMPLATE_DEBUG = DEBUG = True
5
MANAGERS = ADMINS = ()
6
DATABASE_ENGINE = 'sqlite3'
7
DATABASE_NAME = os.path.join(ROOT_PATH, 'testdb.sqlite')
8
9
TIME_ZONE = 'America/Chicago'
10
LANGUAGE_CODE = 'en-us'
11
SITE_ID = 1
12
USE_I18N = True
13
MEDIA_ROOT = ''
14
MEDIA_URL = ''
15
ADMIN_MEDIA_PREFIX = '/media/'
16
SECRET_KEY = '2+@4vnr#v8e273^+a)g$8%dre^dwcn#d&n#8+l6jk7r#$p&3zk'
17
TEMPLATE_LOADERS = (
18
    'django.template.loaders.filesystem.load_template_source',
19
    'django.template.loaders.app_directories.load_template_source',
20
)
21
TEMPLATE_CONTEXT_PROCESSORS = (
22
    "django.core.context_processors.auth",
23
    "django.core.context_processors.debug",
24
    "django.core.context_processors.i18n",
25
    "django.core.context_processors.request",
26
)
27
MIDDLEWARE_CLASSES = (
28
    'django.middleware.common.CommonMiddleware',
29
    'django.contrib.sessions.middleware.SessionMiddleware',
30
    'django.contrib.auth.middleware.AuthenticationMiddleware',
31
)
32
ROOT_URLCONF = 'urls'
33
TEMPLATE_DIRS = (os.path.join(ROOT_PATH, 'templates'),)
34
INSTALLED_APPS = (
35
    'django_roa',
36
    'django_roa_server',
37
    'django.contrib.auth',
38
    'django.contrib.contenttypes',
39
    'django.contrib.sessions',
40
    #'django.contrib.sites',
41
    #'django.contrib.admin',
42
)
43
APPEND_SLASH = False

Up to file-list test_projects/django_roa_server/urls.py:

1
from django.conf import settings
2
from django.conf.urls.defaults import *
3
4
from django_roa_server.views import MethodDispatcher
5
6
7
urlpatterns = patterns('',
8
    (r'^(?P<app_label>[_\w]+)/(?P<model_name>[_\w]+)/?(?P<object_id>[\d]+)?/?$', MethodDispatcher()),
9
)
10
11
#urlpatterns = patterns('django_roa_server.views',
12
#    (r'^([^/]+)/([^/]+)/?$', 'resource'),
13
#    (r'^([^/]+)/([^/]+)/(.+)/?$', 'resource_id'),
14
#)

Up to file-list test_projects/django_roa_server/views.py:

1
import logging
2
from sets import Set
3
4
from django.conf import settings
5
from django.contrib.auth.models import User
6
from django.core.exceptions import ObjectDoesNotExist
7
from django.core import serializers
8
from django.db import models
9
from django.http import Http404, HttpResponse, HttpResponseNotAllowed
10
from django.shortcuts import get_object_or_404, _get_queryset
11
12
_MIMETYPE = {
13
    'json': 'application/json',
14
    'xml': 'application/xml'
15
}
16
17
# create logger
18
logger = logging.getLogger("django_roa_server log")
19
logger.setLevel(logging.DEBUG)
20
# create console handler and set level to debug
21
ch = logging.StreamHandler()
22
ch.setLevel(logging.DEBUG)
23
ch.setFormatter(logging.Formatter("%(name)s - %(message)s"))
24
# add ch to logger
25
logger.addHandler(ch)
26
27
28
def serialize(f):
29
    """
30
    Decorator to serialize responses.
31
    """
32
    def wrapped(self, request, *args, **kwargs):
33
        format = request.GET.get('format', 'json')
34
        mimetype = _MIMETYPE.get(format, 'text/plain')
35
        try:
36
            result = f(self, request, *args, **kwargs)
37
        except ObjectDoesNotExist:
38
            response = HttpResponse('ERROR', mimetype=mimetype)
39
            response.status_code = 404
40
            return response
41
        
42
        # count
43
        try:
44
            response = HttpResponse(int(result), mimetype=mimetype)
45
            return response
46
        except TypeError:
47
            pass
48
        
49
        if result:
50
            # serialization
51
            response = serializers.serialize(format, result)
52
            response = response.replace('_server', '_client')
53
            response = HttpResponse(response, mimetype=mimetype)
54
            return response
55
        return HttpResponse('OK', mimetype=mimetype)
56
    return wrapped
57
58
59
class MethodDispatcher(object):
60
    
61
    def __call__(self, request, app_label, model_name, object_id):
62
        """
63
        Dispatch the request given the method and object_id argument.
64
        """
65
        model = models.get_model(app_label, model_name)
66
        method = request.method
67
        logger.debug(u"Request: %s %s %s" % (method, model.__name__, object_id))
68
        if object_id is None:
69
            if method == 'GET':
70
                return self.index(request, model)
71
            elif method == 'POST':
72
                return self.add(request, model)
73
        else:
74
            object = get_object_or_404(model, id=object_id)
75
            if method == 'GET':
76
                return self.retrieve(request, model, object)
77
            elif method == 'PUT':
78
                return self.modify(request, model, object)
79
            elif method == 'DELETE':
80
                return self.delete(request, model, object)
81
    
82
    ######################
83
    ## Resource methods ##
84
    ######################    
85
    @serialize
86
    def index(self, request, model):
87
        """
88
        Returns a list of objects given request args.
89
        """
90
        # Initialization
91
        queryset = _get_queryset(model)
92
        
93
        # Filtering
94
        filters, excludes = {}, {}
95
        for k, v in request.GET.iteritems():
96
            if k.startswith('filter_'):
97
                filters[k[7:]] = v
98
            if k.startswith('exclude_'):
99
                excludes[k[8:]] = v
100
        queryset = queryset.filter(*filters.items()).exclude(*excludes.items())
101
        
102
        # Ordering
103
        if 'order_by' in request.GET:
104
            order_bys = request.GET['order_by'].replace('remotemodel_ptr', 'id').split(',')
105
            queryset = queryset.order_by(*order_bys)
106
        
107
        # Counting
108
        if 'count' in request.GET:
109
            counter = queryset.count()
110
            logger.debug(u'Count: %s objects' % counter)
111
            return counter
112
        
113
        # Slicing
114
        limit_start = int(request.GET.get('limit_start', 0))
115
        limit_stop = request.GET.get('limit_stop', False) and int(request.GET['limit_stop']) or None
116
        queryset = queryset[limit_start:limit_stop]
117
        
118
        obj_list = list(queryset)
119
        if not obj_list:
120
            raise Http404('No %s matches the given query.' % queryset.model._meta.object_name)
121
        logger.debug(u'Objects: %s retrieved' % obj_list)
122
        return obj_list
123
124
    @serialize
125
    def add(self, request, model):
126
        """
127
        Creates a new object given request args, returned as a list.
128
        """
129
        data = request.REQUEST.copy()
130
        keys = []
131
        for dict_ in data.dicts:
132
            keys += dict_.keys()
133
        values = dict([(f.name, data.get(f.name, None)) \
134
                            for f in model._meta.fields \
135
                                if data.get(f.name) != 'None'])
136
        for key in keys:
137
            if key.endswith('_id') and key not in values:
138
                values[str(key)] = int(data[key])
139
                del values[key[:-3]]
140
        
141
        object = model.objects.create(**values)
142
        response = [object]
143
        logger.debug(u'Object "%s" created' % object)
144
        return response
145
146
    ####################
147
    ## Object methods ##
148
    ####################
149
    @serialize
150
    def retrieve(self, request, model, object):
151
        """
152
        Returns an object as a list.
153
        """
154
        response = [object]
155
        logger.debug(u'Object "%s" retrieved' % object)
156
        return response
157
    
158
    @serialize
159
    def delete(self, request, model, object):
160
        """
161
        Deletes an object.
162
        """
163
        object.delete()
164
        logger.debug(u'Object "%s" deleted, remains %s' % (object, model.objects.all()))
165
    
166
    @serialize
167
    def modify(self, request, model, object):
168
        """
169
        Modifies an object given request args, returned as a list.
170
        """
171
        data = request.REQUEST.copy()
172
        keys = []
173
        for dict_ in data.dicts:
174
            keys += dict_.keys()
175
        keys = Set(keys).intersection(Set([f.name for f in model._meta.fields]))
176
        for k in keys:
177
            setattr(object, k, data[k])
178
        object.save()
179
        response = [object]
180
        logger.debug(u'Object "%s" modified' % object)
181
        return response