• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
312# Views.
313
314class 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
675class 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