blob: 95e4fc866ff2b3e9c2563744403dc3517613c233 [file] [log] [blame]
showard7c785282008-05-29 19:45:12 +00001"""
2Extensions to Django's model logic.
3"""
4
showarda5288b42009-07-28 20:06:08 +00005import re
6import django.core.exceptions
showard7c785282008-05-29 19:45:12 +00007from django.db import models as dbmodels, backend, connection
showarda5288b42009-07-28 20:06:08 +00008from django.db.models.sql import query
showard7e67b432010-01-20 01:13:04 +00009import django.db.models.sql.where
showard7c785282008-05-29 19:45:12 +000010from django.utils import datastructures
Prashanth B489b91d2014-03-15 12:17:16 -070011from autotest_lib.frontend.afe import rdb_model_extensions
showard56e93772008-10-06 10:06:22 +000012from autotest_lib.frontend.afe import readonly_connection
showard7c785282008-05-29 19:45:12 +000013
Prashanth B489b91d2014-03-15 12:17:16 -070014
15class ValidationError(django.core.exceptions.ValidationError):
jadmanski0afbb632008-06-06 21:10:57 +000016 """\
showarda5288b42009-07-28 20:06:08 +000017 Data validation error in adding or updating an object. The associated
jadmanski0afbb632008-06-06 21:10:57 +000018 value is a dictionary mapping field names to error strings.
19 """
showard7c785282008-05-29 19:45:12 +000020
21
showard09096d82008-07-07 23:20:49 +000022def _wrap_with_readonly(method):
mbligh1ef218d2009-08-03 16:57:56 +000023 def wrapper_method(*args, **kwargs):
24 readonly_connection.connection().set_django_connection()
25 try:
26 return method(*args, **kwargs)
27 finally:
28 readonly_connection.connection().unset_django_connection()
29 wrapper_method.__name__ = method.__name__
30 return wrapper_method
showard09096d82008-07-07 23:20:49 +000031
32
showarda5288b42009-07-28 20:06:08 +000033def _quote_name(name):
34 """Shorthand for connection.ops.quote_name()."""
35 return connection.ops.quote_name(name)
36
37
showard09096d82008-07-07 23:20:49 +000038def _wrap_generator_with_readonly(generator):
39 """
40 We have to wrap generators specially. Assume it performs
41 the query on the first call to next().
42 """
43 def wrapper_generator(*args, **kwargs):
44 generator_obj = generator(*args, **kwargs)
showard56e93772008-10-06 10:06:22 +000045 readonly_connection.connection().set_django_connection()
showard09096d82008-07-07 23:20:49 +000046 try:
47 first_value = generator_obj.next()
48 finally:
showard56e93772008-10-06 10:06:22 +000049 readonly_connection.connection().unset_django_connection()
showard09096d82008-07-07 23:20:49 +000050 yield first_value
51
52 while True:
53 yield generator_obj.next()
54
55 wrapper_generator.__name__ = generator.__name__
56 return wrapper_generator
57
58
59def _make_queryset_readonly(queryset):
60 """
61 Wrap all methods that do database queries with a readonly connection.
62 """
63 db_query_methods = ['count', 'get', 'get_or_create', 'latest', 'in_bulk',
64 'delete']
65 for method_name in db_query_methods:
66 method = getattr(queryset, method_name)
67 wrapped_method = _wrap_with_readonly(method)
68 setattr(queryset, method_name, wrapped_method)
69
70 queryset.iterator = _wrap_generator_with_readonly(queryset.iterator)
71
72
73class ReadonlyQuerySet(dbmodels.query.QuerySet):
74 """
75 QuerySet object that performs all database queries with the read-only
76 connection.
77 """
showarda5288b42009-07-28 20:06:08 +000078 def __init__(self, model=None, *args, **kwargs):
79 super(ReadonlyQuerySet, self).__init__(model, *args, **kwargs)
showard09096d82008-07-07 23:20:49 +000080 _make_queryset_readonly(self)
81
82
83 def values(self, *fields):
showarda5288b42009-07-28 20:06:08 +000084 return self._clone(klass=ReadonlyValuesQuerySet,
85 setup=True, _fields=fields)
showard09096d82008-07-07 23:20:49 +000086
87
88class ReadonlyValuesQuerySet(dbmodels.query.ValuesQuerySet):
showarda5288b42009-07-28 20:06:08 +000089 def __init__(self, model=None, *args, **kwargs):
90 super(ReadonlyValuesQuerySet, self).__init__(model, *args, **kwargs)
showard09096d82008-07-07 23:20:49 +000091 _make_queryset_readonly(self)
92
93
beepscc9fc702013-12-02 12:45:38 -080094class LeasedHostManager(dbmodels.Manager):
95 """Query manager for unleased, unlocked hosts.
96 """
97 def get_query_set(self):
98 return (super(LeasedHostManager, self).get_query_set().filter(
99 leased=0, locked=0))
100
101
showard7c785282008-05-29 19:45:12 +0000102class ExtendedManager(dbmodels.Manager):
jadmanski0afbb632008-06-06 21:10:57 +0000103 """\
104 Extended manager supporting subquery filtering.
105 """
showard7c785282008-05-29 19:45:12 +0000106
showardf828c772010-01-25 21:49:42 +0000107 class CustomQuery(query.Query):
showard7e67b432010-01-20 01:13:04 +0000108 def __init__(self, *args, **kwargs):
showardf828c772010-01-25 21:49:42 +0000109 super(ExtendedManager.CustomQuery, self).__init__(*args, **kwargs)
showard7e67b432010-01-20 01:13:04 +0000110 self._custom_joins = []
111
112
showarda5288b42009-07-28 20:06:08 +0000113 def clone(self, klass=None, **kwargs):
showardf828c772010-01-25 21:49:42 +0000114 obj = super(ExtendedManager.CustomQuery, self).clone(klass)
showard7e67b432010-01-20 01:13:04 +0000115 obj._custom_joins = list(self._custom_joins)
showarda5288b42009-07-28 20:06:08 +0000116 return obj
showard08f981b2008-06-24 21:59:03 +0000117
showard7e67b432010-01-20 01:13:04 +0000118
119 def combine(self, rhs, connector):
showardf828c772010-01-25 21:49:42 +0000120 super(ExtendedManager.CustomQuery, self).combine(rhs, connector)
showard7e67b432010-01-20 01:13:04 +0000121 if hasattr(rhs, '_custom_joins'):
122 self._custom_joins.extend(rhs._custom_joins)
123
124
125 def add_custom_join(self, table, condition, join_type,
126 condition_values=(), alias=None):
127 if alias is None:
128 alias = table
129 join_dict = dict(table=table,
130 condition=condition,
131 condition_values=condition_values,
132 join_type=join_type,
133 alias=alias)
134 self._custom_joins.append(join_dict)
135
136
showard7e67b432010-01-20 01:13:04 +0000137 @classmethod
138 def convert_query(self, query_set):
139 """
showardf828c772010-01-25 21:49:42 +0000140 Convert the query set's "query" attribute to a CustomQuery.
showard7e67b432010-01-20 01:13:04 +0000141 """
142 # Make a copy of the query set
143 query_set = query_set.all()
144 query_set.query = query_set.query.clone(
showardf828c772010-01-25 21:49:42 +0000145 klass=ExtendedManager.CustomQuery,
showard7e67b432010-01-20 01:13:04 +0000146 _custom_joins=[])
147 return query_set
showard43a3d262008-11-12 18:17:05 +0000148
149
showard7e67b432010-01-20 01:13:04 +0000150 class _WhereClause(object):
151 """Object allowing us to inject arbitrary SQL into Django queries.
showard43a3d262008-11-12 18:17:05 +0000152
showard7e67b432010-01-20 01:13:04 +0000153 By using this instead of extra(where=...), we can still freely combine
154 queries with & and |.
showarda5288b42009-07-28 20:06:08 +0000155 """
showard7e67b432010-01-20 01:13:04 +0000156 def __init__(self, clause, values=()):
157 self._clause = clause
158 self._values = values
showarda5288b42009-07-28 20:06:08 +0000159
showard7e67b432010-01-20 01:13:04 +0000160
Dale Curtis74a314b2011-06-23 14:55:46 -0700161 def as_sql(self, qn=None, connection=None):
showard7e67b432010-01-20 01:13:04 +0000162 return self._clause, self._values
163
164
165 def relabel_aliases(self, change_map):
166 return
showard43a3d262008-11-12 18:17:05 +0000167
168
showard8b0ea222009-12-23 19:23:03 +0000169 def add_join(self, query_set, join_table, join_key, join_condition='',
showard7e67b432010-01-20 01:13:04 +0000170 join_condition_values=(), join_from_key=None, alias=None,
171 suffix='', exclude=False, force_left_join=False):
172 """Add a join to query_set.
173
174 Join looks like this:
175 (INNER|LEFT) JOIN <join_table> AS <alias>
176 ON (<this table>.<join_from_key> = <join_table>.<join_key>
177 and <join_condition>)
178
showard0957a842009-05-11 19:25:08 +0000179 @param join_table table to join to
180 @param join_key field referencing back to this model to use for the join
181 @param join_condition extra condition for the ON clause of the join
showard7e67b432010-01-20 01:13:04 +0000182 @param join_condition_values values to substitute into join_condition
183 @param join_from_key column on this model to join from.
showard8b0ea222009-12-23 19:23:03 +0000184 @param alias alias to use for for join
185 @param suffix suffix to add to join_table for the join alias, if no
186 alias is provided
showard0957a842009-05-11 19:25:08 +0000187 @param exclude if true, exclude rows that match this join (will use a
showarda5288b42009-07-28 20:06:08 +0000188 LEFT OUTER JOIN and an appropriate WHERE condition)
showardc4780402009-08-31 18:31:34 +0000189 @param force_left_join - if true, a LEFT OUTER JOIN will be used
190 instead of an INNER JOIN regardless of other options
showard0957a842009-05-11 19:25:08 +0000191 """
showard7e67b432010-01-20 01:13:04 +0000192 join_from_table = query_set.model._meta.db_table
193 if join_from_key is None:
194 join_from_key = self.model._meta.pk.name
195 if alias is None:
196 alias = join_table + suffix
197 full_join_key = _quote_name(alias) + '.' + _quote_name(join_key)
198 full_join_condition = '%s = %s.%s' % (full_join_key,
199 _quote_name(join_from_table),
200 _quote_name(join_from_key))
showard43a3d262008-11-12 18:17:05 +0000201 if join_condition:
202 full_join_condition += ' AND (' + join_condition + ')'
203 if exclude or force_left_join:
showarda5288b42009-07-28 20:06:08 +0000204 join_type = query_set.query.LOUTER
showard43a3d262008-11-12 18:17:05 +0000205 else:
showarda5288b42009-07-28 20:06:08 +0000206 join_type = query_set.query.INNER
showard43a3d262008-11-12 18:17:05 +0000207
showardf828c772010-01-25 21:49:42 +0000208 query_set = self.CustomQuery.convert_query(query_set)
showard7e67b432010-01-20 01:13:04 +0000209 query_set.query.add_custom_join(join_table,
210 full_join_condition,
211 join_type,
212 condition_values=join_condition_values,
213 alias=alias)
showard43a3d262008-11-12 18:17:05 +0000214
showard7e67b432010-01-20 01:13:04 +0000215 if exclude:
216 query_set = query_set.extra(where=[full_join_key + ' IS NULL'])
217
218 return query_set
219
220
221 def _info_for_many_to_one_join(self, field, join_to_query, alias):
222 """
223 @param field: the ForeignKey field on the related model
224 @param join_to_query: the query over the related model that we're
225 joining to
226 @param alias: alias of joined table
227 """
228 info = {}
229 rhs_table = join_to_query.model._meta.db_table
230 info['rhs_table'] = rhs_table
231 info['rhs_column'] = field.column
232 info['lhs_column'] = field.rel.get_related_field().column
233 rhs_where = join_to_query.query.where
234 rhs_where.relabel_aliases({rhs_table: alias})
Dale Curtis74a314b2011-06-23 14:55:46 -0700235 compiler = join_to_query.query.get_compiler(using=join_to_query.db)
236 initial_clause, values = compiler.as_sql()
237 all_clauses = (initial_clause,)
238 if hasattr(join_to_query.query, 'extra_where'):
239 all_clauses += join_to_query.query.extra_where
240 info['where_clause'] = (
241 ' AND '.join('(%s)' % clause for clause in all_clauses))
showard7e67b432010-01-20 01:13:04 +0000242 info['values'] = values
243 return info
244
245
246 def _info_for_many_to_many_join(self, m2m_field, join_to_query, alias,
247 m2m_is_on_this_model):
248 """
249 @param m2m_field: a Django field representing the M2M relationship.
250 It uses a pivot table with the following structure:
251 this model table <---> M2M pivot table <---> joined model table
252 @param join_to_query: the query over the related model that we're
253 joining to.
254 @param alias: alias of joined table
255 """
256 if m2m_is_on_this_model:
257 # referenced field on this model
258 lhs_id_field = self.model._meta.pk
259 # foreign key on the pivot table referencing lhs_id_field
260 m2m_lhs_column = m2m_field.m2m_column_name()
261 # foreign key on the pivot table referencing rhd_id_field
262 m2m_rhs_column = m2m_field.m2m_reverse_name()
263 # referenced field on related model
264 rhs_id_field = m2m_field.rel.get_related_field()
265 else:
266 lhs_id_field = m2m_field.rel.get_related_field()
267 m2m_lhs_column = m2m_field.m2m_reverse_name()
268 m2m_rhs_column = m2m_field.m2m_column_name()
269 rhs_id_field = join_to_query.model._meta.pk
270
271 info = {}
272 info['rhs_table'] = m2m_field.m2m_db_table()
273 info['rhs_column'] = m2m_lhs_column
274 info['lhs_column'] = lhs_id_field.column
275
276 # select the ID of related models relevant to this join. we can only do
277 # a single join, so we need to gather this information up front and
278 # include it in the join condition.
279 rhs_ids = join_to_query.values_list(rhs_id_field.attname, flat=True)
280 assert len(rhs_ids) == 1, ('Many-to-many custom field joins can only '
281 'match a single related object.')
282 rhs_id = rhs_ids[0]
283
284 info['where_clause'] = '%s.%s = %s' % (_quote_name(alias),
285 _quote_name(m2m_rhs_column),
286 rhs_id)
287 info['values'] = ()
288 return info
289
290
291 def join_custom_field(self, query_set, join_to_query, alias,
292 left_join=True):
293 """Join to a related model to create a custom field in the given query.
294
295 This method is used to construct a custom field on the given query based
296 on a many-valued relationsip. join_to_query should be a simple query
297 (no joins) on the related model which returns at most one related row
298 per instance of this model.
299
300 For many-to-one relationships, the joined table contains the matching
301 row from the related model it one is related, NULL otherwise.
302
303 For many-to-many relationships, the joined table contains the matching
304 row if it's related, NULL otherwise.
305 """
306 relationship_type, field = self.determine_relationship(
307 join_to_query.model)
308
309 if relationship_type == self.MANY_TO_ONE:
310 info = self._info_for_many_to_one_join(field, join_to_query, alias)
311 elif relationship_type == self.M2M_ON_RELATED_MODEL:
312 info = self._info_for_many_to_many_join(
313 m2m_field=field, join_to_query=join_to_query, alias=alias,
314 m2m_is_on_this_model=False)
315 elif relationship_type ==self.M2M_ON_THIS_MODEL:
316 info = self._info_for_many_to_many_join(
317 m2m_field=field, join_to_query=join_to_query, alias=alias,
318 m2m_is_on_this_model=True)
319
320 return self.add_join(query_set, info['rhs_table'], info['rhs_column'],
321 join_from_key=info['lhs_column'],
322 join_condition=info['where_clause'],
323 join_condition_values=info['values'],
324 alias=alias,
325 force_left_join=left_join)
326
327
showardf828c772010-01-25 21:49:42 +0000328 def key_on_joined_table(self, join_to_query):
329 """Get a non-null column on the table joined for the given query.
330
331 This analyzes the join that would be produced if join_to_query were
332 passed to join_custom_field.
333 """
334 relationship_type, field = self.determine_relationship(
335 join_to_query.model)
336 if relationship_type == self.MANY_TO_ONE:
337 return join_to_query.model._meta.pk.column
338 return field.m2m_column_name() # any column on the M2M table will do
339
340
showard7e67b432010-01-20 01:13:04 +0000341 def add_where(self, query_set, where, values=()):
342 query_set = query_set.all()
343 query_set.query.where.add(self._WhereClause(where, values),
344 django.db.models.sql.where.AND)
showardc4780402009-08-31 18:31:34 +0000345 return query_set
showard7c785282008-05-29 19:45:12 +0000346
347
showardeaccf8f2009-04-16 03:11:33 +0000348 def _get_quoted_field(self, table, field):
showarda5288b42009-07-28 20:06:08 +0000349 return _quote_name(table) + '.' + _quote_name(field)
showard5ef36e92008-07-02 16:37:09 +0000350
351
showard7c199df2008-10-03 10:17:15 +0000352 def get_key_on_this_table(self, key_field=None):
showard5ef36e92008-07-02 16:37:09 +0000353 if key_field is None:
354 # default to primary key
355 key_field = self.model._meta.pk.column
356 return self._get_quoted_field(self.model._meta.db_table, key_field)
357
358
showardeaccf8f2009-04-16 03:11:33 +0000359 def escape_user_sql(self, sql):
360 return sql.replace('%', '%%')
361
showard5ef36e92008-07-02 16:37:09 +0000362
showard0957a842009-05-11 19:25:08 +0000363 def _custom_select_query(self, query_set, selects):
Dale Curtis74a314b2011-06-23 14:55:46 -0700364 compiler = query_set.query.get_compiler(using=query_set.db)
365 sql, params = compiler.as_sql()
showarda5288b42009-07-28 20:06:08 +0000366 from_ = sql[sql.find(' FROM'):]
367
368 if query_set.query.distinct:
showard0957a842009-05-11 19:25:08 +0000369 distinct = 'DISTINCT '
370 else:
371 distinct = ''
showarda5288b42009-07-28 20:06:08 +0000372
373 sql_query = ('SELECT ' + distinct + ','.join(selects) + from_)
showard0957a842009-05-11 19:25:08 +0000374 cursor = readonly_connection.connection().cursor()
375 cursor.execute(sql_query, params)
376 return cursor.fetchall()
377
378
showard68693f72009-05-20 00:31:53 +0000379 def _is_relation_to(self, field, model_class):
380 return field.rel and field.rel.to is model_class
showard0957a842009-05-11 19:25:08 +0000381
382
showard7e67b432010-01-20 01:13:04 +0000383 MANY_TO_ONE = object()
384 M2M_ON_RELATED_MODEL = object()
385 M2M_ON_THIS_MODEL = object()
386
387 def determine_relationship(self, related_model):
388 """
389 Determine the relationship between this model and related_model.
390
391 related_model must have some sort of many-valued relationship to this
392 manager's model.
393 @returns (relationship_type, field), where relationship_type is one of
394 MANY_TO_ONE, M2M_ON_RELATED_MODEL, M2M_ON_THIS_MODEL, and field
395 is the Django field object for the relationship.
396 """
397 # look for a foreign key field on related_model relating to this model
398 for field in related_model._meta.fields:
399 if self._is_relation_to(field, self.model):
400 return self.MANY_TO_ONE, field
401
402 # look for an M2M field on related_model relating to this model
403 for field in related_model._meta.many_to_many:
404 if self._is_relation_to(field, self.model):
405 return self.M2M_ON_RELATED_MODEL, field
406
407 # maybe this model has the many-to-many field
408 for field in self.model._meta.many_to_many:
409 if self._is_relation_to(field, related_model):
410 return self.M2M_ON_THIS_MODEL, field
411
412 raise ValueError('%s has no relation to %s' %
413 (related_model, self.model))
414
415
showard68693f72009-05-20 00:31:53 +0000416 def _get_pivot_iterator(self, base_objects_by_id, related_model):
showard0957a842009-05-11 19:25:08 +0000417 """
showard68693f72009-05-20 00:31:53 +0000418 Determine the relationship between this model and related_model, and
419 return a pivot iterator.
420 @param base_objects_by_id: dict of instances of this model indexed by
421 their IDs
422 @returns a pivot iterator, which yields a tuple (base_object,
423 related_object) for each relationship between a base object and a
424 related object. all base_object instances come from base_objects_by_id.
showard7e67b432010-01-20 01:13:04 +0000425 Note -- this depends on Django model internals.
showard0957a842009-05-11 19:25:08 +0000426 """
showard7e67b432010-01-20 01:13:04 +0000427 relationship_type, field = self.determine_relationship(related_model)
428 if relationship_type == self.MANY_TO_ONE:
429 return self._many_to_one_pivot(base_objects_by_id,
430 related_model, field)
431 elif relationship_type == self.M2M_ON_RELATED_MODEL:
432 return self._many_to_many_pivot(
showard68693f72009-05-20 00:31:53 +0000433 base_objects_by_id, related_model, field.m2m_db_table(),
434 field.m2m_reverse_name(), field.m2m_column_name())
showard7e67b432010-01-20 01:13:04 +0000435 else:
436 assert relationship_type == self.M2M_ON_THIS_MODEL
437 return self._many_to_many_pivot(
showard68693f72009-05-20 00:31:53 +0000438 base_objects_by_id, related_model, field.m2m_db_table(),
439 field.m2m_column_name(), field.m2m_reverse_name())
showard0957a842009-05-11 19:25:08 +0000440
showard0957a842009-05-11 19:25:08 +0000441
showard68693f72009-05-20 00:31:53 +0000442 def _many_to_one_pivot(self, base_objects_by_id, related_model,
443 foreign_key_field):
444 """
445 @returns a pivot iterator - see _get_pivot_iterator()
446 """
447 filter_data = {foreign_key_field.name + '__pk__in':
448 base_objects_by_id.keys()}
449 for related_object in related_model.objects.filter(**filter_data):
showarda5a72c92009-08-20 23:35:21 +0000450 # lookup base object in the dict, rather than grabbing it from the
451 # related object. we need to return instances from the dict, not
452 # fresh instances of the same models (and grabbing model instances
453 # from the related models incurs a DB query each time).
454 base_object_id = getattr(related_object, foreign_key_field.attname)
455 base_object = base_objects_by_id[base_object_id]
showard68693f72009-05-20 00:31:53 +0000456 yield base_object, related_object
457
458
459 def _query_pivot_table(self, base_objects_by_id, pivot_table,
460 pivot_from_field, pivot_to_field):
showard0957a842009-05-11 19:25:08 +0000461 """
462 @param id_list list of IDs of self.model objects to include
463 @param pivot_table the name of the pivot table
464 @param pivot_from_field a field name on pivot_table referencing
465 self.model
466 @param pivot_to_field a field name on pivot_table referencing the
467 related model.
showard68693f72009-05-20 00:31:53 +0000468 @returns pivot list of IDs (base_id, related_id)
showard0957a842009-05-11 19:25:08 +0000469 """
470 query = """
471 SELECT %(from_field)s, %(to_field)s
472 FROM %(table)s
473 WHERE %(from_field)s IN (%(id_list)s)
474 """ % dict(from_field=pivot_from_field,
475 to_field=pivot_to_field,
476 table=pivot_table,
showard68693f72009-05-20 00:31:53 +0000477 id_list=','.join(str(id_) for id_
478 in base_objects_by_id.iterkeys()))
showard0957a842009-05-11 19:25:08 +0000479 cursor = readonly_connection.connection().cursor()
480 cursor.execute(query)
showard68693f72009-05-20 00:31:53 +0000481 return cursor.fetchall()
showard0957a842009-05-11 19:25:08 +0000482
483
showard68693f72009-05-20 00:31:53 +0000484 def _many_to_many_pivot(self, base_objects_by_id, related_model,
485 pivot_table, pivot_from_field, pivot_to_field):
486 """
487 @param pivot_table: see _query_pivot_table
488 @param pivot_from_field: see _query_pivot_table
489 @param pivot_to_field: see _query_pivot_table
490 @returns a pivot iterator - see _get_pivot_iterator()
491 """
492 id_pivot = self._query_pivot_table(base_objects_by_id, pivot_table,
493 pivot_from_field, pivot_to_field)
494
495 all_related_ids = list(set(related_id for base_id, related_id
496 in id_pivot))
497 related_objects_by_id = related_model.objects.in_bulk(all_related_ids)
498
499 for base_id, related_id in id_pivot:
500 yield base_objects_by_id[base_id], related_objects_by_id[related_id]
501
502
503 def populate_relationships(self, base_objects, related_model,
showard0957a842009-05-11 19:25:08 +0000504 related_list_name):
505 """
showard68693f72009-05-20 00:31:53 +0000506 For each instance of this model in base_objects, add a field named
507 related_list_name listing all the related objects of type related_model.
508 related_model must be in a many-to-one or many-to-many relationship with
509 this model.
510 @param base_objects - list of instances of this model
511 @param related_model - model class related to this model
512 @param related_list_name - attribute name in which to store the related
513 object list.
showard0957a842009-05-11 19:25:08 +0000514 """
showard68693f72009-05-20 00:31:53 +0000515 if not base_objects:
showard0957a842009-05-11 19:25:08 +0000516 # if we don't bail early, we'll get a SQL error later
517 return
showard0957a842009-05-11 19:25:08 +0000518
showard68693f72009-05-20 00:31:53 +0000519 base_objects_by_id = dict((base_object._get_pk_val(), base_object)
520 for base_object in base_objects)
521 pivot_iterator = self._get_pivot_iterator(base_objects_by_id,
522 related_model)
showard0957a842009-05-11 19:25:08 +0000523
showard68693f72009-05-20 00:31:53 +0000524 for base_object in base_objects:
525 setattr(base_object, related_list_name, [])
526
527 for base_object, related_object in pivot_iterator:
528 getattr(base_object, related_list_name).append(related_object)
showard0957a842009-05-11 19:25:08 +0000529
530
jamesrene3656232010-03-02 00:00:30 +0000531class ModelWithInvalidQuerySet(dbmodels.query.QuerySet):
532 """
533 QuerySet that handles delete() properly for models with an "invalid" bit
534 """
535 def delete(self):
536 for model in self:
537 model.delete()
538
539
540class ModelWithInvalidManager(ExtendedManager):
541 """
542 Manager for objects with an "invalid" bit
543 """
544 def get_query_set(self):
545 return ModelWithInvalidQuerySet(self.model)
546
547
548class ValidObjectsManager(ModelWithInvalidManager):
jadmanski0afbb632008-06-06 21:10:57 +0000549 """
550 Manager returning only objects with invalid=False.
551 """
552 def get_query_set(self):
553 queryset = super(ValidObjectsManager, self).get_query_set()
554 return queryset.filter(invalid=False)
showard7c785282008-05-29 19:45:12 +0000555
556
Prashanth B489b91d2014-03-15 12:17:16 -0700557class ModelExtensions(rdb_model_extensions.ModelValidators):
jadmanski0afbb632008-06-06 21:10:57 +0000558 """\
Prashanth B489b91d2014-03-15 12:17:16 -0700559 Mixin with convenience functions for models, built on top of
560 the model validators in rdb_model_extensions.
jadmanski0afbb632008-06-06 21:10:57 +0000561 """
562 # TODO: at least some of these functions really belong in a custom
563 # Manager class
showard7c785282008-05-29 19:45:12 +0000564
jadmanski0afbb632008-06-06 21:10:57 +0000565 @classmethod
566 def convert_human_readable_values(cls, data, to_human_readable=False):
567 """\
568 Performs conversions on user-supplied field data, to make it
569 easier for users to pass human-readable data.
showard7c785282008-05-29 19:45:12 +0000570
jadmanski0afbb632008-06-06 21:10:57 +0000571 For all fields that have choice sets, convert their values
572 from human-readable strings to enum values, if necessary. This
573 allows users to pass strings instead of the corresponding
574 integer values.
showard7c785282008-05-29 19:45:12 +0000575
jadmanski0afbb632008-06-06 21:10:57 +0000576 For all foreign key fields, call smart_get with the supplied
577 data. This allows the user to pass either an ID value or
578 the name of the object as a string.
showard7c785282008-05-29 19:45:12 +0000579
jadmanski0afbb632008-06-06 21:10:57 +0000580 If to_human_readable=True, perform the inverse - i.e. convert
581 numeric values to human readable values.
showard7c785282008-05-29 19:45:12 +0000582
jadmanski0afbb632008-06-06 21:10:57 +0000583 This method modifies data in-place.
584 """
585 field_dict = cls.get_field_dict()
586 for field_name in data:
showarde732ee72008-09-23 19:15:43 +0000587 if field_name not in field_dict or data[field_name] is None:
jadmanski0afbb632008-06-06 21:10:57 +0000588 continue
589 field_obj = field_dict[field_name]
590 # convert enum values
591 if field_obj.choices:
592 for choice_data in field_obj.choices:
593 # choice_data is (value, name)
594 if to_human_readable:
595 from_val, to_val = choice_data
596 else:
597 to_val, from_val = choice_data
598 if from_val == data[field_name]:
599 data[field_name] = to_val
600 break
601 # convert foreign key values
602 elif field_obj.rel:
showarda4ea5742009-02-17 20:56:23 +0000603 dest_obj = field_obj.rel.to.smart_get(data[field_name],
604 valid_only=False)
showardf8b19042009-05-12 17:22:49 +0000605 if to_human_readable:
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -0800606 # parameterized_jobs do not have a name_field
607 if (field_name != 'parameterized_job' and
608 dest_obj.name_field is not None):
showardf8b19042009-05-12 17:22:49 +0000609 data[field_name] = getattr(dest_obj,
610 dest_obj.name_field)
jadmanski0afbb632008-06-06 21:10:57 +0000611 else:
showardb0a73032009-03-27 18:35:41 +0000612 data[field_name] = dest_obj
showard7c785282008-05-29 19:45:12 +0000613
614
showard7c785282008-05-29 19:45:12 +0000615
616
Dale Curtis74a314b2011-06-23 14:55:46 -0700617 def _validate_unique(self):
jadmanski0afbb632008-06-06 21:10:57 +0000618 """\
619 Validate that unique fields are unique. Django manipulators do
620 this too, but they're a huge pain to use manually. Trust me.
621 """
622 errors = {}
623 cls = type(self)
624 field_dict = self.get_field_dict()
625 manager = cls.get_valid_manager()
626 for field_name, field_obj in field_dict.iteritems():
627 if not field_obj.unique:
628 continue
showard7c785282008-05-29 19:45:12 +0000629
jadmanski0afbb632008-06-06 21:10:57 +0000630 value = getattr(self, field_name)
showardbd18ab72009-09-18 21:20:27 +0000631 if value is None and field_obj.auto_created:
632 # don't bother checking autoincrement fields about to be
633 # generated
634 continue
635
jadmanski0afbb632008-06-06 21:10:57 +0000636 existing_objs = manager.filter(**{field_name : value})
637 num_existing = existing_objs.count()
showard7c785282008-05-29 19:45:12 +0000638
jadmanski0afbb632008-06-06 21:10:57 +0000639 if num_existing == 0:
640 continue
641 if num_existing == 1 and existing_objs[0].id == self.id:
642 continue
643 errors[field_name] = (
644 'This value must be unique (%s)' % (value))
645 return errors
showard7c785282008-05-29 19:45:12 +0000646
647
showarda5288b42009-07-28 20:06:08 +0000648 def _validate(self):
649 """
650 First coerces all fields on this instance to their proper Python types.
651 Then runs validation on every field. Returns a dictionary of
652 field_name -> error_list.
653
654 Based on validate() from django.db.models.Model in Django 0.96, which
655 was removed in Django 1.0. It should reappear in a later version. See:
656 http://code.djangoproject.com/ticket/6845
657 """
658 error_dict = {}
659 for f in self._meta.fields:
660 try:
661 python_value = f.to_python(
662 getattr(self, f.attname, f.get_default()))
663 except django.core.exceptions.ValidationError, e:
jamesren1e0a4ce2010-04-21 17:45:11 +0000664 error_dict[f.name] = str(e)
showarda5288b42009-07-28 20:06:08 +0000665 continue
666
667 if not f.blank and not python_value:
668 error_dict[f.name] = 'This field is required.'
669 continue
670
671 setattr(self, f.attname, python_value)
672
673 return error_dict
674
675
jadmanski0afbb632008-06-06 21:10:57 +0000676 def do_validate(self):
showarda5288b42009-07-28 20:06:08 +0000677 errors = self._validate()
Dale Curtis74a314b2011-06-23 14:55:46 -0700678 unique_errors = self._validate_unique()
jadmanski0afbb632008-06-06 21:10:57 +0000679 for field_name, error in unique_errors.iteritems():
680 errors.setdefault(field_name, error)
681 if errors:
682 raise ValidationError(errors)
showard7c785282008-05-29 19:45:12 +0000683
684
jadmanski0afbb632008-06-06 21:10:57 +0000685 # actually (externally) useful methods follow
showard7c785282008-05-29 19:45:12 +0000686
jadmanski0afbb632008-06-06 21:10:57 +0000687 @classmethod
688 def add_object(cls, data={}, **kwargs):
689 """\
690 Returns a new object created with the given data (a dictionary
691 mapping field names to values). Merges any extra keyword args
692 into data.
693 """
Prashanth B489b91d2014-03-15 12:17:16 -0700694 data = dict(data)
695 data.update(kwargs)
696 data = cls.prepare_data_args(data)
697 cls.convert_human_readable_values(data)
jadmanski0afbb632008-06-06 21:10:57 +0000698 data = cls.provide_default_values(data)
Prashanth B489b91d2014-03-15 12:17:16 -0700699
jadmanski0afbb632008-06-06 21:10:57 +0000700 obj = cls(**data)
701 obj.do_validate()
702 obj.save()
703 return obj
showard7c785282008-05-29 19:45:12 +0000704
705
jadmanski0afbb632008-06-06 21:10:57 +0000706 def update_object(self, data={}, **kwargs):
707 """\
708 Updates the object with the given data (a dictionary mapping
709 field names to values). Merges any extra keyword args into
710 data.
711 """
Prashanth B489b91d2014-03-15 12:17:16 -0700712 data = dict(data)
713 data.update(kwargs)
714 data = self.prepare_data_args(data)
715 self.convert_human_readable_values(data)
716
jadmanski0afbb632008-06-06 21:10:57 +0000717 for field_name, value in data.iteritems():
showardb0a73032009-03-27 18:35:41 +0000718 setattr(self, field_name, value)
jadmanski0afbb632008-06-06 21:10:57 +0000719 self.do_validate()
720 self.save()
showard7c785282008-05-29 19:45:12 +0000721
722
showard8bfb5cb2009-10-07 20:49:15 +0000723 # see query_objects()
724 _SPECIAL_FILTER_KEYS = ('query_start', 'query_limit', 'sort_by',
725 'extra_args', 'extra_where', 'no_distinct')
726
727
jadmanski0afbb632008-06-06 21:10:57 +0000728 @classmethod
showard8bfb5cb2009-10-07 20:49:15 +0000729 def _extract_special_params(cls, filter_data):
730 """
731 @returns a tuple of dicts (special_params, regular_filters), where
732 special_params contains the parameters we handle specially and
733 regular_filters is the remaining data to be handled by Django.
734 """
735 regular_filters = dict(filter_data)
736 special_params = {}
737 for key in cls._SPECIAL_FILTER_KEYS:
738 if key in regular_filters:
739 special_params[key] = regular_filters.pop(key)
740 return special_params, regular_filters
741
742
743 @classmethod
744 def apply_presentation(cls, query, filter_data):
745 """
746 Apply presentation parameters -- sorting and paging -- to the given
747 query.
748 @returns new query with presentation applied
749 """
750 special_params, _ = cls._extract_special_params(filter_data)
751 sort_by = special_params.get('sort_by', None)
752 if sort_by:
753 assert isinstance(sort_by, list) or isinstance(sort_by, tuple)
showard8b0ea222009-12-23 19:23:03 +0000754 query = query.extra(order_by=sort_by)
showard8bfb5cb2009-10-07 20:49:15 +0000755
756 query_start = special_params.get('query_start', None)
757 query_limit = special_params.get('query_limit', None)
758 if query_start is not None:
759 if query_limit is None:
760 raise ValueError('Cannot pass query_start without query_limit')
761 # query_limit is passed as a page size
showard7074b742009-10-12 20:30:04 +0000762 query_limit += query_start
763 return query[query_start:query_limit]
showard8bfb5cb2009-10-07 20:49:15 +0000764
765
766 @classmethod
767 def query_objects(cls, filter_data, valid_only=True, initial_query=None,
768 apply_presentation=True):
jadmanski0afbb632008-06-06 21:10:57 +0000769 """\
770 Returns a QuerySet object for querying the given model_class
771 with the given filter_data. Optional special arguments in
772 filter_data include:
773 -query_start: index of first return to return
774 -query_limit: maximum number of results to return
775 -sort_by: list of fields to sort on. prefixing a '-' onto a
776 field name changes the sort to descending order.
777 -extra_args: keyword args to pass to query.extra() (see Django
778 DB layer documentation)
showarda5288b42009-07-28 20:06:08 +0000779 -extra_where: extra WHERE clause to append
showard8bfb5cb2009-10-07 20:49:15 +0000780 -no_distinct: if True, a DISTINCT will not be added to the SELECT
jadmanski0afbb632008-06-06 21:10:57 +0000781 """
showard8bfb5cb2009-10-07 20:49:15 +0000782 special_params, regular_filters = cls._extract_special_params(
783 filter_data)
showard7c785282008-05-29 19:45:12 +0000784
showard7ac7b7a2008-07-21 20:24:29 +0000785 if initial_query is None:
786 if valid_only:
787 initial_query = cls.get_valid_manager()
788 else:
789 initial_query = cls.objects
showard8bfb5cb2009-10-07 20:49:15 +0000790
791 query = initial_query.filter(**regular_filters)
792
793 use_distinct = not special_params.get('no_distinct', False)
showard7ac7b7a2008-07-21 20:24:29 +0000794 if use_distinct:
795 query = query.distinct()
showard7c785282008-05-29 19:45:12 +0000796
showard8bfb5cb2009-10-07 20:49:15 +0000797 extra_args = special_params.get('extra_args', {})
798 extra_where = special_params.get('extra_where', None)
799 if extra_where:
800 # escape %'s
801 extra_where = cls.objects.escape_user_sql(extra_where)
802 extra_args.setdefault('where', []).append(extra_where)
jadmanski0afbb632008-06-06 21:10:57 +0000803 if extra_args:
804 query = query.extra(**extra_args)
showard09096d82008-07-07 23:20:49 +0000805 query = query._clone(klass=ReadonlyQuerySet)
showard7c785282008-05-29 19:45:12 +0000806
showard8bfb5cb2009-10-07 20:49:15 +0000807 if apply_presentation:
808 query = cls.apply_presentation(query, filter_data)
809
810 return query
showard7c785282008-05-29 19:45:12 +0000811
812
jadmanski0afbb632008-06-06 21:10:57 +0000813 @classmethod
showard585c2ab2008-07-23 19:29:49 +0000814 def query_count(cls, filter_data, initial_query=None):
jadmanski0afbb632008-06-06 21:10:57 +0000815 """\
816 Like query_objects, but retreive only the count of results.
817 """
818 filter_data.pop('query_start', None)
819 filter_data.pop('query_limit', None)
showard585c2ab2008-07-23 19:29:49 +0000820 query = cls.query_objects(filter_data, initial_query=initial_query)
821 return query.count()
showard7c785282008-05-29 19:45:12 +0000822
823
jadmanski0afbb632008-06-06 21:10:57 +0000824 @classmethod
825 def clean_object_dicts(cls, field_dicts):
826 """\
827 Take a list of dicts corresponding to object (as returned by
828 query.values()) and clean the data to be more suitable for
829 returning to the user.
830 """
showarde732ee72008-09-23 19:15:43 +0000831 for field_dict in field_dicts:
832 cls.clean_foreign_keys(field_dict)
showard21baa452008-10-21 00:08:39 +0000833 cls._convert_booleans(field_dict)
showarde732ee72008-09-23 19:15:43 +0000834 cls.convert_human_readable_values(field_dict,
835 to_human_readable=True)
showard7c785282008-05-29 19:45:12 +0000836
837
jadmanski0afbb632008-06-06 21:10:57 +0000838 @classmethod
showard8bfb5cb2009-10-07 20:49:15 +0000839 def list_objects(cls, filter_data, initial_query=None):
jadmanski0afbb632008-06-06 21:10:57 +0000840 """\
841 Like query_objects, but return a list of dictionaries.
842 """
showard7ac7b7a2008-07-21 20:24:29 +0000843 query = cls.query_objects(filter_data, initial_query=initial_query)
showard8bfb5cb2009-10-07 20:49:15 +0000844 extra_fields = query.query.extra_select.keys()
845 field_dicts = [model_object.get_object_dict(extra_fields=extra_fields)
showarde732ee72008-09-23 19:15:43 +0000846 for model_object in query]
jadmanski0afbb632008-06-06 21:10:57 +0000847 return field_dicts
showard7c785282008-05-29 19:45:12 +0000848
849
jadmanski0afbb632008-06-06 21:10:57 +0000850 @classmethod
showarda4ea5742009-02-17 20:56:23 +0000851 def smart_get(cls, id_or_name, valid_only=True):
jadmanski0afbb632008-06-06 21:10:57 +0000852 """\
853 smart_get(integer) -> get object by ID
854 smart_get(string) -> get object by name_field
jadmanski0afbb632008-06-06 21:10:57 +0000855 """
showarda4ea5742009-02-17 20:56:23 +0000856 if valid_only:
857 manager = cls.get_valid_manager()
858 else:
859 manager = cls.objects
860
861 if isinstance(id_or_name, (int, long)):
862 return manager.get(pk=id_or_name)
jamesren3e9f6092010-03-11 21:32:10 +0000863 if isinstance(id_or_name, basestring) and hasattr(cls, 'name_field'):
showarda4ea5742009-02-17 20:56:23 +0000864 return manager.get(**{cls.name_field : id_or_name})
865 raise ValueError(
866 'Invalid positional argument: %s (%s)' % (id_or_name,
867 type(id_or_name)))
showard7c785282008-05-29 19:45:12 +0000868
869
showardbe3ec042008-11-12 18:16:07 +0000870 @classmethod
871 def smart_get_bulk(cls, id_or_name_list):
872 invalid_inputs = []
873 result_objects = []
874 for id_or_name in id_or_name_list:
875 try:
876 result_objects.append(cls.smart_get(id_or_name))
877 except cls.DoesNotExist:
878 invalid_inputs.append(id_or_name)
879 if invalid_inputs:
mbligh7a3ebe32008-12-01 17:10:33 +0000880 raise cls.DoesNotExist('The following %ss do not exist: %s'
881 % (cls.__name__.lower(),
882 ', '.join(invalid_inputs)))
showardbe3ec042008-11-12 18:16:07 +0000883 return result_objects
884
885
showard8bfb5cb2009-10-07 20:49:15 +0000886 def get_object_dict(self, extra_fields=None):
jadmanski0afbb632008-06-06 21:10:57 +0000887 """\
showard8bfb5cb2009-10-07 20:49:15 +0000888 Return a dictionary mapping fields to this object's values. @param
889 extra_fields: list of extra attribute names to include, in addition to
890 the fields defined on this object.
jadmanski0afbb632008-06-06 21:10:57 +0000891 """
showard8bfb5cb2009-10-07 20:49:15 +0000892 fields = self.get_field_dict().keys()
893 if extra_fields:
894 fields += extra_fields
jadmanski0afbb632008-06-06 21:10:57 +0000895 object_dict = dict((field_name, getattr(self, field_name))
showarde732ee72008-09-23 19:15:43 +0000896 for field_name in fields)
jadmanski0afbb632008-06-06 21:10:57 +0000897 self.clean_object_dicts([object_dict])
showardd3dc1992009-04-22 21:01:40 +0000898 self._postprocess_object_dict(object_dict)
jadmanski0afbb632008-06-06 21:10:57 +0000899 return object_dict
showard7c785282008-05-29 19:45:12 +0000900
901
showardd3dc1992009-04-22 21:01:40 +0000902 def _postprocess_object_dict(self, object_dict):
903 """For subclasses to override."""
904 pass
905
906
jadmanski0afbb632008-06-06 21:10:57 +0000907 @classmethod
908 def get_valid_manager(cls):
909 return cls.objects
showard7c785282008-05-29 19:45:12 +0000910
911
showard2bab8f42008-11-12 18:15:22 +0000912 def _record_attributes(self, attributes):
913 """
914 See on_attribute_changed.
915 """
916 assert not isinstance(attributes, basestring)
917 self._recorded_attributes = dict((attribute, getattr(self, attribute))
918 for attribute in attributes)
919
920
921 def _check_for_updated_attributes(self):
922 """
923 See on_attribute_changed.
924 """
925 for attribute, original_value in self._recorded_attributes.iteritems():
926 new_value = getattr(self, attribute)
927 if original_value != new_value:
928 self.on_attribute_changed(attribute, original_value)
929 self._record_attributes(self._recorded_attributes.keys())
930
931
932 def on_attribute_changed(self, attribute, old_value):
933 """
934 Called whenever an attribute is updated. To be overridden.
935
936 To use this method, you must:
937 * call _record_attributes() from __init__() (after making the super
938 call) with a list of attributes for which you want to be notified upon
939 change.
940 * call _check_for_updated_attributes() from save().
941 """
942 pass
943
944
showard7c785282008-05-29 19:45:12 +0000945class ModelWithInvalid(ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000946 """
947 Overrides model methods save() and delete() to support invalidation in
948 place of actual deletion. Subclasses must have a boolean "invalid"
949 field.
950 """
showard7c785282008-05-29 19:45:12 +0000951
showarda5288b42009-07-28 20:06:08 +0000952 def save(self, *args, **kwargs):
showardddb90992009-02-11 23:39:32 +0000953 first_time = (self.id is None)
954 if first_time:
955 # see if this object was previously added and invalidated
956 my_name = getattr(self, self.name_field)
957 filters = {self.name_field : my_name, 'invalid' : True}
958 try:
959 old_object = self.__class__.objects.get(**filters)
showardafd97de2009-10-01 18:45:09 +0000960 self.resurrect_object(old_object)
showardddb90992009-02-11 23:39:32 +0000961 except self.DoesNotExist:
962 # no existing object
963 pass
showard7c785282008-05-29 19:45:12 +0000964
showarda5288b42009-07-28 20:06:08 +0000965 super(ModelWithInvalid, self).save(*args, **kwargs)
showard7c785282008-05-29 19:45:12 +0000966
967
showardafd97de2009-10-01 18:45:09 +0000968 def resurrect_object(self, old_object):
969 """
970 Called when self is about to be saved for the first time and is actually
971 "undeleting" a previously deleted object. Can be overridden by
972 subclasses to copy data as desired from the deleted entry (but this
973 superclass implementation must normally be called).
974 """
975 self.id = old_object.id
976
977
jadmanski0afbb632008-06-06 21:10:57 +0000978 def clean_object(self):
979 """
980 This method is called when an object is marked invalid.
981 Subclasses should override this to clean up relationships that
showardafd97de2009-10-01 18:45:09 +0000982 should no longer exist if the object were deleted.
983 """
jadmanski0afbb632008-06-06 21:10:57 +0000984 pass
showard7c785282008-05-29 19:45:12 +0000985
986
jadmanski0afbb632008-06-06 21:10:57 +0000987 def delete(self):
Dale Curtis74a314b2011-06-23 14:55:46 -0700988 self.invalid = self.invalid
jadmanski0afbb632008-06-06 21:10:57 +0000989 assert not self.invalid
990 self.invalid = True
991 self.save()
992 self.clean_object()
showard7c785282008-05-29 19:45:12 +0000993
994
jadmanski0afbb632008-06-06 21:10:57 +0000995 @classmethod
996 def get_valid_manager(cls):
997 return cls.valid_objects
showard7c785282008-05-29 19:45:12 +0000998
999
jadmanski0afbb632008-06-06 21:10:57 +00001000 class Manipulator(object):
1001 """
1002 Force default manipulators to look only at valid objects -
1003 otherwise they will match against invalid objects when checking
1004 uniqueness.
1005 """
1006 @classmethod
1007 def _prepare(cls, model):
1008 super(ModelWithInvalid.Manipulator, cls)._prepare(model)
1009 cls.manager = model.valid_objects
showardf8b19042009-05-12 17:22:49 +00001010
1011
1012class ModelWithAttributes(object):
1013 """
1014 Mixin class for models that have an attribute model associated with them.
1015 The attribute model is assumed to have its value field named "value".
1016 """
1017
1018 def _get_attribute_model_and_args(self, attribute):
1019 """
1020 Subclasses should override this to return a tuple (attribute_model,
1021 keyword_args), where attribute_model is a model class and keyword_args
1022 is a dict of args to pass to attribute_model.objects.get() to get an
1023 instance of the given attribute on this object.
1024 """
Dale Curtis74a314b2011-06-23 14:55:46 -07001025 raise NotImplementedError
showardf8b19042009-05-12 17:22:49 +00001026
1027
1028 def set_attribute(self, attribute, value):
1029 attribute_model, get_args = self._get_attribute_model_and_args(
1030 attribute)
1031 attribute_object, _ = attribute_model.objects.get_or_create(**get_args)
1032 attribute_object.value = value
1033 attribute_object.save()
1034
1035
1036 def delete_attribute(self, attribute):
1037 attribute_model, get_args = self._get_attribute_model_and_args(
1038 attribute)
1039 try:
1040 attribute_model.objects.get(**get_args).delete()
showard16245422009-09-08 16:28:15 +00001041 except attribute_model.DoesNotExist:
showardf8b19042009-05-12 17:22:49 +00001042 pass
1043
1044
1045 def set_or_delete_attribute(self, attribute, value):
1046 if value is None:
1047 self.delete_attribute(attribute)
1048 else:
1049 self.set_attribute(attribute, value)
showard26b7ec72009-12-21 22:43:57 +00001050
1051
1052class ModelWithHashManager(dbmodels.Manager):
1053 """Manager for use with the ModelWithHash abstract model class"""
1054
1055 def create(self, **kwargs):
1056 raise Exception('ModelWithHash manager should use get_or_create() '
1057 'instead of create()')
1058
1059
1060 def get_or_create(self, **kwargs):
1061 kwargs['the_hash'] = self.model._compute_hash(**kwargs)
1062 return super(ModelWithHashManager, self).get_or_create(**kwargs)
1063
1064
1065class ModelWithHash(dbmodels.Model):
1066 """Superclass with methods for dealing with a hash column"""
1067
1068 the_hash = dbmodels.CharField(max_length=40, unique=True)
1069
1070 objects = ModelWithHashManager()
1071
1072 class Meta:
1073 abstract = True
1074
1075
1076 @classmethod
1077 def _compute_hash(cls, **kwargs):
1078 raise NotImplementedError('Subclasses must override _compute_hash()')
1079
1080
1081 def save(self, force_insert=False, **kwargs):
1082 """Prevents saving the model in most cases
1083
1084 We want these models to be immutable, so the generic save() operation
1085 will not work. These models should be instantiated through their the
1086 model.objects.get_or_create() method instead.
1087
1088 The exception is that save(force_insert=True) will be allowed, since
1089 that creates a new row. However, the preferred way to make instances of
1090 these models is through the get_or_create() method.
1091 """
1092 if not force_insert:
1093 # Allow a forced insert to happen; if it's a duplicate, the unique
1094 # constraint will catch it later anyways
1095 raise Exception('ModelWithHash is immutable')
1096 super(ModelWithHash, self).save(force_insert=force_insert, **kwargs)