1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
|
3
|
|
|
from django.contrib import messages |
4
|
|
|
from django.contrib.auth.decorators import permission_required |
5
|
|
|
from django.http import HttpResponseRedirect |
6
|
|
|
from django.shortcuts import get_object_or_404, render |
7
|
|
|
from django.urls import reverse |
8
|
|
|
from django.utils.decorators import method_decorator |
9
|
|
|
from django.utils.translation import gettext_lazy as _ |
10
|
|
|
from django.views.generic import DetailView |
11
|
|
|
from django.views.generic.base import TemplateView, View |
12
|
|
|
from django.views.generic.edit import CreateView, UpdateView |
13
|
|
|
from guardian.decorators import permission_required as object_permission_required |
14
|
|
|
|
15
|
|
|
from tcms.testcases.forms import ( |
16
|
|
|
CaseNotifyFormSet, |
17
|
|
|
CloneCaseForm, |
18
|
|
|
SearchCaseForm, |
19
|
|
|
TestCaseForm, |
20
|
|
|
) |
21
|
|
|
from tcms.testcases.models import Template, TestCase |
22
|
|
|
from tcms.testplans.models import TestPlan |
23
|
|
|
|
24
|
|
|
|
25
|
|
|
def plan_from_request_or_none(request): # pylint: disable=missing-permission-required |
26
|
|
|
"""Get TestPlan from REQUEST |
27
|
|
|
|
28
|
|
|
This method relies on the existence of from_plan within REQUEST. |
29
|
|
|
""" |
30
|
|
|
test_plan_id = request.POST.get("from_plan") or request.GET.get("from_plan") |
31
|
|
|
if not test_plan_id: |
32
|
|
|
return None |
33
|
|
|
return get_object_or_404(TestPlan, pk=test_plan_id) |
34
|
|
|
|
35
|
|
|
|
36
|
|
|
@method_decorator(permission_required("testcases.add_testcase"), name="dispatch") |
37
|
|
|
class NewCaseView(CreateView): |
38
|
|
|
model = TestCase |
39
|
|
|
form_class = TestCaseForm |
40
|
|
|
template_name = "testcases/mutable.html" |
41
|
|
|
|
42
|
|
|
def get_form(self, form_class=None): |
43
|
|
|
form = super().get_form(form_class) |
44
|
|
|
# clear fields which are set dynamically via JavaScript |
45
|
|
|
form.populate(self.request.POST.get("product", -1)) |
46
|
|
|
return form |
47
|
|
|
|
48
|
|
|
def get_form_kwargs(self): |
49
|
|
|
kwargs = super().get_form_kwargs() |
50
|
|
|
kwargs["initial"].update( # pylint: disable=objects-update-used |
51
|
|
|
{ |
52
|
|
|
"author": self.request.user, |
53
|
|
|
} |
54
|
|
|
) |
55
|
|
|
|
56
|
|
|
test_plan = plan_from_request_or_none(self.request) |
57
|
|
|
if test_plan: |
58
|
|
|
kwargs["initial"]["product"] = test_plan.product_id |
59
|
|
|
|
60
|
|
|
return kwargs |
61
|
|
|
|
62
|
|
|
def get_context_data(self, **kwargs): |
63
|
|
|
context = super().get_context_data(**kwargs) |
64
|
|
|
context["test_plan"] = plan_from_request_or_none(self.request) |
65
|
|
|
context["notify_formset"] = kwargs.get("notify_formset") or CaseNotifyFormSet() |
66
|
|
|
context["templates"] = Template.objects.all() |
67
|
|
|
return context |
68
|
|
|
|
69
|
|
|
def form_valid(self, form): |
70
|
|
|
test_plan = plan_from_request_or_none(self.request) |
71
|
|
|
|
72
|
|
|
notify_formset = CaseNotifyFormSet(self.request.POST) |
73
|
|
|
if notify_formset.is_valid(): |
74
|
|
|
test_case = form.save() |
75
|
|
|
if test_plan: |
76
|
|
|
test_plan.add_case(test_case) |
77
|
|
|
|
78
|
|
|
notify_formset.instance = test_case |
79
|
|
|
notify_formset.save() |
80
|
|
|
|
81
|
|
|
return HttpResponseRedirect(reverse("testcases-get", args=[test_case.pk])) |
82
|
|
|
|
83
|
|
|
# taken from FormMixin.form_invalid() |
84
|
|
|
return self.render_to_response( |
85
|
|
|
self.get_context_data(notify_formset=notify_formset) |
86
|
|
|
) |
87
|
|
|
|
88
|
|
|
|
89
|
|
|
@method_decorator(permission_required("testcases.view_testcase"), name="dispatch") |
90
|
|
|
class TestCaseSearchView(TemplateView): |
91
|
|
|
""" |
92
|
|
|
Shows the search form which uses JSON RPC to fetch the results |
93
|
|
|
""" |
94
|
|
|
|
95
|
|
|
template_name = "testcases/search.html" |
96
|
|
|
|
97
|
|
|
def get_context_data(self, **kwargs): |
98
|
|
|
form = SearchCaseForm(self.request.GET) |
99
|
|
|
if self.request.GET.get("product"): |
100
|
|
|
form.populate(product_id=self.request.GET["product"]) |
101
|
|
|
else: |
102
|
|
|
form.populate() |
103
|
|
|
|
104
|
|
|
return { |
105
|
|
|
"form": form, |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
|
109
|
|
|
@method_decorator( |
110
|
|
|
object_permission_required( |
111
|
|
|
"testcases.view_testcase", (TestCase, "pk", "pk"), accept_global_perms=True |
112
|
|
|
), |
113
|
|
|
name="dispatch", |
114
|
|
|
) |
115
|
|
|
class TestCaseGetView(DetailView): |
116
|
|
|
|
117
|
|
|
model = TestCase |
118
|
|
|
template_name = "testcases/get.html" |
119
|
|
|
http_method_names = ["get"] |
120
|
|
|
|
121
|
|
|
def get_context_data(self, **kwargs): |
122
|
|
|
context = super().get_context_data(**kwargs) |
123
|
|
|
context["executions"] = self.object.executions.select_related( |
124
|
|
|
"run", "tested_by", "assignee", "case", "status" |
125
|
|
|
).order_by("run__plan", "run") |
126
|
|
|
context["OBJECT_MENU_ITEMS"] = [ |
127
|
|
|
( |
128
|
|
|
"...", |
129
|
|
|
[ |
130
|
|
|
( |
131
|
|
|
_("Edit"), |
132
|
|
|
reverse("testcases-edit", args=[self.object.pk]), |
133
|
|
|
), |
134
|
|
|
( |
135
|
|
|
_("Clone"), |
136
|
|
|
reverse("testcases-clone") + f"?c={self.object.pk}", |
137
|
|
|
), |
138
|
|
|
( |
139
|
|
|
_("History"), |
140
|
|
|
f"/admin/testcases/testcase/{self.object.pk}/history/", |
141
|
|
|
), |
142
|
|
|
("-", "-"), |
143
|
|
|
( |
144
|
|
|
_("Object permissions"), |
145
|
|
|
reverse( |
146
|
|
|
"admin:testcases_testcase_permissions", |
147
|
|
|
args=[self.object.pk], |
148
|
|
|
), |
149
|
|
|
), |
150
|
|
|
("-", "-"), |
151
|
|
|
( |
152
|
|
|
_("Delete"), |
153
|
|
|
reverse( |
154
|
|
|
"admin:testcases_testcase_delete", |
155
|
|
|
args=[self.object.pk], |
156
|
|
|
), |
157
|
|
|
), |
158
|
|
|
], |
159
|
|
|
) |
160
|
|
|
] |
161
|
|
|
|
162
|
|
|
return context |
163
|
|
|
|
164
|
|
|
|
165
|
|
|
@method_decorator( |
166
|
|
|
object_permission_required( |
167
|
|
|
"testcases.change_testcase", (TestCase, "pk", "pk"), accept_global_perms=True |
168
|
|
|
), |
169
|
|
|
name="dispatch", |
170
|
|
|
) |
171
|
|
|
class EditTestCaseView(UpdateView): |
172
|
|
|
|
173
|
|
|
model = TestCase |
174
|
|
|
template_name = "testcases/mutable.html" |
175
|
|
|
form_class = TestCaseForm |
176
|
|
|
|
177
|
|
|
def form_valid(self, form): |
178
|
|
|
notify_formset = CaseNotifyFormSet(self.request.POST, instance=self.object) |
179
|
|
|
if notify_formset.is_valid(): |
180
|
|
|
notify_formset.save() |
181
|
|
|
return super().form_valid(form) |
182
|
|
|
|
183
|
|
|
# taken from FormMixin.form_invalid() |
184
|
|
|
return self.render_to_response( |
185
|
|
|
self.get_context_data(notify_formset=notify_formset) |
186
|
|
|
) |
187
|
|
|
|
188
|
|
|
def get_context_data(self, **kwargs): |
189
|
|
|
context = super().get_context_data(**kwargs) |
190
|
|
|
context["notify_formset"] = kwargs.get("notify_formset") or CaseNotifyFormSet( |
191
|
|
|
instance=self.object |
192
|
|
|
) |
193
|
|
|
return context |
194
|
|
|
|
195
|
|
|
def get_form(self, form_class=None): |
196
|
|
|
form = super().get_form(form_class) |
197
|
|
|
if self.request.POST.get("product"): |
198
|
|
|
form.populate(product_id=self.request.POST["product"]) |
199
|
|
|
else: |
200
|
|
|
form.populate(product_id=self.object.category.product_id) |
201
|
|
|
return form |
202
|
|
|
|
203
|
|
|
def get_initial(self): |
204
|
|
|
default_tester = None |
205
|
|
|
if self.object.default_tester_id: |
206
|
|
|
default_tester = self.object.default_tester.email |
207
|
|
|
|
208
|
|
|
return { |
209
|
|
|
"product": self.object.category.product_id, |
210
|
|
|
"default_tester": default_tester, |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
|
214
|
|
|
@method_decorator(permission_required("testcases.add_testcase"), name="dispatch") |
215
|
|
|
class CloneTestCaseView(View): |
216
|
|
|
"""Clone one case or multiple case into other plan or plans""" |
217
|
|
|
|
218
|
|
|
template_name = "testcases/clone.html" |
219
|
|
|
http_method_names = ["get", "post"] |
220
|
|
|
|
221
|
|
|
def post(self, request): |
222
|
|
|
if not self._is_request_data_valid(request): |
223
|
|
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) |
224
|
|
|
|
225
|
|
|
# Do the clone action |
226
|
|
|
clone_form = CloneCaseForm(request.POST) |
227
|
|
|
clone_form.populate(case_ids=request.POST.getlist("case")) |
228
|
|
|
|
229
|
|
|
if clone_form.is_valid(): |
230
|
|
|
for tc_src in clone_form.cleaned_data["case"]: |
231
|
|
|
tc_dest = tc_src.clone(request.user, clone_form.cleaned_data["plan"]) |
232
|
|
|
|
233
|
|
|
# Detect the number of items and redirect to correct one |
234
|
|
|
if len(clone_form.cleaned_data["case"]) == 1: |
235
|
|
|
return HttpResponseRedirect( |
236
|
|
|
reverse( |
237
|
|
|
"testcases-get", |
238
|
|
|
args=[ |
239
|
|
|
tc_dest.pk, |
|
|
|
|
240
|
|
|
], |
241
|
|
|
) |
242
|
|
|
) |
243
|
|
|
|
244
|
|
|
if len(clone_form.cleaned_data["plan"]) == 1: |
245
|
|
|
test_plan = clone_form.cleaned_data["plan"][0] |
246
|
|
|
return HttpResponseRedirect( |
247
|
|
|
reverse("test_plan_url_short", args=[test_plan.pk]) |
248
|
|
|
) |
249
|
|
|
|
250
|
|
|
# Otherwise tell the user the clone action is successful |
251
|
|
|
messages.add_message( |
252
|
|
|
request, messages.SUCCESS, _("TestCase cloning was successful") |
253
|
|
|
) |
254
|
|
|
return HttpResponseRedirect(reverse("plans-search")) |
255
|
|
|
|
256
|
|
|
# invalid form |
257
|
|
|
messages.add_message(request, messages.ERROR, clone_form.errors) |
258
|
|
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) |
259
|
|
|
|
260
|
|
|
def get(self, request): |
261
|
|
|
if not self._is_request_data_valid(request, "c"): |
262
|
|
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) |
263
|
|
|
|
264
|
|
|
# account for short param names in URI |
265
|
|
|
get_params = request.GET.copy() |
266
|
|
|
get_params.setlist("case", request.GET.getlist("c")) |
267
|
|
|
del get_params["c"] |
268
|
|
|
|
269
|
|
|
clone_form = CloneCaseForm(get_params) |
270
|
|
|
clone_form.populate(case_ids=get_params.getlist("case")) |
271
|
|
|
|
272
|
|
|
context = { |
273
|
|
|
"form": clone_form, |
274
|
|
|
} |
275
|
|
|
return render(request, self.template_name, context) |
276
|
|
|
|
277
|
|
|
@staticmethod |
278
|
|
|
def _is_request_data_valid(request, field_name="case"): |
279
|
|
|
request_data = getattr(request, request.method) |
280
|
|
|
|
281
|
|
|
if field_name not in request_data: |
282
|
|
|
messages.add_message( |
283
|
|
|
request, messages.ERROR, _("At least one TestCase is required") |
284
|
|
|
) |
285
|
|
|
return False |
286
|
|
|
|
287
|
|
|
return True |
288
|
|
|
|