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 class EmbeddedGraphingQuery(dbmodels.Model, model_logic.ModelExtensions):
    313     """Models an embedded graphing query."""
    314     url_token = dbmodels.TextField(null=False, blank=False)
    315     graph_type = dbmodels.CharField(max_length=16, null=False, blank=False)
    316     params = dbmodels.TextField(null=False, blank=False)
    317     last_updated = dbmodels.DateTimeField(null=False, blank=False,
    318                                           editable=False)
    319     # refresh_time shows the time at which a thread is updating the cached
    320     # image, or NULL if no one is updating the image. This is used so that only
    321     # one thread is updating the cached image at a time (see
    322     # graphing_utils.handle_plot_request).
    323     refresh_time = dbmodels.DateTimeField(editable=False)
    324     cached_png = dbmodels.TextField(editable=False)
    325 
    326     class Meta:
    327         """Metadata for class EmbeddedGraphingQuery."""
    328         db_table = 'tko_embedded_graphing_queries'
    329 
    330 
    331 # Views.
    332 
    333 class TestViewManager(TempManager):
    334     """A Test View Manager."""
    335 
    336     def get_query_set(self):
    337         query = super(TestViewManager, self).get_query_set()
    338 
    339         # add extra fields to selects, using the SQL itself as the "alias"
    340         extra_select = dict((sql, sql)
    341                             for sql in self.model.extra_fields.iterkeys())
    342         return query.extra(select=extra_select)
    343 
    344 
    345     def _get_include_exclude_suffix(self, exclude):
    346         if exclude:
    347             return '_exclude'
    348         return '_include'
    349 
    350 
    351     def _add_attribute_join(self, query_set, join_condition,
    352                             suffix=None, exclude=False):
    353         if suffix is None:
    354             suffix = self._get_include_exclude_suffix(exclude)
    355         return self.add_join(query_set, 'tko_test_attributes',
    356                              join_key='test_idx',
    357                              join_condition=join_condition,
    358                              suffix=suffix, exclude=exclude)
    359 
    360 
    361     def _add_label_pivot_table_join(self, query_set, suffix, join_condition='',
    362                                     exclude=False, force_left_join=False):
    363         return self.add_join(query_set, 'tko_test_labels_tests',
    364                              join_key='test_id',
    365                              join_condition=join_condition,
    366                              suffix=suffix, exclude=exclude,
    367                              force_left_join=force_left_join)
    368 
    369 
    370     def _add_label_joins(self, query_set, suffix=''):
    371         query_set = self._add_label_pivot_table_join(
    372                 query_set, suffix=suffix, force_left_join=True)
    373 
    374         # since we're not joining from the original table, we can't use
    375         # self.add_join() again
    376         second_join_alias = 'tko_test_labels' + suffix
    377         second_join_condition = ('%s.id = %s.testlabel_id' %
    378                                  (second_join_alias,
    379                                   'tko_test_labels_tests' + suffix))
    380         query_set.query.add_custom_join('tko_test_labels',
    381                                         second_join_condition,
    382                                         query_set.query.LOUTER,
    383                                         alias=second_join_alias)
    384         return query_set
    385 
    386 
    387     def _get_label_ids_from_names(self, label_names):
    388         label_ids = list( # listifying avoids a double query below
    389                 TestLabel.objects.filter(name__in=label_names)
    390                 .values_list('name', 'id'))
    391         if len(label_ids) < len(set(label_names)):
    392             raise ValueError('Not all labels found: %s' %
    393                              ', '.join(label_names))
    394         return dict(name_and_id for name_and_id in label_ids)
    395 
    396 
    397     def _include_or_exclude_labels(self, query_set, label_names, exclude=False):
    398         label_ids = self._get_label_ids_from_names(label_names).itervalues()
    399         suffix = self._get_include_exclude_suffix(exclude)
    400         condition = ('tko_test_labels_tests%s.testlabel_id IN (%s)' %
    401                      (suffix,
    402                       ','.join(str(label_id) for label_id in label_ids)))
    403         return self._add_label_pivot_table_join(query_set,
    404                                                 join_condition=condition,
    405                                                 suffix=suffix,
    406                                                 exclude=exclude)
    407 
    408 
    409     def _add_custom_select(self, query_set, select_name, select_sql):
    410         return query_set.extra(select={select_name: select_sql})
    411 
    412 
    413     def _add_select_value(self, query_set, alias):
    414         return self._add_custom_select(query_set, alias,
    415                                        _quote_name(alias) + '.value')
    416 
    417 
    418     def _add_select_ifnull(self, query_set, alias, non_null_value):
    419         select_sql = "IF(%s.id IS NOT NULL, '%s', NULL)" % (_quote_name(alias),
    420                                                             non_null_value)
    421         return self._add_custom_select(query_set, alias, select_sql)
    422 
    423 
    424     def _join_test_label_column(self, query_set, label_name, label_id):
    425         alias = 'test_label_' + label_name
    426         label_query = TestLabel.objects.filter(name=label_name)
    427         query_set = Test.objects.join_custom_field(query_set, label_query,
    428                                                    alias)
    429 
    430         query_set = self._add_select_ifnull(query_set, alias, label_name)
    431         return query_set
    432 
    433 
    434     def _join_test_label_columns(self, query_set, label_names):
    435         label_id_map = self._get_label_ids_from_names(label_names)
    436         for label_name in label_names:
    437             query_set = self._join_test_label_column(query_set, label_name,
    438                                                      label_id_map[label_name])
    439         return query_set
    440 
    441 
    442     def _join_test_attribute(self, query_set, attribute, alias=None,
    443                              extra_join_condition=None):
    444         """
    445         Join the given TestView QuerySet to TestAttribute.  The resulting query
    446         has an additional column for the given attribute named
    447         "attribute_<attribute name>".
    448         """
    449         if not alias:
    450             alias = 'test_attribute_' + attribute
    451         attribute_query = TestAttribute.objects.filter(attribute=attribute)
    452         if extra_join_condition:
    453             attribute_query = attribute_query.extra(
    454                     where=[extra_join_condition])
    455         query_set = Test.objects.join_custom_field(query_set, attribute_query,
    456                                                    alias)
    457 
    458         query_set = self._add_select_value(query_set, alias)
    459         return query_set
    460 
    461 
    462     def _join_machine_label_columns(self, query_set, machine_label_names):
    463         for label_name in machine_label_names:
    464             alias = 'machine_label_' + label_name
    465             condition = "FIND_IN_SET('%s', %s)" % (
    466                     label_name, _quote_name(alias) + '.value')
    467             query_set = self._join_test_attribute(
    468                     query_set, 'host-labels',
    469                     alias=alias, extra_join_condition=condition)
    470             query_set = self._add_select_ifnull(query_set, alias, label_name)
    471         return query_set
    472 
    473 
    474     def _join_one_iteration_key(self, query_set, result_key, first_alias=None):
    475         alias = 'iteration_result_' + result_key
    476         iteration_query = IterationResult.objects.filter(attribute=result_key)
    477         if first_alias:
    478             # after the first join, we need to match up iteration indices,
    479             # otherwise each join will expand the query by the number of
    480             # iterations and we'll have extraneous rows
    481             iteration_query = iteration_query.extra(
    482                     where=['%s.iteration = %s.iteration'
    483                            % (_quote_name(alias), _quote_name(first_alias))])
    484 
    485         query_set = Test.objects.join_custom_field(query_set, iteration_query,
    486                                                    alias, left_join=False)
    487         # select the iteration value and index for this join
    488         query_set = self._add_select_value(query_set, alias)
    489         if not first_alias:
    490             # for first join, add iteration index select too
    491             query_set = self._add_custom_select(
    492                     query_set, 'iteration_index',
    493                     _quote_name(alias) + '.iteration')
    494 
    495         return query_set, alias
    496 
    497 
    498     def _join_iteration_results(self, test_view_query_set, result_keys):
    499         """Join the given TestView QuerySet to IterationResult for one result.
    500 
    501         The resulting query looks like a TestView query but has one row per
    502         iteration.  Each row includes all the attributes of TestView, an
    503         attribute for each key in result_keys and an iteration_index attribute.
    504 
    505         We accomplish this by joining the TestView query to IterationResult
    506         once per result key.  Each join is restricted on the result key (and on
    507         the test index, like all one-to-many joins).  For the first join, this
    508         is the only restriction, so each TestView row expands to a row per
    509         iteration (per iteration that includes the key, of course).  For each
    510         subsequent join, we also restrict the iteration index to match that of
    511         the initial join.  This makes each subsequent join produce exactly one
    512         result row for each input row.  (This assumes each iteration contains
    513         the same set of keys.  Results are undefined if that's not true.)
    514         """
    515         if not result_keys:
    516             return test_view_query_set
    517 
    518         query_set, first_alias = self._join_one_iteration_key(
    519                 test_view_query_set, result_keys[0])
    520         for result_key in result_keys[1:]:
    521             query_set, _ = self._join_one_iteration_key(query_set, result_key,
    522                                                         first_alias=first_alias)
    523         return query_set
    524 
    525 
    526     def _join_job_keyvals(self, query_set, job_keyvals):
    527         for job_keyval in job_keyvals:
    528             alias = 'job_keyval_' + job_keyval
    529             keyval_query = JobKeyval.objects.filter(key=job_keyval)
    530             query_set = Job.objects.join_custom_field(query_set, keyval_query,
    531                                                        alias)
    532             query_set = self._add_select_value(query_set, alias)
    533         return query_set
    534 
    535 
    536     def _join_iteration_attributes(self, query_set, iteration_attributes):
    537         for attribute in iteration_attributes:
    538             alias = 'iteration_attribute_' + attribute
    539             attribute_query = IterationAttribute.objects.filter(
    540                     attribute=attribute)
    541             query_set = Test.objects.join_custom_field(query_set,
    542                                                        attribute_query, alias)
    543             query_set = self._add_select_value(query_set, alias)
    544         return query_set
    545 
    546 
    547     def get_query_set_with_joins(self, filter_data):
    548         """Add joins for querying over test-related items.
    549 
    550         These parameters are supported going forward:
    551         * test_attribute_fields: list of attribute names.  Each attribute will
    552                 be available as a column attribute_<name>.value.
    553         * test_label_fields: list of label names.  Each label will be available
    554                 as a column label_<name>.id, non-null iff the label is present.
    555         * iteration_result_fields: list of iteration result names.  Each
    556                 result will be available as a column iteration_<name>.value.
    557                 Note that this changes the semantics to return iterations
    558                 instead of tests -- if a test has multiple iterations, a row
    559                 will be returned for each one.  The iteration index is also
    560                 available as iteration_<name>.iteration.
    561         * machine_label_fields: list of machine label names.  Each will be
    562                 available as a column machine_label_<name>.id, non-null iff the
    563                 label is present on the machine used in the test.
    564         * job_keyval_fields: list of job keyval names. Each value will be
    565                 available as a column job_keyval_<name>.id, non-null iff the
    566                 keyval is present in the AFE job.
    567         * iteration_attribute_fields: list of iteration attribute names. Each
    568                 attribute will be available as a column
    569                 iteration_attribute<name>.id, non-null iff the attribute is
    570                 present.
    571 
    572         These parameters are deprecated:
    573         * include_labels
    574         * exclude_labels
    575         * include_attributes_where
    576         * exclude_attributes_where
    577 
    578         Additionally, this method adds joins if the following strings are
    579         present in extra_where (this is also deprecated):
    580         * test_labels
    581         * test_attributes_host_labels
    582 
    583         @param filter_data: Data by which to filter.
    584 
    585         @return A QuerySet.
    586 
    587         """
    588         query_set = self.get_query_set()
    589 
    590         test_attributes = filter_data.pop('test_attribute_fields', [])
    591         for attribute in test_attributes:
    592             query_set = self._join_test_attribute(query_set, attribute)
    593 
    594         test_labels = filter_data.pop('test_label_fields', [])
    595         query_set = self._join_test_label_columns(query_set, test_labels)
    596 
    597         machine_labels = filter_data.pop('machine_label_fields', [])
    598         query_set = self._join_machine_label_columns(query_set, machine_labels)
    599 
    600         iteration_keys = filter_data.pop('iteration_result_fields', [])
    601         query_set = self._join_iteration_results(query_set, iteration_keys)
    602 
    603         job_keyvals = filter_data.pop('job_keyval_fields', [])
    604         query_set = self._join_job_keyvals(query_set, job_keyvals)
    605 
    606         iteration_attributes = filter_data.pop('iteration_attribute_fields', [])
    607         query_set = self._join_iteration_attributes(query_set,
    608                                                     iteration_attributes)
    609 
    610         # everything that follows is deprecated behavior
    611 
    612         joined = False
    613 
    614         extra_where = filter_data.get('extra_where', '')
    615         if 'tko_test_labels' in extra_where:
    616             query_set = self._add_label_joins(query_set)
    617             joined = True
    618 
    619         include_labels = filter_data.pop('include_labels', [])
    620         exclude_labels = filter_data.pop('exclude_labels', [])
    621         if include_labels:
    622             query_set = self._include_or_exclude_labels(query_set,
    623                                                         include_labels)
    624             joined = True
    625         if exclude_labels:
    626             query_set = self._include_or_exclude_labels(query_set,
    627                                                         exclude_labels,
    628                                                         exclude=True)
    629             joined = True
    630 
    631         include_attributes_where = filter_data.pop('include_attributes_where',
    632                                                    '')
    633         exclude_attributes_where = filter_data.pop('exclude_attributes_where',
    634                                                    '')
    635         if include_attributes_where:
    636             query_set = self._add_attribute_join(
    637                 query_set,
    638                 join_condition=self.escape_user_sql(include_attributes_where))
    639             joined = True
    640         if exclude_attributes_where:
    641             query_set = self._add_attribute_join(
    642                 query_set,
    643                 join_condition=self.escape_user_sql(exclude_attributes_where),
    644                 exclude=True)
    645             joined = True
    646 
    647         if not joined:
    648             filter_data['no_distinct'] = True
    649 
    650         if 'tko_test_attributes_host_labels' in extra_where:
    651             query_set = self._add_attribute_join(
    652                 query_set, suffix='_host_labels',
    653                 join_condition='tko_test_attributes_host_labels.attribute = '
    654                                '"host-labels"')
    655 
    656         return query_set
    657 
    658 
    659     def query_test_ids(self, filter_data, apply_presentation=True):
    660         """Queries for test IDs.
    661 
    662         @param filter_data: Data by which to filter.
    663         @param apply_presentation: Whether or not to apply presentation
    664             parameters.
    665 
    666         @return A list of test IDs.
    667 
    668         """
    669         query = self.model.query_objects(filter_data,
    670                                          apply_presentation=apply_presentation)
    671         dicts = query.values('test_idx')
    672         return [item['test_idx'] for item in dicts]
    673 
    674 
    675     def query_test_label_ids(self, filter_data):
    676         """Queries for test label IDs.
    677 
    678         @param filter_data: Data by which to filter.
    679 
    680         @return A list of test label IDs.
    681 
    682         """
    683         query_set = self.model.query_objects(filter_data)
    684         query_set = self._add_label_joins(query_set, suffix='_list')
    685         rows = self._custom_select_query(query_set, ['tko_test_labels_list.id'])
    686         return [row[0] for row in rows if row[0] is not None]
    687 
    688 
    689     def escape_user_sql(self, sql):
    690         sql = super(TestViewManager, self).escape_user_sql(sql)
    691         return sql.replace('test_idx', self.get_key_on_this_table('test_idx'))
    692 
    693 
    694 class TestView(dbmodels.Model, model_logic.ModelExtensions):
    695     """Models a test view."""
    696     extra_fields = {
    697             'DATE(job_queued_time)': 'job queued day',
    698             'DATE(test_finished_time)': 'test finished day',
    699     }
    700 
    701     group_fields = [
    702             'test_name',
    703             'status',
    704             'kernel',
    705             'hostname',
    706             'job_tag',
    707             'job_name',
    708             'platform',
    709             'reason',
    710             'job_owner',
    711             'job_queued_time',
    712             'DATE(job_queued_time)',
    713             'test_started_time',
    714             'test_finished_time',
    715             'DATE(test_finished_time)',
    716     ]
    717 
    718     test_idx = dbmodels.IntegerField('test index', primary_key=True)
    719     job_idx = dbmodels.IntegerField('job index', null=True, blank=True)
    720     test_name = dbmodels.CharField(blank=True, max_length=90)
    721     subdir = dbmodels.CharField('subdirectory', blank=True, max_length=180)
    722     kernel_idx = dbmodels.IntegerField('kernel index')
    723     status_idx = dbmodels.IntegerField('status index')
    724     reason = dbmodels.CharField(blank=True, max_length=3072)
    725     machine_idx = dbmodels.IntegerField('host index')
    726     test_started_time = dbmodels.DateTimeField(null=True, blank=True)
    727     test_finished_time = dbmodels.DateTimeField(null=True, blank=True)
    728     job_tag = dbmodels.CharField(blank=True, max_length=300)
    729     job_name = dbmodels.CharField(blank=True, max_length=300)
    730     job_owner = dbmodels.CharField('owner', blank=True, max_length=240)
    731     job_queued_time = dbmodels.DateTimeField(null=True, blank=True)
    732     job_started_time = dbmodels.DateTimeField(null=True, blank=True)
    733     job_finished_time = dbmodels.DateTimeField(null=True, blank=True)
    734     afe_job_id = dbmodels.IntegerField(null=True)
    735     hostname = dbmodels.CharField(blank=True, max_length=300)
    736     platform = dbmodels.CharField(blank=True, max_length=240)
    737     machine_owner = dbmodels.CharField(blank=True, max_length=240)
    738     kernel_hash = dbmodels.CharField(blank=True, max_length=105)
    739     kernel_base = dbmodels.CharField(blank=True, max_length=90)
    740     kernel = dbmodels.CharField(blank=True, max_length=300)
    741     status = dbmodels.CharField(blank=True, max_length=30)
    742     invalid = dbmodels.BooleanField(blank=True)
    743     invalidates_test_idx = dbmodels.IntegerField(null=True, blank=True)
    744 
    745     objects = TestViewManager()
    746 
    747     def save(self):
    748         raise NotImplementedError('TestView is read-only')
    749 
    750 
    751     def delete(self):
    752         raise NotImplementedError('TestView is read-only')
    753 
    754 
    755     @classmethod
    756     def query_objects(cls, filter_data, initial_query=None,
    757                       apply_presentation=True):
    758         if initial_query is None:
    759             initial_query = cls.objects.get_query_set_with_joins(filter_data)
    760         return super(TestView, cls).query_objects(
    761                 filter_data, initial_query=initial_query,
    762                 apply_presentation=apply_presentation)
    763 
    764     class Meta:
    765         """Metadata for class TestView."""
    766         db_table = 'tko_test_view_2'
    767