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