Passed
Push — master ( 60d4f6...ee4439 )
by
unknown
02:46
created

tcms.testplans.views.NewTestPlanView.post()   A

Complexity

Conditions 2

Size

Total Lines 28
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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