commit | author | age
|
5e3521
|
1 |
/*
|
JM |
2 |
* Copyright 2014 gitblit.com.
|
|
3 |
*
|
|
4 |
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5 |
* you may not use this file except in compliance with the License.
|
|
6 |
* You may obtain a copy of the License at
|
|
7 |
*
|
|
8 |
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9 |
*
|
|
10 |
* Unless required by applicable law or agreed to in writing, software
|
|
11 |
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13 |
* See the License for the specific language governing permissions and
|
|
14 |
* limitations under the License.
|
|
15 |
*/
|
|
16 |
package com.gitblit.wicket.pages;
|
|
17 |
|
|
18 |
import java.util.ArrayList;
|
|
19 |
import java.util.Arrays;
|
|
20 |
import java.util.Collections;
|
|
21 |
import java.util.List;
|
|
22 |
import java.util.Set;
|
|
23 |
import java.util.TreeSet;
|
|
24 |
|
|
25 |
import org.apache.wicket.PageParameters;
|
9d60ff
|
26 |
import org.apache.wicket.ajax.AjaxRequestTarget;
|
JM |
27 |
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
|
5e3521
|
28 |
import org.apache.wicket.markup.html.basic.Label;
|
JM |
29 |
import org.apache.wicket.markup.html.form.Button;
|
|
30 |
import org.apache.wicket.markup.html.form.DropDownChoice;
|
|
31 |
import org.apache.wicket.markup.html.form.Form;
|
|
32 |
import org.apache.wicket.markup.html.form.TextField;
|
|
33 |
import org.apache.wicket.markup.html.panel.Fragment;
|
|
34 |
import org.apache.wicket.model.IModel;
|
|
35 |
import org.apache.wicket.model.Model;
|
06eb51
|
36 |
import org.eclipse.jgit.lib.Repository;
|
5e3521
|
37 |
|
06eb51
|
38 |
import com.gitblit.Constants;
|
5e3521
|
39 |
import com.gitblit.Constants.AccessPermission;
|
c211e9
|
40 |
import com.gitblit.Constants.AuthorizationControl;
|
5e3521
|
41 |
import com.gitblit.models.RegistrantAccessPermission;
|
JM |
42 |
import com.gitblit.models.TicketModel;
|
|
43 |
import com.gitblit.models.TicketModel.Change;
|
|
44 |
import com.gitblit.models.TicketModel.Field;
|
|
45 |
import com.gitblit.models.TicketModel.Status;
|
|
46 |
import com.gitblit.models.TicketModel.Type;
|
|
47 |
import com.gitblit.models.UserModel;
|
|
48 |
import com.gitblit.tickets.TicketMilestone;
|
|
49 |
import com.gitblit.tickets.TicketNotifier;
|
|
50 |
import com.gitblit.tickets.TicketResponsible;
|
|
51 |
import com.gitblit.utils.StringUtils;
|
|
52 |
import com.gitblit.wicket.GitBlitWebSession;
|
|
53 |
import com.gitblit.wicket.WicketUtils;
|
|
54 |
import com.gitblit.wicket.panels.MarkdownTextArea;
|
388f49
|
55 |
import com.google.common.base.Optional;
|
5e3521
|
56 |
|
JM |
57 |
/**
|
|
58 |
* Page for editing a ticket.
|
|
59 |
*
|
|
60 |
* @author James Moger
|
|
61 |
*
|
|
62 |
*/
|
|
63 |
public class EditTicketPage extends RepositoryPage {
|
|
64 |
|
|
65 |
static final String NIL = "<nil>";
|
|
66 |
|
|
67 |
static final String ESC_NIL = StringUtils.escapeForHtml(NIL, false);
|
|
68 |
|
|
69 |
private IModel<TicketModel.Type> typeModel;
|
|
70 |
|
|
71 |
private IModel<String> titleModel;
|
|
72 |
|
|
73 |
private MarkdownTextArea descriptionEditor;
|
|
74 |
|
|
75 |
private IModel<String> topicModel;
|
06eb51
|
76 |
|
JM |
77 |
private IModel<String> mergeToModel;
|
5e3521
|
78 |
|
7ae1fa
|
79 |
private IModel<Status> statusModel;
|
JM |
80 |
|
5e3521
|
81 |
private IModel<TicketResponsible> responsibleModel;
|
JM |
82 |
|
|
83 |
private IModel<TicketMilestone> milestoneModel;
|
|
84 |
|
|
85 |
private Label descriptionPreview;
|
e2c0c9
|
86 |
|
f9c78c
|
87 |
private IModel<TicketModel.Priority> priorityModel;
|
e2c0c9
|
88 |
|
f9c78c
|
89 |
private IModel<TicketModel.Severity> severityModel;
|
5e3521
|
90 |
|
JM |
91 |
public EditTicketPage(PageParameters params) {
|
|
92 |
super(params);
|
|
93 |
|
|
94 |
UserModel currentUser = GitBlitWebSession.get().getUser();
|
|
95 |
if (currentUser == null) {
|
|
96 |
currentUser = UserModel.ANONYMOUS;
|
|
97 |
}
|
|
98 |
|
|
99 |
long ticketId = 0L;
|
|
100 |
try {
|
|
101 |
String h = WicketUtils.getObject(params);
|
|
102 |
ticketId = Long.parseLong(h);
|
|
103 |
} catch (Exception e) {
|
|
104 |
setResponsePage(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName));
|
|
105 |
}
|
|
106 |
|
|
107 |
TicketModel ticket = app().tickets().getTicket(getRepositoryModel(), ticketId);
|
7ca053
|
108 |
if (ticket == null
|
JM |
109 |
|| !currentUser.canEdit(ticket, getRepositoryModel())
|
|
110 |
|| !app().tickets().isAcceptingTicketUpdates(getRepositoryModel())) {
|
|
111 |
setResponsePage(TicketsPage.class, WicketUtils.newObjectParameter(repositoryName, "" + ticketId));
|
856a13
|
112 |
|
JM |
113 |
// create a placeholder object so we don't trigger NPEs
|
|
114 |
ticket = new TicketModel();
|
5e3521
|
115 |
}
|
JM |
116 |
|
|
117 |
typeModel = Model.of(ticket.type);
|
a59627
|
118 |
titleModel = Model.of(ticket.title);
|
JM |
119 |
topicModel = Model.of(ticket.topic == null ? "" : ticket.topic);
|
5e3521
|
120 |
responsibleModel = Model.of();
|
JM |
121 |
milestoneModel = Model.of();
|
f1b882
|
122 |
mergeToModel = Model.of(ticket.mergeTo == null ? getRepositoryModel().mergeTo : ticket.mergeTo);
|
7ae1fa
|
123 |
statusModel = Model.of(ticket.status);
|
f9c78c
|
124 |
priorityModel = Model.of(ticket.priority);
|
PM |
125 |
severityModel = Model.of(ticket.severity);
|
5e3521
|
126 |
|
JM |
127 |
setStatelessHint(false);
|
|
128 |
setOutputMarkupId(true);
|
|
129 |
|
9d60ff
|
130 |
Form<Void> form = new Form<Void>("editForm");
|
5e3521
|
131 |
add(form);
|
JM |
132 |
|
|
133 |
List<Type> typeChoices;
|
|
134 |
if (ticket.isProposal()) {
|
|
135 |
typeChoices = Arrays.asList(Type.Proposal);
|
|
136 |
} else {
|
|
137 |
typeChoices = Arrays.asList(TicketModel.Type.choices());
|
|
138 |
}
|
|
139 |
form.add(new DropDownChoice<TicketModel.Type>("type", typeModel, typeChoices));
|
7ae1fa
|
140 |
|
5e3521
|
141 |
form.add(new TextField<String>("title", titleModel));
|
JM |
142 |
form.add(new TextField<String>("topic", topicModel));
|
|
143 |
|
a59627
|
144 |
final IModel<String> markdownPreviewModel = Model.of(ticket.body == null ? "" : ticket.body);
|
5e3521
|
145 |
descriptionPreview = new Label("descriptionPreview", markdownPreviewModel);
|
JM |
146 |
descriptionPreview.setEscapeModelStrings(false);
|
|
147 |
descriptionPreview.setOutputMarkupId(true);
|
|
148 |
form.add(descriptionPreview);
|
|
149 |
|
|
150 |
descriptionEditor = new MarkdownTextArea("description", markdownPreviewModel, descriptionPreview);
|
|
151 |
descriptionEditor.setRepository(repositoryName);
|
|
152 |
descriptionEditor.setText(ticket.body);
|
|
153 |
form.add(descriptionEditor);
|
|
154 |
|
7ca053
|
155 |
// status
|
JM |
156 |
List<Status> statusChoices;
|
|
157 |
if (ticket.isClosed()) {
|
|
158 |
statusChoices = Arrays.asList(ticket.status, Status.Open);
|
|
159 |
} else if (ticket.isProposal()) {
|
|
160 |
statusChoices = Arrays.asList(TicketModel.Status.proposalWorkflow);
|
|
161 |
} else if (ticket.isBug()) {
|
|
162 |
statusChoices = Arrays.asList(TicketModel.Status.bugWorkflow);
|
5e3521
|
163 |
} else {
|
7ca053
|
164 |
statusChoices = Arrays.asList(TicketModel.Status.requestWorkflow);
|
5e3521
|
165 |
}
|
7ca053
|
166 |
Fragment status = new Fragment("status", "statusFragment", this);
|
JM |
167 |
status.add(new DropDownChoice<TicketModel.Status>("status", statusModel, statusChoices));
|
|
168 |
form.add(status);
|
|
169 |
|
e2c0c9
|
170 |
List<TicketModel.Severity> severityChoices = Arrays.asList(TicketModel.Severity.choices());
|
JM |
171 |
form.add(new DropDownChoice<TicketModel.Severity>("severity", severityModel, severityChoices));
|
|
172 |
|
cc1c3f
|
173 |
if (currentUser.canAdmin(ticket, getRepositoryModel())) {
|
JM |
174 |
// responsible
|
|
175 |
Set<String> userlist = new TreeSet<String>(ticket.getParticipants());
|
7ca053
|
176 |
|
c211e9
|
177 |
if (UserModel.ANONYMOUS.canPush(getRepositoryModel())
|
JM |
178 |
|| AuthorizationControl.AUTHENTICATED == getRepositoryModel().authorizationControl) {
|
|
179 |
// authorization is ANONYMOUS or AUTHENTICATED (i.e. all users can be set responsible)
|
eb6129
|
180 |
userlist.addAll(app().users().getAllUsernames());
|
JM |
181 |
} else {
|
c211e9
|
182 |
// authorization is by NAMED users (users with PUSH permission can be set responsible)
|
eb6129
|
183 |
for (RegistrantAccessPermission rp : app().repositories().getUserAccessPermissions(getRepositoryModel())) {
|
805468
|
184 |
if (rp.permission.atLeast(AccessPermission.PUSH)) {
|
eb6129
|
185 |
userlist.add(rp.registrant);
|
JM |
186 |
}
|
7ca053
|
187 |
}
|
JM |
188 |
}
|
|
189 |
|
cc1c3f
|
190 |
List<TicketResponsible> responsibles = new ArrayList<TicketResponsible>();
|
JM |
191 |
for (String username : userlist) {
|
|
192 |
UserModel user = app().users().getUserModel(username);
|
eb6129
|
193 |
if (user != null && !user.disabled) {
|
cc1c3f
|
194 |
TicketResponsible responsible = new TicketResponsible(user);
|
JM |
195 |
responsibles.add(responsible);
|
|
196 |
if (user.username.equals(ticket.responsible)) {
|
|
197 |
responsibleModel.setObject(responsible);
|
|
198 |
}
|
|
199 |
}
|
7ca053
|
200 |
}
|
cc1c3f
|
201 |
Collections.sort(responsibles);
|
JM |
202 |
responsibles.add(new TicketResponsible(NIL, "", ""));
|
|
203 |
Fragment responsible = new Fragment("responsible", "responsibleFragment", this);
|
|
204 |
responsible.add(new DropDownChoice<TicketResponsible>("responsible", responsibleModel, responsibles));
|
|
205 |
form.add(responsible.setVisible(!responsibles.isEmpty()));
|
7ca053
|
206 |
|
cc1c3f
|
207 |
// milestone
|
JM |
208 |
List<TicketMilestone> milestones = app().tickets().getMilestones(getRepositoryModel(), Status.Open);
|
|
209 |
for (TicketMilestone milestone : milestones) {
|
|
210 |
if (milestone.name.equals(ticket.milestone)) {
|
|
211 |
milestoneModel.setObject(milestone);
|
|
212 |
break;
|
|
213 |
}
|
7ca053
|
214 |
}
|
cc1c3f
|
215 |
if (milestoneModel.getObject() == null && !StringUtils.isEmpty(ticket.milestone)) {
|
JM |
216 |
// ensure that this unrecognized milestone is listed
|
|
217 |
// so that we get the <nil> selection.
|
|
218 |
TicketMilestone tms = new TicketMilestone(ticket.milestone);
|
|
219 |
milestones.add(tms);
|
|
220 |
milestoneModel.setObject(tms);
|
|
221 |
}
|
|
222 |
if (!milestones.isEmpty()) {
|
|
223 |
milestones.add(new TicketMilestone(NIL));
|
|
224 |
}
|
|
225 |
|
e2c0c9
|
226 |
// milestone
|
cc1c3f
|
227 |
Fragment milestone = new Fragment("milestone", "milestoneFragment", this);
|
JM |
228 |
milestone.add(new DropDownChoice<TicketMilestone>("milestone", milestoneModel, milestones));
|
|
229 |
form.add(milestone.setVisible(!milestones.isEmpty()));
|
|
230 |
|
f9c78c
|
231 |
// priority
|
e2c0c9
|
232 |
Fragment priority = new Fragment("priority", "priorityFragment", this);
|
f9c78c
|
233 |
List<TicketModel.Priority> priorityChoices = Arrays.asList(TicketModel.Priority.choices());
|
e2c0c9
|
234 |
priority.add(new DropDownChoice<TicketModel.Priority>("priority", priorityModel, priorityChoices));
|
JM |
235 |
form.add(priority);
|
|
236 |
|
cc1c3f
|
237 |
// mergeTo (integration branch)
|
JM |
238 |
List<String> branches = new ArrayList<String>();
|
|
239 |
for (String branch : getRepositoryModel().getLocalBranches()) {
|
|
240 |
// exclude ticket branches
|
|
241 |
if (!branch.startsWith(Constants.R_TICKET)) {
|
|
242 |
branches.add(Repository.shortenRefName(branch));
|
|
243 |
}
|
|
244 |
}
|
f1b882
|
245 |
branches.remove(Repository.shortenRefName(getRepositoryModel().mergeTo));
|
JM |
246 |
branches.add(0, Repository.shortenRefName(getRepositoryModel().mergeTo));
|
cc1c3f
|
247 |
|
JM |
248 |
Fragment mergeto = new Fragment("mergeto", "mergeToFragment", this);
|
|
249 |
mergeto.add(new DropDownChoice<String>("mergeto", mergeToModel, branches));
|
|
250 |
form.add(mergeto.setVisible(!branches.isEmpty()));
|
|
251 |
} else {
|
|
252 |
// user can not admin this ticket
|
|
253 |
form.add(new Label("responsible").setVisible(false));
|
|
254 |
form.add(new Label("milestone").setVisible(false));
|
|
255 |
form.add(new Label("mergeto").setVisible(false));
|
e2c0c9
|
256 |
form.add(new Label("priority").setVisible(false));
|
7ca053
|
257 |
}
|
9d60ff
|
258 |
|
JM |
259 |
form.add(new AjaxButton("update") {
|
|
260 |
|
|
261 |
private static final long serialVersionUID = 1L;
|
|
262 |
|
|
263 |
@Override
|
|
264 |
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
|
|
265 |
long ticketId = 0L;
|
|
266 |
try {
|
|
267 |
String h = WicketUtils.getObject(getPageParameters());
|
|
268 |
ticketId = Long.parseLong(h);
|
|
269 |
} catch (Exception e) {
|
|
270 |
setResponsePage(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName));
|
|
271 |
}
|
|
272 |
|
|
273 |
TicketModel ticket = app().tickets().getTicket(getRepositoryModel(), ticketId);
|
|
274 |
|
|
275 |
String createdBy = GitBlitWebSession.get().getUsername();
|
|
276 |
Change change = new Change(createdBy);
|
|
277 |
|
|
278 |
String title = titleModel.getObject();
|
|
279 |
if (StringUtils.isEmpty(title)) {
|
|
280 |
return;
|
|
281 |
}
|
|
282 |
|
|
283 |
if (!ticket.title.equals(title)) {
|
|
284 |
// title change
|
|
285 |
change.setField(Field.title, title);
|
|
286 |
}
|
|
287 |
|
388f49
|
288 |
String description = Optional.fromNullable(descriptionEditor.getText()).or("");
|
9d60ff
|
289 |
if ((StringUtils.isEmpty(ticket.body) && !StringUtils.isEmpty(description))
|
JM |
290 |
|| (!StringUtils.isEmpty(ticket.body) && !ticket.body.equals(description))) {
|
|
291 |
// description change
|
|
292 |
change.setField(Field.body, description);
|
|
293 |
}
|
|
294 |
|
|
295 |
Status status = statusModel.getObject();
|
|
296 |
if (!ticket.status.equals(status)) {
|
|
297 |
// status change
|
|
298 |
change.setField(Field.status, status);
|
|
299 |
}
|
|
300 |
|
|
301 |
Type type = typeModel.getObject();
|
|
302 |
if (!ticket.type.equals(type)) {
|
|
303 |
// type change
|
|
304 |
change.setField(Field.type, type);
|
|
305 |
}
|
|
306 |
|
388f49
|
307 |
String topic = Optional.fromNullable(topicModel.getObject()).or("");
|
9d60ff
|
308 |
if ((StringUtils.isEmpty(ticket.topic) && !StringUtils.isEmpty(topic))
|
388f49
|
309 |
|| (!StringUtils.isEmpty(ticket.topic) && !ticket.topic.equals(topic))) {
|
9d60ff
|
310 |
// topic change
|
JM |
311 |
change.setField(Field.topic, topic);
|
|
312 |
}
|
|
313 |
|
|
314 |
TicketResponsible responsible = responsibleModel == null ? null : responsibleModel.getObject();
|
|
315 |
if (responsible != null && !responsible.username.equals(ticket.responsible)) {
|
|
316 |
// responsible change
|
|
317 |
change.setField(Field.responsible, responsible.username);
|
|
318 |
if (!StringUtils.isEmpty(responsible.username)) {
|
|
319 |
if (!ticket.isWatching(responsible.username)) {
|
|
320 |
change.watch(responsible.username);
|
|
321 |
}
|
|
322 |
}
|
|
323 |
}
|
|
324 |
|
|
325 |
TicketMilestone milestone = milestoneModel == null ? null : milestoneModel.getObject();
|
|
326 |
if (milestone != null && !milestone.name.equals(ticket.milestone)) {
|
|
327 |
// milestone change
|
|
328 |
if (NIL.equals(milestone.name)) {
|
|
329 |
change.setField(Field.milestone, "");
|
|
330 |
} else {
|
|
331 |
change.setField(Field.milestone, milestone.name);
|
|
332 |
}
|
|
333 |
}
|
e2c0c9
|
334 |
|
f9c78c
|
335 |
TicketModel.Priority priority = priorityModel.getObject();
|
PM |
336 |
if (!ticket.priority.equals(priority))
|
|
337 |
{
|
|
338 |
change.setField(Field.priority, priority);
|
|
339 |
}
|
9d60ff
|
340 |
|
f9c78c
|
341 |
TicketModel.Severity severity = severityModel.getObject();
|
PM |
342 |
if (!ticket.severity.equals(severity))
|
|
343 |
{
|
|
344 |
change.setField(Field.severity, severity);
|
|
345 |
}
|
e2c0c9
|
346 |
|
9d60ff
|
347 |
String mergeTo = mergeToModel.getObject();
|
JM |
348 |
if ((StringUtils.isEmpty(ticket.mergeTo) && !StringUtils.isEmpty(mergeTo))
|
|
349 |
|| (!StringUtils.isEmpty(mergeTo) && !mergeTo.equals(ticket.mergeTo))) {
|
|
350 |
// integration branch change
|
|
351 |
change.setField(Field.mergeTo, mergeTo);
|
|
352 |
}
|
|
353 |
|
|
354 |
if (change.hasFieldChanges()) {
|
|
355 |
if (!ticket.isWatching(createdBy)) {
|
|
356 |
change.watch(createdBy);
|
|
357 |
}
|
|
358 |
ticket = app().tickets().updateTicket(getRepositoryModel(), ticket.number, change);
|
|
359 |
if (ticket != null) {
|
|
360 |
TicketNotifier notifier = app().tickets().createNotifier();
|
|
361 |
notifier.sendMailing(ticket);
|
60bbdf
|
362 |
redirectTo(TicketsPage.class, WicketUtils.newObjectParameter(getRepositoryModel().name, "" + ticket.number));
|
9d60ff
|
363 |
} else {
|
JM |
364 |
// TODO error
|
|
365 |
}
|
|
366 |
} else {
|
|
367 |
// nothing to change?!
|
60bbdf
|
368 |
redirectTo(TicketsPage.class, WicketUtils.newObjectParameter(getRepositoryModel().name, "" + ticket.number));
|
9d60ff
|
369 |
}
|
JM |
370 |
}
|
|
371 |
});
|
|
372 |
|
5e3521
|
373 |
Button cancel = new Button("cancel") {
|
JM |
374 |
private static final long serialVersionUID = 1L;
|
|
375 |
|
|
376 |
@Override
|
|
377 |
public void onSubmit() {
|
|
378 |
setResponsePage(TicketsPage.class, getPageParameters());
|
|
379 |
}
|
|
380 |
};
|
|
381 |
cancel.setDefaultFormProcessing(false);
|
|
382 |
form.add(cancel);
|
|
383 |
}
|
|
384 |
|
|
385 |
@Override
|
|
386 |
protected String getPageName() {
|
|
387 |
return getString("gb.editTicket");
|
|
388 |
}
|
|
389 |
|
|
390 |
@Override
|
|
391 |
protected Class<? extends BasePage> getRepoNavPageClass() {
|
|
392 |
return TicketsPage.class;
|
|
393 |
}
|
|
394 |
}
|