blob: 580e7cdf20534944dd64d319da41e4d1a0af66eb [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()
172 all_clauses = (initial_clause,)
173 if hasattr(join_to_query.query, 'extra_where'):
174 all_clauses += join_to_query.query.extra_where
175 info['where_clause'] = (
176 ' AND '.join('(%s)' % clause for clause in all_clauses))
showard7e67b432010-01-20 01:13:04 +0000177 info['values'] = values
178 return info
179
180
181 def _info_for_many_to_many_join(self, m2m_field, join_to_query, alias,
182 m2m_is_on_this_model):
183 """
184 @param m2m_field: a Django field representing the M2M relationship.
185 It uses a pivot table with the following structure:
186 this model table <---> M2M pivot table <---> joined model table
187 @param join_to_query: the query over the related model that we're
188 joining to.
189 @param alias: alias of joined table
190 """
191 if m2m_is_on_this_model:
192 # referenced field on this model
193 lhs_id_field = self.model._meta.pk
194 # foreign key on the pivot table referencing lhs_id_field
195 m2m_lhs_column = m2m_field.m2m_column_name()
196 # foreign key on the pivot table referencing rhd_id_field
197 m2m_rhs_column = m2m_field.m2m_reverse_name()
198 # referenced field on related model
199 rhs_id_field = m2m_field.rel.get_related_field()
200 else:
201 lhs_id_field = m2m_field.rel.get_related_field()
202 m2m_lhs_column = m2m_field.m2m_reverse_name()
203 m2m_rhs_column = m2m_field.m2m_column_name()
204 rhs_id_field = join_to_query.model._meta.pk
205
206 info = {}
207 info['rhs_table'] = m2m_field.m2m_db_table()
208 info['rhs_column'] = m2m_lhs_column
209 info['lhs_column'] = lhs_id_field.column
210
211 # select the ID of related models relevant to this join. we can only do
212 # a single join, so we need to gather this information up front and
213 # include it in the join condition.
214 rhs_ids = join_to_query.values_list(rhs_id_field.attname, flat=True)
215 assert len(rhs_ids) == 1, ('Many-to-many custom field joins can only '
216 'match a single related object.')
217 rhs_id = rhs_ids[0]
218
219 info['where_clause'] = '%s.%s = %s' % (_quote_name(alias),
220 _quote_name(m2m_rhs_column),
221 rhs_id)
222 info['values'] = ()
223 return info
224
225
226 def join_custom_field(self, query_set, join_to_query, alias,
227 left_join=True):
228 """Join to a related model to create a custom field in the given query.
229
230 This method is used to construct a custom field on the given query based
231 on a many-valued relationsip. join_to_query should be a simple query
232 (no joins) on the related model which returns at most one related row
233 per instance of this model.
234
235 For many-to-one relationships, the joined table contains the matching
236 row from the related model it one is related, NULL otherwise.
237
238 For many-to-many relationships, the joined table contains the matching
239 row if it's related, NULL otherwise.
240 """
241 relationship_type, field = self.determine_relationship(
242 join_to_query.model)
243
244 if relationship_type == self.MANY_TO_ONE:
245 info = self._info_for_many_to_one_join(field, join_to_query, alias)
246 elif relationship_type == self.M2M_ON_RELATED_MODEL:
247 info = self._info_for_many_to_many_join(
248 m2m_field=field, join_to_query=join_to_query, alias=alias,
249 m2m_is_on_this_model=False)
250 elif relationship_type ==self.M2M_ON_THIS_MODEL:
251 info = self._info_for_many_to_many_join(
252 m2m_field=field, join_to_query=join_to_query, alias=alias,
253 m2m_is_on_this_model=True)
254
255 return self.add_join(query_set, info['rhs_table'], info['rhs_column'],
256 join_from_key=info['lhs_column'],
257 join_condition=info['where_clause'],
258 join_condition_values=info['values'],
259 alias=alias,
260 force_left_join=left_join)
261
262
263 def add_where(self, query_set, where, values=()):
264 query_set = query_set.all()
265 query_set.query.where.add(self._WhereClause(where, values),
266 django.db.models.sql.where.AND)
showardc4780402009-08-31 18:31:34 +0000267 return query_set
showard7c785282008-05-29 19:45:12 +0000268
269
showardeaccf8f2009-04-16 03:11:33 +0000270 def _get_quoted_field(self, table, field):
showarda5288b42009-07-28 20:06:08 +0000271 return _quote_name(table) + '.' + _quote_name(field)
showard5ef36e92008-07-02 16:37:09 +0000272
273
showard7c199df2008-10-03 10:17:15 +0000274 def get_key_on_this_table(self, key_field=None):
showard5ef36e92008-07-02 16:37:09 +0000275 if key_field is None:
276 # default to primary key
277 key_field = self.model._meta.pk.column
278 return self._get_quoted_field(self.model._meta.db_table, key_field)
279
280
showardeaccf8f2009-04-16 03:11:33 +0000281 def escape_user_sql(self, sql):
282 return sql.replace('%', '%%')
283
showard5ef36e92008-07-02 16:37:09 +0000284
showard0957a842009-05-11 19:25:08 +0000285 def _custom_select_query(self, query_set, selects):
Jakob Juelich7bef8412014-10-14 19:11:54 -0700286 """Execute a custom select query.
287
288 @param query_set: query set as returned by query_objects.
289 @param selects: Tables/Columns to select, e.g. tko_test_labels_list.id.
290
291 @returns: Result of the query as returned by cursor.fetchall().
292 """
Dale Curtis74a314b2011-06-23 14:55:46 -0700293 compiler = query_set.query.get_compiler(using=query_set.db)
294 sql, params = compiler.as_sql()
showarda5288b42009-07-28 20:06:08 +0000295 from_ = sql[sql.find(' FROM'):]
296
297 if query_set.query.distinct:
showard0957a842009-05-11 19:25:08 +0000298 distinct = 'DISTINCT '
299 else:
300 distinct = ''
showarda5288b42009-07-28 20:06:08 +0000301
302 sql_query = ('SELECT ' + distinct + ','.join(selects) + from_)
Jakob Juelich7bef8412014-10-14 19:11:54 -0700303 # Chose the connection that's responsible for this type of object
304 cursor = connections[query_set.db].cursor()
showard0957a842009-05-11 19:25:08 +0000305 cursor.execute(sql_query, params)
306 return cursor.fetchall()
307
308
showard68693f72009-05-20 00:31:53 +0000309 def _is_relation_to(self, field, model_class):
310 return field.rel and field.rel.to is model_class
showard0957a842009-05-11 19:25:08 +0000311
312
showard7e67b432010-01-20 01:13:04 +0000313 MANY_TO_ONE = object()
314 M2M_ON_RELATED_MODEL = object()
315 M2M_ON_THIS_MODEL = object()
316
317 def determine_relationship(self, related_model):
318 """
319 Determine the relationship between this model and related_model.
320
321 related_model must have some sort of many-valued relationship to this
322 manager's model.
323 @returns (relationship_type, field), where relationship_type is one of
324 MANY_TO_ONE, M2M_ON_RELATED_MODEL, M2M_ON_THIS_MODEL, and field
325 is the Django field object for the relationship.
326 """
327 # look for a foreign key field on related_model relating to this model
328 for field in related_model._meta.fields:
329 if self._is_relation_to(field, self.model):
330 return self.MANY_TO_ONE, field
331
332 # look for an M2M field on related_model relating to this model
333 for field in related_model._meta.many_to_many:
334 if self._is_relation_to(field, self.model):
335 return self.M2M_ON_RELATED_MODEL, field
336
337 # maybe this model has the many-to-many field
338 for field in self.model._meta.many_to_many:
339 if self._is_relation_to(field, related_model):
340 return self.M2M_ON_THIS_MODEL, field
341
342 raise ValueError('%s has no relation to %s' %
343 (related_model, self.model))
344
345
showard68693f72009-05-20 00:31:53 +0000346 def _get_pivot_iterator(self, base_objects_by_id, related_model):
showard0957a842009-05-11 19:25:08 +0000347 """
showard68693f72009-05-20 00:31:53 +0000348 Determine the relationship between this model and related_model, and
349 return a pivot iterator.
350 @param base_objects_by_id: dict of instances of this model indexed by
351 their IDs
352 @returns a pivot iterator, which yields a tuple (base_object,
353 related_object) for each relationship between a base object and a
354 related object. all base_object instances come from base_objects_by_id.
showard7e67b432010-01-20 01:13:04 +0000355 Note -- this depends on Django model internals.
showard0957a842009-05-11 19:25:08 +0000356 """
showard7e67b432010-01-20 01:13:04 +0000357 relationship_type, field = self.determine_relationship(related_model)
358 if relationship_type == self.MANY_TO_ONE:
359 return self._many_to_one_pivot(base_objects_by_id,
360 related_model, field)
361 elif relationship_type == self.M2M_ON_RELATED_MODEL:
362 return self._many_to_many_pivot(
showard68693f72009-05-20 00:31:53 +0000363 base_objects_by_id, related_model, field.m2m_db_table(),
364 field.m2m_reverse_name(), field.m2m_column_name())
showard7e67b432010-01-20 01:13:04 +0000365 else:
366 assert relationship_type == self.M2M_ON_THIS_MODEL
367 return self._many_to_many_pivot(
showard68693f72009-05-20 00:31:53 +0000368 base_objects_by_id, related_model, field.m2m_db_table(),
369 field.m2m_column_name(), field.m2m_reverse_name())
showard0957a842009-05-11 19:25:08 +0000370
showard0957a842009-05-11 19:25:08 +0000371
showard68693f72009-05-20 00:31:53 +0000372 def _many_to_one_pivot(self, base_objects_by_id, related_model,
373 foreign_key_field):
374 """
375 @returns a pivot iterator - see _get_pivot_iterator()
376 """
377 filter_data = {foreign_key_field.name + '__pk__in':
378 base_objects_by_id.keys()}
379 for related_object in related_model.objects.filter(**filter_data):
showarda5a72c92009-08-20 23:35:21 +0000380 # lookup base object in the dict, rather than grabbing it from the
381 # related object. we need to return instances from the dict, not
382 # fresh instances of the same models (and grabbing model instances
383 # from the related models incurs a DB query each time).
384 base_object_id = getattr(related_object, foreign_key_field.attname)
385 base_object = base_objects_by_id[base_object_id]
showard68693f72009-05-20 00:31:53 +0000386 yield base_object, related_object
387
388
389 def _query_pivot_table(self, base_objects_by_id, pivot_table,
Jakob Juelich7bef8412014-10-14 19:11:54 -0700390 pivot_from_field, pivot_to_field, related_model):
showard0957a842009-05-11 19:25:08 +0000391 """
392 @param id_list list of IDs of self.model objects to include
393 @param pivot_table the name of the pivot table
394 @param pivot_from_field a field name on pivot_table referencing
395 self.model
396 @param pivot_to_field a field name on pivot_table referencing the
397 related model.
Jakob Juelich7bef8412014-10-14 19:11:54 -0700398 @param related_model the related model
399
showard68693f72009-05-20 00:31:53 +0000400 @returns pivot list of IDs (base_id, related_id)
showard0957a842009-05-11 19:25:08 +0000401 """
402 query = """
403 SELECT %(from_field)s, %(to_field)s
404 FROM %(table)s
405 WHERE %(from_field)s IN (%(id_list)s)
406 """ % dict(from_field=pivot_from_field,
407 to_field=pivot_to_field,
408 table=pivot_table,
showard68693f72009-05-20 00:31:53 +0000409 id_list=','.join(str(id_) for id_
410 in base_objects_by_id.iterkeys()))
Jakob Juelich7bef8412014-10-14 19:11:54 -0700411
412 # Chose the connection that's responsible for this type of object
413 # The databases for related_model and the current model will always
414 # be the same, related_model is just easier to obtain here because
415 # self is only a ExtendedManager, not the object.
416 cursor = connections[related_model.objects.db].cursor()
showard0957a842009-05-11 19:25:08 +0000417 cursor.execute(query)
showard68693f72009-05-20 00:31:53 +0000418 return cursor.fetchall()
showard0957a842009-05-11 19:25:08 +0000419
420
showard68693f72009-05-20 00:31:53 +0000421 def _many_to_many_pivot(self, base_objects_by_id, related_model,
422 pivot_table, pivot_from_field, pivot_to_field):
423 """
424 @param pivot_table: see _query_pivot_table
425 @param pivot_from_field: see _query_pivot_table
426 @param pivot_to_field: see _query_pivot_table
427 @returns a pivot iterator - see _get_pivot_iterator()
428 """
429 id_pivot = self._query_pivot_table(base_objects_by_id, pivot_table,
Jakob Juelich7bef8412014-10-14 19:11:54 -0700430 pivot_from_field, pivot_to_field,
431 related_model)
showard68693f72009-05-20 00:31:53 +0000432
433 all_related_ids = list(set(related_id for base_id, related_id
434 in id_pivot))
435 related_objects_by_id = related_model.objects.in_bulk(all_related_ids)
436
437 for base_id, related_id in id_pivot:
438 yield base_objects_by_id[base_id], related_objects_by_id[related_id]
439
440
441 def populate_relationships(self, base_objects, related_model,
showard0957a842009-05-11 19:25:08 +0000442 related_list_name):
443 """
showard68693f72009-05-20 00:31:53 +0000444 For each instance of this model in base_objects, add a field named
445 related_list_name listing all the related objects of type related_model.
446 related_model must be in a many-to-one or many-to-many relationship with
447 this model.
448 @param base_objects - list of instances of this model
449 @param related_model - model class related to this model
450 @param related_list_name - attribute name in which to store the related
451 object list.
showard0957a842009-05-11 19:25:08 +0000452 """
showard68693f72009-05-20 00:31:53 +0000453 if not base_objects:
showard0957a842009-05-11 19:25:08 +0000454 # if we don't bail early, we'll get a SQL error later
455 return
showard0957a842009-05-11 19:25:08 +0000456
showard68693f72009-05-20 00:31:53 +0000457 base_objects_by_id = dict((base_object._get_pk_val(), base_object)
458 for base_object in base_objects)
459 pivot_iterator = self._get_pivot_iterator(base_objects_by_id,
460 related_model)
showard0957a842009-05-11 19:25:08 +0000461
showard68693f72009-05-20 00:31:53 +0000462 for base_object in base_objects:
463 setattr(base_object, related_list_name, [])
464
465 for base_object, related_object in pivot_iterator:
466 getattr(base_object, related_list_name).append(related_object)
showard0957a842009-05-11 19:25:08 +0000467
468
jamesrene3656232010-03-02 00:00:30 +0000469class ModelWithInvalidQuerySet(dbmodels.query.QuerySet):
470 """
471 QuerySet that handles delete() properly for models with an "invalid" bit
472 """
473 def delete(self):
474 for model in self:
475 model.delete()
476
477
478class ModelWithInvalidManager(ExtendedManager):
479 """
480 Manager for objects with an "invalid" bit
481 """
482 def get_query_set(self):
483 return ModelWithInvalidQuerySet(self.model)
484
485
486class ValidObjectsManager(ModelWithInvalidManager):
jadmanski0afbb632008-06-06 21:10:57 +0000487 """
488 Manager returning only objects with invalid=False.
489 """
490 def get_query_set(self):
491 queryset = super(ValidObjectsManager, self).get_query_set()
492 return queryset.filter(invalid=False)
showard7c785282008-05-29 19:45:12 +0000493
494
Prashanth B489b91d2014-03-15 12:17:16 -0700495class ModelExtensions(rdb_model_extensions.ModelValidators):
jadmanski0afbb632008-06-06 21:10:57 +0000496 """\
Prashanth B489b91d2014-03-15 12:17:16 -0700497 Mixin with convenience functions for models, built on top of
498 the model validators in rdb_model_extensions.
jadmanski0afbb632008-06-06 21:10:57 +0000499 """
500 # TODO: at least some of these functions really belong in a custom
501 # Manager class
showard7c785282008-05-29 19:45:12 +0000502
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700503
504 SERIALIZATION_LINKS_TO_FOLLOW = set()
505 """
506 To be able to send jobs and hosts to shards, it's necessary to find their
507 dependencies.
508 The most generic approach for this would be to traverse all relationships
509 to other objects recursively. This would list all objects that are related
510 in any way.
511 But this approach finds too many objects: If a host should be transferred,
512 all it's relationships would be traversed. This would find an acl group.
513 If then the acl group's relationships are traversed, the relationship
514 would be followed backwards and many other hosts would be found.
515
516 This mapping tells that algorithm which relations to follow explicitly.
517 """
518
Jakob Juelichf865d332014-09-29 10:47:49 -0700519
Fang Deng86248502014-12-18 16:38:00 -0800520 SERIALIZATION_LINKS_TO_KEEP = set()
521 """This set stores foreign keys which we don't want to follow, but
522 still want to include in the serialized dictionary. For
523 example, we follow the relationship `Host.hostattribute_set`,
524 but we do not want to follow `HostAttributes.host_id` back to
525 to Host, which would otherwise lead to a circle. However, we still
526 like to serialize HostAttribute.`host_id`."""
527
Jakob Juelichf865d332014-09-29 10:47:49 -0700528 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set()
529 """
530 On deserializion, if the object to persist already exists, local fields
531 will only be updated, if their name is in this set.
532 """
533
534
jadmanski0afbb632008-06-06 21:10:57 +0000535 @classmethod
536 def convert_human_readable_values(cls, data, to_human_readable=False):
537 """\
538 Performs conversions on user-supplied field data, to make it
539 easier for users to pass human-readable data.
showard7c785282008-05-29 19:45:12 +0000540
jadmanski0afbb632008-06-06 21:10:57 +0000541 For all fields that have choice sets, convert their values
542 from human-readable strings to enum values, if necessary. This
543 allows users to pass strings instead of the corresponding
544 integer values.
showard7c785282008-05-29 19:45:12 +0000545
jadmanski0afbb632008-06-06 21:10:57 +0000546 For all foreign key fields, call smart_get with the supplied
547 data. This allows the user to pass either an ID value or
548 the name of the object as a string.
showard7c785282008-05-29 19:45:12 +0000549
jadmanski0afbb632008-06-06 21:10:57 +0000550 If to_human_readable=True, perform the inverse - i.e. convert
551 numeric values to human readable values.
showard7c785282008-05-29 19:45:12 +0000552
jadmanski0afbb632008-06-06 21:10:57 +0000553 This method modifies data in-place.
554 """
555 field_dict = cls.get_field_dict()
556 for field_name in data:
showarde732ee72008-09-23 19:15:43 +0000557 if field_name not in field_dict or data[field_name] is None:
jadmanski0afbb632008-06-06 21:10:57 +0000558 continue
559 field_obj = field_dict[field_name]
560 # convert enum values
561 if field_obj.choices:
562 for choice_data in field_obj.choices:
563 # choice_data is (value, name)
564 if to_human_readable:
565 from_val, to_val = choice_data
566 else:
567 to_val, from_val = choice_data
568 if from_val == data[field_name]:
569 data[field_name] = to_val
570 break
571 # convert foreign key values
572 elif field_obj.rel:
showarda4ea5742009-02-17 20:56:23 +0000573 dest_obj = field_obj.rel.to.smart_get(data[field_name],
574 valid_only=False)
showardf8b19042009-05-12 17:22:49 +0000575 if to_human_readable:
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -0800576 # parameterized_jobs do not have a name_field
577 if (field_name != 'parameterized_job' and
578 dest_obj.name_field is not None):
showardf8b19042009-05-12 17:22:49 +0000579 data[field_name] = getattr(dest_obj,
580 dest_obj.name_field)
jadmanski0afbb632008-06-06 21:10:57 +0000581 else:
showardb0a73032009-03-27 18:35:41 +0000582 data[field_name] = dest_obj
showard7c785282008-05-29 19:45:12 +0000583
584
showard7c785282008-05-29 19:45:12 +0000585
586
Dale Curtis74a314b2011-06-23 14:55:46 -0700587 def _validate_unique(self):
jadmanski0afbb632008-06-06 21:10:57 +0000588 """\
589 Validate that unique fields are unique. Django manipulators do
590 this too, but they're a huge pain to use manually. Trust me.
591 """
592 errors = {}
593 cls = type(self)
594 field_dict = self.get_field_dict()
595 manager = cls.get_valid_manager()
596 for field_name, field_obj in field_dict.iteritems():
597 if not field_obj.unique:
598 continue
showard7c785282008-05-29 19:45:12 +0000599
jadmanski0afbb632008-06-06 21:10:57 +0000600 value = getattr(self, field_name)
showardbd18ab72009-09-18 21:20:27 +0000601 if value is None and field_obj.auto_created:
602 # don't bother checking autoincrement fields about to be
603 # generated
604 continue
605
jadmanski0afbb632008-06-06 21:10:57 +0000606 existing_objs = manager.filter(**{field_name : value})
607 num_existing = existing_objs.count()
showard7c785282008-05-29 19:45:12 +0000608
jadmanski0afbb632008-06-06 21:10:57 +0000609 if num_existing == 0:
610 continue
611 if num_existing == 1 and existing_objs[0].id == self.id:
612 continue
613 errors[field_name] = (
614 'This value must be unique (%s)' % (value))
615 return errors
showard7c785282008-05-29 19:45:12 +0000616
617
showarda5288b42009-07-28 20:06:08 +0000618 def _validate(self):
619 """
620 First coerces all fields on this instance to their proper Python types.
621 Then runs validation on every field. Returns a dictionary of
622 field_name -> error_list.
623
624 Based on validate() from django.db.models.Model in Django 0.96, which
625 was removed in Django 1.0. It should reappear in a later version. See:
626 http://code.djangoproject.com/ticket/6845
627 """
628 error_dict = {}
629 for f in self._meta.fields:
630 try:
631 python_value = f.to_python(
632 getattr(self, f.attname, f.get_default()))
633 except django.core.exceptions.ValidationError, e:
jamesren1e0a4ce2010-04-21 17:45:11 +0000634 error_dict[f.name] = str(e)
showarda5288b42009-07-28 20:06:08 +0000635 continue
636
637 if not f.blank and not python_value:
638 error_dict[f.name] = 'This field is required.'
639 continue
640
641 setattr(self, f.attname, python_value)
642
643 return error_dict
644
645
jadmanski0afbb632008-06-06 21:10:57 +0000646 def do_validate(self):
showarda5288b42009-07-28 20:06:08 +0000647 errors = self._validate()
Dale Curtis74a314b2011-06-23 14:55:46 -0700648 unique_errors = self._validate_unique()
jadmanski0afbb632008-06-06 21:10:57 +0000649 for field_name, error in unique_errors.iteritems():
650 errors.setdefault(field_name, error)
651 if errors:
652 raise ValidationError(errors)
showard7c785282008-05-29 19:45:12 +0000653
654
jadmanski0afbb632008-06-06 21:10:57 +0000655 # actually (externally) useful methods follow
showard7c785282008-05-29 19:45:12 +0000656
jadmanski0afbb632008-06-06 21:10:57 +0000657 @classmethod
658 def add_object(cls, data={}, **kwargs):
659 """\
660 Returns a new object created with the given data (a dictionary
661 mapping field names to values). Merges any extra keyword args
662 into data.
663 """
Prashanth B489b91d2014-03-15 12:17:16 -0700664 data = dict(data)
665 data.update(kwargs)
666 data = cls.prepare_data_args(data)
667 cls.convert_human_readable_values(data)
jadmanski0afbb632008-06-06 21:10:57 +0000668 data = cls.provide_default_values(data)
Prashanth B489b91d2014-03-15 12:17:16 -0700669
jadmanski0afbb632008-06-06 21:10:57 +0000670 obj = cls(**data)
671 obj.do_validate()
672 obj.save()
673 return obj
showard7c785282008-05-29 19:45:12 +0000674
675
jadmanski0afbb632008-06-06 21:10:57 +0000676 def update_object(self, data={}, **kwargs):
677 """\
678 Updates the object with the given data (a dictionary mapping
679 field names to values). Merges any extra keyword args into
680 data.
681 """
Prashanth B489b91d2014-03-15 12:17:16 -0700682 data = dict(data)
683 data.update(kwargs)
684 data = self.prepare_data_args(data)
685 self.convert_human_readable_values(data)
jadmanski0afbb632008-06-06 21:10:57 +0000686 for field_name, value in data.iteritems():
showardb0a73032009-03-27 18:35:41 +0000687 setattr(self, field_name, value)
jadmanski0afbb632008-06-06 21:10:57 +0000688 self.do_validate()
689 self.save()
showard7c785282008-05-29 19:45:12 +0000690
691
showard8bfb5cb2009-10-07 20:49:15 +0000692 # see query_objects()
693 _SPECIAL_FILTER_KEYS = ('query_start', 'query_limit', 'sort_by',
694 'extra_args', 'extra_where', 'no_distinct')
695
696
jadmanski0afbb632008-06-06 21:10:57 +0000697 @classmethod
showard8bfb5cb2009-10-07 20:49:15 +0000698 def _extract_special_params(cls, filter_data):
699 """
700 @returns a tuple of dicts (special_params, regular_filters), where
701 special_params contains the parameters we handle specially and
702 regular_filters is the remaining data to be handled by Django.
703 """
704 regular_filters = dict(filter_data)
705 special_params = {}
706 for key in cls._SPECIAL_FILTER_KEYS:
707 if key in regular_filters:
708 special_params[key] = regular_filters.pop(key)
709 return special_params, regular_filters
710
711
712 @classmethod
713 def apply_presentation(cls, query, filter_data):
714 """
715 Apply presentation parameters -- sorting and paging -- to the given
716 query.
717 @returns new query with presentation applied
718 """
719 special_params, _ = cls._extract_special_params(filter_data)
720 sort_by = special_params.get('sort_by', None)
721 if sort_by:
722 assert isinstance(sort_by, list) or isinstance(sort_by, tuple)
showard8b0ea222009-12-23 19:23:03 +0000723 query = query.extra(order_by=sort_by)
showard8bfb5cb2009-10-07 20:49:15 +0000724
725 query_start = special_params.get('query_start', None)
726 query_limit = special_params.get('query_limit', None)
727 if query_start is not None:
728 if query_limit is None:
729 raise ValueError('Cannot pass query_start without query_limit')
730 # query_limit is passed as a page size
showard7074b742009-10-12 20:30:04 +0000731 query_limit += query_start
732 return query[query_start:query_limit]
showard8bfb5cb2009-10-07 20:49:15 +0000733
734
735 @classmethod
736 def query_objects(cls, filter_data, valid_only=True, initial_query=None,
737 apply_presentation=True):
jadmanski0afbb632008-06-06 21:10:57 +0000738 """\
739 Returns a QuerySet object for querying the given model_class
740 with the given filter_data. Optional special arguments in
741 filter_data include:
742 -query_start: index of first return to return
743 -query_limit: maximum number of results to return
744 -sort_by: list of fields to sort on. prefixing a '-' onto a
745 field name changes the sort to descending order.
746 -extra_args: keyword args to pass to query.extra() (see Django
747 DB layer documentation)
showarda5288b42009-07-28 20:06:08 +0000748 -extra_where: extra WHERE clause to append
showard8bfb5cb2009-10-07 20:49:15 +0000749 -no_distinct: if True, a DISTINCT will not be added to the SELECT
jadmanski0afbb632008-06-06 21:10:57 +0000750 """
showard8bfb5cb2009-10-07 20:49:15 +0000751 special_params, regular_filters = cls._extract_special_params(
752 filter_data)
showard7c785282008-05-29 19:45:12 +0000753
showard7ac7b7a2008-07-21 20:24:29 +0000754 if initial_query is None:
755 if valid_only:
756 initial_query = cls.get_valid_manager()
757 else:
758 initial_query = cls.objects
showard8bfb5cb2009-10-07 20:49:15 +0000759
760 query = initial_query.filter(**regular_filters)
761
762 use_distinct = not special_params.get('no_distinct', False)
showard7ac7b7a2008-07-21 20:24:29 +0000763 if use_distinct:
764 query = query.distinct()
showard7c785282008-05-29 19:45:12 +0000765
showard8bfb5cb2009-10-07 20:49:15 +0000766 extra_args = special_params.get('extra_args', {})
767 extra_where = special_params.get('extra_where', None)
768 if extra_where:
769 # escape %'s
770 extra_where = cls.objects.escape_user_sql(extra_where)
771 extra_args.setdefault('where', []).append(extra_where)
jadmanski0afbb632008-06-06 21:10:57 +0000772 if extra_args:
773 query = query.extra(**extra_args)
Jakob Juelich7bef8412014-10-14 19:11:54 -0700774 # TODO: Use readonly connection for these queries.
775 # This has been disabled, because it's not used anyway, as the
776 # configured readonly user is the same as the real user anyway.
showard7c785282008-05-29 19:45:12 +0000777
showard8bfb5cb2009-10-07 20:49:15 +0000778 if apply_presentation:
779 query = cls.apply_presentation(query, filter_data)
780
781 return query
showard7c785282008-05-29 19:45:12 +0000782
783
jadmanski0afbb632008-06-06 21:10:57 +0000784 @classmethod
showard585c2ab2008-07-23 19:29:49 +0000785 def query_count(cls, filter_data, initial_query=None):
jadmanski0afbb632008-06-06 21:10:57 +0000786 """\
787 Like query_objects, but retreive only the count of results.
788 """
789 filter_data.pop('query_start', None)
790 filter_data.pop('query_limit', None)
showard585c2ab2008-07-23 19:29:49 +0000791 query = cls.query_objects(filter_data, initial_query=initial_query)
792 return query.count()
showard7c785282008-05-29 19:45:12 +0000793
794
jadmanski0afbb632008-06-06 21:10:57 +0000795 @classmethod
796 def clean_object_dicts(cls, field_dicts):
797 """\
798 Take a list of dicts corresponding to object (as returned by
799 query.values()) and clean the data to be more suitable for
800 returning to the user.
801 """
showarde732ee72008-09-23 19:15:43 +0000802 for field_dict in field_dicts:
803 cls.clean_foreign_keys(field_dict)
showard21baa452008-10-21 00:08:39 +0000804 cls._convert_booleans(field_dict)
showarde732ee72008-09-23 19:15:43 +0000805 cls.convert_human_readable_values(field_dict,
806 to_human_readable=True)
showard7c785282008-05-29 19:45:12 +0000807
808
jadmanski0afbb632008-06-06 21:10:57 +0000809 @classmethod
showard8bfb5cb2009-10-07 20:49:15 +0000810 def list_objects(cls, filter_data, initial_query=None):
jadmanski0afbb632008-06-06 21:10:57 +0000811 """\
812 Like query_objects, but return a list of dictionaries.
813 """
showard7ac7b7a2008-07-21 20:24:29 +0000814 query = cls.query_objects(filter_data, initial_query=initial_query)
showard8bfb5cb2009-10-07 20:49:15 +0000815 extra_fields = query.query.extra_select.keys()
816 field_dicts = [model_object.get_object_dict(extra_fields=extra_fields)
showarde732ee72008-09-23 19:15:43 +0000817 for model_object in query]
jadmanski0afbb632008-06-06 21:10:57 +0000818 return field_dicts
showard7c785282008-05-29 19:45:12 +0000819
820
jadmanski0afbb632008-06-06 21:10:57 +0000821 @classmethod
showarda4ea5742009-02-17 20:56:23 +0000822 def smart_get(cls, id_or_name, valid_only=True):
jadmanski0afbb632008-06-06 21:10:57 +0000823 """\
824 smart_get(integer) -> get object by ID
825 smart_get(string) -> get object by name_field
jadmanski0afbb632008-06-06 21:10:57 +0000826 """
showarda4ea5742009-02-17 20:56:23 +0000827 if valid_only:
828 manager = cls.get_valid_manager()
829 else:
830 manager = cls.objects
831
832 if isinstance(id_or_name, (int, long)):
833 return manager.get(pk=id_or_name)
jamesren3e9f6092010-03-11 21:32:10 +0000834 if isinstance(id_or_name, basestring) and hasattr(cls, 'name_field'):
showarda4ea5742009-02-17 20:56:23 +0000835 return manager.get(**{cls.name_field : id_or_name})
836 raise ValueError(
837 'Invalid positional argument: %s (%s)' % (id_or_name,
838 type(id_or_name)))
showard7c785282008-05-29 19:45:12 +0000839
840
showardbe3ec042008-11-12 18:16:07 +0000841 @classmethod
842 def smart_get_bulk(cls, id_or_name_list):
843 invalid_inputs = []
844 result_objects = []
845 for id_or_name in id_or_name_list:
846 try:
847 result_objects.append(cls.smart_get(id_or_name))
848 except cls.DoesNotExist:
849 invalid_inputs.append(id_or_name)
850 if invalid_inputs:
mbligh7a3ebe32008-12-01 17:10:33 +0000851 raise cls.DoesNotExist('The following %ss do not exist: %s'
852 % (cls.__name__.lower(),
853 ', '.join(invalid_inputs)))
showardbe3ec042008-11-12 18:16:07 +0000854 return result_objects
855
856
showard8bfb5cb2009-10-07 20:49:15 +0000857 def get_object_dict(self, extra_fields=None):
jadmanski0afbb632008-06-06 21:10:57 +0000858 """\
showard8bfb5cb2009-10-07 20:49:15 +0000859 Return a dictionary mapping fields to this object's values. @param
860 extra_fields: list of extra attribute names to include, in addition to
861 the fields defined on this object.
jadmanski0afbb632008-06-06 21:10:57 +0000862 """
showard8bfb5cb2009-10-07 20:49:15 +0000863 fields = self.get_field_dict().keys()
864 if extra_fields:
865 fields += extra_fields
jadmanski0afbb632008-06-06 21:10:57 +0000866 object_dict = dict((field_name, getattr(self, field_name))
showarde732ee72008-09-23 19:15:43 +0000867 for field_name in fields)
jadmanski0afbb632008-06-06 21:10:57 +0000868 self.clean_object_dicts([object_dict])
showardd3dc1992009-04-22 21:01:40 +0000869 self._postprocess_object_dict(object_dict)
jadmanski0afbb632008-06-06 21:10:57 +0000870 return object_dict
showard7c785282008-05-29 19:45:12 +0000871
872
showardd3dc1992009-04-22 21:01:40 +0000873 def _postprocess_object_dict(self, object_dict):
874 """For subclasses to override."""
875 pass
876
877
jadmanski0afbb632008-06-06 21:10:57 +0000878 @classmethod
879 def get_valid_manager(cls):
880 return cls.objects
showard7c785282008-05-29 19:45:12 +0000881
882
showard2bab8f42008-11-12 18:15:22 +0000883 def _record_attributes(self, attributes):
884 """
885 See on_attribute_changed.
886 """
887 assert not isinstance(attributes, basestring)
888 self._recorded_attributes = dict((attribute, getattr(self, attribute))
889 for attribute in attributes)
890
891
892 def _check_for_updated_attributes(self):
893 """
894 See on_attribute_changed.
895 """
896 for attribute, original_value in self._recorded_attributes.iteritems():
897 new_value = getattr(self, attribute)
898 if original_value != new_value:
899 self.on_attribute_changed(attribute, original_value)
900 self._record_attributes(self._recorded_attributes.keys())
901
902
903 def on_attribute_changed(self, attribute, old_value):
904 """
905 Called whenever an attribute is updated. To be overridden.
906
907 To use this method, you must:
908 * call _record_attributes() from __init__() (after making the super
909 call) with a list of attributes for which you want to be notified upon
910 change.
911 * call _check_for_updated_attributes() from save().
912 """
913 pass
914
915
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700916 def serialize(self, include_dependencies=True):
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700917 """Serializes the object with dependencies.
918
919 The variable SERIALIZATION_LINKS_TO_FOLLOW defines which dependencies
920 this function will serialize with the object.
921
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700922 @param include_dependencies: Whether or not to follow relations to
923 objects this object depends on.
924 This parameter is used when uploading
925 jobs from a shard to the master, as the
926 master already has all the dependent
927 objects.
928
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700929 @returns: Dictionary representation of the object.
930 """
931 serialized = {}
MK Ryu5cfd96a2015-01-30 15:31:23 -0800932 timer = autotest_stats.Timer('serialize_latency.%s' % (
933 type(self).__name__))
934 with timer.get_client('local'):
935 for field in self._meta.concrete_model._meta.local_fields:
936 if field.rel is None:
937 serialized[field.name] = field._get_val_from_obj(self)
MK Ryudf4d4232015-03-06 11:18:47 -0800938 elif field.name in self.SERIALIZATION_LINKS_TO_KEEP:
MK Ryu5cfd96a2015-01-30 15:31:23 -0800939 # attname will contain "_id" suffix for foreign keys,
940 # e.g. HostAttribute.host will be serialized as 'host_id'.
941 # Use it for easy deserialization.
942 serialized[field.attname] = field._get_val_from_obj(self)
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700943
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700944 if include_dependencies:
MK Ryu5cfd96a2015-01-30 15:31:23 -0800945 with timer.get_client('related'):
946 for link in self.SERIALIZATION_LINKS_TO_FOLLOW:
947 serialized[link] = self._serialize_relation(link)
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700948
949 return serialized
950
951
952 def _serialize_relation(self, link):
953 """Serializes dependent objects given the name of the relation.
954
955 @param link: Name of the relation to take objects from.
956
957 @returns For To-Many relationships a list of the serialized related
958 objects, for To-One relationships the serialized related object.
959 """
960 try:
961 attr = getattr(self, link)
962 except AttributeError:
963 # One-To-One relationships that point to None may raise this
964 return None
965
966 if attr is None:
967 return None
968 if hasattr(attr, 'all'):
969 return [obj.serialize() for obj in attr.all()]
970 return attr.serialize()
971
972
Jakob Juelichf88fa932014-09-03 17:58:04 -0700973 @classmethod
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700974 def _split_local_from_foreign_values(cls, data):
975 """This splits local from foreign values in a serialized object.
976
977 @param data: The serialized object.
978
979 @returns A tuple of two lists, both containing tuples in the form
980 (link_name, link_value). The first list contains all links
981 for local fields, the second one contains those for foreign
982 fields/objects.
983 """
984 links_to_local_values, links_to_related_values = [], []
985 for link, value in data.iteritems():
986 if link in cls.SERIALIZATION_LINKS_TO_FOLLOW:
987 # It's a foreign key
988 links_to_related_values.append((link, value))
989 else:
Fang Deng86248502014-12-18 16:38:00 -0800990 # It's a local attribute or a foreign key
991 # we don't want to follow.
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700992 links_to_local_values.append((link, value))
993 return links_to_local_values, links_to_related_values
994
995
Jakob Juelichf865d332014-09-29 10:47:49 -0700996 @classmethod
997 def _filter_update_allowed_fields(cls, data):
998 """Filters data and returns only files that updates are allowed on.
999
1000 This is i.e. needed for syncing aborted bits from the master to shards.
1001
1002 Local links are only allowed to be updated, if they are in
1003 SERIALIZATION_LOCAL_LINKS_TO_UPDATE.
1004 Overwriting existing values is allowed in order to be able to sync i.e.
1005 the aborted bit from the master to a shard.
1006
1007 The whitelisting mechanism is in place to prevent overwriting local
1008 status: If all fields were overwritten, jobs would be completely be
1009 set back to their original (unstarted) state.
1010
1011 @param data: List with tuples of the form (link_name, link_value), as
1012 returned by _split_local_from_foreign_values.
1013
1014 @returns List of the same format as data, but only containing data for
1015 fields that updates are allowed on.
1016 """
1017 return [pair for pair in data
1018 if pair[0] in cls.SERIALIZATION_LOCAL_LINKS_TO_UPDATE]
1019
1020
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -08001021 @classmethod
1022 def delete_matching_record(cls, **filter_args):
1023 """Delete records matching the filter.
1024
1025 @param filter_args: Arguments for the django filter
1026 used to locate the record to delete.
1027 """
1028 try:
1029 existing_record = cls.objects.get(**filter_args)
1030 except cls.DoesNotExist:
1031 return
1032 existing_record.delete()
1033
1034
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001035 def _deserialize_local(self, data):
1036 """Set local attributes from a list of tuples.
1037
1038 @param data: List of tuples like returned by
1039 _split_local_from_foreign_values.
1040 """
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -08001041 if not data:
1042 return
1043
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001044 for link, value in data:
1045 setattr(self, link, value)
1046 # Overwridden save() methods are prone to errors, so don't execute them.
1047 # This is because:
1048 # - the overwritten methods depend on ACL groups that don't yet exist
1049 # and don't handle errors
1050 # - the overwritten methods think this object already exists in the db
1051 # because the id is already set
1052 super(type(self), self).save()
1053
1054
1055 def _deserialize_relations(self, data):
1056 """Set foreign attributes from a list of tuples.
1057
1058 This deserialized the related objects using their own deserialize()
1059 function and then sets the relation.
1060
1061 @param data: List of tuples like returned by
1062 _split_local_from_foreign_values.
1063 """
1064 for link, value in data:
1065 self._deserialize_relation(link, value)
1066 # See comment in _deserialize_local
1067 super(type(self), self).save()
1068
1069
1070 @classmethod
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -08001071 def get_record(cls, data):
1072 """Retrieve a record with the data in the given input arg.
1073
1074 @param data: A dictionary containing the information to use in a query
1075 for data. If child models have different constraints of
1076 uniqueness they should override this model.
1077
1078 @return: An object with matching data.
1079
1080 @raises DoesNotExist: If a record with the given data doesn't exist.
1081 """
1082 return cls.objects.get(id=data['id'])
1083
1084
1085 @classmethod
Jakob Juelichf88fa932014-09-03 17:58:04 -07001086 def deserialize(cls, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001087 """Recursively deserializes and saves an object with it's dependencies.
Jakob Juelichf88fa932014-09-03 17:58:04 -07001088
1089 This takes the result of the serialize method and creates objects
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001090 in the database that are just like the original.
1091
1092 If an object of the same type with the same id already exists, it's
Jakob Juelichf865d332014-09-29 10:47:49 -07001093 local values will be left untouched, unless they are explicitly
1094 whitelisted in SERIALIZATION_LOCAL_LINKS_TO_UPDATE.
1095
1096 Deserialize will always recursively propagate to all related objects
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001097 present in data though.
1098 I.e. this is necessary to add users to an already existing acl-group.
Jakob Juelichf88fa932014-09-03 17:58:04 -07001099
1100 @param data: Representation of an object and its dependencies, as
1101 returned by serialize.
1102
1103 @returns: The object represented by data if it didn't exist before,
1104 otherwise the object that existed before and has the same type
1105 and id as the one described by data.
1106 """
1107 if data is None:
1108 return None
1109
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001110 local, related = cls._split_local_from_foreign_values(data)
Jakob Juelichf88fa932014-09-03 17:58:04 -07001111 try:
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -08001112 instance = cls.get_record(data)
Jakob Juelichf865d332014-09-29 10:47:49 -07001113 local = cls._filter_update_allowed_fields(local)
Jakob Juelichf88fa932014-09-03 17:58:04 -07001114 except cls.DoesNotExist:
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001115 instance = cls()
Jakob Juelichf88fa932014-09-03 17:58:04 -07001116
MK Ryu5cfd96a2015-01-30 15:31:23 -08001117 timer = autotest_stats.Timer('deserialize_latency.%s' % (
1118 type(instance).__name__))
1119 with timer.get_client('local'):
1120 instance._deserialize_local(local)
1121 with timer.get_client('related'):
1122 instance._deserialize_relations(related)
Jakob Juelichf88fa932014-09-03 17:58:04 -07001123
1124 return instance
1125
1126
Jakob Juelicha94efe62014-09-18 16:02:49 -07001127 def sanity_check_update_from_shard(self, shard, updated_serialized,
1128 *args, **kwargs):
1129 """Check if an update sent from a shard is legitimate.
1130
1131 @raises error.UnallowedRecordsSentToMaster if an update is not
1132 legitimate.
1133 """
1134 raise NotImplementedError(
1135 'sanity_check_update_from_shard must be implemented by subclass %s '
1136 'for type %s' % type(self))
1137
1138
Prashanth Balasubramanian75be1d32014-11-25 18:03:09 -08001139 @transaction.commit_on_success
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001140 def update_from_serialized(self, serialized):
1141 """Updates local fields of an existing object from a serialized form.
1142
1143 This is different than the normal deserialize() in the way that it
1144 does update local values, which deserialize doesn't, but doesn't
1145 recursively propagate to related objects, which deserialize() does.
1146
1147 The use case of this function is to update job records on the master
1148 after the jobs have been executed on a slave, as the master is not
1149 interested in updates for users, labels, specialtasks, etc.
1150
1151 @param serialized: Representation of an object and its dependencies, as
1152 returned by serialize.
1153
1154 @raises ValueError: if serialized contains related objects, i.e. not
1155 only local fields.
1156 """
1157 local, related = (
1158 self._split_local_from_foreign_values(serialized))
1159 if related:
1160 raise ValueError('Serialized must not contain foreign '
1161 'objects: %s' % related)
1162
1163 self._deserialize_local(local)
1164
1165
Jakob Juelichf88fa932014-09-03 17:58:04 -07001166 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001167 """Allows overriding the deserialization behaviour by subclasses."""
Jakob Juelichf88fa932014-09-03 17:58:04 -07001168 raise NotImplementedError(
1169 'custom_deserialize_relation must be implemented by subclass %s '
1170 'for relation %s' % (type(self), link))
1171
1172
1173 def _deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001174 """Deserializes related objects and sets references on this object.
1175
1176 Relations that point to a list of objects are handled automatically.
1177 For many-to-one or one-to-one relations custom_deserialize_relation
1178 must be overridden by the subclass.
1179
1180 Related objects are deserialized using their deserialize() method.
1181 Thereby they and their dependencies are created if they don't exist
1182 and saved to the database.
1183
1184 @param link: Name of the relation.
1185 @param data: Serialized representation of the related object(s).
1186 This means a list of dictionaries for to-many relations,
1187 just a dictionary for to-one relations.
1188 """
Jakob Juelichf88fa932014-09-03 17:58:04 -07001189 field = getattr(self, link)
1190
1191 if field and hasattr(field, 'all'):
1192 self._deserialize_2m_relation(link, data, field.model)
1193 else:
1194 self.custom_deserialize_relation(link, data)
1195
1196
1197 def _deserialize_2m_relation(self, link, data, related_class):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001198 """Deserialize related objects for one to-many relationship.
1199
1200 @param link: Name of the relation.
1201 @param data: Serialized representation of the related objects.
1202 This is a list with of dictionaries.
Fang Dengff361592015-02-02 15:27:34 -08001203 @param related_class: A class representing a django model, with which
1204 this class has a one-to-many relationship.
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001205 """
Jakob Juelichf88fa932014-09-03 17:58:04 -07001206 relation_set = getattr(self, link)
Fang Dengff361592015-02-02 15:27:34 -08001207 if related_class == self.get_attribute_model():
1208 # When deserializing a model together with
1209 # its attributes, clear all the exising attributes to ensure
1210 # db consistency. Note 'update' won't be sufficient, as we also
1211 # want to remove any attributes that no longer exist in |data|.
1212 #
1213 # core_filters is a dictionary of filters, defines how
1214 # RelatedMangager would query for the 1-to-many relationship. E.g.
1215 # Host.objects.get(
1216 # id=20).hostattribute_set.core_filters = {host_id:20}
1217 # We use it to delete objects related to the current object.
1218 related_class.objects.filter(**relation_set.core_filters).delete()
Jakob Juelichf88fa932014-09-03 17:58:04 -07001219 for serialized in data:
1220 relation_set.add(related_class.deserialize(serialized))
1221
1222
Fang Dengff361592015-02-02 15:27:34 -08001223 @classmethod
1224 def get_attribute_model(cls):
1225 """Return the attribute model.
1226
1227 Subclass with attribute-like model should override this to
1228 return the attribute model class. This method will be
1229 called by _deserialize_2m_relation to determine whether
1230 to clear the one-to-many relations first on deserialization of object.
1231 """
1232 return None
1233
1234
showard7c785282008-05-29 19:45:12 +00001235class ModelWithInvalid(ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001236 """
1237 Overrides model methods save() and delete() to support invalidation in
1238 place of actual deletion. Subclasses must have a boolean "invalid"
1239 field.
1240 """
showard7c785282008-05-29 19:45:12 +00001241
showarda5288b42009-07-28 20:06:08 +00001242 def save(self, *args, **kwargs):
showardddb90992009-02-11 23:39:32 +00001243 first_time = (self.id is None)
1244 if first_time:
1245 # see if this object was previously added and invalidated
1246 my_name = getattr(self, self.name_field)
1247 filters = {self.name_field : my_name, 'invalid' : True}
1248 try:
1249 old_object = self.__class__.objects.get(**filters)
showardafd97de2009-10-01 18:45:09 +00001250 self.resurrect_object(old_object)
showardddb90992009-02-11 23:39:32 +00001251 except self.DoesNotExist:
1252 # no existing object
1253 pass
showard7c785282008-05-29 19:45:12 +00001254
showarda5288b42009-07-28 20:06:08 +00001255 super(ModelWithInvalid, self).save(*args, **kwargs)
showard7c785282008-05-29 19:45:12 +00001256
1257
showardafd97de2009-10-01 18:45:09 +00001258 def resurrect_object(self, old_object):
1259 """
1260 Called when self is about to be saved for the first time and is actually
1261 "undeleting" a previously deleted object. Can be overridden by
1262 subclasses to copy data as desired from the deleted entry (but this
1263 superclass implementation must normally be called).
1264 """
1265 self.id = old_object.id
1266
1267
jadmanski0afbb632008-06-06 21:10:57 +00001268 def clean_object(self):
1269 """
1270 This method is called when an object is marked invalid.
1271 Subclasses should override this to clean up relationships that
showardafd97de2009-10-01 18:45:09 +00001272 should no longer exist if the object were deleted.
1273 """
jadmanski0afbb632008-06-06 21:10:57 +00001274 pass
showard7c785282008-05-29 19:45:12 +00001275
1276
jadmanski0afbb632008-06-06 21:10:57 +00001277 def delete(self):
Dale Curtis74a314b2011-06-23 14:55:46 -07001278 self.invalid = self.invalid
jadmanski0afbb632008-06-06 21:10:57 +00001279 assert not self.invalid
1280 self.invalid = True
1281 self.save()
1282 self.clean_object()
showard7c785282008-05-29 19:45:12 +00001283
1284
jadmanski0afbb632008-06-06 21:10:57 +00001285 @classmethod
1286 def get_valid_manager(cls):
1287 return cls.valid_objects
showard7c785282008-05-29 19:45:12 +00001288
1289
jadmanski0afbb632008-06-06 21:10:57 +00001290 class Manipulator(object):
1291 """
1292 Force default manipulators to look only at valid objects -
1293 otherwise they will match against invalid objects when checking
1294 uniqueness.
1295 """
1296 @classmethod
1297 def _prepare(cls, model):
1298 super(ModelWithInvalid.Manipulator, cls)._prepare(model)
1299 cls.manager = model.valid_objects
showardf8b19042009-05-12 17:22:49 +00001300
1301
1302class ModelWithAttributes(object):
1303 """
1304 Mixin class for models that have an attribute model associated with them.
1305 The attribute model is assumed to have its value field named "value".
1306 """
1307
1308 def _get_attribute_model_and_args(self, attribute):
1309 """
1310 Subclasses should override this to return a tuple (attribute_model,
1311 keyword_args), where attribute_model is a model class and keyword_args
1312 is a dict of args to pass to attribute_model.objects.get() to get an
1313 instance of the given attribute on this object.
1314 """
Dale Curtis74a314b2011-06-23 14:55:46 -07001315 raise NotImplementedError
showardf8b19042009-05-12 17:22:49 +00001316
1317
1318 def set_attribute(self, attribute, value):
1319 attribute_model, get_args = self._get_attribute_model_and_args(
1320 attribute)
1321 attribute_object, _ = attribute_model.objects.get_or_create(**get_args)
1322 attribute_object.value = value
1323 attribute_object.save()
1324
1325
1326 def delete_attribute(self, attribute):
1327 attribute_model, get_args = self._get_attribute_model_and_args(
1328 attribute)
1329 try:
1330 attribute_model.objects.get(**get_args).delete()
showard16245422009-09-08 16:28:15 +00001331 except attribute_model.DoesNotExist:
showardf8b19042009-05-12 17:22:49 +00001332 pass
1333
1334
1335 def set_or_delete_attribute(self, attribute, value):
1336 if value is None:
1337 self.delete_attribute(attribute)
1338 else:
1339 self.set_attribute(attribute, value)
showard26b7ec72009-12-21 22:43:57 +00001340
1341
1342class ModelWithHashManager(dbmodels.Manager):
1343 """Manager for use with the ModelWithHash abstract model class"""
1344
1345 def create(self, **kwargs):
1346 raise Exception('ModelWithHash manager should use get_or_create() '
1347 'instead of create()')
1348
1349
1350 def get_or_create(self, **kwargs):
1351 kwargs['the_hash'] = self.model._compute_hash(**kwargs)
1352 return super(ModelWithHashManager, self).get_or_create(**kwargs)
1353
1354
1355class ModelWithHash(dbmodels.Model):
1356 """Superclass with methods for dealing with a hash column"""
1357
1358 the_hash = dbmodels.CharField(max_length=40, unique=True)
1359
1360 objects = ModelWithHashManager()
1361
1362 class Meta:
1363 abstract = True
1364
1365
1366 @classmethod
1367 def _compute_hash(cls, **kwargs):
1368 raise NotImplementedError('Subclasses must override _compute_hash()')
1369
1370
1371 def save(self, force_insert=False, **kwargs):
1372 """Prevents saving the model in most cases
1373
1374 We want these models to be immutable, so the generic save() operation
1375 will not work. These models should be instantiated through their the
1376 model.objects.get_or_create() method instead.
1377
1378 The exception is that save(force_insert=True) will be allowed, since
1379 that creates a new row. However, the preferred way to make instances of
1380 these models is through the get_or_create() method.
1381 """
1382 if not force_insert:
1383 # Allow a forced insert to happen; if it's a duplicate, the unique
1384 # constraint will catch it later anyways
1385 raise Exception('ModelWithHash is immutable')
1386 super(ModelWithHash, self).save(force_insert=force_insert, **kwargs)