Passed
Push — master ( 6824bb...d8ce69 )
by Alexander
02:17
created

tcms.testplans.views   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 578
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 57
eloc 362
dl 0
loc 578
rs 5.04
c 0
b 0
f 0

12 Functions

Rating   Name   Duplication   Size   Complexity  
B get_all() 0 63 8
A calculate_stats_for_testplans() 0 25 3
A get_number_of_plans_cases() 0 18 2
A search() 0 10 1
A get_number_of_children_plans() 0 17 2
A get_number_of_plans_runs() 0 17 2
A get() 0 21 3
A update_plan_email_settings() 0 11 1
A attachment() 0 9 1
B clone() 0 71 7
A printable() 0 21 3
B edit() 0 54 4

8 Methods

Rating   Name   Duplication   Size   Complexity  
A NewTestPlanView.get() 0 8 1
A NewTestPlanView.post() 0 28 2
A ReorderCasesView.post() 0 18 4
A UpdateParentView.post() 0 13 3
A LinkCasesSearchView.get() 0 13 1
A LinkCasesSearchView.post() 0 32 3
A LinkCasesView.post() 0 13 3
A DeleteCasesView.post() 0 14 3

How to fix   Complexity   

Complexity

Complex classes like tcms.testplans.views often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
3
import datetime
4
5
from django.utils.decorators import method_decorator
6
from django.conf import settings
7
from django.contrib.auth.decorators import permission_required
8
from django.core.exceptions import ObjectDoesNotExist
9
from django.urls import reverse
10
from django.db.models import Count
11
from django.contrib import messages
12
from django.http import HttpResponseRedirect
13
from django.http import Http404, HttpResponsePermanentRedirect
14
from django.http import JsonResponse
15
from django.shortcuts import get_object_or_404, render
16
from django.views.decorators.http import require_GET, require_POST
17
from django.views.decorators.http import require_http_methods
18
from django.utils.translation import ugettext_lazy as _
19
from django.views.generic import View
20
from uuslug import slugify
21
22
from tcms.search import remove_from_request_path
23
from tcms.search.order import order_plan_queryset
24
from tcms.testcases.forms import SearchCaseForm, QuickSearchCaseForm
25
from tcms.testcases.models import TestCaseStatus
26
from tcms.testcases.models import TestCase, TestCasePlan
27
from tcms.testcases.views import get_selected_testcases
28
from tcms.testcases.views import printable as testcases_printable
29
from tcms.testplans.forms import ClonePlanForm
30
from tcms.testplans.forms import NewPlanForm
31
from tcms.testplans.forms import SearchPlanForm
32
from tcms.testplans.models import TestPlan, PlanType
33
from tcms.testruns.models import TestRun
34
35
36
def update_plan_email_settings(test_plan, form):
37
    """Update test plan's email settings"""
38
    test_plan.emailing.notify_on_plan_update = form.cleaned_data[
39
        'notify_on_plan_update']
40
    test_plan.emailing.notify_on_case_update = form.cleaned_data[
41
        'notify_on_case_update']
42
    test_plan.emailing.auto_to_plan_author = form.cleaned_data['auto_to_plan_author']
43
    test_plan.emailing.auto_to_case_owner = form.cleaned_data['auto_to_case_owner']
44
    test_plan.emailing.auto_to_case_default_tester = form.cleaned_data[
45
        'auto_to_case_default_tester']
46
    test_plan.emailing.save()
47
48
49
# _____________________________________________________________________________
50
# view functons
51
52
@method_decorator(permission_required('testplans.add_testplan'), name='dispatch')
53
class NewTestPlanView(View):
54
    template_name = 'testplans/mutable.html'
55
56
    def get(self, request):
57
        form = NewPlanForm()
58
59
        context_data = {
60
            'form': form
61
        }
62
63
        return render(request, self.template_name, context_data)
64
65
    def post(self, request):
66
        form = NewPlanForm(request.POST)
67
        form.populate(product_id=request.POST.get('product'))
68
69
        if form.is_valid():
70
            test_plan = TestPlan.objects.create(
71
                product=form.cleaned_data['product'],
72
                author=request.user,
73
                product_version=form.cleaned_data['product_version'],
74
                type=form.cleaned_data['type'],
75
                name=form.cleaned_data['name'],
76
                create_date=datetime.datetime.now(),
77
                extra_link=form.cleaned_data['extra_link'],
78
                parent=form.cleaned_data['parent'],
79
                text=form.cleaned_data['text'],
80
                is_active=form.cleaned_data['is_active'],
81
            )
82
83
            update_plan_email_settings(test_plan, form)
84
85
            return HttpResponseRedirect(
86
                reverse('test_plan_url_short', args=[test_plan.plan_id, ])
87
            )
88
89
        context_data = {
90
            'form': form,
91
        }
92
        return render(request, self.template_name, context_data)
93
94
95
@require_GET
96
def get_all(request):
97
    """Display all testplans"""
98
    # todo: this function can be replaced with the existing JSON-RPC search
99
    # TODO: this function now only performs a forward feature, no queries
100
    # need here. All of it will be removed in the future.
101
    # If it's not a search the page will be blank
102
    tps = TestPlan.objects.none()
103
    query_result = False
104
    order_by = request.GET.get('order_by', 'create_date')
105
    asc = bool(request.GET.get('asc', None))
106
    # if it's a search request the page will be fill
107
    if request.GET:
108
        search_form = SearchPlanForm(request.GET)
109
        search_form.populate(product_id=request.GET.get('product'))
110
111
        if search_form.is_valid():
112
            query_result = True
113
            # build a QuerySet:
114
            tps = TestPlan.list(search_form.cleaned_data)
115
            tps = tps.select_related('author', 'type', 'product')
116
            tps = order_plan_queryset(tps, order_by, asc)
117
    else:
118
        # Set search active plans only by default
119
        search_form = SearchPlanForm(initial={'is_active': True})
120
121
    # fixme: this view is scheduled for deletion
122
    # TestCase clone workflow must be redesigned so it uses
123
    # the TestPlan search page
124
    template_name = ''
125
    if request.GET.get('action') == 'clone_case':
126
        template_name = 'case/clone_select_plan.html'
127
        tps = tps.order_by('name')
128
129
    # used in tree preview & TestCase add plan
130
    # fixme: must be replaced by JSON RPC and the
131
    # JavaScript dialog that displays the preview
132
    # should be converted to Patternfly
133
    # todo: when this is done SearchPlanForm must contain only the
134
    # fields which are used in search.html
135
    if request.GET.get('t') == 'html':
136
        if request.GET.get('f') == 'preview':
137
            template_name = 'plan/preview.html'
138
139
    query_url = remove_from_request_path(request, 'order_by')
140
    if asc:
141
        query_url = remove_from_request_path(query_url, 'asc')
142
    else:
143
        query_url = '%s&asc=True' % query_url
144
    page_type = request.GET.get('page_type', 'pagination')
145
    query_url_page_type = remove_from_request_path(request, 'page_type')
146
    if query_url_page_type:
147
        query_url_page_type = remove_from_request_path(query_url_page_type, 'page')
148
149
    context_data = {
150
        'test_plans': tps,
151
        'query_result': query_result,
152
        'search_plan_form': search_form,
153
        'query_url': query_url,
154
        'query_url_page_type': query_url_page_type,
155
        'page_type': page_type
156
    }
157
    return render(request, template_name, context_data)
158
159
160
@require_GET
161
def search(request):
162
    form = SearchPlanForm(request.GET)
163
    form.populate(product_id=request.GET.get('product'))
164
165
    context_data = {
166
        'form': form,
167
        'plan_types': PlanType.objects.all().only('pk', 'name').order_by('name'),
168
    }
169
    return render(request, 'testplans/search.html', context_data)
170
171
172
def get_number_of_plans_cases(plan_ids):
173
    """Get the number of cases related to each plan
174
175
    Arguments:
176
    - plan_ids: a tuple or list of TestPlans' id
177
178
    Return value:
179
    Return value is an dict object, where key is plan_id and the value is the
180
    total count.
181
    """
182
    query_set = TestCasePlan.objects.filter(plan__in=plan_ids).values('plan').annotate(
183
        total_count=Count('pk')).order_by('-plan')
184
185
    number_of_plan_cases = {}
186
    for item in query_set:
187
        number_of_plan_cases[item['plan']] = item['total_count']
188
189
    return number_of_plan_cases
190
191
192
def get_number_of_plans_runs(plan_ids):
193
    """Get the number of runs related to each plan
194
195
    Arguments:
196
    - plan_ids: a tuple or list of TestPlans' id
197
198
    Return value:
199
    Return value is an dict object, where key is plan_id and the value is the
200
    total count.
201
    """
202
    query_set = TestRun.objects.filter(plan__in=plan_ids).values('plan').annotate(
203
        total_count=Count('pk')).order_by('-plan')
204
    number_of_plan_runs = {}
205
    for item in query_set:
206
        number_of_plan_runs[item['plan']] = item['total_count']
207
208
    return number_of_plan_runs
209
210
211
def get_number_of_children_plans(plan_ids):
212
    """Get the number of children plans related to each plan
213
214
    Arguments:
215
    - plan_ids: a tuple or list of TestPlans' id
216
217
    Return value:
218
    Return value is an dict object, where key is plan_id and the value is the
219
    total count.
220
    """
221
    query_set = TestPlan.objects.filter(parent__in=plan_ids).values('parent').annotate(
222
        total_count=Count('parent')).order_by('-parent')
223
    number_of_children_plans = {}
224
    for item in query_set:
225
        number_of_children_plans[item['parent']] = item['total_count']
226
227
    return number_of_children_plans
228
229
230
def calculate_stats_for_testplans(plans):
231
    """Attach the number of cases and runs for each TestPlan
232
233
    Arguments:
234
    - plans: the queryset of TestPlans
235
236
    Return value:
237
    A list of TestPlans, each of which is attached the statistics which is
238
    with prefix cal meaning calculation result.
239
    """
240
    plan_ids = []
241
    for plan in plans:
242
        plan_ids.append(plan.pk)
243
244
    cases_counts = get_number_of_plans_cases(plan_ids)
245
    runs_counts = get_number_of_plans_runs(plan_ids)
246
    children_counts = get_number_of_children_plans(plan_ids)
247
248
    # Attach calculated statistics to each object of TestPlan
249
    for plan in plans:
250
        setattr(plan, 'cal_cases_count', cases_counts.get(plan.pk, 0))
251
        setattr(plan, 'cal_runs_count', runs_counts.get(plan.pk, 0))
252
        setattr(plan, 'cal_children_count', children_counts.get(plan.pk, 0))
253
254
    return plans
255
256
257
def get(request, plan_id, slug=None, template_name='plan/get.html'):
258
    """Display the plan details."""
259
260
    try:
261
        test_plan = TestPlan.objects.select_related().get(plan_id=plan_id)
262
    except ObjectDoesNotExist:
263
        raise Http404
264
265
    if slug is None:
266
        return HttpResponsePermanentRedirect(reverse('test_plan_url',
267
                                                     args=[plan_id, slugify(test_plan.name)]))
268
269
    # Initial the case counter
270
    confirm_status_name = 'CONFIRMED'
271
    test_plan.run_case = test_plan.case.filter(case_status__name=confirm_status_name)
272
    test_plan.review_case = test_plan.case.exclude(case_status__name=confirm_status_name)
273
274
    context_data = {
275
        'test_plan': test_plan,
276
    }
277
    return render(request, template_name, context_data)
278
279
280
@require_http_methods(['GET', 'POST'])
281
@permission_required('testplans.change_testplan')
282
def edit(request, plan_id):
283
    """Edit test plan view"""
284
285
    try:
286
        test_plan = TestPlan.objects.select_related().get(plan_id=plan_id)
287
    except ObjectDoesNotExist:
288
        raise Http404
289
290
    # If the form is submitted
291
    if request.method == "POST":
292
        form = NewPlanForm(request.POST)
293
        form.populate(product_id=request.POST.get('product'))
294
295
        # FIXME: Error handle
296
        if form.is_valid():
297
            test_plan.name = form.cleaned_data['name']
298
            test_plan.parent = form.cleaned_data['parent']
299
            test_plan.product = form.cleaned_data['product']
300
            test_plan.product_version = form.cleaned_data['product_version']
301
            test_plan.type = form.cleaned_data['type']
302
            test_plan.is_active = form.cleaned_data['is_active']
303
            test_plan.extra_link = form.cleaned_data['extra_link']
304
            test_plan.text = form.cleaned_data['text']
305
            test_plan.save()
306
307
            # Update plan email settings
308
            update_plan_email_settings(test_plan, form)
309
            return HttpResponseRedirect(
310
                reverse('test_plan_url', args=[plan_id, slugify(test_plan.name)]))
311
    else:
312
        form = NewPlanForm(initial={
313
            'name': test_plan.name,
314
            'product': test_plan.product_id,
315
            'product_version': test_plan.product_version_id,
316
            'type': test_plan.type_id,
317
            'text': test_plan.text,
318
            'parent': test_plan.parent_id,
319
            'is_active': test_plan.is_active,
320
            'extra_link': test_plan.extra_link,
321
            'auto_to_plan_author': test_plan.emailing.auto_to_plan_author,
322
            'auto_to_case_owner': test_plan.emailing.auto_to_case_owner,
323
            'auto_to_case_default_tester': test_plan.emailing.auto_to_case_default_tester,
324
            'notify_on_plan_update': test_plan.emailing.notify_on_plan_update,
325
            'notify_on_case_update': test_plan.emailing.notify_on_case_update,
326
        })
327
        form.populate(product_id=test_plan.product_id)
328
329
    context_data = {
330
        'test_plan': test_plan,
331
        'form': form,
332
    }
333
    return render(request, 'testplans/mutable.html', context_data)
334
335
336
@require_POST
337
@permission_required('testplans.add_testplan')
338
def clone(request):
339
    """Clone testplan"""
340
341
    if 'plan' not in request.POST:
342
        messages.add_message(request,
343
                             messages.ERROR,
344
                             _('TestPlan is required'))
345
        # redirect back where we came from
346
        return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
347
348
    plan_id = request.POST.get('plan')
349
    test_plan = get_object_or_404(TestPlan, pk=int(plan_id))
350
351
    post_data = request.POST.copy()
352
    if not request.POST.get('name'):
353
        post_data['name'] = test_plan.make_cloned_name()
354
355
    clone_form = ClonePlanForm(post_data)
356
    clone_form.populate(product_id=request.POST.get('product_id'))
357
358
    # if required values are missing we are still going to show
359
    # the form below, otherwise clone & redirect
360
    if clone_form.is_valid():
361
        clone_options = clone_form.cleaned_data
362
363
        # Create new test plan.
364
        new_name = clone_options['name']
365
366
        clone_params = dict(
367
            # Cloned plan properties
368
            new_name=new_name,
369
            product=clone_options['product'],
370
            version=clone_options['product_version'],
371
            set_parent=clone_options['set_parent'],
372
373
            # Link or copy cases
374
            link_cases=clone_options['link_testcases'],
375
            copy_cases=clone_options['copy_testcases'],
376
            default_component_initial_owner=request.user,
377
        )
378
379
        assign_me_as_plan_author = not clone_options['keep_orignal_author']
380
        if assign_me_as_plan_author:
381
            clone_params['new_original_author'] = request.user
382
383
        assign_me_as_copied_case_author = \
384
            clone_options['copy_testcases'] and \
385
            not clone_options['maintain_case_orignal_author']
386
        if assign_me_as_copied_case_author:
387
            clone_params['new_case_author'] = request.user
388
389
        # pylint: disable=invalid-name
390
        assign_me_as_copied_case_default_tester = \
391
            clone_options['copy_testcases'] and \
392
            not clone_options['keep_case_default_tester']
393
        if assign_me_as_copied_case_default_tester:
394
            clone_params['new_case_default_tester'] = request.user
395
396
        cloned_plan = test_plan.clone(**clone_params)
397
398
        return HttpResponseRedirect(
399
            reverse('test_plan_url_short', args=[cloned_plan.plan_id]))
400
401
    # clone form wasn't valid
402
    context_data = {
403
        'test_plan': test_plan,
404
        'clone_form': clone_form,
405
    }
406
    return render(request, 'plan/clone.html', context_data)
407
408
409
def attachment(request, plan_id, template_name='plan/attachment.html'):
410
    """Manage attached files"""
411
412
    test_plan = get_object_or_404(TestPlan, plan_id=plan_id)
413
    context_data = {
414
        'test_plan': test_plan,
415
        'limit': settings.FILE_UPLOAD_MAX_SIZE,
416
    }
417
    return render(request, template_name, context_data)
418
419
420
class ReorderCasesView(View):
421
    """Reorder cases"""
422
423
    http_method_names = ['post']
424
425
    def post(self, request, plan_id):
426
        if 'case' not in request.POST:
427
            return JsonResponse({
428
                'rc': 1,
429
                'response': 'At least one case is required to re-order.'
430
            })
431
432
        case_ids = []
433
        for case_id in request.POST.getlist('case'):
434
            case_ids.append(int(case_id))
435
436
        cases = TestCasePlan.objects.filter(case_id__in=case_ids, plan=plan_id).only('case_id')
437
438
        for case in cases:
439
            case.sortkey = (case_ids.index(case.case_id) + 1) * 10
440
            case.save()
441
442
        return JsonResponse({'rc': 0, 'response': 'ok'})
443
444
445
@method_decorator(permission_required('testplans.change_testplan'), name='dispatch')
446
class UpdateParentView(View):
447
    """Updates TestPlan.parent. Called from the front-end."""
448
449
    http_method_names = ['post']
450
451
    def post(self, request):
452
        parent_id = int(request.POST.get('parent_id'))
453
        if parent_id == 0:
454
            parent_id = None
455
456
        child_ids = request.POST.getlist('child_ids[]')
457
458
        for child_pk in child_ids:
459
            test_plan = get_object_or_404(TestPlan, pk=int(child_pk))
460
            test_plan.parent_id = parent_id
461
            test_plan.save()
462
463
        return JsonResponse({'rc': 0, 'response': 'ok'})
464
465
466
class LinkCasesView(View):
467
    """Link cases to plan"""
468
469
    @method_decorator(permission_required('testcases.add_testcaseplan'))
470
    def post(self, request, plan_id):
471
        plan = get_object_or_404(TestPlan.objects.only('pk'), pk=int(plan_id))
472
473
        case_ids = []
474
        for case_id in request.POST.getlist('case'):
475
            case_ids.append(int(case_id))
476
477
        cases = TestCase.objects.filter(case_id__in=case_ids).only('pk')
478
        for case in cases:
479
            plan.add_case(case)
480
481
        return HttpResponseRedirect(reverse('test_plan_url', args=[plan_id, slugify(plan.name)]))
482
483
484
class LinkCasesSearchView(View):
485
    """Search cases for linking to plan"""
486
487
    template_name = 'plan/search_case.html'
488
489
    def get(self, request, plan_id):
490
        plan = get_object_or_404(TestPlan, pk=int(plan_id))
491
492
        normal_form = SearchCaseForm(initial={
493
            'product': plan.product_id,
494
            'product_version': plan.product_version_id,
495
            'case_status_id': TestCaseStatus.get_confirmed()
496
        })
497
        quick_form = QuickSearchCaseForm()
498
        return render(self.request, self.template_name, {
499
            'search_form': normal_form,
500
            'quick_form': quick_form,
501
            'test_plan': plan,
502
        })
503
504
    def post(self, request, plan_id):
505
        plan = get_object_or_404(TestPlan, pk=int(plan_id))
506
507
        search_mode = request.POST.get('search_mode')
508
        if search_mode == 'quick':
509
            form = quick_form = QuickSearchCaseForm(request.POST)
510
            normal_form = SearchCaseForm()
511
        else:
512
            form = normal_form = SearchCaseForm(request.POST)
513
            form.populate(product_id=request.POST.get('product'))
514
            quick_form = QuickSearchCaseForm()
515
516
        cases = []
517
        if form.is_valid():
518
            cases = TestCase.list(form.cleaned_data)
519
            cases = cases.select_related(
520
                'author', 'default_tester', 'case_status', 'priority'
521
            ).only(
522
                'pk', 'summary', 'create_date', 'author__email',
523
                'default_tester__email', 'case_status__name',
524
                'priority__value'
525
            ).exclude(
526
                case_id__in=plan.case.values_list('case_id', flat=True))
527
528
        context = {
529
            'test_plan': plan,
530
            'test_cases': cases,
531
            'search_form': normal_form,
532
            'quick_form': quick_form,
533
            'search_mode': search_mode
534
        }
535
        return render(request, self.template_name, context=context)
536
537
538
class DeleteCasesView(View):
539
    """Delete selected cases from plan"""
540
541
    def post(self, request, plan_id):
542
        plan = get_object_or_404(TestPlan.objects.only('pk'), pk=int(plan_id))
543
544
        if 'case' not in request.POST:
545
            return JsonResponse({
546
                'rc': 1,
547
                'response': 'At least one case is required to delete.'
548
            })
549
550
        cases = get_selected_testcases(request).only('pk')
551
        for case in cases:
552
            plan.delete_case(case=case)
553
554
        return JsonResponse({'rc': 0, 'response': 'ok'})
555
556
557
@require_POST
558
def printable(request):
559
    """Create the printable copy for plan"""
560
    plan_pk = request.POST.get('plan', 0)
561
562
    if not plan_pk:
563
        messages.add_message(request,
564
                             messages.ERROR,
565
                             _('At least one test plan is required for print'))
566
        return HttpResponseRedirect(reverse('core-views-index'))
567
568
    try:
569
        TestPlan.objects.get(pk=plan_pk)
570
    except (ValueError, TestPlan.DoesNotExist):
571
        messages.add_message(request,
572
                             messages.ERROR,
573
                             _('Test Plan "%s" does not exist') % plan_pk)
574
        return HttpResponseRedirect(reverse('core-views-index'))
575
576
    # rendering is actually handled by testcases.views.printable()
577
    return testcases_printable(request)
578