Passed
Push — master ( cddcf6...1d3855 )
by Alexander
01:59
created

tcms/testcases/views.py (2 issues)

1
# -*- coding: utf-8 -*-
2
3
import itertools
4
5
from django.conf import settings
6
from django.contrib import messages
7
from django.test import modify_settings
8
from django.contrib.auth.decorators import permission_required
9
from django.core.exceptions import ObjectDoesNotExist
10
from django.urls import reverse
11
from django.http import HttpResponseRedirect, Http404
12
from django.shortcuts import get_object_or_404, render
13
from django.utils.decorators import method_decorator
14
from django.utils.translation import ugettext_lazy as _
15
from django.views.decorators.http import require_GET
16
from django.views.decorators.http import require_POST
17
from django.views.generic.base import TemplateView
18
19
from tcms.core.contrib.comments.utils import get_comments
20
from tcms.search import remove_from_request_path
21
from tcms.search.order import order_case_queryset
22
from tcms.testcases.models import TestCase, TestCaseStatus, \
23
    TestCasePlan
24
from tcms.management.models import Priority, Tag
25
from tcms.testplans.models import TestPlan
26
from tcms.testruns.models import TestExecution
27
from tcms.testruns.models import TestExecutionStatus
28
from tcms.testcases.forms import NewCaseForm, \
29
    SearchCaseForm, CaseNotifyForm, CloneCaseForm
30
from tcms.testplans.forms import SearchPlanForm
31
from tcms.testcases.fields import MultipleEmailField
32
33
34
TESTCASE_OPERATION_ACTIONS = (
35
    'search', 'sort', 'update',
36
    'remove',  # including remove tag from cases
37
    'add',  # including add tag to cases
38
    'change',
39
    'delete_cases',  # unlink cases from a TestPlan
40
)
41
42
43
# _____________________________________________________________________________
44
# helper functions
45
46
47
def plan_from_request_or_none(request):
48
    """Get TestPlan from REQUEST
49
50
    This method relies on the existence of from_plan within REQUEST.
51
    """
52
    test_plan_id = request.POST.get("from_plan") or request.GET.get("from_plan")
53
    if not test_plan_id:
54
        return None
55
    return get_object_or_404(TestPlan, plan_id=test_plan_id)
56
57
58
def update_case_email_settings(test_case, n_form):
59
    """Update testcase's email settings."""
60
61
    test_case.emailing.notify_on_case_update = n_form.cleaned_data[
62
        'notify_on_case_update']
63
    test_case.emailing.notify_on_case_delete = n_form.cleaned_data[
64
        'notify_on_case_delete']
65
    test_case.emailing.auto_to_case_author = n_form.cleaned_data[
66
        'author']
67
    test_case.emailing.auto_to_case_tester = n_form.cleaned_data[
68
        'default_tester_of_case']
69
    test_case.emailing.auto_to_run_manager = n_form.cleaned_data[
70
        'managers_of_runs']
71
    test_case.emailing.auto_to_run_tester = n_form.cleaned_data[
72
        'default_testers_of_runs']
73
    test_case.emailing.auto_to_case_run_assignee = n_form.cleaned_data[
74
        'assignees_of_case_runs']
75
76
    default_tester = n_form.cleaned_data['default_tester_of_case']
77
    if (default_tester and test_case.default_tester_id):
78
        test_case.emailing.auto_to_case_tester = True
79
80
    # Continue to update CC list
81
    valid_emails = n_form.cleaned_data['cc_list']
82
    test_case.emailing.cc_list = MultipleEmailField.delimiter.join(valid_emails)
83
84
    test_case.emailing.save()
85
86
87
def group_case_bugs(bugs):
88
    """Group bugs using bug_id."""
89
    grouped_bugs = []
90
91
    for _pk, _bugs in itertools.groupby(bugs, lambda b: b.bug_id):
92
        grouped_bugs.append((_pk, list(_bugs)))
93
94
    return grouped_bugs
95
96
97
@method_decorator(permission_required('testcases.add_testcase'), name='dispatch')
98
class NewCaseView(TemplateView):
99
100
    template_name = 'testcases/mutable.html'
101
102
    def get(self, request, *args, **kwargs):
103
        test_plan = plan_from_request_or_none(request)
104
105
        default_form_parameters = {}
106
        if test_plan:
107
            default_form_parameters['product'] = test_plan.product_id
108
109
        form = NewCaseForm(initial=default_form_parameters)
110
111
        context_data = {
112
            'test_plan': test_plan,
113
            'form': form,
114
            'notify_form': CaseNotifyForm(),
115
        }
116
117
        return render(request, self.template_name, context_data)
118
119
    def post(self, request, *args, **kwargs):
120
        test_plan = plan_from_request_or_none(request)
121
122
        form = NewCaseForm(request.POST)
123
        if request.POST.get('product'):
124
            form.populate(product_id=request.POST['product'])
125
        else:
126
            form.populate()
127
128
        notify_form = CaseNotifyForm(request.POST)
129
130
        if form.is_valid() and notify_form.is_valid():
131
            test_case = self.create_test_case(form, notify_form, test_plan)
132
            if test_plan:
133
                return HttpResponseRedirect(
134
                    '%s?from_plan=%s' % (reverse('testcases-get', args=[test_case.pk]),
135
                                         test_plan.pk))
136
137
            return HttpResponseRedirect(reverse('testcases-get', args=[test_case.pk]))
138
139
        context_data = {
140
            'test_plan': test_plan,
141
            'form': form,
142
            'notify_form': notify_form
143
        }
144
145
        return render(request, self.template_name, context_data)
146
147
    def create_test_case(self, form, notify_form, test_plan):
148
        """Create new test case"""
149
        test_case = TestCase.create(author=self.request.user, values=form.cleaned_data)
150
151
        # Assign the case to the plan
152
        if test_plan:
153
            test_plan.add_case(test_case)
154
155
        update_case_email_settings(test_case, notify_form)
156
157
        return test_case
158
159
160
def get_testcaseplan_sortkey_pk_for_testcases(plan, tc_ids):
161
    """Get each TestCase' sortkey and related TestCasePlan's pk"""
162
    qs = TestCasePlan.objects.filter(case__in=tc_ids)
163
    if plan is not None:
164
        qs = qs.filter(plan__pk=plan.pk)
165
    qs = qs.values('pk', 'sortkey', 'case')
166
    return dict([(item['case'], {
167
        'testcaseplan_pk': item['pk'],
168
        'sortkey': item['sortkey']
169
    }) for item in qs])
170
171
172
def calculate_for_testcases(plan, testcases):
173
    """Calculate extra data for TestCases
174
175
    Attach TestCasePlan.sortkey, TestCasePlan.pk, and the number of bugs of
176
    each TestCase.
177
178
    :param plan: the TestPlan containing searched TestCases. None means testcases
179
                 are not limited to a specific TestPlan.
180
    :param testcases: a queryset of TestCases.
181
    """
182
    tc_ids = []
183
    for test_case in testcases:
184
        tc_ids.append(test_case.pk)
185
186
    sortkey_tcpkan_pks = get_testcaseplan_sortkey_pk_for_testcases(
187
        plan, tc_ids)
188
189
    for test_case in testcases:
190
        data = sortkey_tcpkan_pks.get(test_case.pk, None)
191
        if data:
192
            # todo: these properties appear to be redundant since the same
193
            # info should be available from the test_case query
194
            setattr(test_case, 'cal_sortkey', data['sortkey'])
195
            setattr(test_case, 'cal_testcaseplan_pk', data['testcaseplan_pk'])
196
        else:
197
            setattr(test_case, 'cal_sortkey', None)
198
            setattr(test_case, 'cal_testcaseplan_pk', None)
199
200
    return testcases
201
202
203
def get_case_status(template_type):
204
    """Get part or all TestCaseStatus according to template type"""
205
    confirmed_status_name = 'CONFIRMED'
206
    if template_type == 'case':
207
        d_status = TestCaseStatus.objects.filter(name=confirmed_status_name)
208
    elif template_type == 'review_case':
209
        d_status = TestCaseStatus.objects.exclude(name=confirmed_status_name)
210
    else:
211
        d_status = TestCaseStatus.objects.all()
212
    return d_status
213
214
215
@require_POST
216
def build_cases_search_form(request, populate=None, plan=None):
217
    """Build search form preparing for quering TestCases"""
218
    # Initial the form and template
219
    action = request.POST.get('a')
220
    if action in TESTCASE_OPERATION_ACTIONS:
221
        search_form = SearchCaseForm(request.POST)
222
        request.session['items_per_page'] = \
223
            request.POST.get('items_per_page', settings.DEFAULT_PAGE_SIZE)
224
    else:
225
        d_status = get_case_status(request.POST.get('template_type'))
226
        d_status_ids = d_status.values_list('pk', flat=True)
227
        items_per_page = request.session.get('items_per_page',
228
                                             settings.DEFAULT_PAGE_SIZE)
229
        search_form = SearchCaseForm(initial={
230
            'case_status': d_status_ids,
231
            'items_per_page': items_per_page})
232
233
    if populate:
234
        if request.POST.get('product'):
235
            search_form.populate(product_id=request.POST['product'])
236
        elif plan and plan.product_id:
237
            search_form.populate(product_id=plan.product_id)
238
        else:
239
            search_form.populate()
240
241
    return search_form
242
243
244
def paginate_testcases(request, testcases):
245
    """Paginate queried TestCases
246
247
    Arguments:
248
    - request: django's HttpRequest from which to get pagination data
249
    - testcases: an object queryset representing already queried TestCases
250
251
    Return value: return the queryset for chain call
252
    """
253
254
    page_index = int(request.POST.get('page_index', 1))
255
    page_size = int(request.POST.get(
256
        'items_per_page',
257
        request.session.get(
258
            'items_per_page', settings.DEFAULT_PAGE_SIZE
259
        )
260
    ))
261
    offset = (page_index - 1) * page_size
262
    return testcases[offset:offset + page_size]
263
264
265
def sort_queried_testcases(request, testcases):
266
    """Sort querid TestCases according to sort key
267
268
    Arguments:
269
    - request: REQUEST object
270
    - testcases: object of QuerySet containing queried TestCases
271
    """
272
    order_by = request.POST.get('order_by', 'create_date')
273
    asc = bool(request.POST.get('asc', None))
274
    tcs = order_case_queryset(testcases, order_by, asc)
275
    # default sorted by sortkey
276
    tcs = tcs.order_by('testcaseplan__sortkey')
277
    # Resort the order
278
    # if sorted by 'sortkey'(foreign key field)
279
    case_sort_by = request.POST.get('case_sort_by')
280
    if case_sort_by:
281
        if case_sort_by not in ['sortkey', '-sortkey']:
282
            tcs = tcs.order_by(case_sort_by)
283
        elif case_sort_by == 'sortkey':
284
            tcs = tcs.order_by('testcaseplan__sortkey')
285
        else:
286
            tcs = tcs.order_by('-testcaseplan__sortkey')
287
    return tcs
288
289
290
def query_testcases_from_request(request, plan=None):
291
    """Query TestCases according to criterias coming within REQUEST
292
293
    :param request: the REQUEST object.
294
    :param plan: instance of TestPlan to restrict only those TestCases belongs to
295
                 the TestPlan. Can be None. As you know, query from all TestCases.
296
    """
297
    search_form = build_cases_search_form(request, True, plan)
298
299
    action = request.POST.get('a')
300
    if action == 'initial':
301
        # todo: build_cases_search_form will also check TESTCASE_OPERATION_ACTIONS
302
        # and return slightly different values in case of initialization
303
        # move the check there and just execute the query here if the data
304
        # is valid
305
        d_status = get_case_status(request.POST.get('template_type'))
306
        tcs = TestCase.objects.filter(case_status__in=d_status)
307
    elif action in TESTCASE_OPERATION_ACTIONS and search_form.is_valid():
308
        tcs = TestCase.list(search_form.cleaned_data, plan)
309
    else:
310
        tcs = TestCase.objects.none()
311
312
    # Search the relationship
313
    if plan:
314
        tcs = tcs.filter(plan=plan)
315
316
    tcs = tcs.select_related('author',
317
                             'default_tester',
318
                             'case_status',
319
                             'priority',
320
                             'category',
321
                             'reviewer')
322
    return tcs, search_form
323
324
325
def get_selected_testcases(request):
326
    """Get selected TestCases from client side
327
328
    TestCases are selected in two cases. One is user selects part of displayed
329
    TestCases, where there should be at least one variable named case, whose
330
    value is the TestCase Id. Another one is user selects all TestCases based
331
    on previous filter criterias even through there are non-displayed ones. In
332
    this case, another variable selectAll appears in the REQUEST. Whatever its
333
    value is.
334
335
    If neither variables mentioned exists, empty query result is returned.
336
337
    Arguments:
338
    - request: REQUEST object.
339
    """
340
    method = request.POST or request.GET
341
    if method.get('selectAll', None):
342
        plan = plan_from_request_or_none(request)
343
        cases, _search_form = query_testcases_from_request(request, plan)
344
        return cases
345
346
    return TestCase.objects.filter(pk__in=method.getlist('case'))
347
348
349
def load_more_cases(request, template_name='plan/cases_rows.html'):
350
    """Loading more TestCases"""
351
    plan = plan_from_request_or_none(request)
352
    cases = []
353
    selected_case_ids = []
354
    if plan is not None:
355
        cases, _search_form = query_testcases_from_request(request, plan)
356
        cases = sort_queried_testcases(request, cases)
357
        cases = paginate_testcases(request, cases)
358
        cases = calculate_for_testcases(plan, cases)
359
        selected_case_ids = []
360
        for test_case in cases:
361
            selected_case_ids.append(test_case.pk)
362
    context_data = {
363
        'test_plan': plan,
364
        'test_cases': cases,
365
        'selected_case_ids': selected_case_ids,
366
        'case_status': TestCaseStatus.objects.all(),
367
    }
368
    return render(request, template_name, context_data)
369
370
371
def get_tags_from_cases(case_ids, plan=None):
372
    """Get all tags from test cases
373
374
    @param cases: an iterable object containing test cases' ids
375
    @type cases: list, tuple
376
377
    @param plan: TestPlan object
378
379
    @return: a list containing all found tags with id and name
380
    @rtype: list
381
    """
382
    query = Tag.objects.filter(case__in=case_ids).distinct().order_by('name')
383
    if plan:
384
        query = query.filter(case__plan=plan)
385
386
    return query
387
388
389
@require_POST
390
def list_all(request):
391
    """
392
    Generate the TestCase list for the UI tabs in TestPlan page view.
393
    """
394
    # Intial the plan in plan details page
395
    test_plan = plan_from_request_or_none(request)
396
    if not test_plan:
397
        messages.add_message(request,
398
                             messages.ERROR,
399
                             _('TestPlan not specified or does not exist'))
400
        return HttpResponseRedirect(reverse('core-views-index'))
401
402
    tcs, search_form = query_testcases_from_request(request, test_plan)
403
    tcs = sort_queried_testcases(request, tcs)
404
    total_cases_count = tcs.count()
405
406
    # Get the tags own by the cases
407
    ttags = get_tags_from_cases((case.pk for case in tcs), test_plan)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable case does not seem to be defined.
Loading history...
408
409
    tcs = paginate_testcases(request, tcs)
410
411
    # There are several extra information related to each TestCase to be shown
412
    # also. This step must be the very final one, because the calculation of
413
    # related data requires related TestCases' IDs, that is the queryset of
414
    # TestCases should be evaluated in advance.
415
    tcs = calculate_for_testcases(test_plan, tcs)
416
417
    # generating a query_url with order options
418
    #
419
    # FIXME: query_url is always equivlant to None&asc=True whatever what
420
    # criterias specified in filter form, or just with default filter
421
    # conditions during loading TestPlan page.
422
    query_url = remove_from_request_path(request, 'order_by')
423
    asc = bool(request.POST.get('asc', None))
424
    if asc:
425
        query_url = remove_from_request_path(query_url, 'asc')
426
    else:
427
        query_url = '%s&asc=True' % query_url
428
429
    selected_case_ids = []
430
    for test_case in get_selected_testcases(request):
431
        selected_case_ids.append(test_case.pk)
432
433
    context_data = {
434
        'test_cases': tcs,
435
        'test_plan': test_plan,
436
        'search_form': search_form,
437
        # selected_case_ids is used in template to decide whether or not this TestCase is selected
438
        'selected_case_ids': selected_case_ids,
439
        'case_status': TestCaseStatus.objects.all(),
440
        'priorities': Priority.objects.filter(is_active=True),
441
        'case_own_tags': ttags,
442
        'query_url': query_url,
443
444
        # Load more is a POST request, so POST parameters are required only.
445
        # Remember this for loading more cases with the same as criterias.
446
        'search_criterias': request.body.decode(),
447
        'total_cases_count': total_cases_count,
448
    }
449
    return render(request, 'plan/get_cases.html', context_data)
450
451
452
@require_GET
453
def search(request):
454
    """
455
        Shows the search form which uses JSON RPC to fetch the resuts
456
    """
457
    form = SearchCaseForm(request.GET)
458
    if request.GET.get('product'):
459
        form.populate(product_id=request.GET['product'])
460
    else:
461
        form.populate()
462
463
    context_data = {
464
        'form': form,
465
    }
466
    return render(request, 'testcases/search.html', context_data)
467
468
469
class SimpleTestCaseView(TemplateView):
470
    """Simple read-only TestCase View used in TestPlan page"""
471
472
    template_name = 'case/get_details.html'
473
    review_mode = None
474
475
    def get(self, request, *args, **kwargs):
476
        self.review_mode = request.GET.get('review_mode')
477
        return super().get(request, *args, **kwargs)
478
479
    def get_context_data(self, **kwargs):
480
        data = super().get_context_data(**kwargs)
481
482
        case = TestCase.objects.get(pk=kwargs['case_id'])
483
        data.update({
484
            'test_case': case,
485
            'review_mode': self.review_mode,
486
            'components': case.component.only('name'),
487
            'tags': case.tag.only('name'),
488
            'case_comments': get_comments(case),
489
        })
490
491
        return data
492
493
494
class TestCaseExecutionDetailPanelView(TemplateView):
495
    """Display execution detail in run page"""
496
497
    template_name = 'case/get_details_case_run.html'
498
    caserun_id = None
499
    case_text_version = None
500
501
    def get(self, request, *args, **kwargs):
502
        try:
503
            self.caserun_id = int(request.GET.get('case_run_id'))
504
            self.case_text_version = int(request.GET.get('case_text_version'))
505
        except (TypeError, ValueError):
506
            raise Http404
507
508
        return super().get(request, *args, **kwargs)
509
510
    def get_context_data(self, **kwargs):
511
        data = super().get_context_data(**kwargs)
512
513
        case = TestCase.objects.get(pk=kwargs['case_id'])
514
        execution = TestExecution.objects.get(pk=self.caserun_id)
515
516
        # Data of TestCase
517
        test_case_text = case.get_text_with_version(self.case_text_version)
518
519
        # Data of TestExecution
520
        execution_comments = get_comments(execution)
521
522
        execution_status = TestExecutionStatus.objects.values('pk', 'name').order_by('pk')
523
        bugs = group_case_bugs(execution.case.get_bugs().order_by('bug_id'))
524
525
        data.update({
526
            'test_case': case,
527
            'test_case_text': test_case_text,
528
529
            'execution': execution,
530
            'comments_count': len(execution_comments),
531
            'execution_comments': execution_comments,
532
            'execution_logs': execution.history.all(),
533
            'execution_status': execution_status,
534
            'grouped_case_bugs': bugs,
535
        })
536
537
        return data
538
539
540
def get(request, case_id):
541
    """Get the case content"""
542
    # Get the case
543
    try:
544
        test_case = TestCase.objects.select_related(
545
            'author', 'default_tester',
546
            'category', 'category',
547
            'priority', 'case_status').get(case_id=case_id)
548
    except ObjectDoesNotExist:
549
        raise Http404
550
551
    # Get the test executions
552
    tcrs = test_case.case_run.select_related(
553
        'run', 'tested_by',
554
        'assignee', 'case',
555
        'case', 'status').order_by('run__plan', 'run')
556
557
    # Render the page
558
    context_data = {
559
        'test_case': test_case,
560
        'test_case_runs': tcrs,
561
    }
562
563
    url_params = "?case=%d" % test_case.pk
564
    case_edit_url = reverse('testcases-edit', args=[test_case.pk])
565
    test_plan = request.GET.get('from_plan', 0)
566
    if test_plan:
567
        url_params += "&from_plan=%s" % test_plan
568
        case_edit_url += "?from_plan=%s" % test_plan
569
570
    with modify_settings(
571
            MENU_ITEMS={'append': [
572
                ('...', [
573
                    (
574
                        _('Edit'),
575
                        case_edit_url
576
                    ),
577
                    (
578
                        _('Clone'),
579
                        reverse('testcases-clone') + url_params
580
                    ),
581
                    (
582
                        _('History'),
583
                        "/admin/testcases/testcase/%d/history/" % test_case.pk
584
                    ),
585
                    ('-', '-'),
586
                    (
587
                        _('Delete'),
588
                        reverse('admin:testcases_testcase_delete', args=[test_case.pk])
589
                    )])]}):
590
        return render(request, 'testcases/get.html', context_data)
591
592
593
@require_POST
594
def printable(request, template_name='case/printable.html'):
595
    """
596
        Create the printable copy for plan/case.
597
        Only CONFIRMED TestCases are printed when printing a TestPlan!
598
    """
599
    # fixme: remove when TestPlan and TestCase templates have been converted to Patternfly
600
    # instead of generating the print values on the backend we can use CSS to do
601
    # this in the browser
602
    # search only by case PK. Used when printing selected cases
603
    case_ids = request.POST.getlist('case')
604
    case_filter = {'pk__in': case_ids}
605
606
    test_plan = None
607
    # plan_pk is passed from the TestPlan.printable function
608
    # but it doesn't pass IDs of individual cases to be printed
609
    if not case_ids:
610
        plan_pk = request.POST.get('plan', 0)
611
        try:
612
            test_plan = TestPlan.objects.get(pk=plan_pk)
613
            # search cases from a TestPlan, used when printing entire plan
614
            case_filter = {
615
                'pk__in': test_plan.case.all(),
616
                'case_status': TestCaseStatus.objects.get(name='CONFIRMED').pk,
617
            }
618
        except (ValueError, TestPlan.DoesNotExist):
619
            test_plan = None
620
621
    tcs = TestCase.objects.filter(**case_filter).values(
622
        'case_id', 'summary', 'text'
623
    ).order_by('case_id')
624
625
    context_data = {
626
        'test_plan': test_plan,
627
        'test_cases': tcs,
628
    }
629
    return render(request, template_name, context_data)
630
631
632
def update_testcase(request, test_case, tc_form):
633
    """Updating information of specific TestCase
634
635
    This is called by views.edit internally. Don't call this directly.
636
637
    Arguments:
638
    - test_case: instance of a TestCase being updated
639
    - tc_form: instance of django.forms.Form, holding validated data.
640
    """
641
642
    # TODO: this entire function doesn't seem very useful
643
    # part if it was logging the changes but now this is
644
    # done by simple_history. Should we remove it ???
645
    # Modify the contents
646
    fields = ['summary',
647
              'case_status',
648
              'category',
649
              'priority',
650
              'notes',
651
              'text',
652
              'is_automated',
653
              'script',
654
              'arguments',
655
              'extra_link',
656
              'requirement']
657
658
    for field in fields:
659
        if getattr(test_case, field) != tc_form.cleaned_data[field]:
660
            setattr(test_case, field, tc_form.cleaned_data[field])
661
    try:
662
        if test_case.default_tester != tc_form.cleaned_data['default_tester']:
663
            test_case.default_tester = tc_form.cleaned_data['default_tester']
664
    except ObjectDoesNotExist:
665
        pass
666
667
    test_case.save()
668
669
670
@permission_required('testcases.change_testcase')
671
def edit(request, case_id):
672
    """Edit case detail"""
673
    try:
674
        test_case = TestCase.objects.select_related().get(case_id=case_id)
675
    except ObjectDoesNotExist:
676
        raise Http404
677
678
    test_plan = plan_from_request_or_none(request)
679
    from_plan = ""
680
    if test_plan:
681
        from_plan = "?from_plan=%d" % test_plan.pk
682
683
    if request.method == "POST":
684
        form = NewCaseForm(request.POST)
685
        if request.POST.get('product'):
686
            form.populate(product_id=request.POST['product'])
687
        elif test_plan:
688
            form.populate(product_id=test_plan.product_id)
689
        else:
690
            form.populate()
691
692
        n_form = CaseNotifyForm(request.POST)
693
694
        if form.is_valid() and n_form.is_valid():
695
            update_testcase(request, test_case, form)
696
            update_case_email_settings(test_case, n_form)
697
698
            return HttpResponseRedirect(
699
                reverse('testcases-get', args=[case_id, ]) + from_plan
700
            )
701
702
    else:
703
        # Notification form initial
704
        n_form = CaseNotifyForm(initial={
705
            'notify_on_case_update': test_case.emailing.notify_on_case_update,
706
            'notify_on_case_delete': test_case.emailing.notify_on_case_delete,
707
            'author': test_case.emailing.auto_to_case_author,
708
            'default_tester_of_case': test_case.emailing.auto_to_case_tester,
709
            'managers_of_runs': test_case.emailing.auto_to_run_manager,
710
            'default_testers_of_runs': test_case.emailing.auto_to_run_tester,
711
            'assignees_of_case_runs': test_case.emailing.auto_to_case_run_assignee,
712
            'cc_list': MultipleEmailField.delimiter.join(
713
                test_case.emailing.get_cc_list()),
714
        })
715
716
        components = []
717
        for component in test_case.component.all():
718
            components.append(component.pk)
719
720
        default_tester = None
721
        if test_case.default_tester_id:
722
            default_tester = test_case.default_tester.email
723
724
        form = NewCaseForm(initial={
725
            'summary': test_case.summary,
726
            'default_tester': default_tester,
727
            'requirement': test_case.requirement,
728
            'is_automated': test_case.is_automated,
729
            'script': test_case.script,
730
            'arguments': test_case.arguments,
731
            'extra_link': test_case.extra_link,
732
            'case_status': test_case.case_status_id,
733
            'priority': test_case.priority_id,
734
            'product': test_case.category.product_id,
735
            'category': test_case.category_id,
736
            'notes': test_case.notes,
737
            'text': test_case.text,
738
        })
739
740
        form.populate(product_id=test_case.category.product_id)
741
742
    context_data = {
743
        'test_case': test_case,
744
        'test_plan': test_plan,
745
        'form': form,
746
        'notify_form': n_form,
747
    }
748
    return render(request, 'testcases/mutable.html', context_data)
749
750
751
@permission_required('testcases.add_testcase')
752
def clone(request, template_name='case/clone.html'):
753
    """Clone one case or multiple case into other plan or plans"""
754
755
    request_data = getattr(request, request.method)
756
757
    if 'selectAll' not in request_data and 'case' not in request_data:
758
        messages.add_message(request,
759
                             messages.ERROR,
760
                             _('At least one TestCase is required'))
761
        # redirect back where we came from
762
        return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
763
764
    test_plan_src = plan_from_request_or_none(request)
765
    test_plan = None
766
    search_plan_form = SearchPlanForm()
767
768
    # Do the clone action
769
    if request.method == 'POST':
770
        clone_form = CloneCaseForm(request.POST)
771
        clone_form.populate(case_ids=request.POST.getlist('case'))
772
773
        if clone_form.is_valid():
774
            tcs_src = clone_form.cleaned_data['case']
775
            for tc_src in tcs_src:
776
                if clone_form.cleaned_data['copy_case']:
777
                    tc_dest = TestCase.objects.create(
778
                        is_automated=tc_src.is_automated,
779
                        script=tc_src.script,
780
                        arguments=tc_src.arguments,
781
                        extra_link=tc_src.extra_link,
782
                        summary=tc_src.summary,
783
                        requirement=tc_src.requirement,
784
                        case_status=TestCaseStatus.get_proposed(),
785
                        category=tc_src.category,
786
                        priority=tc_src.priority,
787
                        notes=tc_src.notes,
788
                        text=tc_src.text,
789
                        author=clone_form.cleaned_data[
790
                            'maintain_case_orignal_author'] and
791
                        tc_src.author or request.user,
792
                        default_tester=clone_form.cleaned_data[
793
                            'maintain_case_orignal_default_tester'] and
794
                        tc_src.author or request.user,
795
                    )
796
797
                    for test_plan in clone_form.cleaned_data['plan']:
798
                        # copy a case and keep origin case's sortkey
799
                        if test_plan_src:
800
                            try:
801
                                tcp = TestCasePlan.objects.get(plan=test_plan_src,
802
                                                               case=tc_src)
803
                                sortkey = tcp.sortkey
804
                            except ObjectDoesNotExist:
805
                                sortkey = test_plan.get_case_sortkey()
806
                        else:
807
                            sortkey = test_plan.get_case_sortkey()
808
809
                        test_plan.add_case(tc_dest, sortkey)
810
811
                    for tag in tc_src.tag.all():
812
                        tc_dest.add_tag(tag=tag)
813
                else:
814
                    tc_dest = tc_src
815
                    tc_dest.author = request.user
816
                    if clone_form.cleaned_data['maintain_case_orignal_author']:
817
                        tc_dest.author = tc_src.author
818
819
                    tc_dest.default_tester = request.user
820
                    if clone_form.cleaned_data['maintain_case_orignal_default_tester']:
821
                        tc_dest.default_tester = tc_src.default_tester
822
823
                    tc_dest.save()
824
825
                    for test_plan in clone_form.cleaned_data['plan']:
826
                        # create case link and keep origin plan's sortkey
827
                        if test_plan_src:
828
                            try:
829
                                tcp = TestCasePlan.objects.get(plan=test_plan_src,
830
                                                               case=tc_dest)
831
                                sortkey = tcp.sortkey
832
                            except ObjectDoesNotExist:
833
                                sortkey = test_plan.get_case_sortkey()
834
                        else:
835
                            sortkey = test_plan.get_case_sortkey()
836
837
                        test_plan.add_case(tc_dest, sortkey)
838
839
                # Add the cases to plan
840
                for test_plan in clone_form.cleaned_data['plan']:
841
                    # Clone the categories to new product
842
                    if clone_form.cleaned_data['copy_case']:
843
                        try:
844
                            tc_category = test_plan.product.category.get(
845
                                name=tc_src.category.name
846
                            )
847
                        except ObjectDoesNotExist:
848
                            tc_category = test_plan.product.category.create(
849
                                name=tc_src.category.name,
850
                                description=tc_src.category.description,
851
                            )
852
853
                        tc_dest.category = tc_category
854
                        tc_dest.save()
855
                        del tc_category
856
857
                    # Clone the components to new product
858
                    if clone_form.cleaned_data['copy_component'] and \
859
                            clone_form.cleaned_data['copy_case']:
860
                        for component in tc_src.component.all():
861
                            try:
862
                                new_c = test_plan.product.component.get(
863
                                    name=component.name
864
                                )
865
                            except ObjectDoesNotExist:
866
                                new_c = test_plan.product.component.create(
867
                                    name=component.name,
868
                                    initial_owner=request.user,
869
                                    description=component.description,
870
                                )
871
872
                            tc_dest.add_component(new_c)
873
874
            # Detect the number of items and redirect to correct one
875
            cases_count = len(clone_form.cleaned_data['case'])
876
            plans_count = len(clone_form.cleaned_data['plan'])
877
878
            if cases_count == 1 and plans_count == 1:
879
                return HttpResponseRedirect('%s?from_plan=%s' % (
880
                    reverse('testcases-get', args=[tc_dest.pk, ]),
0 ignored issues
show
The variable tc_dest does not seem to be defined in case the for loop on line 775 is not entered. Are you sure this can never be the case?
Loading history...
881
                    test_plan.pk
882
                ))
883
884
            if cases_count == 1:
885
                return HttpResponseRedirect(
886
                    reverse('testcases-get', args=[tc_dest.pk, ])
887
                )
888
889
            if plans_count == 1:
890
                return HttpResponseRedirect(
891
                    reverse('test_plan_url_short', args=[test_plan.pk, ])
892
                )
893
894
            # Otherwise it will prompt to user the clone action is successful.
895
            messages.add_message(request,
896
                                 messages.SUCCESS,
897
                                 _('TestCase cloning was successful'))
898
            return HttpResponseRedirect(reverse('plans-search'))
899
    else:
900
        selected_cases = get_selected_testcases(request)
901
        # Initial the clone case form
902
        clone_form = CloneCaseForm(initial={
903
            'case': selected_cases,
904
            'copy_case': False,
905
            'maintain_case_orignal_author': False,
906
            'maintain_case_orignal_default_tester': False,
907
            'copy_component': True,
908
        })
909
        clone_form.populate(case_ids=selected_cases)
910
911
    # Generate search plan form
912
    if request_data.get('from_plan'):
913
        test_plan = TestPlan.objects.get(plan_id=request_data['from_plan'])
914
        search_plan_form = SearchPlanForm(
915
            initial={'product': test_plan.product_id, 'is_active': True})
916
        search_plan_form.populate(product_id=test_plan.product_id)
917
918
    submit_action = request_data.get('submit', None)
919
    context = {
920
        'test_plan': test_plan,
921
        'search_form': search_plan_form,
922
        'clone_form': clone_form,
923
        'submit_action': submit_action,
924
    }
925
    return render(request, template_name, context)
926
927
928
@permission_required('testcases.add_testcaseattachment')
929
def attachment(request, case_id, template_name='case/attachment.html'):
930
    """Manage test case attachments"""
931
932
    test_case = get_object_or_404(TestCase, case_id=case_id)
933
    test_plan = plan_from_request_or_none(request)
934
935
    context = {
936
        'testplan': test_plan,
937
        'testcase': test_case,
938
        'limit': settings.FILE_UPLOAD_MAX_SIZE,
939
    }
940
    return render(request, template_name, context)
941