forked from django-tastypie/django-tastypie
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ndb_resources.py
302 lines (234 loc) · 10.4 KB
/
ndb_resources.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
from django.core.exceptions import ObjectDoesNotExist
from google.appengine.ext import ndb
from google.appengine.ext.ndb import (StringProperty, TextProperty, KeyProperty,
BooleanProperty)
from tastypie import fields
from tastypie.bundle import Bundle
from tastypie.resources import Resource, ModelDeclarativeMetaclass
class ToOneNDBKeyField(fields.RelatedField):
"""
Provides access to related data via NDB key.
"""
help_text = 'A single related resource. Can be either a URI or set of nested resource data.'
def __init__(self, to, attribute, related_name=None, default=fields.NOT_PROVIDED,
null=False, blank=False, readonly=False, full=False,
unique=False, help_text=None, use_in='all', full_list=True, full_detail=True):
super(ToOneNDBKeyField, self).__init__(
to, attribute, related_name=related_name, default=default,
null=null, blank=blank, readonly=readonly, full=full,
unique=unique, help_text=help_text, use_in=use_in,
full_list=full_list, full_detail=full_detail
)
self.fk_resource = None
def dehydrate(self, bundle):
foreign_obj = None
if isinstance(self.attribute, basestring):
foreign_obj = bundle.obj
try:
foreign_obj = getattr(foreign_obj, self.attribute, None)
except ObjectDoesNotExist:
foreign_obj = None
elif callable(self.attribute):
foreign_obj = self.attribute(bundle)
foreign_obj = foreign_obj.get()
if not foreign_obj:
if not self.null:
raise fields.ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (foreign_obj, self.attribute))
return None
self.fk_resource = self.get_related_resource(foreign_obj)
fk_bundle = Bundle(obj=foreign_obj, request=bundle.request)
return self.dehydrate_related(fk_bundle, self.fk_resource)
def hydrate(self, bundle):
value = super(ToOneNDBKeyField, self).hydrate(bundle)
if value is None:
return value
new_bundle = self.build_related_resource(value, request=bundle.request)
new_bundle.obj = new_bundle.obj.key
return new_bundle
class ToManyNDBKeyField(fields.RelatedField):
"""
This class is UNTESTED!
"""
help_text = 'Many related resources. Can be either a list of URIs or list of individually nested resource data.'
def __init__(self, to, attribute, related_name=None, default=fields.NOT_PROVIDED,
null=False, blank=False, readonly=False, full=False,
unique=False, help_text=None, use_in='all', full_list=True, full_detail=True):
super(ToManyNDBKeyField, self).__init__(
to, attribute, related_name=related_name, default=default,
null=null, blank=blank, readonly=readonly, full=full,
unique=unique, help_text=help_text, use_in=use_in,
full_list=full_list, full_detail=full_detail
)
def dehydrate(self, bundle):
if not bundle.obj or not bundle.obj.pk:
if not self.null:
raise fields.ApiFieldError("The model '%r' does not have a primary key and can not be used in a ToMany context." % bundle.obj)
return []
the_m2ms = None
if isinstance(self.attribute, basestring):
the_m2ms = bundle.obj
try:
the_m2ms = getattr(the_m2ms, self.attribute, None)
except ObjectDoesNotExist:
the_m2ms = None
elif callable(self.attribute):
the_m2ms = self.attribute(bundle)
if not the_m2ms:
if not self.null:
raise fields.ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (the_m2ms, self.attribute))
return []
self.m2m_resources = []
m2m_dehydrated = []
for m2m in the_m2ms.fetch(1000):
m2m_resource = self.get_related_resource(m2m)
m2m_bundle = Bundle(obj=m2m, request=bundle.request)
self.m2m_resources.append(m2m_resource)
m2m_dehydrated.append(self.dehydrate_related(m2m_bundle, m2m_resource))
return m2m_dehydrated
def hydrate(self, bundle):
pass
def hydrate_m2m(self, bundle):
if self.readonly:
return None
if bundle.data.get(self.instance_name) is None:
if self.blank:
return []
elif self.null:
return []
else:
raise fields.ApiFieldError("The '%s' field has no data and doesn't allow a null value." % self.instance_name)
m2m_hydrated = []
for value in bundle.data.get(self.instance_name):
if value is None:
continue
kwargs = {
'request': bundle.request,
}
if self.related_name:
kwargs['related_obj'] = bundle.obj
kwargs['related_name'] = self.related_name
new_bundle = self.build_related_resource(value, **kwargs)
new_bundle.obj = new_bundle.obj.key
m2m_hydrated.append(new_bundle)
return m2m_hydrated
class NDBResource(Resource):
"""
A very basic Tastypie Resource class for NDB models.
NB: *Good* filtering and some NDB model properties are NOT implemented.
"""
__metaclass__ = ModelDeclarativeMetaclass
@classmethod
def api_field_from_model_field(cls, f, default=fields.CharField):
"""
Returns the field type that would likely be associated with each
NDB model type.
"""
result = default
internal_type = type(f).__name__
if internal_type in ('DateProperty', 'DateTimeProperty'):
result = fields.DateTimeField
elif internal_type in ('BooleanProperty',):
result = fields.BooleanField
elif internal_type in ('FloatProperty',):
result = fields.FloatField
elif internal_type in ('IntegerProperty',):
result = fields.IntegerField
elif internal_type in ('TimeProperty',):
result = fields.TimeField
return result
@classmethod
def get_fields(cls, fields=None, excludes=None):
"""
Given any explicit fields to include and fields to exclude, add
additional fields based on the associated model.
"""
final_fields = {}
fields = fields or []
excludes = excludes or []
if not cls._meta.object_class:
return final_fields
for f in cls._meta.object_class._properties.values():
# If the field name is already present, skip
setattr(f, 'name', f._name)
if f.name in cls.base_fields:
continue
# If field is not present in explicit field listing, skip
if fields and f.name not in fields:
continue
# If field is in exclude list, skip
if excludes and f.name in excludes:
continue
api_field_class = cls.api_field_from_model_field(f)
kwargs = {
'attribute': f.name,
'unique': False,
}
if not f._required:
kwargs['null'] = True
kwargs['default'] = ''
kwargs['blank'] = True
if isinstance(f, StringProperty) or isinstance(f, TextProperty):
kwargs['default'] = ''
if isinstance(f, KeyProperty):
kwargs['full'] = True
if f._default:
kwargs['default'] = f._default
if getattr(f, 'auto_now', False):
kwargs['default'] = f.auto_now
if getattr(f, 'auto_now_add', False):
kwargs['default'] = f.auto_now_add
final_fields[f.name] = api_field_class(**kwargs)
final_fields[f.name].instance_name = f.name
return final_fields
# Below are the functions that are required to be implemented for Tastypie
def detail_uri_kwargs(self, bundle_or_obj):
kwargs = {}
if isinstance(bundle_or_obj, Bundle):
kwargs['pk'] = bundle_or_obj.obj.key.urlsafe()
else:
kwargs['pk'] = bundle_or_obj.key.urlsafe()
return kwargs
def get_object_list(self, request):
return self._meta.object_class.query()
def obj_get_list(self, request=None, **kwargs):
filters = {}
bundle = kwargs.get('bundle')
if hasattr(bundle, 'request') and hasattr(bundle.request, 'GET'):
filters = bundle.request.GET.copy()
object_list = self.get_object_list(request)
filtered_list = self.apply_filters(object_list, filters)
return list(filtered_list.fetch(self._meta.max_limit))
def obj_get(self, request=None, **kwargs):
try:
obj_key = ndb.Key(urlsafe=kwargs.get('pk'))
return obj_key.get()
except Exception:
raise ObjectDoesNotExist("Couldn't find an instance of %s" % kwargs.get('pk'))
def obj_create(self, bundle, request=None, **kwargs):
bundle = self.full_hydrate(bundle)
if 'pk' in kwargs:
try:
bundle.obj.key = ndb.Key(urlsafe=kwargs.get('pk'))
except Exception:
raise ObjectDoesNotExist("Couldn't find an instance of %s" % kwargs.get('pk'))
bundle.obj.put()
return bundle
def obj_update(self, bundle, request=None, **kwargs):
return self.obj_create(bundle, request, **kwargs)
def obj_delete_list(self, request=None, **kwargs):
obj_list = self.obj_get_list(request, kwargs)
ndb.delete_multi([obj.key for obj in obj_list])
def obj_delete(self, request=None, **kwargs):
obj = self.obj_get(kwargs)
obj.delete()
def rollback(self, bundles):
pass
def apply_filters(self, object_list, filters=None):
if filters:
for k, v in filters.iteritems():
if hasattr(self.Meta.object_class, k) and k in self.Meta.filtering:
prop = getattr(self.Meta.object_class, k)
if isinstance(prop, BooleanProperty):
v = v.lower().strip() == 'true'
object_list = object_list.filter(prop == v)
return object_list