Coverage for CIResults/views.py: 78%
587 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-19 09:20 +0000
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-19 09:20 +0000
1from django.contrib import messages
2from django.contrib.auth.mixins import PermissionRequiredMixin
3from django.shortcuts import render, redirect, get_object_or_404
4from django.core.exceptions import ValidationError, PermissionDenied
5from django.utils.safestring import mark_safe
6from django.views import View
7from django.views.generic import DetailView, ListView
8from django.views.generic.base import TemplateView
9from django.views.generic.edit import UpdateView, FormView
10from django.http import JsonResponse, HttpResponseRedirect
11from django.db import transaction
12from django.db.models import Prefetch
13from django.urls import reverse
14from django.utils.functional import cached_property
15from django.core.paginator import Paginator
17from collections import namedtuple
19from .models import BugTracker, Bug, Component, Build, Test, Machine, RunConfigTag, RunConfig, TestSuite, TextStatus
20from .models import TestResult, IssueFilter, IssueFilterAssociated, Issue, KnownFailure, UnknownFailure, MachineTag
22from .metrics import metrics_testresult_statuses_stats, metrics_testresult_machines_stats
23from .metrics import metrics_testresult_tests_stats, metrics_testresult_issues_stats
24from .metrics import metrics_knownfailure_statuses_stats, metrics_knownfailure_machines_stats
25from .metrics import metrics_knownfailure_tests_stats, metrics_knownfailure_issues_stats
27from .forms import TestMassRenameForm
29from .filtering import QueryCreator, QueryParser
31import traceback
32import markdown
33import json
34import re
37def index(request, **kwargs):
38 uf = UnknownFailure.objects.select_related("result").prefetch_related(
39 "result__test",
40 "result__test__testsuite",
41 "result__ts_run",
42 "result__ts_run__machine",
43 "result__ts_run__machine__tags",
44 "result__ts_run__runconfig",
45 "result__ts_run__testsuite",
46 "result__status__testsuite",
47 "result__status",
48 "matched_archived_ifas",
49 "matched_archived_ifas__issue__bugs__tracker",
50 )
51 uf = uf.defer("result__dmesg", "result__stdout", "result__stderr")
52 uf = uf.filter(result__ts_run__runconfig__temporary=False).order_by('id')
54 ifas_query = IssueFilterAssociated.objects.filter(deleted_on=None).all()
55 issues = Issue.objects.filter(archived_on=None,
56 expected=False).prefetch_related(Prefetch('filters',
57 to_attr='ifas_cached',
58 queryset=ifas_query),
59 "ifas_cached__filter",
60 "ifas_cached__added_by",
61 "ifas_cached__deleted_by",
62 "bugs__assignee__person",
63 "bugs__tracker",
64 "bugs").order_by('-id')
66 def get_page(object_query, page, page_size=100):
67 paginator = SafePaginator(object_query, page_size)
68 return paginator.page(page)
70 query_get_param = request.GET.copy()
72 suppressed_tests_query = QueryCreator(request, Test, prefix="suppressed_tests").multiple_request_params_to_query()
73 suppressed_tests = suppressed_tests_query.objects.prefetch_related('testsuite').filter(vetted_on=None) \
74 .exclude(first_runconfig=None).prefetch_related('first_runconfig')
75 suppressed_tests_page = query_get_param.get('suppressed_tests', 1)
77 suppressed_machines_query = QueryCreator(request, Machine,
78 prefix="suppressed_machine_lst").multiple_request_params_to_query()
79 suppressed_machines = suppressed_machines_query.objects.filter(vetted_on=None)
80 suppressed_machines_page = query_get_param.get('suppressed_machines', 1)
82 paginated_tables = {
83 'suppressed_tests': {
84 'table': suppressed_tests,
85 'page': suppressed_tests_page,
86 },
87 'suppressed_machines': {
88 'table': suppressed_machines,
89 'page': suppressed_machines_page,
90 }
91 }
92 for table_name in paginated_tables:
93 if table_name == query_get_param.get('table') and (page := query_get_param.get('page')):
94 paginated_tables[table_name]['page'] = page
95 paginated_tables[table_name]['table'] = get_page(
96 paginated_tables[table_name]['table'],
97 paginated_tables[table_name]['page'],
98 )
99 query_get_param.setlist(table_name, [paginated_tables[table_name]['page']])
101 suppressed_statuses_query = QueryCreator(request, TextStatus,
102 prefix="suppressed_statuses_lst").multiple_request_params_to_query()
103 suppressed_statuses = suppressed_statuses_query.objects.filter(vetted_on=None)
105 context = {
106 "trackers": BugTracker.objects.all(),
107 "issues": issues,
108 "suppressed_tests": paginated_tables['suppressed_tests']['table'],
109 "suppressed_machines": paginated_tables['suppressed_machines']['table'],
110 "suppressed_statuses": suppressed_statuses,
111 "unknown_failures": uf,
112 "query_get_param": query_get_param,
113 }
114 return render(request, 'CIResults/index.html', context)
117class SafePaginator(Paginator):
118 def validate_number(self, number):
119 try:
120 number = int(number)
121 except ValueError:
122 number = 1
123 return min(number, self.num_pages)
126class IssueListView(ListView):
127 model = Issue
129 def get_queryset(self):
130 self.query = QueryCreator(self.request, self.model).request_to_query()
132 # Prefetch all the necessary things before returning the list
133 ifas_query = IssueFilterAssociated.objects.all()
134 issues = self.query.objects.prefetch_related(Prefetch('filters',
135 to_attr='ifas_cached',
136 queryset=ifas_query),
137 "ifas_cached__filter",
138 "ifas_cached__added_by",
139 "ifas_cached__deleted_by",
140 "bugs__tracker",
141 "bugs")
142 if self.query.orderby is None:
143 issues = issues.order_by('-id')
144 return issues
146 def get_context_data(self):
147 context = super().get_context_data()
148 context["trackers"] = BugTracker.objects.all()
149 context['issues'] = context.pop('object_list')
150 context['filters_model'] = Issue
151 context['query'] = self.query
152 return context
154 def post(self, request, *args, **kwargs):
155 return super().get(request, *args, **kwargs)
158class IssueView(View):
159 ParsedParams = namedtuple('ParsedParams',
160 'bugs bugs_form description error filters')
162 def __parse_params__(self, params):
163 # Keep track of all errors, only commit if no error occured
164 error = False
166 # Parse the bugs
167 bugs = []
168 bugs_form = []
169 for param in params:
170 if param.startswith("bug_id_"):
171 # Check the bug_id
172 bug_id = params[param].strip()
173 if len(bug_id) == 0:
174 continue
176 # Check the tracker
177 suffix = param[7:]
178 tracker_pk = params.get("bug_tracker_{}".format(suffix), -1)
179 try:
180 tracker_pk = int(tracker_pk)
181 tracker = BugTracker.objects.get(pk=tracker_pk)
182 tracker_valid = True
184 # Check the bug
185 try:
186 bug = Bug.objects.get(tracker=tracker, bug_id=bug_id)
187 bugs.append(bug)
188 bug_valid = True
189 except Exception:
190 # The bug does not exist, try to create it
191 try:
192 bug = Bug(tracker=tracker, bug_id=bug_id)
193 bug.poll()
194 bugs.append(bug)
195 bug_valid = True
196 except Exception:
197 error = True
198 bug_valid = False
199 traceback.print_exc()
200 except Exception:
201 error = True
202 tracker_valid = False
203 bug_valid = False
204 traceback.print_exc()
206 # Keep the information about the bugs, in case we need to
207 # use it again to re-draw the page
208 bugs_form.append({"bug_id": bug_id, "tracker_id": tracker_pk,
209 "bug_valid": bug_valid,
210 "tracker_valid": tracker_valid})
212 # Parse the filters
213 filters = []
214 for param in params:
215 if param.startswith("filter_"):
216 try:
217 pk = int(params[param])
218 filters.append(IssueFilter.objects.get(pk=pk))
219 except Exception:
220 error = True
221 continue
223 # Get the description
224 description = params.get("description", "")
226 return self.ParsedParams(bugs=bugs, bugs_form=bugs_form,
227 description=description, error=error,
228 filters=filters)
230 def __fetch_from_db__(self, pk):
231 issue = get_object_or_404(Issue, pk=pk)
233 bugs = issue.bugs.all()
234 bugs_form = []
235 for bug in bugs:
236 bugs_form.append({"bug_id": bug.bug_id, "tracker_id": bug.tracker.id,
237 "bug_valid": True, "tracker_valid": True})
239 filters_assoc = IssueFilterAssociated.objects.filter(deleted_on=None, issue=issue)
240 filters = [e.filter for e in filters_assoc]
242 return self.ParsedParams(bugs=bugs, bugs_form=bugs_form,
243 description=issue.description, error=False,
244 filters=filters)
246 @transaction.atomic
247 def __save__(self, pk, d, user):
248 if d.error:
249 raise ValueError("The data provided contained error")
251 if len(d.filters) == 0:
252 raise ValueError("No filters found")
254 if len(d.bugs) == 0:
255 raise ValueError("No bugs found")
257 # Try to find the issue to edit, if applicable
258 try:
259 issue = Issue.objects.get(id=pk)
261 # Update the fields
262 issue.description = d.description
263 issue.save()
264 except Issue.DoesNotExist:
265 issue = Issue.objects.create(filer=user.email, added_by=user,
266 description=d.description)
268 # Add the bugs and filters that were set
269 issue.set_bugs(d.bugs)
270 issue.set_filters(d.filters, user)
272 def __show_page__(self, request, pk, d):
273 # Pre-allocate a line for the bug
274 if len(d.bugs_form) == 0:
275 d.bugs_form.append({"bug_id": "", "tracker_id": -1,
276 "bug_valid": True, "tracker_valid": True})
278 # find out whether we are creating or editing an issue
279 if pk is None:
280 save_url = reverse('CIResults-issue', kwargs={"action": 'create'})
281 issue_title = "New Issue"
282 pk = ""
283 else:
284 save_url = reverse('CIResults-issue', kwargs={"action": "edit",
285 "pk": pk})
286 issue_title = "Edit Issue {}".format(pk)
288 filters = list(IssueFilter.objects.filter(hidden=False).order_by("-id")[:20])
289 for f in d.filters:
290 if f not in filters:
291 filters.append(f)
293 # Get the list of tests referenced by this issue, then add all the ones
294 # that have an associated first runconfig
295 tests = set()
296 for f in d.filters:
297 tests.update(f.tests.all())
298 tests.update(Test.objects.prefetch_related("testsuite").exclude(first_runconfig=None))
300 # Render the page
301 context = {
302 "issue_id": pk,
303 "issue_title": issue_title,
304 "tags": RunConfigTag.objects.all(),
305 "machine_tags": MachineTag.objects.all(),
306 "machines": Machine.objects.all(),
307 "tests": tests,
308 "statuses": TextStatus.objects.prefetch_related("testsuite").all(),
309 "trackers": BugTracker.objects.all(),
310 "filters": filters,
311 "field_bugs": d.bugs_form, # This data may not be valid or commited yet
312 "field_filters": d.filters,
313 "field_description": d.description,
314 "save_url": save_url,
315 "filters_model": UnknownFailure,
316 }
317 return render(request, 'CIResults/issue.html', context)
319 def __edit_issue__(self, request, action, pk, params):
320 if pk is not None and len(params) == 0:
321 if not request.user.has_perm('CIResults.change_issue'):
322 raise PermissionDenied()
324 # We are editing an issue and have not provided updates for it
325 d = self.__fetch_from_db__(pk)
326 return self.__show_page__(request, pk, d)
327 else:
328 if not request.user.has_perm('CIResults.add_issue'):
329 raise PermissionDenied()
331 # In any other case, just display what we have, or default values
332 d = self.__parse_params__(params)
334 # Save, if possible. Otherwise, just show the page again
335 try:
336 self.__save__(pk, d, request.user)
337 return redirect('CIResults-index')
338 except ValueError:
339 return self.__show_page__(request, pk, d)
341 def __route_request__(self, request, kwargs, params):
342 post_actions = ["archive", "restore", "hide", "show"]
344 action = kwargs['action']
345 if action in post_actions:
346 if request.method != 'POST':
347 raise ValidationError("The action '{}' is unsupported in a {} request".format(action, request.method))
349 # Check permissions
350 if not request.user.has_perm('CIResults.{}_issue'.format(action)):
351 raise PermissionDenied()
353 issue = get_object_or_404(Issue, pk=kwargs.get('pk'))
355 if action == 'archive':
356 issue.archive(request.user)
357 else:
358 getattr(issue, action)()
360 return redirect(request.META.get('HTTP_REFERER', 'CIResults-index'))
361 else:
362 # Action == edit or create
363 return self.__edit_issue__(request, action,
364 kwargs.get('pk'), params)
366 def get(self, request, *args, **kwargs):
367 return self.__route_request__(request, kwargs, request.GET)
369 def post(self, request, *args, **kwargs):
370 return self.__route_request__(request, kwargs, request.POST)
373class IssueFilterView(View):
374 def __update_stats__(self, stats, result):
375 for tag in result.ts_run.runconfig.tags.all():
376 stats['tags'].add((tag.id, str(tag)))
377 stats['machines'].add((result.ts_run.machine.id, str(result.ts_run.machine)))
378 if len(result.ts_run.machine.tags_cached) == 0:
379 stats['tagless_machines'].add((result.ts_run.machine.id, str(result.ts_run.machine)))
380 else:
381 for tag in result.ts_run.machine.tags_cached:
382 stats['machine_tags'].add((tag.id, str(tag)))
383 stats['tests'].add((result.test.id, str(result.test)))
384 stats['statuses'].add((result.status.id, str(result.status)))
386 def __check_regexp__(self, params, field, errors):
387 try:
388 regexp = params.get(field, "")
389 re.compile(regexp, re.DOTALL)
390 return True
391 except re.error as e:
392 errors.append({"field": field, "msg": str(e)})
393 return False
395 def __parse_filter_from_params__(self, params) -> IssueFilter:
396 filter = IssueFilter(description=params.get('description'),
397 stdout_regex=params.get('stdout_regex', ""),
398 stderr_regex=params.get('stderr_regex', ""),
399 dmesg_regex=params.get('dmesg_regex', ""),
400 user_query=params.get('user_query', ""))
401 filter.tags_ids_cached = set(params.get("tags", []))
402 filter.__machines_cached__ = set(Machine.objects.filter(id__in=params.get("machines", [])))
403 filter.__machine_tags_cached__ = set(MachineTag.objects.filter(id__in=params.get("machine_tags", [])))
404 filter.tests_ids_cached = set(params.get("tests", []))
405 filter.statuses_ids_cached = set(params.get("statuses", []))
407 # Assign for purpose of converting to user query
408 filter.tags_cached = set(RunConfigTag.objects.filter(id__in=params.get("tags", [])))
409 filter.tests_cached = set(Test.objects.filter(id__in=params.get("tests", [])))
410 filter.statuses_cached = set(TextStatus.objects.filter(id__in=params.get("statuses", [])))
411 return filter
413 def __stats__(self, request):
414 stats = {
415 "matched": {
416 "tags": set(),
417 "machine_tags": set(),
418 "machines": set(),
419 "tagless_machines": set(),
420 "tests": set(),
421 "statuses": set()
422 },
423 "errors": [
424 ],
425 }
427 # Read the parameters and perform some input validation
428 params = json.loads(request.body.decode())
429 self.__check_regexp__(params, 'stdout_regex', stats['errors'])
430 self.__check_regexp__(params, 'stderr_regex', stats['errors'])
431 self.__check_regexp__(params, 'dmesg_regex', stats['errors'])
433 filter = self.__parse_filter_from_params__(params)
435 parser = QueryParser(UnknownFailure, filter.equivalent_user_query)
436 if parser.error:
437 stats['errors'].append({"field": "user_query", "msg": parser.error})
439 # Check the filter
440 if len(stats['errors']) == 0:
441 filter = self.__parse_filter_from_params__(params)
443 # Check which failures are matched by this filter
444 matched_failures = (
445 parser.objects.select_related("result")
446 .prefetch_related(
447 "result__ts_run__runconfig__tags",
448 "result__ts_run__machine",
449 "result__ts_run__machine__tags",
450 "result__test",
451 "result__test__testsuite",
452 "result__status",
453 )
454 .filter(result__ts_run__runconfig__temporary=False)
455 .defer("result__dmesg", "result__stdout", "result__stderr")
456 )
458 for failure in matched_failures:
459 self.__update_stats__(stats['matched'], failure.result)
461 for key in stats['matched']:
462 stats['matched'][key] = sorted(stats['matched'][key])
463 stats['matched']['failure_total'] = UnknownFailure.objects.filter(
464 result__ts_run__runconfig__temporary=False
465 ).count()
466 stats['matched']['failure_count'] = len(matched_failures)
467 else:
468 for key in stats['matched']:
469 stats['matched'][key] = sorted(stats['matched'][key])
470 stats['matched']['failure_total'] = 0
471 stats['matched']['failure_count'] = 0
473 return JsonResponse(stats)
475 def __convert_to_user_query__(self, request):
476 params = json.loads(request.body.decode())
477 filter = self.__parse_filter_from_params__(params)
478 return JsonResponse({"userQuery": filter._to_user_query()})
480 def post(self, request, *args, **kwargs):
481 # Technically, this view cannot edit filters, but it deals with a lot of
482 # input and it is only useful when editing issues, so it is safer to hide it
483 if not request.user.has_perm('CIResults.change_issue'):
484 raise PermissionDenied()
486 action = kwargs.get('action')
487 if action == "stats":
488 return self.__stats__(request)
489 if action == "convert-to-user-query":
490 return self.__convert_to_user_query__(request)
492 raise ValidationError("The action '{}' is unsupported".format(action))
495class MassVettingView(PermissionRequiredMixin, View):
496 def post(self, request, *args, **kwargs):
497 object_ids = set()
498 for param in request.POST:
499 try:
500 object_ids.add(int(param))
501 except ValueError:
502 continue
504 objects = self.model.objects.filter(pk__in=object_ids)
505 for obj in objects:
506 obj.vet()
508 objects_str = [str(o) for o in objects]
509 messages.success(self.request, "Successfully vetted the {} object(s): {}".format(len(objects),
510 ", ".join(objects_str)))
512 return redirect('CIResults-index')
515class MachineMassVettingView(MassVettingView):
516 model = Machine
517 permission_required = 'CIResults.vet_machine'
520class TestMassVettingView(MassVettingView):
521 model = Test
522 permission_required = 'CIResults.vet_test'
525class TextStatusMassVettingView(MassVettingView):
526 model = TextStatus
527 permission_required = 'CIResults.vet_textstatus'
530class TestEditView(PermissionRequiredMixin, UpdateView):
531 model = Test
532 fields = ['public', 'vetted_on']
533 template_name = 'CIResults/test_edit.html'
534 permission_required = 'CIResults.change_test'
536 def get_success_url(self):
537 messages.success(self.request, "The test {} has been edited".format(self.object))
538 return reverse("CIResults-tests")
541class MachineEditView(PermissionRequiredMixin, UpdateView):
542 model = Machine
543 fields = ['description', 'public', 'vetted_on', 'aliases', 'color_hex', 'tags']
544 template_name = 'CIResults/machine_edit.html'
545 permission_required = 'CIResults.change_machine'
547 def get_success_url(self):
548 messages.success(self.request, "The machine {} has been edited".format(self.object))
549 return reverse("CIResults-machines")
552class TestMassRenameView(PermissionRequiredMixin, FormView):
553 form_class = TestMassRenameForm
554 template_name = 'CIResults/test_mass_rename.html'
555 permission_required = 'CIResults.change_test'
557 def get_success_url(self):
558 return reverse("CIResults-tests")
560 def form_valid(self, form):
561 if not form.is_valid():
562 return super().form_valid(form)
564 if self.request.POST.get('action') == 'check':
565 return render(self.request, self.template_name, {'form': form})
566 else:
567 form.do_renaming()
568 messages.success(self.request,
569 "Renamed {} tests ('{}' -> '{}')".format(len(form.affected_tests),
570 form.cleaned_data.get('substring_from'),
571 form.cleaned_data.get('substring_to')))
572 return super().form_valid(form)
575class TestRenameView(PermissionRequiredMixin, UpdateView):
576 model = Test
577 fields = ['name']
578 template_name = 'CIResults/test_rename.html'
579 permission_required = 'CIResults.change_test'
581 def form_valid(self, form):
582 # We do not want to save any change, we just want to call the test's rename method
583 self.object.rename(form.cleaned_data['name'])
584 messages.success(self.request, "The test {} has been renamed to {}".format(self.object,
585 form.cleaned_data['name']))
586 return HttpResponseRedirect(reverse("CIResults-tests"))
589class filterView(DetailView):
590 model = IssueFilter
591 template_name = 'CIResults/filter.html'
594class SimpleSearchableMixin:
595 @property
596 def query(self):
597 return self.request.GET.get('q', '')
599 # TODO: Add a nice UI for this
600 def get_paginate_by(self, queryset):
601 return self.request.GET.get('page_size', self.paginate_by)
603 def get_context_data(self):
604 context = super().get_context_data()
605 context['search_query'] = self.query
606 context['query_get_param'] = "q=" + self.query
607 return context
610class TestListView(ListView):
611 model = Test
612 paginator_class = SafePaginator
613 paginate_by = 100
615 def get_queryset(self):
616 self.query = QueryCreator(self.request, self.model).multiple_request_params_to_query()
617 tests = self.query.objects.order_by('testsuite__name', 'name').prefetch_related('testsuite',
618 'first_runconfig')
619 return tests.exclude(first_runconfig=None)
621 def get_context_data(self):
622 context = super().get_context_data()
623 context['query_get_param'] = self.request.GET.copy()
624 return context
627class MachineListView(ListView):
628 model = Machine
629 paginator_class = SafePaginator
630 paginate_by = 100
632 def get_queryset(self):
633 self.query = QueryCreator(self.request, self.model).multiple_request_params_to_query()
634 machines = self.query.objects.order_by('name').prefetch_related('aliases', 'tags')
636 return machines
638 def get_context_data(self):
639 context = super().get_context_data()
640 context['query_get_param'] = self.request.GET.copy()
641 return context
644class UserFiltrableMixin:
645 # TODO: Add a nice UI for this
646 def get_paginate_by(self, queryset):
647 return self.request.GET.get('page_size', self.paginate_by)
649 def get_context_data(self):
650 context = super().get_context_data()
651 context['query_get_param'] = self.request.GET.copy()
652 context['query'] = self.query
654 return context
657class TestResultListView(UserFiltrableMixin, ListView):
658 model = TestResult
659 paginator_class = SafePaginator
660 paginate_by = 100
662 def get_userquery(self):
663 return None
665 def get_queryset(self):
666 if self.get_userquery() is None:
667 self.query = QueryCreator(self.request, self.model).request_to_query()
668 else:
669 self.query = self.model.from_user_filters(**self.get_userquery())
671 if not self.query.is_empty:
672 query = self.query.objects
673 query = query.filter(ts_run__runconfig__temporary=False) # Do not show results from temp runs
674 query = query.prefetch_related('test',
675 'test__testsuite',
676 'ts_run',
677 'ts_run__machine',
678 'ts_run__runconfig',
679 'ts_run__testsuite',
680 'status',
681 'status__testsuite',
682 'known_failures__matched_ifa__filter',
683 'known_failures__matched_ifa__issue__bugs__tracker')
684 query = query.defer('stdout', 'stderr', 'dmesg')
685 if self.query.orderby is None:
686 query = query.order_by('-ts_run__runconfig__added_on')
687 return query
688 else:
689 return self.model.objects.none().order_by('id')
691 def get_context_data(self):
692 context = super().get_context_data()
693 context['results'] = context.pop('object_list')
695 context["statuses_chart"] = metrics_testresult_statuses_stats(context['results']).stats()
696 context["machines_chart"] = metrics_testresult_machines_stats(context['results']).stats()
697 context["tests_chart"] = metrics_testresult_tests_stats(context['results']).stats()
698 context["issues_chart"] = metrics_testresult_issues_stats(context['results']).stats()
700 if self.get_userquery() is not None:
701 context['filter_url'] = reverse('CIResults-results')
703 return context
705 def post(self, request, *args, **kwargs):
706 return super().get(request, *args, **kwargs)
709class KnownFailureListView(UserFiltrableMixin, ListView):
710 model = KnownFailure
711 paginator_class = SafePaginator
712 paginate_by = 100
714 def get_userquery(self):
715 return None
717 def get_queryset(self):
718 if self.get_userquery() is None:
719 self.query = QueryCreator(self.request, self.model).request_to_query()
720 else:
721 self.query = self.model.from_user_filters(**self.get_userquery())
723 if not self.query.is_empty:
724 query = self.query.objects
725 query = query.filter(result__ts_run__runconfig__temporary=False) # Do not show results from temp runs
726 query = query.prefetch_related('result__test',
727 'result__test__testsuite',
728 'result__ts_run',
729 'result__ts_run__machine',
730 'result__ts_run__runconfig',
731 'result__ts_run__testsuite',
732 'result__status',
733 'result__status__testsuite',
734 'matched_ifa__filter',
735 'matched_ifa__issue__bugs__tracker')
736 if self.query.orderby is None:
737 query = query.order_by('-result__ts_run__runconfig__added_on')
738 return query
739 else:
740 return self.model.objects.none().order_by('id')
742 def get_context_data(self):
743 context = super().get_context_data()
744 context['failures'] = context.pop('object_list')
746 context["statuses_chart"] = metrics_knownfailure_statuses_stats(context['failures']).stats()
747 context["machines_chart"] = metrics_knownfailure_machines_stats(context['failures']).stats()
748 context["tests_chart"] = metrics_knownfailure_tests_stats(context['failures']).stats()
749 context["issues_chart"] = metrics_knownfailure_issues_stats(context['failures']).stats()
751 if self.get_userquery() is not None:
752 context['filter_url'] = reverse('CIResults-knownfailures')
754 return context
756 def post(self, request, *args, **kwargs):
757 return super().get(request, *args, **kwargs)
760class IssueDetailView(KnownFailureListView):
761 template_name = 'CIResults/issue_detail.html'
763 @cached_property
764 def issue(self):
765 return get_object_or_404(Issue, pk=self.kwargs['pk'])
767 def get_userquery(self):
768 return {'query': "issue_id={}".format(self.issue.id)}
770 def get_context_data(self):
771 context = super().get_context_data()
772 context['issue'] = self.issue
773 return context
776class IFADetailView(KnownFailureListView):
777 template_name = 'CIResults/ifa_detail.html'
779 @cached_property
780 def ifa(self):
781 return get_object_or_404(IssueFilterAssociated, pk=self.kwargs['pk'])
783 def get_userquery(self):
784 return {'query': "ifa_id={}".format(self.ifa.id)}
786 def get_context_data(self):
787 context = super().get_context_data()
788 context['ifa'] = self.ifa
789 return context
792class TestSuiteDetailView(KnownFailureListView):
793 template_name = 'CIResults/testsuite_detail.html'
795 @cached_property
796 def testsuite(self):
797 return get_object_or_404(TestSuite, pk=self.kwargs['pk'])
799 def get_userquery(self):
800 return {'query': "testsuite_name='{}'".format(self.testsuite.name)}
802 def get_context_data(self):
803 context = super().get_context_data()
804 context['testsuite'] = self.testsuite
805 return context
808class TestDetailView(KnownFailureListView):
809 template_name = 'CIResults/test_detail.html'
811 @cached_property
812 def test(self):
813 return get_object_or_404(Test, pk=self.kwargs['pk'])
815 def get_userquery(self):
816 return {'query': "test_name='{}'".format(self.test.name)}
818 def get_context_data(self):
819 context = super().get_context_data()
820 context['test'] = self.test
821 return context
824class TextStatusDetailView(KnownFailureListView):
825 template_name = 'CIResults/textstatus_detail.html'
827 @cached_property
828 def status(self):
829 return get_object_or_404(TextStatus, pk=self.kwargs['pk'])
831 def get_userquery(self):
832 return {'query': "status_name='{}'".format(self.status.name)}
834 def get_context_data(self):
835 context = super().get_context_data()
836 context['status'] = self.status
837 return context
840class MachineDetailView(KnownFailureListView):
841 template_name = 'CIResults/machine_detail.html'
843 @cached_property
844 def machine(self):
845 return get_object_or_404(Machine, pk=self.kwargs['pk'])
847 def get_userquery(self):
848 return {'query': "machine_name='{}'".format(self.machine.name)}
850 def get_context_data(self):
851 context = super().get_context_data()
852 context['machine'] = self.machine
853 return context
856class TestResultDetailView(TestResultListView):
857 template_name = 'CIResults/testresult_detail.html'
859 @cached_property
860 def testresult(self):
861 return get_object_or_404(TestResult, pk=self.kwargs['pk'])
863 def get_userquery(self):
864 return {'query': "test_name='{}' AND machine_name='{}'".format(self.testresult.test.name,
865 self.testresult.ts_run.machine.name)}
867 def get_context_data(self):
868 context = super().get_context_data()
869 context['testresult'] = self.testresult
870 return context
873class RunConfigDetailView(KnownFailureListView):
874 template_name = 'CIResults/runconfig_detail.html'
876 @cached_property
877 def runconfig(self):
878 return get_object_or_404(RunConfig, pk=self.kwargs['pk'])
880 def get_userquery(self):
881 return {'query': "runconfig_name='{}'".format(self.runconfig.name)}
883 def get_context_data(self):
884 context = super().get_context_data()
885 context['runconfig'] = self.runconfig
886 return context
889class RunConfigTagDetailView(KnownFailureListView):
890 template_name = 'CIResults/runconfig_tag_detail.html'
892 @cached_property
893 def tag(self):
894 return get_object_or_404(RunConfigTag, pk=self.kwargs['pk'])
896 def get_userquery(self):
897 return {'query': "runconfig_tag='{}'".format(self.tag.name)}
899 def get_context_data(self):
900 context = super().get_context_data()
901 context['tag'] = self.tag
902 return context
905class BuildDetailView(KnownFailureListView):
906 template_name = 'CIResults/build_detail.html'
908 @cached_property
909 def build(self):
910 return get_object_or_404(Build, pk=self.kwargs['pk'])
912 def get_userquery(self):
913 return {'query': "build_name='{}'".format(self.build.name)}
915 def get_context_data(self):
916 context = super().get_context_data()
917 context['build'] = self.build
918 return context
921class ComponentDetailView(KnownFailureListView):
922 template_name = 'CIResults/component_detail.html'
924 @cached_property
925 def component(self):
926 return get_object_or_404(Component, pk=self.kwargs['pk'])
928 def get_userquery(self):
929 return {'query': "component_name='{}'".format(self.component.name)}
931 def get_context_data(self):
932 context = super().get_context_data()
933 context['component'] = self.component
934 return context
937class ResultsCompareView(TemplateView):
938 template_name = "CIResults/compare_results.html"
940 def name_to_runconfig(self, name):
941 if name is None:
942 return None, False
944 run = RunConfig.objects.filter(name=name).first()
945 return run, run is None
947 def urlify(self, markdown):
948 regex = re.compile(r'(https?:\/\/[^\s,;]+)')
949 return regex.sub(r'<\1>', markdown)
951 def get_context_data(self):
952 run_from, from_error = self.name_to_runconfig(self.request.GET.get('from'))
953 run_to, to_error = self.name_to_runconfig(self.request.GET.get('to'))
954 no_compress = self.request.GET.get('nocompress') is not None
955 query = QueryCreator(self.request, TestResult).request_to_query()
957 context = super().get_context_data()
958 context['runconfigs'] = RunConfig.objects.filter(temporary=False).order_by('-added_on')[0:50]
959 context['from_error'] = from_error
960 context['to_error'] = to_error
961 context['no_compress'] = no_compress
962 context['filters_model'] = TestResult
963 context['query'] = query
965 if run_from and run_to:
966 diff = run_from.compare(run_to, no_compress=no_compress, query=query)
967 context['diff'] = diff
968 context['html_result'] = mark_safe(markdown.markdown(self.urlify(diff.text),
969 extensions=['nl2br']))
971 return context