1
|
|
|
# Copyright (c) 2019-2021 Alexander Todorov <[email protected]> |
2
|
|
|
|
3
|
|
|
# Licensed under the GPL 2.0: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
4
|
|
|
|
5
|
|
|
|
6
|
|
|
from django.contrib.auth.decorators import permission_required |
7
|
|
|
from django.http import HttpResponseRedirect |
8
|
|
|
from django.urls import reverse |
9
|
|
|
from django.utils.decorators import method_decorator |
10
|
|
|
from django.utils.translation import gettext_lazy as _ |
11
|
|
|
from django.views.generic import DetailView |
12
|
|
|
from django.views.generic.base import TemplateView, View |
13
|
|
|
from django.views.generic.edit import CreateView, UpdateView |
14
|
|
|
from guardian.decorators import permission_required as object_permission_required |
15
|
|
|
|
16
|
|
|
from tcms.bugs.forms import BugCommentForm, NewBugForm |
17
|
|
|
from tcms.bugs.models import Bug |
18
|
|
|
from tcms.core.helpers.comments import add_comment |
19
|
|
|
from tcms.management.models import Component |
20
|
|
|
|
21
|
|
|
|
22
|
|
|
@method_decorator( |
23
|
|
|
object_permission_required( |
24
|
|
|
"bugs.view_bug", (Bug, "pk", "pk"), accept_global_perms=True |
25
|
|
|
), |
26
|
|
|
name="dispatch", |
27
|
|
|
) |
28
|
|
|
class Get(DetailView): |
29
|
|
|
model = Bug |
30
|
|
|
template_name = "bugs/get.html" |
31
|
|
|
http_method_names = ["get"] |
32
|
|
|
|
33
|
|
|
def get_context_data(self, **kwargs): |
34
|
|
|
context = super().get_context_data(**kwargs) |
35
|
|
|
context["comment_form"] = BugCommentForm() |
36
|
|
|
context["comment_form"].populate(self.object.pk) |
37
|
|
|
context["executions"] = self.object.executions.all() |
38
|
|
|
context["OBJECT_MENU_ITEMS"] = [ |
39
|
|
|
( |
40
|
|
|
"...", |
41
|
|
|
[ |
42
|
|
|
(_("Edit"), reverse("bugs-edit", args=[self.object.pk])), |
43
|
|
|
("-", "-"), |
44
|
|
|
( |
45
|
|
|
_("Object permissions"), |
46
|
|
|
reverse("admin:bugs_bug_permissions", args=[self.object.pk]), |
47
|
|
|
), |
48
|
|
|
("-", "-"), |
49
|
|
|
( |
50
|
|
|
_("Delete"), |
51
|
|
|
reverse("admin:bugs_bug_delete", args=[self.object.pk]), |
52
|
|
|
), |
53
|
|
|
], |
54
|
|
|
) |
55
|
|
|
] |
56
|
|
|
|
57
|
|
|
return context |
58
|
|
|
|
59
|
|
|
|
60
|
|
|
@method_decorator(permission_required("bugs.add_bug"), name="dispatch") |
61
|
|
|
class New(CreateView): |
62
|
|
|
model = Bug |
63
|
|
|
form_class = NewBugForm |
64
|
|
|
template_name = "bugs/mutable.html" |
65
|
|
|
|
66
|
|
|
def get_context_data(self, **kwargs): |
67
|
|
|
context = super().get_context_data(**kwargs) |
68
|
|
|
context["page_title"] = _("New Bug") |
69
|
|
|
context["form_post_url"] = reverse("bugs-new") |
70
|
|
|
return context |
71
|
|
|
|
72
|
|
|
def get_form_kwargs(self): |
73
|
|
|
kwargs = super().get_form_kwargs() |
74
|
|
|
kwargs["initial"].update( # pylint: disable=objects-update-used |
75
|
|
|
{ |
76
|
|
|
"reporter": self.request.user, |
77
|
|
|
} |
78
|
|
|
) |
79
|
|
|
return kwargs |
80
|
|
|
|
81
|
|
|
def get_form(self, form_class=None): |
82
|
|
|
form = super().get_form(form_class) |
83
|
|
|
# clear fields which are set dynamically via JavaScript |
84
|
|
|
form.populate(self.request.POST.get("product", -1)) |
85
|
|
|
return form |
86
|
|
|
|
87
|
|
|
def form_valid(self, form): |
88
|
|
|
response = super().form_valid(form) |
89
|
|
|
|
90
|
|
|
if not self.object.assignee: |
91
|
|
|
self.object.assignee = New.find_assignee(self.request.POST) |
92
|
|
|
self.object.save() |
93
|
|
|
add_comment([self.object], form.cleaned_data["text"], self.request.user) |
94
|
|
|
|
95
|
|
|
return response |
96
|
|
|
|
97
|
|
|
@staticmethod |
98
|
|
|
def assignee_from_components(components): |
99
|
|
|
""" |
100
|
|
|
Return the first owner which is assigned to any of the |
101
|
|
|
components. This is as best as we can to automatically figure |
102
|
|
|
out who should be assigned to this bug. |
103
|
|
|
""" |
104
|
|
|
for component in components: |
105
|
|
|
if component.initial_owner: |
106
|
|
|
return component.initial_owner |
107
|
|
|
|
108
|
|
|
return None |
109
|
|
|
|
110
|
|
|
@staticmethod |
111
|
|
|
def find_assignee(data): |
112
|
|
|
""" |
113
|
|
|
Try to automatically find an assignee for Bug by first scanning |
114
|
|
|
TestCase components (if present) and then components for the |
115
|
|
|
product. |
116
|
|
|
""" |
117
|
|
|
assignee = None |
118
|
|
|
if "_execution" in data: |
119
|
|
|
assignee = New.assignee_from_components( |
120
|
|
|
data["_execution"].case.component.all() |
121
|
|
|
) |
122
|
|
|
del data["_execution"] |
123
|
|
|
|
124
|
|
|
if not assignee: |
125
|
|
|
assignee = New.assignee_from_components( |
126
|
|
|
Component.objects.filter(product=data["product"]) |
127
|
|
|
) |
128
|
|
|
|
129
|
|
|
return assignee |
130
|
|
|
|
131
|
|
|
@staticmethod |
132
|
|
|
def create_bug(data): |
133
|
|
|
""" |
134
|
|
|
Helper method used within Issue Tracker integration. |
135
|
|
|
|
136
|
|
|
:param data: Untrusted input, usually via HTTP request |
137
|
|
|
:type data: dict |
138
|
|
|
:return: bug |
139
|
|
|
:rtype: Model |
140
|
|
|
""" |
141
|
|
|
bug = None |
142
|
|
|
|
143
|
|
|
if "assignee" not in data or not data["assignee"]: |
144
|
|
|
data["assignee"] = New.find_assignee(data) |
145
|
|
|
|
146
|
|
|
text = data["text"] |
147
|
|
|
del data["text"] |
148
|
|
|
|
149
|
|
|
bug = Bug.objects.create(**data) |
150
|
|
|
add_comment([bug], text, bug.reporter) |
151
|
|
|
|
152
|
|
|
return bug |
153
|
|
|
|
154
|
|
|
|
155
|
|
|
@method_decorator( |
156
|
|
|
object_permission_required( |
157
|
|
|
"bugs.change_bug", (Bug, "pk", "pk"), accept_global_perms=True |
158
|
|
|
), |
159
|
|
|
name="dispatch", |
160
|
|
|
) |
161
|
|
|
class Edit(UpdateView): |
162
|
|
|
model = Bug |
163
|
|
|
form_class = NewBugForm |
164
|
|
|
template_name = "bugs/mutable.html" |
165
|
|
|
_values_before_update = {} |
166
|
|
|
|
167
|
|
|
def _record_changes(self, new_data): |
168
|
|
|
result = "" |
169
|
|
|
for field in self.model._meta.fields: |
170
|
|
|
field = field.name |
171
|
|
|
if ( |
172
|
|
|
field in new_data |
173
|
|
|
and self._values_before_update[field] != new_data[field] |
174
|
|
|
): |
175
|
|
|
_before_update = self._values_before_update[field] |
176
|
|
|
_after_update = new_data[field] |
177
|
|
|
result += f"{field.title()}: {_before_update} -> {_after_update}\n" |
178
|
|
|
if result: |
179
|
|
|
add_comment([self.object], result, self.request.user) |
180
|
|
|
|
181
|
|
|
def get_initial(self): |
182
|
|
|
return { |
183
|
|
|
"assignee": self.object.assignee, |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
def get_context_data(self, **kwargs): |
187
|
|
|
context = super().get_context_data(**kwargs) |
188
|
|
|
context["page_title"] = _("Edit bug") |
189
|
|
|
context["form_post_url"] = reverse( |
190
|
|
|
"bugs-edit", |
191
|
|
|
args=[ |
192
|
|
|
self.object.pk, |
193
|
|
|
], |
194
|
|
|
) |
195
|
|
|
return context |
196
|
|
|
|
197
|
|
|
def get_object(self, queryset=None): |
198
|
|
|
obj = super().get_object(queryset) |
199
|
|
|
for field in self.model._meta.fields: |
200
|
|
|
self._values_before_update[field.name] = getattr(obj, field.name) |
201
|
|
|
return obj |
202
|
|
|
|
203
|
|
|
def form_valid(self, form): |
204
|
|
|
self._record_changes(form.cleaned_data) |
205
|
|
|
return super().form_valid(form) |
206
|
|
|
|
207
|
|
|
|
208
|
|
|
@method_decorator(permission_required("django_comments.add_comment"), name="dispatch") |
209
|
|
|
class AddComment(View): |
210
|
|
|
http_methods = ["post"] |
211
|
|
|
|
212
|
|
|
def post(self, request): |
213
|
|
|
form = BugCommentForm(request.POST) |
214
|
|
|
request_action = request.POST.get("action") |
215
|
|
|
|
216
|
|
|
if form.is_valid(): |
217
|
|
|
bug = form.cleaned_data["bug"] |
218
|
|
|
if form.cleaned_data["text"]: |
219
|
|
|
add_comment([bug], form.cleaned_data["text"], request.user) |
220
|
|
|
|
221
|
|
|
if request_action == "close": |
222
|
|
|
bug.status = False |
223
|
|
|
add_comment([bug], _("*bug closed*"), request.user) |
224
|
|
|
|
225
|
|
|
if request_action == "reopen": |
226
|
|
|
bug.status = True |
227
|
|
|
add_comment([bug], _("*bug reopened*"), request.user) |
228
|
|
|
bug.save() |
229
|
|
|
|
230
|
|
|
return HttpResponseRedirect(reverse("bugs-get", args=[bug.pk])) |
|
|
|
|
231
|
|
|
|
232
|
|
|
|
233
|
|
|
@method_decorator(permission_required("bugs.view_bug"), name="dispatch") |
234
|
|
|
class Search(TemplateView): |
235
|
|
|
template_name = "bugs/search.html" |
236
|
|
|
|
237
|
|
|
def get_context_data(self, **kwargs): |
238
|
|
|
form = NewBugForm(self.request.GET) |
239
|
|
|
form.populate(self.request.GET.get("product", -1)) |
240
|
|
|
|
241
|
|
|
return {"form": form} |
242
|
|
|
|