Passed
Push — master ( 189f04...13283a )
by Alexander
03:44
created

tcms.testplans.views   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 530
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 52
eloc 333
dl 0
loc 530
rs 7.44
c 0
b 0
f 0

9 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 Clone.post() 0 34 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

10 Functions

Rating   Name   Duplication   Size   Complexity  
A update_plan_email_settings() 0 11 1
B get_all() 0 58 7
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 printable() 0 21 3
A get() 0 22 3
B edit() 0 54 4

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