1from django.db import models as dbmodels, connection
2from autotest_lib.frontend.afe import model_logic, readonly_connection
3
4_quote_name = connection.ops.quote_name
5
6class 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
124class 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
136class 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
148class 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
160class 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
170class 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
189class 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
200class 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
240class 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
254class 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
270class 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
285class 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
300class 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
312class 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
333class 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
694class 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