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