Home | History | Annotate | Download | only in tko
      1 from django.db import models as dbmodels, connection
      2 from autotest_lib.frontend.afe import model_logic, readonly_connection
      3 
      4 _quote_name = connection.ops.quote_name
      5 
      6 class TempManager(model_logic.ExtendedManager):
      7     """A Temp Manager."""
      8     _GROUP_COUNT_NAME = 'group_count'
      9 
     10     def _get_key_unless_is_function(self, field):
     11         if '(' in field:
     12             return field
     13         return self.get_key_on_this_table(field)
     14 
     15 
     16     def _get_field_names(self, fields, extra_select_fields={}):
     17         field_names = []
     18         for field in fields:
     19             if field in extra_select_fields:
     20                 field_names.append(extra_select_fields[field][0])
     21             else:
     22                 field_names.append(self._get_key_unless_is_function(field))
     23         return field_names
     24 
     25 
     26     def _get_group_query_sql(self, query, group_by):
     27         compiler = query.query.get_compiler(using=query.db)
     28         sql, params = compiler.as_sql()
     29 
     30 
     31         # insert GROUP BY clause into query
     32         group_fields = self._get_field_names(group_by, query.query.extra_select)
     33         group_by_clause = ' GROUP BY ' + ', '.join(group_fields)
     34         group_by_position = sql.rfind('ORDER BY')
     35         if group_by_position == -1:
     36             group_by_position = len(sql)
     37         sql = (sql[:group_by_position] +
     38                group_by_clause + ' ' +
     39                sql[group_by_position:])
     40 
     41         return sql, params
     42 
     43 
     44     def _get_column_names(self, cursor):
     45         """Gets the column names from the cursor description.
     46 
     47         This method exists so that it can be mocked in the unit test for
     48         sqlite3 compatibility.
     49 
     50         """
     51         return [column_info[0] for column_info in cursor.description]
     52 
     53 
     54     def execute_group_query(self, query, group_by):
     55         """Performs the given query grouped by the specified fields.
     56 
     57         The given query's extra select fields are added.
     58 
     59         @param query: The query to perform.
     60         @param group_by: The fields by which to group.
     61 
     62         @return A list of dicts, where each dict corresponds to single row and
     63             contains a key for each grouped field as well as all of the extra
     64             select fields.
     65 
     66         """
     67         sql, params = self._get_group_query_sql(query, group_by)
     68         cursor = readonly_connection.cursor()
     69         cursor.execute(sql, params)
     70         field_names = self._get_column_names(cursor)
     71         row_dicts = [dict(zip(field_names, row)) for row in cursor.fetchall()]
     72         return row_dicts
     73 
     74 
     75     def get_count_sql(self, query):
     76         """Get SQL to select a per-group count of unique matches for a query.
     77 
     78         @param query: The query to use.
     79 
     80         @return A tuple (field alias, field SQL).
     81 
     82         """
     83         if query.query.distinct:
     84             pk_field = self.get_key_on_this_table()
     85             count_sql = 'COUNT(DISTINCT %s)' % pk_field
     86         else:
     87             count_sql = 'COUNT(1)'
     88         return self._GROUP_COUNT_NAME, count_sql
     89 
     90 
     91     def _get_num_groups_sql(self, query, group_by):
     92         group_fields = self._get_field_names(group_by, query.query.extra_select)
     93         query = query.order_by() # this can mess up the query and isn't needed
     94 
     95         compiler = query.query.get_compiler(using=query.db)
     96         sql, params = compiler.as_sql()
     97         from_ = sql[sql.find(' FROM'):]
     98         return ('SELECT DISTINCT %s %s' % (','.join(group_fields),
     99                                                   from_),
    100                 params)
    101 
    102 
    103     def _cursor_rowcount(self, cursor):
    104         """To be stubbed by tests"""
    105         return cursor.rowcount
    106 
    107 
    108     def get_num_groups(self, query, group_by):
    109         """Gets the number of distinct groups for a query.
    110 
    111         @param query: The query to use.
    112         @param group_by: The fields by which to group.
    113 
    114         @return The number of distinct groups for the given query grouped by
    115             the fields in group_by.
    116 
    117         """
    118         sql, params = self._get_num_groups_sql(query, group_by)
    119         cursor = readonly_connection.cursor()
    120         cursor.execute(sql, params)
    121         return self._cursor_rowcount(cursor)
    122 
    123 
    124 class Machine(dbmodels.Model):
    125     """Models a machine."""
    126     machine_idx = dbmodels.AutoField(primary_key=True)
    127     hostname = dbmodels.CharField(unique=True, max_length=255)
    128     machine_group = dbmodels.CharField(blank=True, max_length=240)
    129     owner = dbmodels.CharField(blank=True, max_length=240)
    130 
    131     class Meta:
    132         """Metadata for class Machine."""
    133         db_table = 'tko_machines'
    134 
    135 
    136 class Kernel(dbmodels.Model):
    137     """Models a kernel."""
    138     kernel_idx = dbmodels.AutoField(primary_key=True)
    139     kernel_hash = dbmodels.CharField(max_length=105, editable=False)
    140     base = dbmodels.CharField(max_length=90)
    141     printable = dbmodels.CharField(max_length=300)
    142 
    143     class Meta:
    144         """Metadata for class Kernel."""
    145         db_table = 'tko_kernels'
    146 
    147 
    148 class Patch(dbmodels.Model):
    149     """Models a patch."""
    150     kernel = dbmodels.ForeignKey(Kernel, db_column='kernel_idx')
    151     name = dbmodels.CharField(blank=True, max_length=240)
    152     url = dbmodels.CharField(blank=True, max_length=900)
    153     the_hash = dbmodels.CharField(blank=True, max_length=105, db_column='hash')
    154 
    155     class Meta:
    156         """Metadata for class Patch."""
    157         db_table = 'tko_patches'
    158 
    159 
    160 class Status(dbmodels.Model):
    161     """Models a status."""
    162     status_idx = dbmodels.AutoField(primary_key=True)
    163     word = dbmodels.CharField(max_length=30)
    164 
    165     class Meta:
    166         """Metadata for class Status."""
    167         db_table = 'tko_status'
    168 
    169 
    170 class Job(dbmodels.Model, model_logic.ModelExtensions):
    171     """Models a job."""
    172     job_idx = dbmodels.AutoField(primary_key=True)
    173     tag = dbmodels.CharField(unique=True, max_length=100)
    174     label = dbmodels.CharField(max_length=300)
    175     username = dbmodels.CharField(max_length=240)
    176     machine = dbmodels.ForeignKey(Machine, db_column='machine_idx')
    177     queued_time = dbmodels.DateTimeField(null=True, blank=True)
    178     started_time = dbmodels.DateTimeField(null=True, blank=True)
    179     finished_time = dbmodels.DateTimeField(null=True, blank=True)
    180     afe_job_id = dbmodels.IntegerField(null=True, default=None)
    181 
    182     objects = model_logic.ExtendedManager()
    183 
    184     class Meta:
    185         """Metadata for class Job."""
    186         db_table = 'tko_jobs'
    187 
    188 
    189 class JobKeyval(dbmodels.Model):
    190     """Models a job keyval."""
    191     job = dbmodels.ForeignKey(Job)
    192     key = dbmodels.CharField(max_length=90)
    193     value = dbmodels.CharField(blank=True, max_length=300)
    194 
    195     class Meta:
    196         """Metadata for class JobKeyval."""
    197         db_table = 'tko_job_keyvals'
    198 
    199 
    200 class Test(dbmodels.Model, model_logic.ModelExtensions,
    201            model_logic.ModelWithAttributes):
    202     """Models a test."""
    203     test_idx = dbmodels.AutoField(primary_key=True)
    204     job = dbmodels.ForeignKey(Job, db_column='job_idx')
    205     test = dbmodels.CharField(max_length=300)
    206     subdir = dbmodels.CharField(blank=True, max_length=300)
    207     kernel = dbmodels.ForeignKey(Kernel, db_column='kernel_idx')
    208     status = dbmodels.ForeignKey(Status, db_column='status')
    209     reason = dbmodels.CharField(blank=True, max_length=3072)
    210     machine = dbmodels.ForeignKey(Machine, db_column='machine_idx')
    211     finished_time = dbmodels.DateTimeField(null=True, blank=True)
    212     started_time = dbmodels.DateTimeField(null=True, blank=True)
    213     invalid = dbmodels.BooleanField(default=False)
    214     invalidates_test = dbmodels.ForeignKey(
    215             'self', null=True, db_column='invalidates_test_idx',
    216             related_name='invalidates_test_set')
    217 
    218     objects = model_logic.ExtendedManager()
    219 
    220     def _get_attribute_model_and_args(self, attribute):
    221         return TestAttribute, dict(test=self, attribute=attribute,
    222                                    user_created=True)
    223 
    224 
    225     def set_attribute(self, attribute, value):
    226         # ensure non-user-created attributes remain immutable
    227         try:
    228             TestAttribute.objects.get(test=self, attribute=attribute,
    229                                       user_created=False)
    230             raise ValueError('Attribute %s already exists for test %s and is '
    231                              'immutable' % (attribute, self.test_idx))
    232         except TestAttribute.DoesNotExist:
    233             super(Test, self).set_attribute(attribute, value)
    234 
    235     class Meta:
    236         """Metadata for class Test."""
    237         db_table = 'tko_tests'
    238 
    239 
    240 class TestAttribute(dbmodels.Model, model_logic.ModelExtensions):
    241     """Models a test attribute."""
    242     test = dbmodels.ForeignKey(Test, db_column='test_idx')
    243     attribute = dbmodels.CharField(max_length=90)
    244     value = dbmodels.CharField(blank=True, max_length=300)
    245     user_created = dbmodels.BooleanField(default=False)
    246 
    247     objects = model_logic.ExtendedManager()
    248 
    249     class Meta:
    250         """Metadata for class TestAttribute."""
    251         db_table = 'tko_test_attributes'
    252 
    253 
    254 class IterationAttribute(dbmodels.Model, model_logic.ModelExtensions):
    255     """Models an iteration attribute."""
    256     # This isn't really a primary key, but it's necessary to appease Django
    257     # and is harmless as long as we're careful.
    258     test = dbmodels.ForeignKey(Test, db_column='test_idx', primary_key=True)
    259     iteration = dbmodels.IntegerField()
    260     attribute = dbmodels.CharField(max_length=90)
    261     value = dbmodels.CharField(blank=True, max_length=300)
    262 
    263     objects = model_logic.ExtendedManager()
    264 
    265     class Meta:
    266         """Metadata for class IterationAttribute."""
    267         db_table = 'tko_iteration_attributes'
    268 
    269 
    270 class IterationResult(dbmodels.Model, model_logic.ModelExtensions):
    271     """Models an iteration result."""
    272     # See comment on IterationAttribute regarding primary_key=True.
    273     test = dbmodels.ForeignKey(Test, db_column='test_idx', primary_key=True)
    274     iteration = dbmodels.IntegerField()
    275     attribute = dbmodels.CharField(max_length=256)
    276     value = dbmodels.FloatField(null=True, blank=True)
    277 
    278     objects = model_logic.ExtendedManager()
    279 
    280     class Meta:
    281         """Metadata for class IterationResult."""
    282         db_table = 'tko_iteration_result'
    283 
    284 
    285 class TestLabel(dbmodels.Model, model_logic.ModelExtensions):
    286     """Models a test label."""
    287     name = dbmodels.CharField(max_length=80, unique=True)
    288     description = dbmodels.TextField(blank=True)
    289     tests = dbmodels.ManyToManyField(Test, blank=True,
    290                                      db_table='tko_test_labels_tests')
    291 
    292     name_field = 'name'
    293     objects = model_logic.ExtendedManager()
    294 
    295     class Meta:
    296         """Metadata for class TestLabel."""
    297         db_table = 'tko_test_labels'
    298 
    299 
    300 class SavedQuery(dbmodels.Model, model_logic.ModelExtensions):
    301     """Models a saved query."""
    302     # TODO: change this to foreign key once DBs are merged.
    303     owner = dbmodels.CharField(max_length=80)
    304     name = dbmodels.CharField(max_length=100)
    305     url_token = dbmodels.TextField()
    306 
    307     class Meta:
    308         """Metadata for class SavedQuery."""
    309         db_table = 'tko_saved_queries'
    310 
    311 
    312 # Views.
    313 
    314 class TestViewManager(TempManager):
    315     """A Test View Manager."""
    316 
    317     def get_query_set(self):
    318         query = super(TestViewManager, self).get_query_set()
    319 
    320         # add extra fields to selects, using the SQL itself as the "alias"
    321         extra_select = dict((sql, sql)
    322                             for sql in self.model.extra_fields.iterkeys())
    323         return query.extra(select=extra_select)
    324 
    325 
    326     def _get_include_exclude_suffix(self, exclude):
    327         if exclude:
    328             return '_exclude'
    329         return '_include'
    330 
    331 
    332     def _add_attribute_join(self, query_set, join_condition,
    333                             suffix=None, exclude=False):
    334         if suffix is None:
    335             suffix = self._get_include_exclude_suffix(exclude)
    336         return self.add_join(query_set, 'tko_test_attributes',
    337                              join_key='test_idx',
    338                              join_condition=join_condition,
    339                              suffix=suffix, exclude=exclude)
    340 
    341 
    342     def _add_label_pivot_table_join(self, query_set, suffix, join_condition='',
    343                                     exclude=False, force_left_join=False):
    344         return self.add_join(query_set, 'tko_test_labels_tests',
    345                              join_key='test_id',
    346                              join_condition=join_condition,
    347                              suffix=suffix, exclude=exclude,
    348                              force_left_join=force_left_join)
    349 
    350 
    351     def _add_label_joins(self, query_set, suffix=''):
    352         query_set = self._add_label_pivot_table_join(
    353                 query_set, suffix=suffix, force_left_join=True)
    354 
    355         # since we're not joining from the original table, we can't use
    356         # self.add_join() again
    357         second_join_alias = 'tko_test_labels' + suffix
    358         second_join_condition = ('%s.id = %s.testlabel_id' %
    359                                  (second_join_alias,
    360                                   'tko_test_labels_tests' + suffix))
    361         query_set.query.add_custom_join('tko_test_labels',
    362                                         second_join_condition,
    363                                         query_set.query.LOUTER,
    364                                         alias=second_join_alias)
    365         return query_set
    366 
    367 
    368     def _get_label_ids_from_names(self, label_names):
    369         label_ids = list( # listifying avoids a double query below
    370                 TestLabel.objects.filter(name__in=label_names)
    371                 .values_list('name', 'id'))
    372         if len(label_ids) < len(set(label_names)):
    373             raise ValueError('Not all labels found: %s' %
    374                              ', '.join(label_names))
    375         return dict(name_and_id for name_and_id in label_ids)
    376 
    377 
    378     def _include_or_exclude_labels(self, query_set, label_names, exclude=False):
    379         label_ids = self._get_label_ids_from_names(label_names).itervalues()
    380         suffix = self._get_include_exclude_suffix(exclude)
    381         condition = ('tko_test_labels_tests%s.testlabel_id IN (%s)' %
    382                      (suffix,
    383                       ','.join(str(label_id) for label_id in label_ids)))
    384         return self._add_label_pivot_table_join(query_set,
    385                                                 join_condition=condition,
    386                                                 suffix=suffix,
    387                                                 exclude=exclude)
    388 
    389 
    390     def _add_custom_select(self, query_set, select_name, select_sql):
    391         return query_set.extra(select={select_name: select_sql})
    392 
    393 
    394     def _add_select_value(self, query_set, alias):
    395         return self._add_custom_select(query_set, alias,
    396                                        _quote_name(alias) + '.value')
    397 
    398 
    399     def _add_select_ifnull(self, query_set, alias, non_null_value):
    400         select_sql = "IF(%s.id IS NOT NULL, '%s', NULL)" % (_quote_name(alias),
    401                                                             non_null_value)
    402         return self._add_custom_select(query_set, alias, select_sql)
    403 
    404 
    405     def _join_test_label_column(self, query_set, label_name, label_id):
    406         alias = 'test_label_' + label_name
    407         label_query = TestLabel.objects.filter(name=label_name)
    408         query_set = Test.objects.join_custom_field(query_set, label_query,
    409                                                    alias)
    410 
    411         query_set = self._add_select_ifnull(query_set, alias, label_name)
    412         return query_set
    413 
    414 
    415     def _join_test_label_columns(self, query_set, label_names):
    416         label_id_map = self._get_label_ids_from_names(label_names)
    417         for label_name in label_names:
    418             query_set = self._join_test_label_column(query_set, label_name,
    419                                                      label_id_map[label_name])
    420         return query_set
    421 
    422 
    423     def _join_test_attribute(self, query_set, attribute, alias=None,
    424                              extra_join_condition=None):
    425         """
    426         Join the given TestView QuerySet to TestAttribute.  The resulting query
    427         has an additional column for the given attribute named
    428         "attribute_<attribute name>".
    429         """
    430         if not alias:
    431             alias = 'test_attribute_' + attribute
    432         attribute_query = TestAttribute.objects.filter(attribute=attribute)
    433         if extra_join_condition:
    434             attribute_query = attribute_query.extra(
    435                     where=[extra_join_condition])
    436         query_set = Test.objects.join_custom_field(query_set, attribute_query,
    437                                                    alias)
    438 
    439         query_set = self._add_select_value(query_set, alias)
    440         return query_set
    441 
    442 
    443     def _join_machine_label_columns(self, query_set, machine_label_names):
    444         for label_name in machine_label_names:
    445             alias = 'machine_label_' + label_name
    446             condition = "FIND_IN_SET('%s', %s)" % (
    447                     label_name, _quote_name(alias) + '.value')
    448             query_set = self._join_test_attribute(
    449                     query_set, 'host-labels',
    450                     alias=alias, extra_join_condition=condition)
    451             query_set = self._add_select_ifnull(query_set, alias, label_name)
    452         return query_set
    453 
    454 
    455     def _join_one_iteration_key(self, query_set, result_key, first_alias=None):
    456         alias = 'iteration_result_' + result_key
    457         iteration_query = IterationResult.objects.filter(attribute=result_key)
    458         if first_alias:
    459             # after the first join, we need to match up iteration indices,
    460             # otherwise each join will expand the query by the number of
    461             # iterations and we'll have extraneous rows
    462             iteration_query = iteration_query.extra(
    463                     where=['%s.iteration = %s.iteration'
    464                            % (_quote_name(alias), _quote_name(first_alias))])
    465 
    466         query_set = Test.objects.join_custom_field(query_set, iteration_query,
    467                                                    alias, left_join=False)
    468         # select the iteration value and index for this join
    469         query_set = self._add_select_value(query_set, alias)
    470         if not first_alias:
    471             # for first join, add iteration index select too
    472             query_set = self._add_custom_select(
    473                     query_set, 'iteration_index',
    474                     _quote_name(alias) + '.iteration')
    475 
    476         return query_set, alias
    477 
    478 
    479     def _join_iteration_results(self, test_view_query_set, result_keys):
    480         """Join the given TestView QuerySet to IterationResult for one result.
    481 
    482         The resulting query looks like a TestView query but has one row per
    483         iteration.  Each row includes all the attributes of TestView, an
    484         attribute for each key in result_keys and an iteration_index attribute.
    485 
    486         We accomplish this by joining the TestView query to IterationResult
    487         once per result key.  Each join is restricted on the result key (and on
    488         the test index, like all one-to-many joins).  For the first join, this
    489         is the only restriction, so each TestView row expands to a row per
    490         iteration (per iteration that includes the key, of course).  For each
    491         subsequent join, we also restrict the iteration index to match that of
    492         the initial join.  This makes each subsequent join produce exactly one
    493         result row for each input row.  (This assumes each iteration contains
    494         the same set of keys.  Results are undefined if that's not true.)
    495         """
    496         if not result_keys:
    497             return test_view_query_set
    498 
    499         query_set, first_alias = self._join_one_iteration_key(
    500                 test_view_query_set, result_keys[0])
    501         for result_key in result_keys[1:]:
    502             query_set, _ = self._join_one_iteration_key(query_set, result_key,
    503                                                         first_alias=first_alias)
    504         return query_set
    505 
    506 
    507     def _join_job_keyvals(self, query_set, job_keyvals):
    508         for job_keyval in job_keyvals:
    509             alias = 'job_keyval_' + job_keyval
    510             keyval_query = JobKeyval.objects.filter(key=job_keyval)
    511             query_set = Job.objects.join_custom_field(query_set, keyval_query,
    512                                                        alias)
    513             query_set = self._add_select_value(query_set, alias)
    514         return query_set
    515 
    516 
    517     def _join_iteration_attributes(self, query_set, iteration_attributes):
    518         for attribute in iteration_attributes:
    519             alias = 'iteration_attribute_' + attribute
    520             attribute_query = IterationAttribute.objects.filter(
    521                     attribute=attribute)
    522             query_set = Test.objects.join_custom_field(query_set,
    523                                                        attribute_query, alias)
    524             query_set = self._add_select_value(query_set, alias)
    525         return query_set
    526 
    527 
    528     def get_query_set_with_joins(self, filter_data):
    529         """Add joins for querying over test-related items.
    530 
    531         These parameters are supported going forward:
    532         * test_attribute_fields: list of attribute names.  Each attribute will
    533                 be available as a column attribute_<name>.value.
    534         * test_label_fields: list of label names.  Each label will be available
    535                 as a column label_<name>.id, non-null iff the label is present.
    536         * iteration_result_fields: list of iteration result names.  Each
    537                 result will be available as a column iteration_<name>.value.
    538                 Note that this changes the semantics to return iterations
    539                 instead of tests -- if a test has multiple iterations, a row
    540                 will be returned for each one.  The iteration index is also
    541                 available as iteration_<name>.iteration.
    542         * machine_label_fields: list of machine label names.  Each will be
    543                 available as a column machine_label_<name>.id, non-null iff the
    544                 label is present on the machine used in the test.
    545         * job_keyval_fields: list of job keyval names. Each value will be
    546                 available as a column job_keyval_<name>.id, non-null iff the
    547                 keyval is present in the AFE job.
    548         * iteration_attribute_fields: list of iteration attribute names. Each
    549                 attribute will be available as a column
    550                 iteration_attribute<name>.id, non-null iff the attribute is
    551                 present.
    552 
    553         These parameters are deprecated:
    554         * include_labels
    555         * exclude_labels
    556         * include_attributes_where
    557         * exclude_attributes_where
    558 
    559         Additionally, this method adds joins if the following strings are
    560         present in extra_where (this is also deprecated):
    561         * test_labels
    562         * test_attributes_host_labels
    563 
    564         @param filter_data: Data by which to filter.
    565 
    566         @return A QuerySet.
    567 
    568         """
    569         query_set = self.get_query_set()
    570 
    571         test_attributes = filter_data.pop('test_attribute_fields', [])
    572         for attribute in test_attributes:
    573             query_set = self._join_test_attribute(query_set, attribute)
    574 
    575         test_labels = filter_data.pop('test_label_fields', [])
    576         query_set = self._join_test_label_columns(query_set, test_labels)
    577 
    578         machine_labels = filter_data.pop('machine_label_fields', [])
    579         query_set = self._join_machine_label_columns(query_set, machine_labels)
    580 
    581         iteration_keys = filter_data.pop('iteration_result_fields', [])
    582         query_set = self._join_iteration_results(query_set, iteration_keys)
    583 
    584         job_keyvals = filter_data.pop('job_keyval_fields', [])
    585         query_set = self._join_job_keyvals(query_set, job_keyvals)
    586 
    587         iteration_attributes = filter_data.pop('iteration_attribute_fields', [])
    588         query_set = self._join_iteration_attributes(query_set,
    589                                                     iteration_attributes)
    590 
    591         # everything that follows is deprecated behavior
    592 
    593         joined = False
    594 
    595         extra_where = filter_data.get('extra_where', '')
    596         if 'tko_test_labels' in extra_where:
    597             query_set = self._add_label_joins(query_set)
    598             joined = True
    599 
    600         include_labels = filter_data.pop('include_labels', [])
    601         exclude_labels = filter_data.pop('exclude_labels', [])
    602         if include_labels:
    603             query_set = self._include_or_exclude_labels(query_set,
    604                                                         include_labels)
    605             joined = True
    606         if exclude_labels:
    607             query_set = self._include_or_exclude_labels(query_set,
    608                                                         exclude_labels,
    609                                                         exclude=True)
    610             joined = True
    611 
    612         include_attributes_where = filter_data.pop('include_attributes_where',
    613                                                    '')
    614         exclude_attributes_where = filter_data.pop('exclude_attributes_where',
    615                                                    '')
    616         if include_attributes_where:
    617             query_set = self._add_attribute_join(
    618                 query_set,
    619                 join_condition=self.escape_user_sql(include_attributes_where))
    620             joined = True
    621         if exclude_attributes_where:
    622             query_set = self._add_attribute_join(
    623                 query_set,
    624                 join_condition=self.escape_user_sql(exclude_attributes_where),
    625                 exclude=True)
    626             joined = True
    627 
    628         if not joined:
    629             filter_data['no_distinct'] = True
    630 
    631         if 'tko_test_attributes_host_labels' in extra_where:
    632             query_set = self._add_attribute_join(
    633                 query_set, suffix='_host_labels',
    634                 join_condition='tko_test_attributes_host_labels.attribute = '
    635                                '"host-labels"')
    636 
    637         return query_set
    638 
    639 
    640     def query_test_ids(self, filter_data, apply_presentation=True):
    641         """Queries for test IDs.
    642 
    643         @param filter_data: Data by which to filter.
    644         @param apply_presentation: Whether or not to apply presentation
    645             parameters.
    646 
    647         @return A list of test IDs.
    648 
    649         """
    650         query = self.model.query_objects(filter_data,
    651                                          apply_presentation=apply_presentation)
    652         dicts = query.values('test_idx')
    653         return [item['test_idx'] for item in dicts]
    654 
    655 
    656     def query_test_label_ids(self, filter_data):
    657         """Queries for test label IDs.
    658 
    659         @param filter_data: Data by which to filter.
    660 
    661         @return A list of test label IDs.
    662 
    663         """
    664         query_set = self.model.query_objects(filter_data)
    665         query_set = self._add_label_joins(query_set, suffix='_list')
    666         rows = self._custom_select_query(query_set, ['tko_test_labels_list.id'])
    667         return [row[0] for row in rows if row[0] is not None]
    668 
    669 
    670     def escape_user_sql(self, sql):
    671         sql = super(TestViewManager, self).escape_user_sql(sql)
    672         return sql.replace('test_idx', self.get_key_on_this_table('test_idx'))
    673 
    674 
    675 class TestView(dbmodels.Model, model_logic.ModelExtensions):
    676     """Models a test view."""
    677     extra_fields = {
    678             'DATE(job_queued_time)': 'job queued day',
    679             'DATE(test_finished_time)': 'test finished day',
    680     }
    681 
    682     group_fields = [
    683             'test_name',
    684             'status',
    685             'kernel',
    686             'hostname',
    687             'job_tag',
    688             'job_name',
    689             'platform',
    690             'reason',
    691             'job_owner',
    692             'job_queued_time',
    693             'DATE(job_queued_time)',
    694             'test_started_time',
    695             'test_finished_time',
    696             'DATE(test_finished_time)',
    697     ]
    698 
    699     test_idx = dbmodels.IntegerField('test index', primary_key=True)
    700     job_idx = dbmodels.IntegerField('job index', null=True, blank=True)
    701     test_name = dbmodels.CharField(blank=True, max_length=90)
    702     subdir = dbmodels.CharField('subdirectory', blank=True, max_length=180)
    703     kernel_idx = dbmodels.IntegerField('kernel index')
    704     status_idx = dbmodels.IntegerField('status index')
    705     reason = dbmodels.CharField(blank=True, max_length=3072)
    706     machine_idx = dbmodels.IntegerField('host index')
    707     test_started_time = dbmodels.DateTimeField(null=True, blank=True)
    708     test_finished_time = dbmodels.DateTimeField(null=True, blank=True)
    709     job_tag = dbmodels.CharField(blank=True, max_length=300)
    710     job_name = dbmodels.CharField(blank=True, max_length=300)
    711     job_owner = dbmodels.CharField('owner', blank=True, max_length=240)
    712     job_queued_time = dbmodels.DateTimeField(null=True, blank=True)
    713     job_started_time = dbmodels.DateTimeField(null=True, blank=True)
    714     job_finished_time = dbmodels.DateTimeField(null=True, blank=True)
    715     afe_job_id = dbmodels.IntegerField(null=True)
    716     hostname = dbmodels.CharField(blank=True, max_length=300)
    717     platform = dbmodels.CharField(blank=True, max_length=240)
    718     machine_owner = dbmodels.CharField(blank=True, max_length=240)
    719     kernel_hash = dbmodels.CharField(blank=True, max_length=105)
    720     kernel_base = dbmodels.CharField(blank=True, max_length=90)
    721     kernel = dbmodels.CharField(blank=True, max_length=300)
    722     status = dbmodels.CharField(blank=True, max_length=30)
    723     invalid = dbmodels.BooleanField(blank=True)
    724     invalidates_test_idx = dbmodels.IntegerField(null=True, blank=True)
    725 
    726     objects = TestViewManager()
    727 
    728     def save(self):
    729         raise NotImplementedError('TestView is read-only')
    730 
    731 
    732     def delete(self):
    733         raise NotImplementedError('TestView is read-only')
    734 
    735 
    736     @classmethod
    737     def query_objects(cls, filter_data, initial_query=None,
    738                       apply_presentation=True):
    739         if initial_query is None:
    740             initial_query = cls.objects.get_query_set_with_joins(filter_data)
    741         return super(TestView, cls).query_objects(
    742                 filter_data, initial_query=initial_query,
    743                 apply_presentation=apply_presentation)
    744 
    745     class Meta:
    746         """Metadata for class TestView."""
    747         db_table = 'tko_test_view_2'
    748