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.
| commit 0: | 81b112d27748 |
| branch: | default |
| tags: | 0.1 |
20 months ago
Changed (Δ50.1 KB):
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)
1 |
==================== |
|
2 |
django-roa changelog |
|
3 |
==================== |
|
4 |
||
5 |
Version 0.1, 12 December 2008: |
|
6 |
------------------------------ |
|
7 |
||
8 |
Initial release. |
1 |
Copyright (c) 2008-2009, David Larlet |
|
2 |
All rights reserved. |
|
3 |
||
4 |
Redistribution of this application is not permitted. |
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 |
