3 # Copyright 2008 the Melange authors.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Helpers functions for updating different kinds of models in datastore.
21 '"Todd Larsen" <tlarsen@google.com>',
22 '"Sverre Rabbelier" <sverre@rabbelier.nl>',
23 '"Lennard de Rijk" <ljvderijk@gmail.com>',
24 '"Pawel Solyga" <pawel.solyga@gmail.com>',
30 from google
.appengine
.ext
import db
32 from django
.utils
.translation
import ugettext
34 from soc
.cache
import sidebar
35 from soc
.logic
import dicts
36 from soc
.views
import out_of_band
39 class Error(Exception):
40 """Base class for all exceptions raised by this module.
46 class InvalidArgumentError(Error
):
47 """Raised when an invalid argument is passed to a method.
49 For example, if an argument is None, but must always be non-False.
55 class NoEntityError(InvalidArgumentError
):
56 """Raised when no entity is passed to a method that requires one.
63 """Base logic for entity classes.
65 The BaseLogic class functions specific to Entity classes by relying
66 on arguments passed to __init__.
69 def __init__(self
, model
, base_model
=None, scope_logic
=None,
70 name
=None, skip_properties
=None, id_based
=False):
71 """Defines the name, key_name and model for this entity.
75 self
._base
_model
= base_model
76 self
._scope
_logic
= scope_logic
77 self
._id
_based
= id_based
82 self
._name
= self
._model
.__name
__
85 self
._skip
_properties
= skip_properties
87 self
._skip
_properties
= []
90 """Returns the model this logic class uses.
95 def getScopeLogic(self
):
96 """Returns the logic of the enclosing scope.
99 return self
._scope
_logic
101 def getScopeDepth(self
):
102 """Returns the scope depth for this entity.
104 Returns None if any of the parent scopes return None.
107 if not self
._scope
_logic
:
110 depth
= self
._scope
_logic
.logic
.getScopeDepth()
111 return None if (depth
is None) else (depth
+ 1)
113 def getKeyNameFromFields(self
, fields
):
114 """Returns the KeyName constructed from fields dict for this type of entity.
116 The KeyName is in the following format:
117 <key_value1>/<key_value2>/.../<key_valueN>
121 raise InvalidArgumentError
123 key_field_names
= self
.getKeyFieldNames()
125 # check if all key_field_names for this entity are present in fields
126 if not all(field
in fields
.keys() for field
in key_field_names
):
127 raise InvalidArgumentError("Not all the required key fields are present")
129 if not all(fields
.get(field
) for field
in key_field_names
):
130 raise InvalidArgumentError("Not all KeyValues are non-false")
132 # construct the KeyValues in the order given by getKeyFieldNames()
134 for key_field_name
in key_field_names
:
135 keyvalues
.append(fields
[key_field_name
])
137 # construct the KeyName in the appropriate format
138 return '/'.join(keyvalues
)
140 def getFullModelClassName(self
):
141 """Returns fully-qualified model module.class name string.
144 return '%s.%s' % (self
._model
.__module
__, self
._model
.__name
__)
146 def getKeyValuesFromEntity(self
, entity
):
147 """Extracts the key values from entity and returns them.
149 The default implementation uses the scope and link_id as key values.
152 entity: the entity from which to extract the key values
158 return [entity
.scope_path
, entity
.link_id
]
160 def getKeyValuesFromFields(self
, fields
):
161 """Extracts the key values from a dict and returns them.
163 The default implementation uses the scope and link_id as key values.
166 fields: the dict from which to extract the key values
169 if ('scope_path' not in fields
) or ('link_id' not in fields
):
170 raise InvalidArgumentError
172 return [fields
['scope_path'], fields
['link_id']]
174 def getKeyFieldNames(self
):
175 """Returns an array with the names of the Key Fields.
177 The default implementation uses the scope and link_id as key values.
180 return ['scope_path', 'link_id']
182 def getKeyFieldsFromFields(self
, dictionary
):
183 """Does any required massaging and filtering of dictionary.
185 The resulting dictionary contains just the key names, and has any
186 required translations/modifications performed.
189 dictionary: The arguments to massage
193 raise InvalidArgumentError
195 keys
= self
.getKeyFieldNames()
196 values
= self
.getKeyValuesFromFields(dictionary
)
197 key_fields
= dicts
.zip(keys
, values
)
201 def getFromKeyName(self
, key_name
):
202 """"Returns entity for key_name or None if not found.
205 key_name: key name of entity
209 raise Error("getFromKeyName called on an id based logic")
212 raise InvalidArgumentError
214 return self
._model
.get_by_key_name(key_name
)
216 def getFromID(self
, id):
217 """Returns entity for id or None if not found.
223 if not self
._id
_based
:
224 raise Error("getFromID called on a not id based logic")
227 raise InvalidArgumentError
229 return self
._model
.get_by_id(id)
231 def getFromKeyNameOr404(self
, key_name
):
232 """Like getFromKeyName but expects to find an entity.
235 out_of_band.Error if no entity is found
238 entity
= self
.getFromKeyName(key_name
)
243 msg
= ugettext('There is no "%(name)s" named %(key_name)s.') % {
244 'name': self
._name
, 'key_name': key_name
}
246 raise out_of_band
.Error(msg
, status
=404)
248 def getFromIDOr404(self
, id):
249 """Like getFromID but expects to find an entity.
252 out_of_band.Error if no entity is found
255 entity
= self
.getFromID(id)
260 msg
= ugettext('There is no "%(name)s" with id %(id)s.') % {
261 'name': self
._name
, 'id': id}
263 raise out_of_band
.Error(msg
, status
=404)
265 def getFromKeyFields(self
, fields
):
266 """Returns the entity for the specified key names, or None if not found.
269 fields: a dict containing the fields of the entity that
270 uniquely identifies it
274 raise InvalidArgumentError
276 key_fields
= self
.getKeyFieldsFromFields(fields
)
278 if all(key_fields
.values()):
279 key_name
= self
.getKeyNameFromFields(key_fields
)
280 entity
= self
.getFromKeyName(key_name
)
286 def getFromKeyFieldsOr404(self
, fields
):
287 """Like getFromKeyFields but expects to find an entity.
290 out_of_band.Error if no entity is found
293 entity
= self
.getFromKeyFields(fields
)
298 key_fields
= self
.getKeyFieldsFromFields(fields
)
299 format_text
= ugettext('"%(key)s" is "%(value)s"')
301 msg_pairs
= [format_text
% {'key': key
, 'value': value
}
302 for key
, value
in key_fields
.iteritems()]
304 joined_pairs
= ' and '.join(msg_pairs
)
307 'There is no "%(name)s" where %(pairs)s.') % {
308 'name': self
._name
, 'pairs': joined_pairs
}
310 raise out_of_band
.Error(msg
, status
=404)
312 def getForFields(self
, filter=None, unique
=False,
313 limit
=1000, offset
=0, order
=None):
314 """Returns all entities that have the specified properties.
317 filter: a dict for the properties that the entities should have
318 unique: if set, only the first item from the resultset will be returned
319 limit: the amount of entities to fetch at most
320 offset: the position to start at
321 order: a list with the sort order
327 query
= self
.getQueryForFields(filter=filter, order
=order
)
330 result
= query
.fetch(limit
, offset
)
331 except db
.NeedIndexError
, exception
:
333 logging
.exception("%s, model: %s filter: %s, order: %s" %
334 (exception
, self
._model
, filter, order
))
338 return result
[0] if result
else None
342 def getQueryForFields(self
, filter=None, order
=None):
343 """Returns a query with the specified properties.
346 filter: a dict for the properties that the entities should have
347 order: a list with the sort order
350 - Query object instantiated with the given properties
359 orderset
= set([i
.strip('-') for i
in order
])
360 if len(orderset
) != len(order
):
361 raise InvalidArgumentError
363 query
= db
.Query(self
._model
)
365 for key
, value
in filter.iteritems():
366 if isinstance(value
, list) and len(value
) == 1:
368 if isinstance(value
, list):
370 query
.filter(op
, value
)
372 query
.filter(key
, value
)
379 def updateEntityProperties(self
, entity
, entity_properties
, silent
=False):
380 """Update existing entity using supplied properties.
383 entity: a model entity
384 entity_properties: keyword arguments that correspond to entity
385 properties and their values
386 silent: iff True does not call _onUpdate method
389 The original entity with any supplied properties changed.
395 if not entity_properties
:
396 raise InvalidArgumentError
398 properties
= self
._model
.properties()
400 for name
, prop
in properties
.iteritems():
401 # if the property is not updateable or is not updated, skip it
402 if name
in self
._skip
_properties
or (name
not in entity_properties
):
405 if self
._updateField
(entity
, entity_properties
, name
):
406 value
= entity_properties
[name
]
407 prop
.__set
__(entity
, value
)
411 # call the _onUpdate method
413 self
._onUpdate
(entity
)
417 def updateOrCreateFromKeyName(self
, properties
, key_name
):
418 """Update existing entity, or create new one with supplied properties.
421 properties: dict with entity properties and their values
422 key_name: the key_name of the entity that uniquely identifies it
425 the entity corresponding to the key_name, with any supplied
426 properties changed, or a new entity now associated with the
427 supplied key_name and properties.
430 entity
= self
.getFromKeyName(key_name
)
432 create_entity
= not entity
435 for property_name
in properties
:
436 self
._createField
(properties
, property_name
)
438 # entity did not exist, so create one in a transaction
439 entity
= self
._model
.get_or_insert(key_name
, **properties
)
441 # If someone else already created the entity (due to a race), we
442 # should not update the propties (as they 'won' the race).
443 entity
= self
.updateEntityProperties(entity
, properties
, silent
=True)
446 # a new entity has been created call _onCreate
447 self
._onCreate
(entity
)
449 # the entity has been updated call _onUpdate
450 self
._onUpdate
(entity
)
454 def updateOrCreateFromFields(self
, properties
, silent
=False):
455 """Creates a new entity with the supplied properties.
458 properties: dict with entity properties and their values
459 silent: if True, do not run the _onCreate hook
462 for property_name
in properties
:
463 self
._createField
(properties
, property_name
)
466 entity
= self
._model
(**properties
)
469 key_name
= self
.getKeyNameFromFields(properties
)
470 entity
= self
._model
.get_or_insert(key_name
, **properties
)
473 self
._onCreate
(entity
)
477 def isDeletable(self
, entity
):
478 """Returns whether the specified entity can be deleted.
481 entity: an existing entity in datastore
486 def delete(self
, entity
):
487 """Delete existing entity from datastore.
490 entity: an existing entity in datastore
494 # entity has been deleted call _onDelete
495 self
._onDelete
(entity
)
497 def getAll(self
, query
):
498 """Retrieves all entities for the specified query.
507 data
= query
.fetch(chunk
+1, offset
)
509 more
= len(data
) > chunk
515 offset
= offset
+ chunk
519 def entityIterator(self
, queryGen
, batchSize
= 100):
520 """Iterator that yields an entity in batches.
523 queryGen: should return a Query object
524 batchSize: how many entities to retrieve in one datastore call
526 Retrieved from http://tinyurl.com/d887ll (AppEngine cookbook).
529 # AppEngine will not fetch more than 1000 results
530 batchSize
= min(batchSize
,1000)
539 query
.filter("__key__ > ",key
)
540 results
= query
.fetch(batchSize
)
541 for result
in results
:
544 if batchSize
> len(results
):
547 key
= results
[-1].key()
549 def _createField(self
, entity_properties
, name
):
550 """Hook called when a field is created.
552 To be exact, this method is called for each field (that has a value
553 specified) on an entity that is being created.
555 Base classes should override if any special actions need to be
556 taken when a field is created.
559 entity_properties: keyword arguments that correspond to entity
560 properties and their values
561 name: the name of the field to be created
564 if not entity_properties
or (name
not in entity_properties
):
565 raise InvalidArgumentError
567 def _updateField(self
, entity
, entity_properties
, name
):
568 """Hook called when a field is updated.
570 Base classes should override if any special actions need to be
571 taken when a field is updated. The field is not updated if the
572 method does not return a True value.
575 entity: the unaltered entity
576 entity_properties: keyword arguments that correspond to entity
577 properties and their values
578 name: the name of the field to be changed
584 if not entity_properties
or (name
not in entity_properties
):
585 raise InvalidArgumentError
589 def _onCreate(self
, entity
):
590 """Called when an entity has been created.
592 Classes that override this can use it to do any post-creation operations.
600 def _onUpdate(self
, entity
):
601 """Called when an entity has been updated.
603 Classes that override this can use it to do any post-update operations.
609 def _onDelete(self
, entity
):
610 """Called when an entity has been deleted.
612 Classes that override this can use it to do any post-deletion operations.