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) |
|
|
|
|
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, ]), |
|
|
|
|
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
|
|
|
|