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