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