Passed
Push — master ( 62d7d9...8230b1 )
by Alexander
03:29
created

TestRunReportView.get_context_data()   A

Complexity

Conditions 2

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 25
dl 0
loc 40
rs 9.28
c 0
b 0
f 0
cc 2
nop 2
1
# -*- coding: utf-8 -*-
2
3
from datetime import datetime
4
from http import HTTPStatus
5
6
from django.conf import settings
7
from django.contrib import messages
8
from django.contrib.auth.decorators import permission_required
9
from django.contrib.auth import get_user_model
10
from django.contrib.contenttypes.models import ContentType
11
from django.core.exceptions import ObjectDoesNotExist
12
from django.urls import reverse
13
from django.db.models import Count
14
from django.db.models import Q
15
from django.http import HttpResponseRedirect, Http404, JsonResponse
16
from django.shortcuts import get_object_or_404, render
17
from django.utils.decorators import method_decorator
18
from django.utils.translation import ugettext_lazy as _
19
from django.views.generic.base import TemplateView
20
from django.views.generic.base import View
21
22
from django_comments.models import Comment
23
24
from tcms.core.utils import clean_request
25
from tcms.management.models import Priority, Tag
26
from tcms.testcases.models import TestCasePlan, TestCaseStatus, BugSystem
27
from tcms.testcases.views import get_selected_testcases
28
from tcms.testplans.models import TestPlan
29
from tcms.testruns.data import TestExecutionDataMixin
30
from tcms.testruns.forms import NewRunForm, SearchRunForm, BaseRunForm
31
from tcms.testruns.models import TestRun, TestExecution, TestExecutionStatus
32
from tcms.core.contrib.linkreference.models import LinkReference
33
34
35
User = get_user_model()  # pylint: disable=invalid-name
36
37
38
@method_decorator(permission_required('testruns.add_testrun'), name='dispatch')
39
class CreateTestRunView(View):
40
    """Display the create test run page."""
41
42
    template_name = 'testruns/mutable.html'
43
    http_method_names = ['post']
44
45
    def post(self, request):
46
        # If from_plan does not exist will redirect to plans for select a plan
47
        plan_id = request.POST.get('from_plan')
48
        if not plan_id:
49
            messages.add_message(request,
50
                                 messages.ERROR,
51
                                 _('Creating a TestRun requires a TestPlan, select one'))
52
            return HttpResponseRedirect(reverse('plans-search'))
53
54
        if not request.POST.get('case'):
55
            messages.add_message(request,
56
                                 messages.ERROR,
57
                                 _('Creating a TestRun requires at least one TestCase'))
58
            return HttpResponseRedirect(reverse('test_plan_url_short', args=[plan_id]))
59
60
        # Ready to write cases to test plan
61
        test_cases = get_selected_testcases(request)
62
        test_plan = TestPlan.objects.get(plan_id=plan_id)
63
64
        # note: ordered by case_id for test_show_create_new_run_page()
65
        tcs_values = test_cases.select_related('author',
66
                                               'case_status',
67
                                               'category',
68
                                               'priority').order_by('case_id')
69
70
        if request.POST.get('POSTING_TO_CREATE'):
71
            form = NewRunForm(request.POST)
72
            form.populate(product_id=test_plan.product_id)
73
74
            if form.is_valid():
75
                # Process the data in form.cleaned_data
76
                default_tester = form.cleaned_data['default_tester']
77
78
                test_run = TestRun.objects.create(
79
                    product_version=test_plan.product_version,
80
                    stop_date=None,
81
                    summary=form.cleaned_data.get('summary'),
82
                    notes=form.cleaned_data.get('notes'),
83
                    plan=test_plan,
84
                    build=form.cleaned_data['build'],
85
                    manager=form.cleaned_data['manager'],
86
                    default_tester=default_tester,
87
                )
88
89
                loop = 1
90
                for case in form.cleaned_data['case']:
91
                    try:
92
                        tcp = TestCasePlan.objects.get(plan=test_plan, case=case)
93
                        sortkey = tcp.sortkey
94
                    except ObjectDoesNotExist:
95
                        sortkey = loop * 10
96
97
                    test_run.add_case_run(case=case, sortkey=sortkey,
98
                                          assignee=default_tester)
99
                    loop += 1
100
101
                return HttpResponseRedirect(
102
                    reverse('testruns-get', args=[test_run.run_id, ])
103
                )
104
105
        else:
106
            form = NewRunForm(initial={
107
                'summary': 'Test run for %s' % test_plan.name,
108
                'manager': test_plan.author.email,
109
                'default_tester': request.user.email,
110
                'notes': '',
111
            })
112
            form.populate(product_id=test_plan.product_id)
113
114
        context_data = {
115
            'test_plan': test_plan,
116
            'test_cases': tcs_values,
117
            'form': form,
118
        }
119
        return render(request, self.template_name, context_data)
120
121
122
class SearchTestRunView(TemplateView):  # pylint: disable=missing-permission-required
123
124
    template_name = 'testruns/search.html'
125
126
    def get_context_data(self, **kwargs):
127
        form = SearchRunForm(self.request.GET)
128
        form.populate(product_id=self.request.GET.get('product'))
129
130
        return {
131
            'form': form,
132
        }
133
134
135
def _open_run_get_executions(request, run):  # pylint: disable=missing-permission-required
136
    """Prepare for executions list in a TestRun page
137
138
    This is an internal method. Do not call this directly.
139
    """
140
141
    executions = run.case_run.select_related(
142
        'run', 'case'
143
    ).only('run__run_id',
144
           'run__plan',
145
           'status',
146
           'assignee',
147
           'tested_by',
148
           'case_text_version',
149
           'sortkey',
150
           'case__summary',
151
           'case__is_automated',
152
           'case__priority',
153
           'case__category__name'
154
           )
155
156
    # Continue to search the executionss with conditions
157
    # 4. executions preparing for render executions table
158
    executions = executions.filter(**clean_request(request))
159
    order_by = request.GET.get('order_by')
160
    if order_by:
161
        return executions.order_by(order_by)
162
163
    return executions.order_by('sortkey', 'pk')
164
165
166
def open_run_get_comments_subtotal(case_run_ids):
167
    content_type = ContentType.objects.get_for_model(TestExecution)
168
    query_set = Comment.objects.filter(
169
        content_type=content_type,
170
        site_id=settings.SITE_ID,
171
        object_pk__in=case_run_ids,
172
        is_removed=False).values('object_pk').annotate(comment_count=Count('pk')).order_by(
173
            'object_pk')
174
175
    result = ((int(row['object_pk']), row['comment_count']) for row in query_set)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable row does not seem to be defined.
Loading history...
176
    return dict(result)
177
178
179
def open_run_get_users(case_runs):
180
    tester_ids = set()
181
    assignee_ids = set()
182
    for case_run in case_runs:
183
        if case_run.tested_by_id:
184
            tester_ids.add(case_run.tested_by_id)
185
        if case_run.assignee_id:
186
            assignee_ids.add(case_run.assignee_id)
187
    testers = User.objects.filter(
188
        pk__in=tester_ids).values_list('pk', 'username')
189
    assignees = User.objects.filter(
190
        pk__in=assignee_ids).values_list('pk', 'username')
191
    return (dict(testers.iterator()), dict(assignees.iterator()))
192
193
194
class GetTestRunView(TemplateView):  # pylint: disable=missing-permission-required
195
    """Display testrun's details"""
196
197
    template_name = 'run/get.html'
198
199
    def get_context_data(self, **kwargs):
200
        # Get the test run
201
        try:
202
            # 1. get test run itself
203
            test_run = TestRun.objects.select_related().get(run_id=kwargs['run_id'])
204
        except ObjectDoesNotExist:
205
            raise Http404
206
207
        # Get the test executions that belong to the run
208
        # 2. get test run's all executions
209
        test_executions = _open_run_get_executions(self.request, test_run)
210
211
        status = TestExecutionStatus.objects.only('pk', 'name').order_by('pk')
212
213
        # Count the status
214
        # 3. calculate number of executions of each status
215
        status_stats_result = test_run.stats_executions_status(status)
216
217
        # Get the test execution bugs summary
218
        # 6. get the number of bugs of this run
219
        execution_bugs_count = test_run.get_bug_count()
220
221
        return {
222
            'test_run': test_run,
223
            'executions': _walk_executions(test_executions),
224
            'executions_count': len(test_executions),
225
            'status_stats': status_stats_result,
226
            'execution_bugs_count': execution_bugs_count,
227
            'test_status': status,
228
            'priorities': Priority.objects.filter(is_active=True),
229
            'case_own_tags': _get_tags(test_executions),
230
            'bug_trackers': BugSystem.objects.all(),
231
        }
232
233
234
def _get_tags(test_executions):
235
    """Get tag list of testcases"""
236
237
    # Get the list of testcases belong to the run
238
    test_cases = []
239
    for test_execution in test_executions:
240
        test_cases.append(test_execution.case_id)
241
242
    tags = Tag.objects.filter(case__in=test_cases).values_list('name', flat=True)
243
    tags = list(set(tags))
244
    tags.sort()
245
246
    return tags
247
248
249
def _walk_executions(test_executions):
250
    """Walking executions for helping rendering executions table"""
251
252
    priorities = dict(Priority.objects.values_list('pk', 'value'))
253
    testers, assignees = open_run_get_users(test_executions)
254
    execution_pks = []
255
    for execution in test_executions:
256
        execution_pks.append(execution.pk)
257
    comments_subtotal = open_run_get_comments_subtotal(execution_pks)
258
    status = TestExecutionStatus.get_names()
259
260
    for execution in test_executions:
261
        yield (execution,
262
               testers.get(execution.tested_by_id, None),
263
               assignees.get(execution.assignee_id, None),
264
               priorities.get(execution.case.priority_id),
265
               status[execution.status_id],
266
               comments_subtotal.get(execution.pk, 0),
267
               LinkReference.objects.filter(is_defect=True,
268
                                            execution=execution.pk).count())
269
270
271
@permission_required('testruns.change_testrun')
272
def edit(request, run_id):
273
    """Edit test plan view"""
274
275
    try:
276
        test_run = TestRun.objects.select_related().get(run_id=run_id)
277
    except ObjectDoesNotExist:
278
        raise Http404
279
280
    # If the form is submitted
281
    if request.method == "POST":
282
        form = BaseRunForm(request.POST)
283
        form.populate(product_id=test_run.plan.product_id)
284
285
        # FIXME: Error handler
286
        if form.is_valid():
287
            test_run.summary = form.cleaned_data['summary']
288
            # Permission hack
289
            if test_run.manager == request.user or test_run.plan.author == request.user:
290
                test_run.manager = form.cleaned_data['manager']
291
            test_run.default_tester = form.cleaned_data['default_tester']
292
            test_run.build = form.cleaned_data['build']
293
            test_run.product_version = test_run.plan.product_version
294
            test_run.notes = form.cleaned_data['notes']
295
            test_run.save()
296
297
            return HttpResponseRedirect(reverse('testruns-get', args=[run_id, ]))
298
    else:
299
        # Generate a blank form
300
        form = BaseRunForm(initial={
301
            'summary': test_run.summary,
302
            'manager': test_run.manager.email,
303
            'default_tester': (test_run.default_tester and
304
                               test_run.default_tester.email or None),
305
            'version': test_run.product_version_id,
306
            'build': test_run.build_id,
307
            'notes': test_run.notes,
308
        })
309
        form.populate(test_run.plan.product_id)
310
311
    context_data = {
312
        'test_run': test_run,
313
        'test_plan': test_run.plan,
314
        'form': form,
315
    }
316
    return render(request, 'testruns/mutable.html', context_data)
317
318
319
class TestRunReportView(TemplateView,  # pylint: disable=missing-permission-required
320
                        TestExecutionDataMixin):
321
    """Test Run report"""
322
323
    template_name = 'run/report.html'
324
325
    def get_context_data(self, **kwargs):
326
        """Generate report for specific TestRun
327
328
        There are four data source to generate this report.
329
        1. TestRun
330
        2. Test executions included in the TestRun
331
        3. Comments associated with each test execution
332
        4. Statistics
333
        5. bugs
334
        """
335
        run = TestRun.objects.select_related('manager', 'plan').get(pk=kwargs['run_id'])
336
337
        case_runs = TestExecution.objects.filter(
338
            run=run
339
        ).select_related(
340
            'status', 'case', 'tested_by'
341
        ).only(
342
            'close_date',
343
            'status__name',
344
            'case__category__name',
345
            'case__summary', 'case__is_automated',
346
            'tested_by__username'
347
        )
348
        mode_stats = self.stats_mode_executions(case_runs)
349
        summary_stats = self.get_summary_stats(case_runs)
350
351
        comments = self.get_execution_comments(run.pk)
352
        for case_run in case_runs:
353
            case_run.user_comments = comments.get(case_run.pk, [])
354
355
        context = super().get_context_data(**kwargs)
356
        context.update({
357
            'test_run': run,
358
            'executions': case_runs,
359
            'executions_count': len(case_runs),
360
            'mode_stats': mode_stats,
361
            'summary_stats': summary_stats,
362
        })
363
364
        return context
365
366
367
@method_decorator(permission_required('testruns.add_testrun'), name='dispatch')
368
class CloneTestRunView(View):
369
    """Clone cases from filter caserun"""
370
371
    template_name = 'testruns/mutable.html'
372
    http_method_names = ['post']
373
374
    def post(self, request, run_id):
375
        test_run = get_object_or_404(TestRun, run_id=run_id)
376
        confirmed_case_status = TestCaseStatus.get_confirmed()
377
        disabled_cases = 0
378
379
        if request.POST.get('case_run'):
380
            test_cases = []
381
            for test_case_run in test_run.case_run.filter(pk__in=request.POST.getlist('case_run')):
382
                if test_case_run.case.case_status == confirmed_case_status:
383
                    test_cases.append(test_case_run.case)
384
                else:
385
                    disabled_cases += 1
386
        else:
387
            test_cases = None
388
389
        if not test_cases:
390
            messages.add_message(request,
391
                                 messages.ERROR,
392
                                 _('At least one TestCase is required'))
393
            return HttpResponseRedirect(reverse('testruns-get', args=[run_id]))
394
395
        form = NewRunForm(initial={
396
            'summary': _('Clone of ') + test_run.summary,
397
            'notes': test_run.notes,
398
            'manager': test_run.manager,
399
            'build': test_run.build_id,
400
            'default_tester': test_run.default_tester,
401
        })
402
        form.populate(product_id=test_run.plan.product_id)
403
404
        context_data = {
405
            'is_cloning': True,
406
            'disabled_cases': disabled_cases,
407
            'test_plan': test_run.plan,
408
            'test_cases': test_cases,
409
            'form': form,
410
        }
411
        return render(request, self.template_name, context_data)
412
413
414
@method_decorator(permission_required('testruns.change_testrun'), name='dispatch')
415
class ChangeTestRunStatusView(View):
416
    """Change test run finished or running"""
417
418
    http_method_names = ['get']
419
420
    def get(self, request, run_id):
421
        test_run = get_object_or_404(TestRun, run_id=run_id)
422
423
        test_run.update_completion_status(request.GET.get('finished') == '1')
424
        test_run.save()
425
426
        return HttpResponseRedirect(reverse('testruns-get', args=[run_id, ]))
427
428
429
@method_decorator(permission_required('testruns.add_testexecution'), name='dispatch')
430
class AddCasesToRunView(View):
431
    """Add cases to a TestRun"""
432
433
    def post(self, request, run_id):
434
        # Selected cases' ids to add to run
435
        test_cases_ids = request.POST.getlist('case')
436
        if not test_cases_ids:
437
            # user clicked Update button without selecting new Test Cases
438
            # to be dded to TestRun
439
            messages.add_message(request,
440
                                 messages.ERROR,
441
                                 _('At least one TestCase is required'))
442
            return HttpResponseRedirect(reverse('add-cases-to-run', args=[run_id]))
443
444
        try:
445
            test_cases_ids = list(map(int, test_cases_ids))
446
        except (ValueError, TypeError):
447
            # this will happen only on malicious requests
448
            messages.add_message(request,
449
                                 messages.ERROR,
450
                                 _('TestCase ID is not a valid integer'))
451
            return HttpResponseRedirect(reverse('add-cases-to-run', args=[run_id]))
452
453
        try:
454
            test_run = TestRun.objects.select_related('plan').only('plan__plan_id').get(
455
                run_id=run_id)
456
        except ObjectDoesNotExist:
457
            raise Http404
458
459
        executions_ids = test_run.case_run.values_list('case', flat=True)
460
461
        # avoid add cases that are already in current run with pk run_id
462
        test_cases_ids = set(test_cases_ids) - set(executions_ids)
463
464
        test_plan = test_run.plan
465
        test_cases = test_run.plan.case.filter(case_status__name='CONFIRMED').select_related(
466
            'default_tester').only('default_tester_id').filter(
467
                case_id__in=test_cases_ids)
468
469
        if request.POST.get('_use_plan_sortkey'):
470
            test_case_pks = (test_case.pk for test_case in test_cases)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable test_case does not seem to be defined.
Loading history...
471
            query_set = TestCasePlan.objects.filter(
472
                plan=test_plan, case__in=test_case_pks).values('case', 'sortkey')
473
            sort_keys_in_plan = dict((row['case'], row['sortkey']) for row in query_set.iterator())
474
            for test_case in test_cases:
475
                sort_key = sort_keys_in_plan.get(test_case.pk, 0)
476
                test_run.add_case_run(case=test_case, sortkey=sort_key)
477
        else:
478
            for test_case in test_cases:
479
                test_run.add_case_run(case=test_case)
480
481
        return HttpResponseRedirect(reverse('testruns-get',
482
                                            args=[test_run.run_id, ]))
483
484
    def get(self, request, run_id):
485
        # information about TestRun, used in the page header
486
        test_run = TestRun.objects.select_related(
487
            'plan', 'manager', 'build'
488
        ).only(
489
            'plan', 'plan__name',
490
            'manager__email', 'build__name'
491
        ).get(run_id=run_id)
492
493
        # select all CONFIRMED cases from the TestPlan that is a parent
494
        # of this particular TestRun
495
        rows = TestCasePlan.objects.values(
496
            'case',
497
            'case__create_date', 'case__summary',
498
            'case__category__name',
499
            'case__priority__value',
500
            'case__author__username'
501
        ).filter(
502
            plan_id=test_run.plan,
503
            case__case_status=TestCaseStatus.objects.filter(name='CONFIRMED').first().pk
504
        ).order_by('case')  # order b/c of PostgreSQL
505
506
        # also grab a list of all TestCase IDs which are already present in the
507
        # current TestRun so we can mark them as disabled and not allow them to
508
        # be selected
509
        executions = TestExecution.objects.filter(run=run_id).values_list('case', flat=True)
510
511
        data = {
512
            'test_run': test_run,
513
            'confirmed_cases': rows,
514
            'confirmed_cases_count': rows.count(),
515
            'executions_count': len(executions),
516
            'exist_case_run_ids': executions,
517
        }
518
519
        return render(request, 'run/assign_case.html', data)
520
521
522
# todo: this view should be removed in favor of API
523
@method_decorator(permission_required('testruns.change_testrun'), name='dispatch')
524
class ManageTestRunCC(View):
525
    """Add or remove cc from a test run"""
526
527
    template_name = 'run/get_cc.html'
528
    http_method_names = ['get']
529
530
    def get(self, request, run_id):
531
        test_run = get_object_or_404(TestRun, run_id=run_id)
532
        action = request.GET.get('do')
533
        username_or_email = request.GET.get('user')
534
        context_data = {'test_run': test_run, 'is_ajax': True}
535
536
        try:
537
            user = User.objects.get(
538
                Q(username=username_or_email) |
539
                Q(email=username_or_email)
540
            )
541
            context_data['message'] = ''
542
        except ObjectDoesNotExist:
543
            context_data['message'] = _('The user you typed does not exist in database')
544
            return render(request, self.template_name, context_data)
545
546
        if action == 'add':
547
            test_run.add_cc(user=user)
548
        elif action == 'remove':
549
            test_run.remove_cc(user=user)
550
551
        return render(request, self.template_name, context_data)
552
553
554
@method_decorator(permission_required('testruns.change_testexecution'), name='dispatch')
555
class UpdateCaseRunTextView(View):
556
    """Update the IDLE cases to newest text"""
557
558
    http_method_names = ['post']
559
560
    def post(self, request, run_id):
561
        test_run = get_object_or_404(TestRun, run_id=run_id)
562
563
        if request.POST.get('case_run'):
564
            executions = test_run.case_run.filter(pk__in=request.POST.getlist('case_run'))
565
        else:
566
            executions = test_run.case_run.all()
567
568
        executions = executions.filter(status__name='IDLE')
569
570
        for execution in executions:
571
            latest_version = execution.case.history.latest().history_id
572
            if execution.case_text_version != latest_version:
573
                info = "%s: %s -> %s" % (
574
                    execution.case.summary,
575
                    execution.case_text_version,
576
                    latest_version
577
                )
578
                messages.add_message(request, messages.SUCCESS, info)
579
580
                execution.case_text_version = latest_version
581
                execution.save()
582
583
        return HttpResponseRedirect(reverse('testruns-get', args=[run_id]))
584
585
586
def get_caseruns_of_runs(runs, kwargs=None):
587
    """
588
    Filtering argument -
589
        priority
590
        tester
591
        plan tag
592
    """
593
594
    if kwargs is None:
595
        kwargs = {}
596
    plan_tag = kwargs.get('plan_tag', None)
597
    if plan_tag:
598
        runs = runs.filter(plan__tag__name=plan_tag)
599
    caseruns = TestExecution.objects.filter(run__in=runs)
600
    priority = kwargs.get('priority', None)
601
    if priority:
602
        caseruns = caseruns.filter(case__priority__pk=priority)
603
    tester = kwargs.get('tester', None)
604
    if not tester:
605
        caseruns = caseruns.filter(tested_by=None)
606
    if tester:
607
        caseruns = caseruns.filter(tested_by__pk=tester)
608
    status = kwargs.get('status', None)
609
    if status:
610
        caseruns = caseruns.filter(status__name__iexact=status)
611
    return caseruns
612
613
614
@method_decorator(permission_required('testruns.change_testexecution'), name='dispatch')
615
class UpdateAssigneeView(View):
616
    """Updates TestExecution.assignee. Called from the front-end."""
617
618
    http_method_names = ['post']
619
620
    def post(self, request):
621
        assignee = request.POST.get('assignee')
622
        try:
623
            user = User.objects.get(username=assignee)
624
        except User.DoesNotExist:
625
            try:
626
                user = User.objects.get(email=assignee)
627
            except User.DoesNotExist:
628
                return JsonResponse({'rc': 1,
629
                                     'response': _('User %s not found!' % assignee)},
630
                                    status=HTTPStatus.NOT_FOUND)
631
632
        object_ids = request.POST.getlist('ids[]')
633
634
        for caserun_pk in object_ids:
635
            execution = get_object_or_404(TestExecution, pk=int(caserun_pk))
636
            execution.assignee = user
637
            execution.save()
638
639
        return JsonResponse({'rc': 0, 'response': 'ok'})
640
641
642
@method_decorator(permission_required('testruns.change_testexecution'), name='dispatch')
643
class UpdateCaseRunStatusView(View):
644
    """Updates TestExecution.status_id. Called from the front-end."""
645
646
    http_method_names = ['post']
647
648
    def post(self, request):
649
        status_id = int(request.POST.get('status_id'))
650
        object_ids = request.POST.getlist('object_pk[]')
651
652
        for caserun_pk in object_ids:
653
            execution = get_object_or_404(TestExecution, pk=int(caserun_pk))
654
            execution.status_id = status_id
655
            execution.tested_by = request.user
656
            execution.close_date = datetime.now()
657
            execution.save()
658
659
        return JsonResponse({'rc': 0, 'response': 'ok'})
660