blob: 5fe544b65af27fa4b1246a78af3545ac7c154d12 [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 django.core.exceptions
Prashanth Balasubramanian75be1d32014-11-25 18:03:09 -08006from django.db import connection
7from django.db import connections
8from django.db import models as dbmodels
9from django.db import transaction
showarda5288b42009-07-28 20:06:08 +000010from django.db.models.sql import query
showard7e67b432010-01-20 01:13:04 +000011import django.db.models.sql.where
Aviv Keshet14cac442016-11-20 21:44:11 -080012# TODO(akeshet): Replace with monarch stats once we know how to instrument rpc
13# handling with ts_mon.
MK Ryu5cfd96a2015-01-30 15:31:23 -080014from autotest_lib.client.common_lib.cros.graphite import autotest_stats
Prashanth B489b91d2014-03-15 12:17:16 -070015from autotest_lib.frontend.afe import rdb_model_extensions
showard7c785282008-05-29 19:45:12 +000016
Prashanth B489b91d2014-03-15 12:17:16 -070017
18class ValidationError(django.core.exceptions.ValidationError):
jadmanski0afbb632008-06-06 21:10:57 +000019 """\
showarda5288b42009-07-28 20:06:08 +000020 Data validation error in adding or updating an object. The associated
jadmanski0afbb632008-06-06 21:10:57 +000021 value is a dictionary mapping field names to error strings.
22 """
showard7c785282008-05-29 19:45:12 +000023
showarda5288b42009-07-28 20:06:08 +000024def _quote_name(name):
25 """Shorthand for connection.ops.quote_name()."""
26 return connection.ops.quote_name(name)
27
28
beepscc9fc702013-12-02 12:45:38 -080029class LeasedHostManager(dbmodels.Manager):
30 """Query manager for unleased, unlocked hosts.
31 """
32 def get_query_set(self):
33 return (super(LeasedHostManager, self).get_query_set().filter(
34 leased=0, locked=0))
35
36
showard7c785282008-05-29 19:45:12 +000037class ExtendedManager(dbmodels.Manager):
jadmanski0afbb632008-06-06 21:10:57 +000038 """\
39 Extended manager supporting subquery filtering.
40 """
showard7c785282008-05-29 19:45:12 +000041
showardf828c772010-01-25 21:49:42 +000042 class CustomQuery(query.Query):
showard7e67b432010-01-20 01:13:04 +000043 def __init__(self, *args, **kwargs):
showardf828c772010-01-25 21:49:42 +000044 super(ExtendedManager.CustomQuery, self).__init__(*args, **kwargs)
showard7e67b432010-01-20 01:13:04 +000045 self._custom_joins = []
46
47
showarda5288b42009-07-28 20:06:08 +000048 def clone(self, klass=None, **kwargs):
showardf828c772010-01-25 21:49:42 +000049 obj = super(ExtendedManager.CustomQuery, self).clone(klass)
showard7e67b432010-01-20 01:13:04 +000050 obj._custom_joins = list(self._custom_joins)
showarda5288b42009-07-28 20:06:08 +000051 return obj
showard08f981b2008-06-24 21:59:03 +000052
showard7e67b432010-01-20 01:13:04 +000053
54 def combine(self, rhs, connector):
showardf828c772010-01-25 21:49:42 +000055 super(ExtendedManager.CustomQuery, self).combine(rhs, connector)
showard7e67b432010-01-20 01:13:04 +000056 if hasattr(rhs, '_custom_joins'):
57 self._custom_joins.extend(rhs._custom_joins)
58
59
60 def add_custom_join(self, table, condition, join_type,
61 condition_values=(), alias=None):
62 if alias is None:
63 alias = table
64 join_dict = dict(table=table,
65 condition=condition,
66 condition_values=condition_values,
67 join_type=join_type,
68 alias=alias)
69 self._custom_joins.append(join_dict)
70
71
showard7e67b432010-01-20 01:13:04 +000072 @classmethod
73 def convert_query(self, query_set):
74 """
showardf828c772010-01-25 21:49:42 +000075 Convert the query set's "query" attribute to a CustomQuery.
showard7e67b432010-01-20 01:13:04 +000076 """
77 # Make a copy of the query set
78 query_set = query_set.all()
79 query_set.query = query_set.query.clone(
showardf828c772010-01-25 21:49:42 +000080 klass=ExtendedManager.CustomQuery,
showard7e67b432010-01-20 01:13:04 +000081 _custom_joins=[])
82 return query_set
showard43a3d262008-11-12 18:17:05 +000083
84
showard7e67b432010-01-20 01:13:04 +000085 class _WhereClause(object):
86 """Object allowing us to inject arbitrary SQL into Django queries.
showard43a3d262008-11-12 18:17:05 +000087
showard7e67b432010-01-20 01:13:04 +000088 By using this instead of extra(where=...), we can still freely combine
89 queries with & and |.
showarda5288b42009-07-28 20:06:08 +000090 """
showard7e67b432010-01-20 01:13:04 +000091 def __init__(self, clause, values=()):
92 self._clause = clause
93 self._values = values
showarda5288b42009-07-28 20:06:08 +000094
showard7e67b432010-01-20 01:13:04 +000095
Dale Curtis74a314b2011-06-23 14:55:46 -070096 def as_sql(self, qn=None, connection=None):
showard7e67b432010-01-20 01:13:04 +000097 return self._clause, self._values
98
99
100 def relabel_aliases(self, change_map):
101 return
showard43a3d262008-11-12 18:17:05 +0000102
103
showard8b0ea222009-12-23 19:23:03 +0000104 def add_join(self, query_set, join_table, join_key, join_condition='',
showard7e67b432010-01-20 01:13:04 +0000105 join_condition_values=(), join_from_key=None, alias=None,
106 suffix='', exclude=False, force_left_join=False):
107 """Add a join to query_set.
108
109 Join looks like this:
110 (INNER|LEFT) JOIN <join_table> AS <alias>
111 ON (<this table>.<join_from_key> = <join_table>.<join_key>
112 and <join_condition>)
113
showard0957a842009-05-11 19:25:08 +0000114 @param join_table table to join to
115 @param join_key field referencing back to this model to use for the join
116 @param join_condition extra condition for the ON clause of the join
showard7e67b432010-01-20 01:13:04 +0000117 @param join_condition_values values to substitute into join_condition
118 @param join_from_key column on this model to join from.
showard8b0ea222009-12-23 19:23:03 +0000119 @param alias alias to use for for join
120 @param suffix suffix to add to join_table for the join alias, if no
121 alias is provided
showard0957a842009-05-11 19:25:08 +0000122 @param exclude if true, exclude rows that match this join (will use a
showarda5288b42009-07-28 20:06:08 +0000123 LEFT OUTER JOIN and an appropriate WHERE condition)
showardc4780402009-08-31 18:31:34 +0000124 @param force_left_join - if true, a LEFT OUTER JOIN will be used
125 instead of an INNER JOIN regardless of other options
showard0957a842009-05-11 19:25:08 +0000126 """
showard7e67b432010-01-20 01:13:04 +0000127 join_from_table = query_set.model._meta.db_table
128 if join_from_key is None:
129 join_from_key = self.model._meta.pk.name
130 if alias is None:
131 alias = join_table + suffix
132 full_join_key = _quote_name(alias) + '.' + _quote_name(join_key)
133 full_join_condition = '%s = %s.%s' % (full_join_key,
134 _quote_name(join_from_table),
135 _quote_name(join_from_key))
showard43a3d262008-11-12 18:17:05 +0000136 if join_condition:
137 full_join_condition += ' AND (' + join_condition + ')'
138 if exclude or force_left_join:
showarda5288b42009-07-28 20:06:08 +0000139 join_type = query_set.query.LOUTER
showard43a3d262008-11-12 18:17:05 +0000140 else:
showarda5288b42009-07-28 20:06:08 +0000141 join_type = query_set.query.INNER
showard43a3d262008-11-12 18:17:05 +0000142
showardf828c772010-01-25 21:49:42 +0000143 query_set = self.CustomQuery.convert_query(query_set)
showard7e67b432010-01-20 01:13:04 +0000144 query_set.query.add_custom_join(join_table,
145 full_join_condition,
146 join_type,
147 condition_values=join_condition_values,
148 alias=alias)
showard43a3d262008-11-12 18:17:05 +0000149
showard7e67b432010-01-20 01:13:04 +0000150 if exclude:
151 query_set = query_set.extra(where=[full_join_key + ' IS NULL'])
152
153 return query_set
154
155
156 def _info_for_many_to_one_join(self, field, join_to_query, alias):
157 """
158 @param field: the ForeignKey field on the related model
159 @param join_to_query: the query over the related model that we're
160 joining to
161 @param alias: alias of joined table
162 """
163 info = {}
164 rhs_table = join_to_query.model._meta.db_table
165 info['rhs_table'] = rhs_table
166 info['rhs_column'] = field.column
167 info['lhs_column'] = field.rel.get_related_field().column
168 rhs_where = join_to_query.query.where
169 rhs_where.relabel_aliases({rhs_table: alias})
Dale Curtis74a314b2011-06-23 14:55:46 -0700170 compiler = join_to_query.query.get_compiler(using=join_to_query.db)
171 initial_clause, values = compiler.as_sql()
Dan Shi58321512017-02-03 11:09:07 -0800172 # initial_clause is compiled from `join_to_query`, which is a SELECT
173 # query returns at most one record. For it to be used in WHERE clause,
174 # it must be converted to a boolean value using EXISTS.
175 all_clauses = ('EXISTS (%s)' % initial_clause,)
Dale Curtis74a314b2011-06-23 14:55:46 -0700176 if hasattr(join_to_query.query, 'extra_where'):
177 all_clauses += join_to_query.query.extra_where
178 info['where_clause'] = (
179 ' AND '.join('(%s)' % clause for clause in all_clauses))
showard7e67b432010-01-20 01:13:04 +0000180 info['values'] = values
181 return info
182
183
184 def _info_for_many_to_many_join(self, m2m_field, join_to_query, alias,
185 m2m_is_on_this_model):
186 """
187 @param m2m_field: a Django field representing the M2M relationship.
188 It uses a pivot table with the following structure:
189 this model table <---> M2M pivot table <---> joined model table
190 @param join_to_query: the query over the related model that we're
191 joining to.
192 @param alias: alias of joined table
193 """
194 if m2m_is_on_this_model:
195 # referenced field on this model
196 lhs_id_field = self.model._meta.pk
197 # foreign key on the pivot table referencing lhs_id_field
198 m2m_lhs_column = m2m_field.m2m_column_name()
199 # foreign key on the pivot table referencing rhd_id_field
200 m2m_rhs_column = m2m_field.m2m_reverse_name()
201 # referenced field on related model
202 rhs_id_field = m2m_field.rel.get_related_field()
203 else:
204 lhs_id_field = m2m_field.rel.get_related_field()
205 m2m_lhs_column = m2m_field.m2m_reverse_name()
206 m2m_rhs_column = m2m_field.m2m_column_name()
207 rhs_id_field = join_to_query.model._meta.pk
208
209 info = {}
210 info['rhs_table'] = m2m_field.m2m_db_table()
211 info['rhs_column'] = m2m_lhs_column
212 info['lhs_column'] = lhs_id_field.column
213
214 # select the ID of related models relevant to this join. we can only do
215 # a single join, so we need to gather this information up front and
216 # include it in the join condition.
217 rhs_ids = join_to_query.values_list(rhs_id_field.attname, flat=True)
218 assert len(rhs_ids) == 1, ('Many-to-many custom field joins can only '
219 'match a single related object.')
220 rhs_id = rhs_ids[0]
221
222 info['where_clause'] = '%s.%s = %s' % (_quote_name(alias),
223 _quote_name(m2m_rhs_column),
224 rhs_id)
225 info['values'] = ()
226 return info
227
228
229 def join_custom_field(self, query_set, join_to_query, alias,
230 left_join=True):
231 """Join to a related model to create a custom field in the given query.
232
233 This method is used to construct a custom field on the given query based
234 on a many-valued relationsip. join_to_query should be a simple query
235 (no joins) on the related model which returns at most one related row
236 per instance of this model.
237
238 For many-to-one relationships, the joined table contains the matching
239 row from the related model it one is related, NULL otherwise.
240
241 For many-to-many relationships, the joined table contains the matching
242 row if it's related, NULL otherwise.
243 """
244 relationship_type, field = self.determine_relationship(
245 join_to_query.model)
246
247 if relationship_type == self.MANY_TO_ONE:
248 info = self._info_for_many_to_one_join(field, join_to_query, alias)
249 elif relationship_type == self.M2M_ON_RELATED_MODEL:
250 info = self._info_for_many_to_many_join(
251 m2m_field=field, join_to_query=join_to_query, alias=alias,
252 m2m_is_on_this_model=False)
253 elif relationship_type ==self.M2M_ON_THIS_MODEL:
254 info = self._info_for_many_to_many_join(
255 m2m_field=field, join_to_query=join_to_query, alias=alias,
256 m2m_is_on_this_model=True)
257
258 return self.add_join(query_set, info['rhs_table'], info['rhs_column'],
259 join_from_key=info['lhs_column'],
260 join_condition=info['where_clause'],
261 join_condition_values=info['values'],
262 alias=alias,
263 force_left_join=left_join)
264
265
266 def add_where(self, query_set, where, values=()):
267 query_set = query_set.all()
268 query_set.query.where.add(self._WhereClause(where, values),
269 django.db.models.sql.where.AND)
showardc4780402009-08-31 18:31:34 +0000270 return query_set
showard7c785282008-05-29 19:45:12 +0000271
272
showardeaccf8f2009-04-16 03:11:33 +0000273 def _get_quoted_field(self, table, field):
showarda5288b42009-07-28 20:06:08 +0000274 return _quote_name(table) + '.' + _quote_name(field)
showard5ef36e92008-07-02 16:37:09 +0000275
276
showard7c199df2008-10-03 10:17:15 +0000277 def get_key_on_this_table(self, key_field=None):
showard5ef36e92008-07-02 16:37:09 +0000278 if key_field is None:
279 # default to primary key
280 key_field = self.model._meta.pk.column
281 return self._get_quoted_field(self.model._meta.db_table, key_field)
282
283
showardeaccf8f2009-04-16 03:11:33 +0000284 def escape_user_sql(self, sql):
285 return sql.replace('%', '%%')
286
showard5ef36e92008-07-02 16:37:09 +0000287
showard0957a842009-05-11 19:25:08 +0000288 def _custom_select_query(self, query_set, selects):
Jakob Juelich7bef8412014-10-14 19:11:54 -0700289 """Execute a custom select query.
290
291 @param query_set: query set as returned by query_objects.
292 @param selects: Tables/Columns to select, e.g. tko_test_labels_list.id.
293
294 @returns: Result of the query as returned by cursor.fetchall().
295 """
Dale Curtis74a314b2011-06-23 14:55:46 -0700296 compiler = query_set.query.get_compiler(using=query_set.db)
297 sql, params = compiler.as_sql()
showarda5288b42009-07-28 20:06:08 +0000298 from_ = sql[sql.find(' FROM'):]
299
300 if query_set.query.distinct:
showard0957a842009-05-11 19:25:08 +0000301 distinct = 'DISTINCT '
302 else:
303 distinct = ''
showarda5288b42009-07-28 20:06:08 +0000304
305 sql_query = ('SELECT ' + distinct + ','.join(selects) + from_)
Jakob Juelich7bef8412014-10-14 19:11:54 -0700306 # Chose the connection that's responsible for this type of object
307 cursor = connections[query_set.db].cursor()
showard0957a842009-05-11 19:25:08 +0000308 cursor.execute(sql_query, params)
309 return cursor.fetchall()
310
311
showard68693f72009-05-20 00:31:53 +0000312 def _is_relation_to(self, field, model_class):
313 return field.rel and field.rel.to is model_class
showard0957a842009-05-11 19:25:08 +0000314
315
showard7e67b432010-01-20 01:13:04 +0000316 MANY_TO_ONE = object()
317 M2M_ON_RELATED_MODEL = object()
318 M2M_ON_THIS_MODEL = object()
319
320 def determine_relationship(self, related_model):
321 """
322 Determine the relationship between this model and related_model.
323
324 related_model must have some sort of many-valued relationship to this
325 manager's model.
326 @returns (relationship_type, field), where relationship_type is one of
327 MANY_TO_ONE, M2M_ON_RELATED_MODEL, M2M_ON_THIS_MODEL, and field
328 is the Django field object for the relationship.
329 """
330 # look for a foreign key field on related_model relating to this model
331 for field in related_model._meta.fields:
332 if self._is_relation_to(field, self.model):
333 return self.MANY_TO_ONE, field
334
335 # look for an M2M field on related_model relating to this model
336 for field in related_model._meta.many_to_many:
337 if self._is_relation_to(field, self.model):
338 return self.M2M_ON_RELATED_MODEL, field
339
340 # maybe this model has the many-to-many field
341 for field in self.model._meta.many_to_many:
342 if self._is_relation_to(field, related_model):
343 return self.M2M_ON_THIS_MODEL, field
344
345 raise ValueError('%s has no relation to %s' %
346 (related_model, self.model))
347
348
showard68693f72009-05-20 00:31:53 +0000349 def _get_pivot_iterator(self, base_objects_by_id, related_model):
showard0957a842009-05-11 19:25:08 +0000350 """
showard68693f72009-05-20 00:31:53 +0000351 Determine the relationship between this model and related_model, and
352 return a pivot iterator.
353 @param base_objects_by_id: dict of instances of this model indexed by
354 their IDs
355 @returns a pivot iterator, which yields a tuple (base_object,
356 related_object) for each relationship between a base object and a
357 related object. all base_object instances come from base_objects_by_id.
showard7e67b432010-01-20 01:13:04 +0000358 Note -- this depends on Django model internals.
showard0957a842009-05-11 19:25:08 +0000359 """
showard7e67b432010-01-20 01:13:04 +0000360 relationship_type, field = self.determine_relationship(related_model)
361 if relationship_type == self.MANY_TO_ONE:
362 return self._many_to_one_pivot(base_objects_by_id,
363 related_model, field)
364 elif relationship_type == self.M2M_ON_RELATED_MODEL:
365 return self._many_to_many_pivot(
showard68693f72009-05-20 00:31:53 +0000366 base_objects_by_id, related_model, field.m2m_db_table(),
367 field.m2m_reverse_name(), field.m2m_column_name())
showard7e67b432010-01-20 01:13:04 +0000368 else:
369 assert relationship_type == self.M2M_ON_THIS_MODEL
370 return self._many_to_many_pivot(
showard68693f72009-05-20 00:31:53 +0000371 base_objects_by_id, related_model, field.m2m_db_table(),
372 field.m2m_column_name(), field.m2m_reverse_name())
showard0957a842009-05-11 19:25:08 +0000373
showard0957a842009-05-11 19:25:08 +0000374
showard68693f72009-05-20 00:31:53 +0000375 def _many_to_one_pivot(self, base_objects_by_id, related_model,
376 foreign_key_field):
377 """
378 @returns a pivot iterator - see _get_pivot_iterator()
379 """
380 filter_data = {foreign_key_field.name + '__pk__in':
381 base_objects_by_id.keys()}
382 for related_object in related_model.objects.filter(**filter_data):
showarda5a72c92009-08-20 23:35:21 +0000383 # lookup base object in the dict, rather than grabbing it from the
384 # related object. we need to return instances from the dict, not
385 # fresh instances of the same models (and grabbing model instances
386 # from the related models incurs a DB query each time).
387 base_object_id = getattr(related_object, foreign_key_field.attname)
388 base_object = base_objects_by_id[base_object_id]
showard68693f72009-05-20 00:31:53 +0000389 yield base_object, related_object
390
391
392 def _query_pivot_table(self, base_objects_by_id, pivot_table,
Jakob Juelich7bef8412014-10-14 19:11:54 -0700393 pivot_from_field, pivot_to_field, related_model):
showard0957a842009-05-11 19:25:08 +0000394 """
395 @param id_list list of IDs of self.model objects to include
396 @param pivot_table the name of the pivot table
397 @param pivot_from_field a field name on pivot_table referencing
398 self.model
399 @param pivot_to_field a field name on pivot_table referencing the
400 related model.
Jakob Juelich7bef8412014-10-14 19:11:54 -0700401 @param related_model the related model
402
showard68693f72009-05-20 00:31:53 +0000403 @returns pivot list of IDs (base_id, related_id)
showard0957a842009-05-11 19:25:08 +0000404 """
405 query = """
406 SELECT %(from_field)s, %(to_field)s
407 FROM %(table)s
408 WHERE %(from_field)s IN (%(id_list)s)
409 """ % dict(from_field=pivot_from_field,
410 to_field=pivot_to_field,
411 table=pivot_table,
showard68693f72009-05-20 00:31:53 +0000412 id_list=','.join(str(id_) for id_
413 in base_objects_by_id.iterkeys()))
Jakob Juelich7bef8412014-10-14 19:11:54 -0700414
415 # Chose the connection that's responsible for this type of object
416 # The databases for related_model and the current model will always
417 # be the same, related_model is just easier to obtain here because
418 # self is only a ExtendedManager, not the object.
419 cursor = connections[related_model.objects.db].cursor()
showard0957a842009-05-11 19:25:08 +0000420 cursor.execute(query)
showard68693f72009-05-20 00:31:53 +0000421 return cursor.fetchall()
showard0957a842009-05-11 19:25:08 +0000422
423
showard68693f72009-05-20 00:31:53 +0000424 def _many_to_many_pivot(self, base_objects_by_id, related_model,
425 pivot_table, pivot_from_field, pivot_to_field):
426 """
427 @param pivot_table: see _query_pivot_table
428 @param pivot_from_field: see _query_pivot_table
429 @param pivot_to_field: see _query_pivot_table
430 @returns a pivot iterator - see _get_pivot_iterator()
431 """
432 id_pivot = self._query_pivot_table(base_objects_by_id, pivot_table,
Jakob Juelich7bef8412014-10-14 19:11:54 -0700433 pivot_from_field, pivot_to_field,
434 related_model)
showard68693f72009-05-20 00:31:53 +0000435
436 all_related_ids = list(set(related_id for base_id, related_id
437 in id_pivot))
438 related_objects_by_id = related_model.objects.in_bulk(all_related_ids)
439
440 for base_id, related_id in id_pivot:
441 yield base_objects_by_id[base_id], related_objects_by_id[related_id]
442
443
444 def populate_relationships(self, base_objects, related_model,
showard0957a842009-05-11 19:25:08 +0000445 related_list_name):
446 """
showard68693f72009-05-20 00:31:53 +0000447 For each instance of this model in base_objects, add a field named
448 related_list_name listing all the related objects of type related_model.
449 related_model must be in a many-to-one or many-to-many relationship with
450 this model.
451 @param base_objects - list of instances of this model
452 @param related_model - model class related to this model
453 @param related_list_name - attribute name in which to store the related
454 object list.
showard0957a842009-05-11 19:25:08 +0000455 """
showard68693f72009-05-20 00:31:53 +0000456 if not base_objects:
showard0957a842009-05-11 19:25:08 +0000457 # if we don't bail early, we'll get a SQL error later
458 return
showard0957a842009-05-11 19:25:08 +0000459
showard68693f72009-05-20 00:31:53 +0000460 base_objects_by_id = dict((base_object._get_pk_val(), base_object)
461 for base_object in base_objects)
462 pivot_iterator = self._get_pivot_iterator(base_objects_by_id,
463 related_model)
showard0957a842009-05-11 19:25:08 +0000464
showard68693f72009-05-20 00:31:53 +0000465 for base_object in base_objects:
466 setattr(base_object, related_list_name, [])
467
468 for base_object, related_object in pivot_iterator:
469 getattr(base_object, related_list_name).append(related_object)
showard0957a842009-05-11 19:25:08 +0000470
471
jamesrene3656232010-03-02 00:00:30 +0000472class ModelWithInvalidQuerySet(dbmodels.query.QuerySet):
473 """
474 QuerySet that handles delete() properly for models with an "invalid" bit
475 """
476 def delete(self):
477 for model in self:
478 model.delete()
479
480
481class ModelWithInvalidManager(ExtendedManager):
482 """
483 Manager for objects with an "invalid" bit
484 """
485 def get_query_set(self):
486 return ModelWithInvalidQuerySet(self.model)
487
488
489class ValidObjectsManager(ModelWithInvalidManager):
jadmanski0afbb632008-06-06 21:10:57 +0000490 """
491 Manager returning only objects with invalid=False.
492 """
493 def get_query_set(self):
494 queryset = super(ValidObjectsManager, self).get_query_set()
495 return queryset.filter(invalid=False)
showard7c785282008-05-29 19:45:12 +0000496
497
Prashanth B489b91d2014-03-15 12:17:16 -0700498class ModelExtensions(rdb_model_extensions.ModelValidators):
jadmanski0afbb632008-06-06 21:10:57 +0000499 """\
Prashanth B489b91d2014-03-15 12:17:16 -0700500 Mixin with convenience functions for models, built on top of
501 the model validators in rdb_model_extensions.
jadmanski0afbb632008-06-06 21:10:57 +0000502 """
503 # TODO: at least some of these functions really belong in a custom
504 # Manager class
showard7c785282008-05-29 19:45:12 +0000505
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700506
507 SERIALIZATION_LINKS_TO_FOLLOW = set()
508 """
509 To be able to send jobs and hosts to shards, it's necessary to find their
510 dependencies.
511 The most generic approach for this would be to traverse all relationships
512 to other objects recursively. This would list all objects that are related
513 in any way.
514 But this approach finds too many objects: If a host should be transferred,
515 all it's relationships would be traversed. This would find an acl group.
516 If then the acl group's relationships are traversed, the relationship
517 would be followed backwards and many other hosts would be found.
518
519 This mapping tells that algorithm which relations to follow explicitly.
520 """
521
Jakob Juelichf865d332014-09-29 10:47:49 -0700522
Fang Deng86248502014-12-18 16:38:00 -0800523 SERIALIZATION_LINKS_TO_KEEP = set()
524 """This set stores foreign keys which we don't want to follow, but
525 still want to include in the serialized dictionary. For
526 example, we follow the relationship `Host.hostattribute_set`,
527 but we do not want to follow `HostAttributes.host_id` back to
528 to Host, which would otherwise lead to a circle. However, we still
529 like to serialize HostAttribute.`host_id`."""
530
Jakob Juelichf865d332014-09-29 10:47:49 -0700531 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set()
532 """
533 On deserializion, if the object to persist already exists, local fields
534 will only be updated, if their name is in this set.
535 """
536
537
jadmanski0afbb632008-06-06 21:10:57 +0000538 @classmethod
539 def convert_human_readable_values(cls, data, to_human_readable=False):
540 """\
541 Performs conversions on user-supplied field data, to make it
542 easier for users to pass human-readable data.
showard7c785282008-05-29 19:45:12 +0000543
jadmanski0afbb632008-06-06 21:10:57 +0000544 For all fields that have choice sets, convert their values
545 from human-readable strings to enum values, if necessary. This
546 allows users to pass strings instead of the corresponding
547 integer values.
showard7c785282008-05-29 19:45:12 +0000548
jadmanski0afbb632008-06-06 21:10:57 +0000549 For all foreign key fields, call smart_get with the supplied
550 data. This allows the user to pass either an ID value or
551 the name of the object as a string.
showard7c785282008-05-29 19:45:12 +0000552
jadmanski0afbb632008-06-06 21:10:57 +0000553 If to_human_readable=True, perform the inverse - i.e. convert
554 numeric values to human readable values.
showard7c785282008-05-29 19:45:12 +0000555
jadmanski0afbb632008-06-06 21:10:57 +0000556 This method modifies data in-place.
557 """
558 field_dict = cls.get_field_dict()
559 for field_name in data:
showarde732ee72008-09-23 19:15:43 +0000560 if field_name not in field_dict or data[field_name] is None:
jadmanski0afbb632008-06-06 21:10:57 +0000561 continue
562 field_obj = field_dict[field_name]
563 # convert enum values
564 if field_obj.choices:
565 for choice_data in field_obj.choices:
566 # choice_data is (value, name)
567 if to_human_readable:
568 from_val, to_val = choice_data
569 else:
570 to_val, from_val = choice_data
571 if from_val == data[field_name]:
572 data[field_name] = to_val
573 break
574 # convert foreign key values
575 elif field_obj.rel:
showarda4ea5742009-02-17 20:56:23 +0000576 dest_obj = field_obj.rel.to.smart_get(data[field_name],
577 valid_only=False)
showardf8b19042009-05-12 17:22:49 +0000578 if to_human_readable:
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -0800579 # parameterized_jobs do not have a name_field
580 if (field_name != 'parameterized_job' and
581 dest_obj.name_field is not None):
showardf8b19042009-05-12 17:22:49 +0000582 data[field_name] = getattr(dest_obj,
583 dest_obj.name_field)
jadmanski0afbb632008-06-06 21:10:57 +0000584 else:
showardb0a73032009-03-27 18:35:41 +0000585 data[field_name] = dest_obj
showard7c785282008-05-29 19:45:12 +0000586
587
showard7c785282008-05-29 19:45:12 +0000588
589
Dale Curtis74a314b2011-06-23 14:55:46 -0700590 def _validate_unique(self):
jadmanski0afbb632008-06-06 21:10:57 +0000591 """\
592 Validate that unique fields are unique. Django manipulators do
593 this too, but they're a huge pain to use manually. Trust me.
594 """
595 errors = {}
596 cls = type(self)
597 field_dict = self.get_field_dict()
598 manager = cls.get_valid_manager()
599 for field_name, field_obj in field_dict.iteritems():
600 if not field_obj.unique:
601 continue
showard7c785282008-05-29 19:45:12 +0000602
jadmanski0afbb632008-06-06 21:10:57 +0000603 value = getattr(self, field_name)
showardbd18ab72009-09-18 21:20:27 +0000604 if value is None and field_obj.auto_created:
605 # don't bother checking autoincrement fields about to be
606 # generated
607 continue
608
jadmanski0afbb632008-06-06 21:10:57 +0000609 existing_objs = manager.filter(**{field_name : value})
610 num_existing = existing_objs.count()
showard7c785282008-05-29 19:45:12 +0000611
jadmanski0afbb632008-06-06 21:10:57 +0000612 if num_existing == 0:
613 continue
614 if num_existing == 1 and existing_objs[0].id == self.id:
615 continue
616 errors[field_name] = (
617 'This value must be unique (%s)' % (value))
618 return errors
showard7c785282008-05-29 19:45:12 +0000619
620
showarda5288b42009-07-28 20:06:08 +0000621 def _validate(self):
622 """
623 First coerces all fields on this instance to their proper Python types.
624 Then runs validation on every field. Returns a dictionary of
625 field_name -> error_list.
626
627 Based on validate() from django.db.models.Model in Django 0.96, which
628 was removed in Django 1.0. It should reappear in a later version. See:
629 http://code.djangoproject.com/ticket/6845
630 """
631 error_dict = {}
632 for f in self._meta.fields:
633 try:
634 python_value = f.to_python(
635 getattr(self, f.attname, f.get_default()))
636 except django.core.exceptions.ValidationError, e:
jamesren1e0a4ce2010-04-21 17:45:11 +0000637 error_dict[f.name] = str(e)
showarda5288b42009-07-28 20:06:08 +0000638 continue
639
640 if not f.blank and not python_value:
641 error_dict[f.name] = 'This field is required.'
642 continue
643
644 setattr(self, f.attname, python_value)
645
646 return error_dict
647
648
jadmanski0afbb632008-06-06 21:10:57 +0000649 def do_validate(self):
showarda5288b42009-07-28 20:06:08 +0000650 errors = self._validate()
Dale Curtis74a314b2011-06-23 14:55:46 -0700651 unique_errors = self._validate_unique()
jadmanski0afbb632008-06-06 21:10:57 +0000652 for field_name, error in unique_errors.iteritems():
653 errors.setdefault(field_name, error)
654 if errors:
655 raise ValidationError(errors)
showard7c785282008-05-29 19:45:12 +0000656
657
jadmanski0afbb632008-06-06 21:10:57 +0000658 # actually (externally) useful methods follow
showard7c785282008-05-29 19:45:12 +0000659
jadmanski0afbb632008-06-06 21:10:57 +0000660 @classmethod
661 def add_object(cls, data={}, **kwargs):
662 """\
663 Returns a new object created with the given data (a dictionary
664 mapping field names to values). Merges any extra keyword args
665 into data.
666 """
Prashanth B489b91d2014-03-15 12:17:16 -0700667 data = dict(data)
668 data.update(kwargs)
669 data = cls.prepare_data_args(data)
670 cls.convert_human_readable_values(data)
jadmanski0afbb632008-06-06 21:10:57 +0000671 data = cls.provide_default_values(data)
Prashanth B489b91d2014-03-15 12:17:16 -0700672
jadmanski0afbb632008-06-06 21:10:57 +0000673 obj = cls(**data)
674 obj.do_validate()
675 obj.save()
676 return obj
showard7c785282008-05-29 19:45:12 +0000677
678
jadmanski0afbb632008-06-06 21:10:57 +0000679 def update_object(self, data={}, **kwargs):
680 """\
681 Updates the object with the given data (a dictionary mapping
682 field names to values). Merges any extra keyword args into
683 data.
684 """
Prashanth B489b91d2014-03-15 12:17:16 -0700685 data = dict(data)
686 data.update(kwargs)
687 data = self.prepare_data_args(data)
688 self.convert_human_readable_values(data)
jadmanski0afbb632008-06-06 21:10:57 +0000689 for field_name, value in data.iteritems():
showardb0a73032009-03-27 18:35:41 +0000690 setattr(self, field_name, value)
jadmanski0afbb632008-06-06 21:10:57 +0000691 self.do_validate()
692 self.save()
showard7c785282008-05-29 19:45:12 +0000693
694
showard8bfb5cb2009-10-07 20:49:15 +0000695 # see query_objects()
696 _SPECIAL_FILTER_KEYS = ('query_start', 'query_limit', 'sort_by',
697 'extra_args', 'extra_where', 'no_distinct')
698
699
jadmanski0afbb632008-06-06 21:10:57 +0000700 @classmethod
showard8bfb5cb2009-10-07 20:49:15 +0000701 def _extract_special_params(cls, filter_data):
702 """
703 @returns a tuple of dicts (special_params, regular_filters), where
704 special_params contains the parameters we handle specially and
705 regular_filters is the remaining data to be handled by Django.
706 """
707 regular_filters = dict(filter_data)
708 special_params = {}
709 for key in cls._SPECIAL_FILTER_KEYS:
710 if key in regular_filters:
711 special_params[key] = regular_filters.pop(key)
712 return special_params, regular_filters
713
714
715 @classmethod
716 def apply_presentation(cls, query, filter_data):
717 """
718 Apply presentation parameters -- sorting and paging -- to the given
719 query.
720 @returns new query with presentation applied
721 """
722 special_params, _ = cls._extract_special_params(filter_data)
723 sort_by = special_params.get('sort_by', None)
724 if sort_by:
725 assert isinstance(sort_by, list) or isinstance(sort_by, tuple)
showard8b0ea222009-12-23 19:23:03 +0000726 query = query.extra(order_by=sort_by)
showard8bfb5cb2009-10-07 20:49:15 +0000727
728 query_start = special_params.get('query_start', None)
729 query_limit = special_params.get('query_limit', None)
730 if query_start is not None:
731 if query_limit is None:
732 raise ValueError('Cannot pass query_start without query_limit')
733 # query_limit is passed as a page size
showard7074b742009-10-12 20:30:04 +0000734 query_limit += query_start
735 return query[query_start:query_limit]
showard8bfb5cb2009-10-07 20:49:15 +0000736
737
738 @classmethod
739 def query_objects(cls, filter_data, valid_only=True, initial_query=None,
740 apply_presentation=True):
jadmanski0afbb632008-06-06 21:10:57 +0000741 """\
742 Returns a QuerySet object for querying the given model_class
743 with the given filter_data. Optional special arguments in
744 filter_data include:
745 -query_start: index of first return to return
746 -query_limit: maximum number of results to return
747 -sort_by: list of fields to sort on. prefixing a '-' onto a
748 field name changes the sort to descending order.
749 -extra_args: keyword args to pass to query.extra() (see Django
750 DB layer documentation)
showarda5288b42009-07-28 20:06:08 +0000751 -extra_where: extra WHERE clause to append
showard8bfb5cb2009-10-07 20:49:15 +0000752 -no_distinct: if True, a DISTINCT will not be added to the SELECT
jadmanski0afbb632008-06-06 21:10:57 +0000753 """
showard8bfb5cb2009-10-07 20:49:15 +0000754 special_params, regular_filters = cls._extract_special_params(
755 filter_data)
showard7c785282008-05-29 19:45:12 +0000756
showard7ac7b7a2008-07-21 20:24:29 +0000757 if initial_query is None:
758 if valid_only:
759 initial_query = cls.get_valid_manager()
760 else:
761 initial_query = cls.objects
showard8bfb5cb2009-10-07 20:49:15 +0000762
763 query = initial_query.filter(**regular_filters)
764
765 use_distinct = not special_params.get('no_distinct', False)
showard7ac7b7a2008-07-21 20:24:29 +0000766 if use_distinct:
767 query = query.distinct()
showard7c785282008-05-29 19:45:12 +0000768
showard8bfb5cb2009-10-07 20:49:15 +0000769 extra_args = special_params.get('extra_args', {})
770 extra_where = special_params.get('extra_where', None)
771 if extra_where:
772 # escape %'s
773 extra_where = cls.objects.escape_user_sql(extra_where)
774 extra_args.setdefault('where', []).append(extra_where)
jadmanski0afbb632008-06-06 21:10:57 +0000775 if extra_args:
776 query = query.extra(**extra_args)
Jakob Juelich7bef8412014-10-14 19:11:54 -0700777 # TODO: Use readonly connection for these queries.
778 # This has been disabled, because it's not used anyway, as the
779 # configured readonly user is the same as the real user anyway.
showard7c785282008-05-29 19:45:12 +0000780
showard8bfb5cb2009-10-07 20:49:15 +0000781 if apply_presentation:
782 query = cls.apply_presentation(query, filter_data)
783
784 return query
showard7c785282008-05-29 19:45:12 +0000785
786
jadmanski0afbb632008-06-06 21:10:57 +0000787 @classmethod
showard585c2ab2008-07-23 19:29:49 +0000788 def query_count(cls, filter_data, initial_query=None):
jadmanski0afbb632008-06-06 21:10:57 +0000789 """\
790 Like query_objects, but retreive only the count of results.
791 """
792 filter_data.pop('query_start', None)
793 filter_data.pop('query_limit', None)
showard585c2ab2008-07-23 19:29:49 +0000794 query = cls.query_objects(filter_data, initial_query=initial_query)
795 return query.count()
showard7c785282008-05-29 19:45:12 +0000796
797
jadmanski0afbb632008-06-06 21:10:57 +0000798 @classmethod
799 def clean_object_dicts(cls, field_dicts):
800 """\
801 Take a list of dicts corresponding to object (as returned by
802 query.values()) and clean the data to be more suitable for
803 returning to the user.
804 """
showarde732ee72008-09-23 19:15:43 +0000805 for field_dict in field_dicts:
806 cls.clean_foreign_keys(field_dict)
showard21baa452008-10-21 00:08:39 +0000807 cls._convert_booleans(field_dict)
showarde732ee72008-09-23 19:15:43 +0000808 cls.convert_human_readable_values(field_dict,
809 to_human_readable=True)
showard7c785282008-05-29 19:45:12 +0000810
811
jadmanski0afbb632008-06-06 21:10:57 +0000812 @classmethod
showard8bfb5cb2009-10-07 20:49:15 +0000813 def list_objects(cls, filter_data, initial_query=None):
jadmanski0afbb632008-06-06 21:10:57 +0000814 """\
815 Like query_objects, but return a list of dictionaries.
816 """
showard7ac7b7a2008-07-21 20:24:29 +0000817 query = cls.query_objects(filter_data, initial_query=initial_query)
showard8bfb5cb2009-10-07 20:49:15 +0000818 extra_fields = query.query.extra_select.keys()
819 field_dicts = [model_object.get_object_dict(extra_fields=extra_fields)
showarde732ee72008-09-23 19:15:43 +0000820 for model_object in query]
jadmanski0afbb632008-06-06 21:10:57 +0000821 return field_dicts
showard7c785282008-05-29 19:45:12 +0000822
823
jadmanski0afbb632008-06-06 21:10:57 +0000824 @classmethod
showarda4ea5742009-02-17 20:56:23 +0000825 def smart_get(cls, id_or_name, valid_only=True):
jadmanski0afbb632008-06-06 21:10:57 +0000826 """\
827 smart_get(integer) -> get object by ID
828 smart_get(string) -> get object by name_field
jadmanski0afbb632008-06-06 21:10:57 +0000829 """
showarda4ea5742009-02-17 20:56:23 +0000830 if valid_only:
831 manager = cls.get_valid_manager()
832 else:
833 manager = cls.objects
834
835 if isinstance(id_or_name, (int, long)):
836 return manager.get(pk=id_or_name)
jamesren3e9f6092010-03-11 21:32:10 +0000837 if isinstance(id_or_name, basestring) and hasattr(cls, 'name_field'):
showarda4ea5742009-02-17 20:56:23 +0000838 return manager.get(**{cls.name_field : id_or_name})
839 raise ValueError(
840 'Invalid positional argument: %s (%s)' % (id_or_name,
841 type(id_or_name)))
showard7c785282008-05-29 19:45:12 +0000842
843
showardbe3ec042008-11-12 18:16:07 +0000844 @classmethod
845 def smart_get_bulk(cls, id_or_name_list):
846 invalid_inputs = []
847 result_objects = []
848 for id_or_name in id_or_name_list:
849 try:
850 result_objects.append(cls.smart_get(id_or_name))
851 except cls.DoesNotExist:
852 invalid_inputs.append(id_or_name)
853 if invalid_inputs:
mbligh7a3ebe32008-12-01 17:10:33 +0000854 raise cls.DoesNotExist('The following %ss do not exist: %s'
855 % (cls.__name__.lower(),
856 ', '.join(invalid_inputs)))
showardbe3ec042008-11-12 18:16:07 +0000857 return result_objects
858
859
showard8bfb5cb2009-10-07 20:49:15 +0000860 def get_object_dict(self, extra_fields=None):
jadmanski0afbb632008-06-06 21:10:57 +0000861 """\
showard8bfb5cb2009-10-07 20:49:15 +0000862 Return a dictionary mapping fields to this object's values. @param
863 extra_fields: list of extra attribute names to include, in addition to
864 the fields defined on this object.
jadmanski0afbb632008-06-06 21:10:57 +0000865 """
showard8bfb5cb2009-10-07 20:49:15 +0000866 fields = self.get_field_dict().keys()
867 if extra_fields:
868 fields += extra_fields
jadmanski0afbb632008-06-06 21:10:57 +0000869 object_dict = dict((field_name, getattr(self, field_name))
showarde732ee72008-09-23 19:15:43 +0000870 for field_name in fields)
jadmanski0afbb632008-06-06 21:10:57 +0000871 self.clean_object_dicts([object_dict])
showardd3dc1992009-04-22 21:01:40 +0000872 self._postprocess_object_dict(object_dict)
jadmanski0afbb632008-06-06 21:10:57 +0000873 return object_dict
showard7c785282008-05-29 19:45:12 +0000874
875
showardd3dc1992009-04-22 21:01:40 +0000876 def _postprocess_object_dict(self, object_dict):
877 """For subclasses to override."""
878 pass
879
880
jadmanski0afbb632008-06-06 21:10:57 +0000881 @classmethod
882 def get_valid_manager(cls):
883 return cls.objects
showard7c785282008-05-29 19:45:12 +0000884
885
showard2bab8f42008-11-12 18:15:22 +0000886 def _record_attributes(self, attributes):
887 """
888 See on_attribute_changed.
889 """
890 assert not isinstance(attributes, basestring)
891 self._recorded_attributes = dict((attribute, getattr(self, attribute))
892 for attribute in attributes)
893
894
895 def _check_for_updated_attributes(self):
896 """
897 See on_attribute_changed.
898 """
899 for attribute, original_value in self._recorded_attributes.iteritems():
900 new_value = getattr(self, attribute)
901 if original_value != new_value:
902 self.on_attribute_changed(attribute, original_value)
903 self._record_attributes(self._recorded_attributes.keys())
904
905
906 def on_attribute_changed(self, attribute, old_value):
907 """
908 Called whenever an attribute is updated. To be overridden.
909
910 To use this method, you must:
911 * call _record_attributes() from __init__() (after making the super
912 call) with a list of attributes for which you want to be notified upon
913 change.
914 * call _check_for_updated_attributes() from save().
915 """
916 pass
917
918
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700919 def serialize(self, include_dependencies=True):
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700920 """Serializes the object with dependencies.
921
922 The variable SERIALIZATION_LINKS_TO_FOLLOW defines which dependencies
923 this function will serialize with the object.
924
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700925 @param include_dependencies: Whether or not to follow relations to
926 objects this object depends on.
927 This parameter is used when uploading
928 jobs from a shard to the master, as the
929 master already has all the dependent
930 objects.
931
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700932 @returns: Dictionary representation of the object.
933 """
934 serialized = {}
MK Ryu5cfd96a2015-01-30 15:31:23 -0800935 timer = autotest_stats.Timer('serialize_latency.%s' % (
936 type(self).__name__))
937 with timer.get_client('local'):
938 for field in self._meta.concrete_model._meta.local_fields:
939 if field.rel is None:
940 serialized[field.name] = field._get_val_from_obj(self)
MK Ryudf4d4232015-03-06 11:18:47 -0800941 elif field.name in self.SERIALIZATION_LINKS_TO_KEEP:
MK Ryu5cfd96a2015-01-30 15:31:23 -0800942 # attname will contain "_id" suffix for foreign keys,
943 # e.g. HostAttribute.host will be serialized as 'host_id'.
944 # Use it for easy deserialization.
945 serialized[field.attname] = field._get_val_from_obj(self)
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700946
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700947 if include_dependencies:
MK Ryu5cfd96a2015-01-30 15:31:23 -0800948 with timer.get_client('related'):
949 for link in self.SERIALIZATION_LINKS_TO_FOLLOW:
950 serialized[link] = self._serialize_relation(link)
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700951
952 return serialized
953
954
955 def _serialize_relation(self, link):
956 """Serializes dependent objects given the name of the relation.
957
958 @param link: Name of the relation to take objects from.
959
960 @returns For To-Many relationships a list of the serialized related
961 objects, for To-One relationships the serialized related object.
962 """
963 try:
964 attr = getattr(self, link)
965 except AttributeError:
966 # One-To-One relationships that point to None may raise this
967 return None
968
969 if attr is None:
970 return None
971 if hasattr(attr, 'all'):
972 return [obj.serialize() for obj in attr.all()]
973 return attr.serialize()
974
975
Jakob Juelichf88fa932014-09-03 17:58:04 -0700976 @classmethod
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700977 def _split_local_from_foreign_values(cls, data):
978 """This splits local from foreign values in a serialized object.
979
980 @param data: The serialized object.
981
982 @returns A tuple of two lists, both containing tuples in the form
983 (link_name, link_value). The first list contains all links
984 for local fields, the second one contains those for foreign
985 fields/objects.
986 """
987 links_to_local_values, links_to_related_values = [], []
988 for link, value in data.iteritems():
989 if link in cls.SERIALIZATION_LINKS_TO_FOLLOW:
990 # It's a foreign key
991 links_to_related_values.append((link, value))
992 else:
Fang Deng86248502014-12-18 16:38:00 -0800993 # It's a local attribute or a foreign key
994 # we don't want to follow.
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700995 links_to_local_values.append((link, value))
996 return links_to_local_values, links_to_related_values
997
998
Jakob Juelichf865d332014-09-29 10:47:49 -0700999 @classmethod
1000 def _filter_update_allowed_fields(cls, data):
1001 """Filters data and returns only files that updates are allowed on.
1002
1003 This is i.e. needed for syncing aborted bits from the master to shards.
1004
1005 Local links are only allowed to be updated, if they are in
1006 SERIALIZATION_LOCAL_LINKS_TO_UPDATE.
1007 Overwriting existing values is allowed in order to be able to sync i.e.
1008 the aborted bit from the master to a shard.
1009
1010 The whitelisting mechanism is in place to prevent overwriting local
1011 status: If all fields were overwritten, jobs would be completely be
1012 set back to their original (unstarted) state.
1013
1014 @param data: List with tuples of the form (link_name, link_value), as
1015 returned by _split_local_from_foreign_values.
1016
1017 @returns List of the same format as data, but only containing data for
1018 fields that updates are allowed on.
1019 """
1020 return [pair for pair in data
1021 if pair[0] in cls.SERIALIZATION_LOCAL_LINKS_TO_UPDATE]
1022
1023
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -08001024 @classmethod
1025 def delete_matching_record(cls, **filter_args):
1026 """Delete records matching the filter.
1027
1028 @param filter_args: Arguments for the django filter
1029 used to locate the record to delete.
1030 """
1031 try:
1032 existing_record = cls.objects.get(**filter_args)
1033 except cls.DoesNotExist:
1034 return
1035 existing_record.delete()
1036
1037
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001038 def _deserialize_local(self, data):
1039 """Set local attributes from a list of tuples.
1040
1041 @param data: List of tuples like returned by
1042 _split_local_from_foreign_values.
1043 """
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -08001044 if not data:
1045 return
1046
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001047 for link, value in data:
1048 setattr(self, link, value)
1049 # Overwridden save() methods are prone to errors, so don't execute them.
1050 # This is because:
1051 # - the overwritten methods depend on ACL groups that don't yet exist
1052 # and don't handle errors
1053 # - the overwritten methods think this object already exists in the db
1054 # because the id is already set
1055 super(type(self), self).save()
1056
1057
1058 def _deserialize_relations(self, data):
1059 """Set foreign attributes from a list of tuples.
1060
1061 This deserialized the related objects using their own deserialize()
1062 function and then sets the relation.
1063
1064 @param data: List of tuples like returned by
1065 _split_local_from_foreign_values.
1066 """
1067 for link, value in data:
1068 self._deserialize_relation(link, value)
1069 # See comment in _deserialize_local
1070 super(type(self), self).save()
1071
1072
1073 @classmethod
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -08001074 def get_record(cls, data):
1075 """Retrieve a record with the data in the given input arg.
1076
1077 @param data: A dictionary containing the information to use in a query
1078 for data. If child models have different constraints of
1079 uniqueness they should override this model.
1080
1081 @return: An object with matching data.
1082
1083 @raises DoesNotExist: If a record with the given data doesn't exist.
1084 """
1085 return cls.objects.get(id=data['id'])
1086
1087
1088 @classmethod
Jakob Juelichf88fa932014-09-03 17:58:04 -07001089 def deserialize(cls, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001090 """Recursively deserializes and saves an object with it's dependencies.
Jakob Juelichf88fa932014-09-03 17:58:04 -07001091
1092 This takes the result of the serialize method and creates objects
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001093 in the database that are just like the original.
1094
1095 If an object of the same type with the same id already exists, it's
Jakob Juelichf865d332014-09-29 10:47:49 -07001096 local values will be left untouched, unless they are explicitly
1097 whitelisted in SERIALIZATION_LOCAL_LINKS_TO_UPDATE.
1098
1099 Deserialize will always recursively propagate to all related objects
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001100 present in data though.
1101 I.e. this is necessary to add users to an already existing acl-group.
Jakob Juelichf88fa932014-09-03 17:58:04 -07001102
1103 @param data: Representation of an object and its dependencies, as
1104 returned by serialize.
1105
1106 @returns: The object represented by data if it didn't exist before,
1107 otherwise the object that existed before and has the same type
1108 and id as the one described by data.
1109 """
1110 if data is None:
1111 return None
1112
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001113 local, related = cls._split_local_from_foreign_values(data)
Jakob Juelichf88fa932014-09-03 17:58:04 -07001114 try:
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -08001115 instance = cls.get_record(data)
Jakob Juelichf865d332014-09-29 10:47:49 -07001116 local = cls._filter_update_allowed_fields(local)
Jakob Juelichf88fa932014-09-03 17:58:04 -07001117 except cls.DoesNotExist:
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001118 instance = cls()
Jakob Juelichf88fa932014-09-03 17:58:04 -07001119
MK Ryu5cfd96a2015-01-30 15:31:23 -08001120 timer = autotest_stats.Timer('deserialize_latency.%s' % (
1121 type(instance).__name__))
1122 with timer.get_client('local'):
1123 instance._deserialize_local(local)
1124 with timer.get_client('related'):
1125 instance._deserialize_relations(related)
Jakob Juelichf88fa932014-09-03 17:58:04 -07001126
1127 return instance
1128
1129
Jakob Juelicha94efe62014-09-18 16:02:49 -07001130 def sanity_check_update_from_shard(self, shard, updated_serialized,
1131 *args, **kwargs):
1132 """Check if an update sent from a shard is legitimate.
1133
1134 @raises error.UnallowedRecordsSentToMaster if an update is not
1135 legitimate.
1136 """
1137 raise NotImplementedError(
1138 'sanity_check_update_from_shard must be implemented by subclass %s '
1139 'for type %s' % type(self))
1140
1141
Prashanth Balasubramanian75be1d32014-11-25 18:03:09 -08001142 @transaction.commit_on_success
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001143 def update_from_serialized(self, serialized):
1144 """Updates local fields of an existing object from a serialized form.
1145
1146 This is different than the normal deserialize() in the way that it
1147 does update local values, which deserialize doesn't, but doesn't
1148 recursively propagate to related objects, which deserialize() does.
1149
1150 The use case of this function is to update job records on the master
1151 after the jobs have been executed on a slave, as the master is not
1152 interested in updates for users, labels, specialtasks, etc.
1153
1154 @param serialized: Representation of an object and its dependencies, as
1155 returned by serialize.
1156
1157 @raises ValueError: if serialized contains related objects, i.e. not
1158 only local fields.
1159 """
1160 local, related = (
1161 self._split_local_from_foreign_values(serialized))
1162 if related:
1163 raise ValueError('Serialized must not contain foreign '
1164 'objects: %s' % related)
1165
1166 self._deserialize_local(local)
1167
1168
Jakob Juelichf88fa932014-09-03 17:58:04 -07001169 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001170 """Allows overriding the deserialization behaviour by subclasses."""
Jakob Juelichf88fa932014-09-03 17:58:04 -07001171 raise NotImplementedError(
1172 'custom_deserialize_relation must be implemented by subclass %s '
1173 'for relation %s' % (type(self), link))
1174
1175
1176 def _deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001177 """Deserializes related objects and sets references on this object.
1178
1179 Relations that point to a list of objects are handled automatically.
1180 For many-to-one or one-to-one relations custom_deserialize_relation
1181 must be overridden by the subclass.
1182
1183 Related objects are deserialized using their deserialize() method.
1184 Thereby they and their dependencies are created if they don't exist
1185 and saved to the database.
1186
1187 @param link: Name of the relation.
1188 @param data: Serialized representation of the related object(s).
1189 This means a list of dictionaries for to-many relations,
1190 just a dictionary for to-one relations.
1191 """
Jakob Juelichf88fa932014-09-03 17:58:04 -07001192 field = getattr(self, link)
1193
1194 if field and hasattr(field, 'all'):
1195 self._deserialize_2m_relation(link, data, field.model)
1196 else:
1197 self.custom_deserialize_relation(link, data)
1198
1199
1200 def _deserialize_2m_relation(self, link, data, related_class):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001201 """Deserialize related objects for one to-many relationship.
1202
1203 @param link: Name of the relation.
1204 @param data: Serialized representation of the related objects.
1205 This is a list with of dictionaries.
Fang Dengff361592015-02-02 15:27:34 -08001206 @param related_class: A class representing a django model, with which
1207 this class has a one-to-many relationship.
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001208 """
Jakob Juelichf88fa932014-09-03 17:58:04 -07001209 relation_set = getattr(self, link)
Fang Dengff361592015-02-02 15:27:34 -08001210 if related_class == self.get_attribute_model():
1211 # When deserializing a model together with
1212 # its attributes, clear all the exising attributes to ensure
1213 # db consistency. Note 'update' won't be sufficient, as we also
1214 # want to remove any attributes that no longer exist in |data|.
1215 #
1216 # core_filters is a dictionary of filters, defines how
1217 # RelatedMangager would query for the 1-to-many relationship. E.g.
1218 # Host.objects.get(
1219 # id=20).hostattribute_set.core_filters = {host_id:20}
1220 # We use it to delete objects related to the current object.
1221 related_class.objects.filter(**relation_set.core_filters).delete()
Jakob Juelichf88fa932014-09-03 17:58:04 -07001222 for serialized in data:
1223 relation_set.add(related_class.deserialize(serialized))
1224
1225
Fang Dengff361592015-02-02 15:27:34 -08001226 @classmethod
1227 def get_attribute_model(cls):
1228 """Return the attribute model.
1229
1230 Subclass with attribute-like model should override this to
1231 return the attribute model class. This method will be
1232 called by _deserialize_2m_relation to determine whether
1233 to clear the one-to-many relations first on deserialization of object.
1234 """
1235 return None
1236
1237
showard7c785282008-05-29 19:45:12 +00001238class ModelWithInvalid(ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001239 """
1240 Overrides model methods save() and delete() to support invalidation in
1241 place of actual deletion. Subclasses must have a boolean "invalid"
1242 field.
1243 """
showard7c785282008-05-29 19:45:12 +00001244
showarda5288b42009-07-28 20:06:08 +00001245 def save(self, *args, **kwargs):
showardddb90992009-02-11 23:39:32 +00001246 first_time = (self.id is None)
1247 if first_time:
1248 # see if this object was previously added and invalidated
1249 my_name = getattr(self, self.name_field)
1250 filters = {self.name_field : my_name, 'invalid' : True}
1251 try:
1252 old_object = self.__class__.objects.get(**filters)
showardafd97de2009-10-01 18:45:09 +00001253 self.resurrect_object(old_object)
showardddb90992009-02-11 23:39:32 +00001254 except self.DoesNotExist:
1255 # no existing object
1256 pass
showard7c785282008-05-29 19:45:12 +00001257
showarda5288b42009-07-28 20:06:08 +00001258 super(ModelWithInvalid, self).save(*args, **kwargs)
showard7c785282008-05-29 19:45:12 +00001259
1260
showardafd97de2009-10-01 18:45:09 +00001261 def resurrect_object(self, old_object):
1262 """
1263 Called when self is about to be saved for the first time and is actually
1264 "undeleting" a previously deleted object. Can be overridden by
1265 subclasses to copy data as desired from the deleted entry (but this
1266 superclass implementation must normally be called).
1267 """
1268 self.id = old_object.id
1269
1270
jadmanski0afbb632008-06-06 21:10:57 +00001271 def clean_object(self):
1272 """
1273 This method is called when an object is marked invalid.
1274 Subclasses should override this to clean up relationships that
showardafd97de2009-10-01 18:45:09 +00001275 should no longer exist if the object were deleted.
1276 """
jadmanski0afbb632008-06-06 21:10:57 +00001277 pass
showard7c785282008-05-29 19:45:12 +00001278
1279
jadmanski0afbb632008-06-06 21:10:57 +00001280 def delete(self):
Dale Curtis74a314b2011-06-23 14:55:46 -07001281 self.invalid = self.invalid
jadmanski0afbb632008-06-06 21:10:57 +00001282 assert not self.invalid
1283 self.invalid = True
1284 self.save()
1285 self.clean_object()
showard7c785282008-05-29 19:45:12 +00001286
1287
jadmanski0afbb632008-06-06 21:10:57 +00001288 @classmethod
1289 def get_valid_manager(cls):
1290 return cls.valid_objects
showard7c785282008-05-29 19:45:12 +00001291
1292
jadmanski0afbb632008-06-06 21:10:57 +00001293 class Manipulator(object):
1294 """
1295 Force default manipulators to look only at valid objects -
1296 otherwise they will match against invalid objects when checking
1297 uniqueness.
1298 """
1299 @classmethod
1300 def _prepare(cls, model):
1301 super(ModelWithInvalid.Manipulator, cls)._prepare(model)
1302 cls.manager = model.valid_objects
showardf8b19042009-05-12 17:22:49 +00001303
1304
1305class ModelWithAttributes(object):
1306 """
1307 Mixin class for models that have an attribute model associated with them.
1308 The attribute model is assumed to have its value field named "value".
1309 """
1310
1311 def _get_attribute_model_and_args(self, attribute):
1312 """
1313 Subclasses should override this to return a tuple (attribute_model,
1314 keyword_args), where attribute_model is a model class and keyword_args
1315 is a dict of args to pass to attribute_model.objects.get() to get an
1316 instance of the given attribute on this object.
1317 """
Dale Curtis74a314b2011-06-23 14:55:46 -07001318 raise NotImplementedError
showardf8b19042009-05-12 17:22:49 +00001319
1320
1321 def set_attribute(self, attribute, value):
1322 attribute_model, get_args = self._get_attribute_model_and_args(
1323 attribute)
1324 attribute_object, _ = attribute_model.objects.get_or_create(**get_args)
1325 attribute_object.value = value
1326 attribute_object.save()
1327
1328
1329 def delete_attribute(self, attribute):
1330 attribute_model, get_args = self._get_attribute_model_and_args(
1331 attribute)
1332 try:
1333 attribute_model.objects.get(**get_args).delete()
showard16245422009-09-08 16:28:15 +00001334 except attribute_model.DoesNotExist:
showardf8b19042009-05-12 17:22:49 +00001335 pass
1336
1337
1338 def set_or_delete_attribute(self, attribute, value):
1339 if value is None:
1340 self.delete_attribute(attribute)
1341 else:
1342 self.set_attribute(attribute, value)
showard26b7ec72009-12-21 22:43:57 +00001343
1344
1345class ModelWithHashManager(dbmodels.Manager):
1346 """Manager for use with the ModelWithHash abstract model class"""
1347
1348 def create(self, **kwargs):
1349 raise Exception('ModelWithHash manager should use get_or_create() '
1350 'instead of create()')
1351
1352
1353 def get_or_create(self, **kwargs):
1354 kwargs['the_hash'] = self.model._compute_hash(**kwargs)
1355 return super(ModelWithHashManager, self).get_or_create(**kwargs)
1356
1357
1358class ModelWithHash(dbmodels.Model):
1359 """Superclass with methods for dealing with a hash column"""
1360
1361 the_hash = dbmodels.CharField(max_length=40, unique=True)
1362
1363 objects = ModelWithHashManager()
1364
1365 class Meta:
1366 abstract = True
1367
1368
1369 @classmethod
1370 def _compute_hash(cls, **kwargs):
1371 raise NotImplementedError('Subclasses must override _compute_hash()')
1372
1373
1374 def save(self, force_insert=False, **kwargs):
1375 """Prevents saving the model in most cases
1376
1377 We want these models to be immutable, so the generic save() operation
1378 will not work. These models should be instantiated through their the
1379 model.objects.get_or_create() method instead.
1380
1381 The exception is that save(force_insert=True) will be allowed, since
1382 that creates a new row. However, the preferred way to make instances of
1383 these models is through the get_or_create() method.
1384 """
1385 if not force_insert:
1386 # Allow a forced insert to happen; if it's a duplicate, the unique
1387 # constraint will catch it later anyways
1388 raise Exception('ModelWithHash is immutable')
1389 super(ModelWithHash, self).save(force_insert=force_insert, **kwargs)