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