Do not die on NeedIndexErrors
[Melange.git] / app / soc / logic / models / base.py
blobdddc7f0c8d9d81f124f56372064f1d02fbab2f2b
1 #!/usr/bin/python2.5
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.
18 """
20 __authors__ = [
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>',
28 import logging
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.
41 """
43 pass
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.
50 """
52 pass
55 class NoEntityError(InvalidArgumentError):
56 """Raised when no entity is passed to a method that requires one.
57 """
59 pass
62 class Logic(object):
63 """Base logic for entity classes.
65 The BaseLogic class functions specific to Entity classes by relying
66 on arguments passed to __init__.
67 """
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.
72 """
74 self._model = model
75 self._base_model = base_model
76 self._scope_logic = scope_logic
77 self._id_based = id_based
79 if name:
80 self._name = name
81 else:
82 self._name = self._model.__name__
84 if skip_properties:
85 self._skip_properties = skip_properties
86 else:
87 self._skip_properties = []
89 def getModel(self):
90 """Returns the model this logic class uses.
91 """
93 return self._model
95 def getScopeLogic(self):
96 """Returns the logic of the enclosing scope.
97 """
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:
108 return 0
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>
120 if not fields:
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()
133 keyvalues = []
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.
151 Args:
152 entity: the entity from which to extract the key values
155 if not entity:
156 raise NoEntityError
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.
165 Args:
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.
188 Args:
189 dictionary: The arguments to massage
192 if not dictionary:
193 raise InvalidArgumentError
195 keys = self.getKeyFieldNames()
196 values = self.getKeyValuesFromFields(dictionary)
197 key_fields = dicts.zip(keys, values)
199 return key_fields
201 def getFromKeyName(self, key_name):
202 """"Returns entity for key_name or None if not found.
204 Args:
205 key_name: key name of entity
208 if self._id_based:
209 raise Error("getFromKeyName called on an id based logic")
211 if not key_name:
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.
219 Args:
220 id: id of entity
223 if not self._id_based:
224 raise Error("getFromID called on a not id based logic")
226 if not id:
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.
234 Raises:
235 out_of_band.Error if no entity is found
238 entity = self.getFromKeyName(key_name)
240 if entity:
241 return entity
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.
251 Raises:
252 out_of_band.Error if no entity is found
255 entity = self.getFromID(id)
257 if entity:
258 return entity
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.
268 Args:
269 fields: a dict containing the fields of the entity that
270 uniquely identifies it
273 if not fields:
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)
281 else:
282 entity = None
284 return entity
286 def getFromKeyFieldsOr404(self, fields):
287 """Like getFromKeyFields but expects to find an entity.
289 Raises:
290 out_of_band.Error if no entity is found
293 entity = self.getFromKeyFields(fields)
295 if entity:
296 return entity
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)
306 msg = ugettext(
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.
316 Args:
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
324 if unique:
325 limit = 1
327 query = self.getQueryForFields(filter=filter, order=order)
329 try:
330 result = query.fetch(limit, offset)
331 except db.NeedIndexError, exception:
332 result = []
333 logging.exception("%s, model: %s filter: %s, order: %s" %
334 (exception, self._model, filter, order))
335 # TODO: send email
337 if unique:
338 return result[0] if result else None
340 return result
342 def getQueryForFields(self, filter=None, order=None):
343 """Returns a query with the specified properties.
345 Args:
346 filter: a dict for the properties that the entities should have
347 order: a list with the sort order
349 Returns:
350 - Query object instantiated with the given properties
353 if not filter:
354 filter = {}
356 if not order:
357 order = []
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:
367 value = value[0]
368 if isinstance(value, list):
369 op = '%s IN' % key
370 query.filter(op, value)
371 else:
372 query.filter(key, value)
374 for key in order:
375 query.order(key)
377 return query
379 def updateEntityProperties(self, entity, entity_properties, silent=False):
380 """Update existing entity using supplied properties.
382 Args:
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
388 Returns:
389 The original entity with any supplied properties changed.
392 if not entity:
393 raise NoEntityError
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):
403 continue
405 if self._updateField(entity, entity_properties, name):
406 value = entity_properties[name]
407 prop.__set__(entity, value)
409 entity.put()
411 # call the _onUpdate method
412 if not silent:
413 self._onUpdate(entity)
415 return entity
417 def updateOrCreateFromKeyName(self, properties, key_name):
418 """Update existing entity, or create new one with supplied properties.
420 Args:
421 properties: dict with entity properties and their values
422 key_name: the key_name of the entity that uniquely identifies it
424 Returns:
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
434 if create_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)
440 else:
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)
445 if create_entity:
446 # a new entity has been created call _onCreate
447 self._onCreate(entity)
448 else:
449 # the entity has been updated call _onUpdate
450 self._onUpdate(entity)
452 return entity
454 def updateOrCreateFromFields(self, properties, silent=False):
455 """Creates a new entity with the supplied properties.
457 Args:
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)
465 if self._id_based:
466 entity = self._model(**properties)
467 entity.put()
468 else:
469 key_name = self.getKeyNameFromFields(properties)
470 entity = self._model.get_or_insert(key_name, **properties)
472 if not silent:
473 self._onCreate(entity)
475 return entity
477 def isDeletable(self, entity):
478 """Returns whether the specified entity can be deleted.
480 Args:
481 entity: an existing entity in datastore
484 return True
486 def delete(self, entity):
487 """Delete existing entity from datastore.
489 Args:
490 entity: an existing entity in datastore
493 entity.delete()
494 # entity has been deleted call _onDelete
495 self._onDelete(entity)
497 def getAll(self, query):
498 """Retrieves all entities for the specified query.
501 chunk = 999
502 offset = 0
503 result = []
504 more = True
506 while(more):
507 data = query.fetch(chunk+1, offset)
509 more = len(data) > chunk
511 if more:
512 del data[chunk]
514 result.extend(data)
515 offset = offset + chunk
517 return result
519 def entityIterator(self, queryGen, batchSize = 100):
520 """Iterator that yields an entity in batches.
522 Args:
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)
532 done = False
533 count = 0
534 key = None
536 while not done:
537 query = queryGen()
538 if key:
539 query.filter("__key__ > ",key)
540 results = query.fetch(batchSize)
541 for result in results:
542 count += 1
543 yield result
544 if batchSize > len(results):
545 done = True
546 else:
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.
558 Args:
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.
574 Args:
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
581 if not entity:
582 raise NoEntityError
584 if not entity_properties or (name not in entity_properties):
585 raise InvalidArgumentError
587 return True
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.
595 if not entity:
596 raise NoEntityError
598 sidebar.flush()
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.
606 if not entity:
607 raise NoEntityError
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.
615 if not entity:
616 raise NoEntityError