James Moger
2014-06-09 3ad8f70923bae17a67328afa5857ff82bf05530d
commit | author | age
5e3521 1 /*
JM 2  * Copyright 2013 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.text.MessageFormat;
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Collections;
667163 22 import java.util.Comparator;
5e3521 23 import java.util.List;
JM 24 import java.util.Set;
25 import java.util.TreeSet;
26
27 import org.apache.wicket.Component;
28 import org.apache.wicket.PageParameters;
29 import org.apache.wicket.behavior.SimpleAttributeModifier;
30 import org.apache.wicket.markup.html.basic.Label;
31 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
32 import org.apache.wicket.markup.html.panel.Fragment;
33 import org.apache.wicket.markup.repeater.Item;
34 import org.apache.wicket.markup.repeater.data.DataView;
35 import org.apache.wicket.markup.repeater.data.ListDataProvider;
36
37 import com.gitblit.Constants.AccessPermission;
38 import com.gitblit.Keys;
39 import com.gitblit.models.RegistrantAccessPermission;
ce048e 40 import com.gitblit.models.RepositoryModel;
5e3521 41 import com.gitblit.models.TicketModel;
JM 42 import com.gitblit.models.TicketModel.Status;
43 import com.gitblit.models.UserModel;
44 import com.gitblit.tickets.QueryBuilder;
45 import com.gitblit.tickets.QueryResult;
46 import com.gitblit.tickets.TicketIndexer.Lucene;
47 import com.gitblit.tickets.TicketLabel;
48 import com.gitblit.tickets.TicketMilestone;
49 import com.gitblit.tickets.TicketResponsible;
50 import com.gitblit.utils.ArrayUtils;
51 import com.gitblit.utils.StringUtils;
52 import com.gitblit.wicket.GitBlitWebSession;
fdd82f 53 import com.gitblit.wicket.TicketsUI;
JM 54 import com.gitblit.wicket.TicketsUI.TicketQuery;
55 import com.gitblit.wicket.TicketsUI.TicketSort;
5e3521 56 import com.gitblit.wicket.WicketUtils;
JM 57 import com.gitblit.wicket.panels.LinkPanel;
fdd82f 58 import com.gitblit.wicket.panels.TicketListPanel;
JM 59 import com.gitblit.wicket.panels.TicketSearchForm;
5e3521 60
fdd82f 61 public class TicketsPage extends RepositoryPage {
5e3521 62
JM 63     final TicketResponsible any;
64
65     public TicketsPage(PageParameters params) {
66         super(params);
67
68         if (!app().tickets().isReady()) {
69             // tickets prohibited
70             setResponsePage(SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryName));
71         } else if (!app().tickets().hasTickets(getRepositoryModel())) {
72             // no tickets for this repository
73             setResponsePage(NoTicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName));
74         } else {
75             String id = WicketUtils.getObject(params);
76             if (id != null) {
77                 // view the ticket with the TicketPage
78                 setResponsePage(TicketPage.class, params);
79             }
80         }
81
82         // set stateless page preference
83         setStatelessHint(true);
84
9f50fd 85         any = new TicketResponsible(getString("gb.any"), "[* TO *]", null);
5e3521 86
JM 87         UserModel user = GitBlitWebSession.get().getUser();
88         boolean isAuthenticated = user != null && user.isAuthenticated;
89
90         final String [] statiiParam = params.getStringArray(Lucene.status.name());
91         final String assignedToParam = params.getString(Lucene.responsible.name(), null);
92         final String milestoneParam = params.getString(Lucene.milestone.name(), null);
93         final String queryParam = params.getString("q", null);
94         final String searchParam = params.getString("s", null);
95         final String sortBy = Lucene.fromString(params.getString("sort", Lucene.created.name())).name();
96         final boolean desc = !"asc".equals(params.getString("direction", "desc"));
97
98         // add search form
fdd82f 99         add(new TicketSearchForm("ticketSearchForm", repositoryName, searchParam, getClass(), params));
5e3521 100
JM 101         final String activeQuery;
102         if (!StringUtils.isEmpty(searchParam)) {
103             activeQuery = searchParam;
104         } else if (StringUtils.isEmpty(queryParam)) {
105             activeQuery = "";
106         } else {
107             activeQuery = queryParam;
108         }
109
110         // build Lucene query from defaults and request parameters
111         QueryBuilder qb = new QueryBuilder(queryParam);
112         if (!qb.containsField(Lucene.rid.name())) {
113             // specify the repository
114             qb.and(Lucene.rid.matches(getRepositoryModel().getRID()));
115         }
116         if (!qb.containsField(Lucene.responsible.name())) {
117             // specify the responsible
118             qb.and(Lucene.responsible.matches(assignedToParam));
119         }
120         if (!qb.containsField(Lucene.milestone.name())) {
121             // specify the milestone
122             qb.and(Lucene.milestone.matches(milestoneParam));
123         }
124         if (!qb.containsField(Lucene.status.name()) && !ArrayUtils.isEmpty(statiiParam)) {
125             // specify the states
126             boolean not = false;
127             QueryBuilder q = new QueryBuilder();
128             for (String state : statiiParam) {
129                 if (state.charAt(0) == '!') {
130                     not = true;
131                     q.and(Lucene.status.doesNotMatch(state.substring(1)));
132                 } else {
133                     q.or(Lucene.status.matches(state));
134                 }
135             }
136             if (not) {
137                 qb.and(q.toString());
138             } else {
139                 qb.and(q.toSubquery().toString());
140             }
141         }
142         final String luceneQuery = qb.build();
143
144         // open milestones
145         List<TicketMilestone> milestones = app().tickets().getMilestones(getRepositoryModel(), Status.Open);
146         TicketMilestone currentMilestone = null;
147         if (!StringUtils.isEmpty(milestoneParam)) {
148             for (TicketMilestone tm : milestones) {
149                 if (tm.name.equals(milestoneParam)) {
150                     // get the milestone (queries the index)
151                     currentMilestone = app().tickets().getMilestone(getRepositoryModel(), milestoneParam);
152                     break;
153                 }
154             }
155
156             if (currentMilestone == null) {
157                 // milestone not found, create a temporary one
158                 currentMilestone = new TicketMilestone(milestoneParam);
a96110 159                 String q = QueryBuilder.q(Lucene.rid.matches(getRepositoryModel().getRID())).and(Lucene.milestone.matches(milestoneParam)).build();
JM 160                 currentMilestone.tickets = app().tickets().queryFor(q, 1, 0, Lucene.number.name(), true);
161                 milestones.add(currentMilestone);
5e3521 162             }
JM 163         }
164
165         Fragment milestonePanel;
166         if (currentMilestone == null) {
167             milestonePanel = new Fragment("milestonePanel", "noMilestoneFragment", this);
168             add(milestonePanel);
169         } else {
170             milestonePanel = new Fragment("milestonePanel", "milestoneProgressFragment", this);
171             milestonePanel.add(new Label("currentMilestone", currentMilestone.name));
172             if (currentMilestone.due == null) {
173                 milestonePanel.add(new Label("currentDueDate", getString("gb.notSpecified")));
174             } else {
175                 milestonePanel.add(WicketUtils.createDateLabel("currentDueDate", currentMilestone.due, GitBlitWebSession
176                         .get().getTimezone(), getTimeUtils(), false));
177             }
178             Label label = new Label("progress");
179             WicketUtils.setCssStyle(label, "width:" + currentMilestone.getProgress() + "%;");
180             milestonePanel.add(label);
181
182             milestonePanel.add(new LinkPanel("openTickets", null,
9f50fd 183                     MessageFormat.format(getString("gb.nOpenTickets"), currentMilestone.getOpenTickets()),
5e3521 184                     TicketsPage.class,
fdd82f 185                     queryParameters(null, currentMilestone.name, TicketsUI.openStatii, null, sortBy, desc, 1)));
5e3521 186
JM 187             milestonePanel.add(new LinkPanel("closedTickets", null,
9f50fd 188                     MessageFormat.format(getString("gb.nClosedTickets"), currentMilestone.getClosedTickets()),
5e3521 189                     TicketsPage.class,
fdd82f 190                     queryParameters(null, currentMilestone.name, TicketsUI.closedStatii, null, sortBy, desc, 1)));
5e3521 191
9f50fd 192             milestonePanel.add(new Label("totalTickets", MessageFormat.format(getString("gb.nTotalTickets"), currentMilestone.getTotalTickets())));
5e3521 193             add(milestonePanel);
JM 194         }
195
196         Fragment milestoneDropdown = new Fragment("milestoneDropdown", "milestoneDropdownFragment", this);
197         PageParameters resetMilestone = queryParameters(queryParam, null, statiiParam, assignedToParam, sortBy, desc, 1);
198         milestoneDropdown.add(new BookmarkablePageLink<Void>("resetMilestone", TicketsPage.class, resetMilestone));
199
200         ListDataProvider<TicketMilestone> milestonesDp = new ListDataProvider<TicketMilestone>(milestones);
201         DataView<TicketMilestone> milestonesMenu = new DataView<TicketMilestone>("milestone", milestonesDp) {
202             private static final long serialVersionUID = 1L;
203
204             @Override
205             public void populateItem(final Item<TicketMilestone> item) {
206                 final TicketMilestone tm = item.getModelObject();
207                 PageParameters params = queryParameters(queryParam, tm.name, statiiParam, assignedToParam, sortBy, desc, 1);
208                 item.add(new LinkPanel("milestoneLink", null, tm.name, TicketsPage.class, params).setRenderBodyOnly(true));
209             }
210         };
211         milestoneDropdown.add(milestonesMenu);
212         milestonePanel.add(milestoneDropdown);
213
214         // search or query tickets
215         int page = Math.max(1,  WicketUtils.getPage(params));
216         int pageSize = app().settings().getInteger(Keys.tickets.perPage, 25);
217         List<QueryResult> results;
218         if (StringUtils.isEmpty(searchParam)) {
219             results = app().tickets().queryFor(luceneQuery, page, pageSize, sortBy, desc);
220         } else {
221             results = app().tickets().searchFor(getRepositoryModel(), searchParam, page, pageSize);
222         }
223         int totalResults = results.size() == 0 ? 0 : results.get(0).totalResults;
224
225         // standard queries
226         add(new BookmarkablePageLink<Void>("changesQuery", TicketsPage.class,
227                 queryParameters(
228                         Lucene.type.matches(TicketModel.Type.Proposal.name()),
229                         milestoneParam,
230                         statiiParam,
231                         assignedToParam,
232                         sortBy,
233                         desc,
234                         1)));
235
236         add(new BookmarkablePageLink<Void>("bugsQuery", TicketsPage.class,
237                 queryParameters(
238                         Lucene.type.matches(TicketModel.Type.Bug.name()),
239                         milestoneParam,
240                         statiiParam,
241                         assignedToParam,
242                         sortBy,
243                         desc,
244                         1)));
245
246         add(new BookmarkablePageLink<Void>("enhancementsQuery", TicketsPage.class,
247                 queryParameters(
248                         Lucene.type.matches(TicketModel.Type.Enhancement.name()),
249                         milestoneParam,
250                         statiiParam,
251                         assignedToParam,
252                         sortBy,
253                         desc,
254                         1)));
255
256         add(new BookmarkablePageLink<Void>("tasksQuery", TicketsPage.class,
257                 queryParameters(
258                         Lucene.type.matches(TicketModel.Type.Task.name()),
259                         milestoneParam,
260                         statiiParam,
261                         assignedToParam,
262                         sortBy,
263                         desc,
264                         1)));
265
266         add(new BookmarkablePageLink<Void>("questionsQuery", TicketsPage.class,
267                 queryParameters(
268                         Lucene.type.matches(TicketModel.Type.Question.name()),
269                         milestoneParam,
270                         statiiParam,
271                         assignedToParam,
272                         sortBy,
273                         desc,
274                         1)));
275
276         add(new BookmarkablePageLink<Void>("resetQuery", TicketsPage.class,
277                 queryParameters(
278                         null,
279                         milestoneParam,
fdd82f 280                         TicketsUI.openStatii,
5e3521 281                         null,
JM 282                         null,
283                         true,
284                         1)));
285
286         if (isAuthenticated) {
287             add(new Label("userDivider"));
288             add(new BookmarkablePageLink<Void>("createdQuery", TicketsPage.class,
289                     queryParameters(
290                             Lucene.createdby.matches(user.username),
291                             milestoneParam,
292                             statiiParam,
293                             assignedToParam,
294                             sortBy,
295                             desc,
296                             1)));
297
298             add(new BookmarkablePageLink<Void>("watchedQuery", TicketsPage.class,
299                     queryParameters(
300                             Lucene.watchedby.matches(user.username),
301                             milestoneParam,
302                             statiiParam,
303                             assignedToParam,
304                             sortBy,
305                             desc,
306                             1)));
307             add(new BookmarkablePageLink<Void>("mentionsQuery", TicketsPage.class,
308                     queryParameters(
309                             Lucene.mentions.matches(user.username),
310                             milestoneParam,
311                             statiiParam,
312                             assignedToParam,
313                             sortBy,
314                             desc,
315                             1)));
316         } else {
317             add(new Label("userDivider").setVisible(false));
318             add(new Label("createdQuery").setVisible(false));
319             add(new Label("watchedQuery").setVisible(false));
320             add(new Label("mentionsQuery").setVisible(false));
321         }
322
323         Set<TicketQuery> dynamicQueries = new TreeSet<TicketQuery>();
324         for (TicketLabel label : app().tickets().getLabels(getRepositoryModel())) {
325             String q = QueryBuilder.q(Lucene.labels.matches(label.name)).build();
326             dynamicQueries.add(new TicketQuery(label.name, q).color(label.color));
327         }
328
329         for (QueryResult ticket : results) {
330             if (!StringUtils.isEmpty(ticket.topic)) {
331                 String q = QueryBuilder.q(Lucene.topic.matches(ticket.topic)).build();
332                 dynamicQueries.add(new TicketQuery(ticket.topic, q));
333             }
334
335             if (!ArrayUtils.isEmpty(ticket.labels)) {
336                 for (String label : ticket.labels) {
337                     String q = QueryBuilder.q(Lucene.labels.matches(label)).build();
338                     dynamicQueries.add(new TicketQuery(label, q));
339                 }
340             }
341         }
342
343         if (dynamicQueries.size() == 0) {
344             add(new Label("dynamicQueries").setVisible(false));
345         } else {
346             Fragment fragment = new Fragment("dynamicQueries", "dynamicQueriesFragment", this);
347             ListDataProvider<TicketQuery> dynamicQueriesDp = new ListDataProvider<TicketQuery>(new ArrayList<TicketQuery>(dynamicQueries));
348             DataView<TicketQuery> dynamicQueriesList = new DataView<TicketQuery>("dynamicQuery", dynamicQueriesDp) {
349                 private static final long serialVersionUID = 1L;
350
351                 @Override
352                 public void populateItem(final Item<TicketQuery> item) {
353                     final TicketQuery tq = item.getModelObject();
354                     Component swatch = new Label("swatch", "&nbsp;").setEscapeModelStrings(false);
355                     if (StringUtils.isEmpty(tq.color)) {
356                         // calculate a color
357                         tq.color = StringUtils.getColor(tq.name);
358                     }
359                     String background = MessageFormat.format("background-color:{0};", tq.color);
360                     swatch.add(new SimpleAttributeModifier("style", background));
361                     item.add(swatch);
362                     if (activeQuery.contains(tq.query)) {
363                         // selected
364                         String q = QueryBuilder.q(activeQuery).remove(tq.query).build();
365                         PageParameters params = queryParameters(q, milestoneParam, statiiParam, assignedToParam, sortBy, desc, 1);
366                         item.add(new LinkPanel("link", "active", tq.name, TicketsPage.class, params).setRenderBodyOnly(true));
367                         Label checked = new Label("checked");
368                         WicketUtils.setCssClass(checked, "iconic-o-x");
369                         item.add(checked);
370                         item.add(new SimpleAttributeModifier("style", background));
371                     } else {
372                         // unselected
373                         String q = QueryBuilder.q(queryParam).toSubquery().and(tq.query).build();
374                         PageParameters params = queryParameters(q, milestoneParam, statiiParam, assignedToParam, sortBy, desc, 1);
375                         item.add(new LinkPanel("link", null, tq.name, TicketsPage.class, params).setRenderBodyOnly(true));
376                         item.add(new Label("checked").setVisible(false));
377                     }
378                 }
379             };
380             fragment.add(dynamicQueriesList);
381             add(fragment);
382         }
383
384         // states
385         if (ArrayUtils.isEmpty(statiiParam)) {
386             add(new Label("selectedStatii", getString("gb.all")));
387         } else {
388             add(new Label("selectedStatii", StringUtils.flattenStrings(Arrays.asList(statiiParam), ",")));
389         }
fdd82f 390         add(new BookmarkablePageLink<Void>("openTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.openStatii, assignedToParam, sortBy, desc, 1)));
JM 391         add(new BookmarkablePageLink<Void>("closedTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.closedStatii, assignedToParam, sortBy, desc, 1)));
5e3521 392         add(new BookmarkablePageLink<Void>("allTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, null, assignedToParam, sortBy, desc, 1)));
JM 393
394         // by status
706251 395         List<Status> statii = new ArrayList<Status>(Arrays.asList(Status.values()));
JM 396         statii.remove(Status.Closed);
5e3521 397         ListDataProvider<Status> resolutionsDp = new ListDataProvider<Status>(statii);
JM 398         DataView<Status> statiiLinks = new DataView<Status>("statii", resolutionsDp) {
399             private static final long serialVersionUID = 1L;
400
401             @Override
402             public void populateItem(final Item<Status> item) {
403                 final Status status = item.getModelObject();
404                 PageParameters p = queryParameters(queryParam, milestoneParam, new String [] { status.name().toLowerCase() }, assignedToParam, sortBy, desc, 1);
fdd82f 405                 String css = TicketsUI.getStatusClass(status);
5e3521 406                 item.add(new LinkPanel("statusLink", css, status.toString(), TicketsPage.class, p).setRenderBodyOnly(true));
JM 407             }
408         };
409         add(statiiLinks);
410
411         // responsible filter
412         List<TicketResponsible> responsibles = new ArrayList<TicketResponsible>();
413         for (RegistrantAccessPermission perm : app().repositories().getUserAccessPermissions(getRepositoryModel())) {
414             if (perm.permission.atLeast(AccessPermission.PUSH)) {
415                 UserModel u = app().users().getUserModel(perm.registrant);
416                 responsibles.add(new TicketResponsible(u));
417             }
418         }
419         Collections.sort(responsibles);
420         responsibles.add(0, any);
421
422         TicketResponsible currentResponsible = null;
423         for (TicketResponsible u : responsibles) {
424             if (u.username.equals(assignedToParam)) {
425                 currentResponsible = u;
426                 break;
427             }
428         }
429
430         add(new Label("currentResponsible", currentResponsible == null ? "" : currentResponsible.displayname));
431         ListDataProvider<TicketResponsible> responsibleDp = new ListDataProvider<TicketResponsible>(responsibles);
432         DataView<TicketResponsible> responsibleMenu = new DataView<TicketResponsible>("responsible", responsibleDp) {
433             private static final long serialVersionUID = 1L;
434
435             @Override
436             public void populateItem(final Item<TicketResponsible> item) {
437                 final TicketResponsible u = item.getModelObject();
438                 PageParameters params = queryParameters(queryParam, milestoneParam, statiiParam, u.username, sortBy, desc, 1);
439                 item.add(new LinkPanel("responsibleLink", null, u.displayname, TicketsPage.class, params).setRenderBodyOnly(true));
440             }
441         };
442         add(responsibleMenu);
443         PageParameters resetResponsibleParams = queryParameters(queryParam, milestoneParam, statiiParam, null, sortBy, desc, 1);
444         add(new BookmarkablePageLink<Void>("resetResponsible", TicketsPage.class, resetResponsibleParams));
445
446         List<TicketSort> sortChoices = new ArrayList<TicketSort>();
447         sortChoices.add(new TicketSort(getString("gb.sortNewest"), Lucene.created.name(), true));
448         sortChoices.add(new TicketSort(getString("gb.sortOldest"), Lucene.created.name(), false));
449         sortChoices.add(new TicketSort(getString("gb.sortMostRecentlyUpdated"), Lucene.updated.name(), true));
450         sortChoices.add(new TicketSort(getString("gb.sortLeastRecentlyUpdated"), Lucene.updated.name(), false));
451         sortChoices.add(new TicketSort(getString("gb.sortMostComments"), Lucene.comments.name(), true));
452         sortChoices.add(new TicketSort(getString("gb.sortLeastComments"), Lucene.comments.name(), false));
453         sortChoices.add(new TicketSort(getString("gb.sortMostPatchsetRevisions"), Lucene.patchsets.name(), true));
454         sortChoices.add(new TicketSort(getString("gb.sortLeastPatchsetRevisions"), Lucene.patchsets.name(), false));
455         sortChoices.add(new TicketSort(getString("gb.sortMostVotes"), Lucene.votes.name(), true));
456         sortChoices.add(new TicketSort(getString("gb.sortLeastVotes"), Lucene.votes.name(), false));
457
458         TicketSort currentSort = sortChoices.get(0);
459         for (TicketSort ts : sortChoices) {
460             if (ts.sortBy.equals(sortBy) && desc == ts.desc) {
461                 currentSort = ts;
462                 break;
463             }
464         }
465         add(new Label("currentSort", currentSort.name));
466
467         ListDataProvider<TicketSort> sortChoicesDp = new ListDataProvider<TicketSort>(sortChoices);
468         DataView<TicketSort> sortMenu = new DataView<TicketSort>("sort", sortChoicesDp) {
469             private static final long serialVersionUID = 1L;
470
471             @Override
472             public void populateItem(final Item<TicketSort> item) {
473                 final TicketSort ts = item.getModelObject();
474                 PageParameters params = queryParameters(queryParam, milestoneParam, statiiParam, assignedToParam, ts.sortBy, ts.desc, 1);
475                 item.add(new LinkPanel("sortLink", null, ts.name, TicketsPage.class, params).setRenderBodyOnly(true));
476             }
477         };
478         add(sortMenu);
479
480
481         // paging links
482         buildPager(queryParam, milestoneParam, statiiParam, assignedToParam, sortBy, desc, page, pageSize, results.size(), totalResults);
483
fdd82f 484         add(new TicketListPanel("ticketList", results, false, false));
5e3521 485
ce048e 486         // new milestone link
JM 487         RepositoryModel repositoryModel = getRepositoryModel();
488         final boolean acceptingUpdates = app().tickets().isAcceptingTicketUpdates(repositoryModel)
489                  && user != null && user.canAdmin(getRepositoryModel());
490         if (acceptingUpdates) {
491             add(new LinkPanel("newMilestone", null, getString("gb.newMilestone"),
492                 NewMilestonePage.class, WicketUtils.newRepositoryParameter(repositoryName)));
493         } else {
494             add(new Label("newMilestone").setVisible(false));
495         }
667163 496
ce048e 497         // milestones list
8d2caa 498         List<TicketMilestone> openMilestones = new ArrayList<TicketMilestone>();
JM 499         List<TicketMilestone> closedMilestones = new ArrayList<TicketMilestone>();
500         for (TicketMilestone milestone : app().tickets().getMilestones(repositoryModel)) {
501             if (milestone.isOpen()) {
502                 openMilestones.add(milestone);
503             } else {
504                 closedMilestones.add(milestone);
505             }
506         }
507         Collections.sort(openMilestones, new Comparator<TicketMilestone>() {
667163 508             @Override
JM 509             public int compare(TicketMilestone o1, TicketMilestone o2) {
510                 return o2.due.compareTo(o1.due);
511             }
512         });
8d2caa 513
JM 514         Collections.sort(closedMilestones, new Comparator<TicketMilestone>() {
515             @Override
516             public int compare(TicketMilestone o1, TicketMilestone o2) {
517                 return o2.due.compareTo(o1.due);
518             }
519         });
520
521         DataView<TicketMilestone> openMilestonesList = milestoneList("openMilestonesList", openMilestones, acceptingUpdates);
522         add(openMilestonesList);
523
524         DataView<TicketMilestone> closedMilestonesList = milestoneList("closedMilestonesList", closedMilestones, acceptingUpdates);
525         add(closedMilestonesList);
526     }
527
528     protected DataView<TicketMilestone> milestoneList(String wicketId, List<TicketMilestone> milestones, final boolean acceptingUpdates) {
529         ListDataProvider<TicketMilestone> milestonesDp = new ListDataProvider<TicketMilestone>(milestones);
530         DataView<TicketMilestone> milestonesList = new DataView<TicketMilestone>(wicketId, milestonesDp) {
5e3521 531             private static final long serialVersionUID = 1L;
JM 532
533             @Override
534             public void populateItem(final Item<TicketMilestone> item) {
8d2caa 535                 Fragment entryPanel = new Fragment("entryPanel", "milestoneListFragment", this);
JM 536                 item.add(entryPanel);
537
5e3521 538                 final TicketMilestone tm = item.getModelObject();
d2d5fc 539                 String [] states;
JM 540                 if (tm.isOpen()) {
541                     states = TicketsUI.openStatii;
542                 } else {
543                     states = TicketsUI.closedStatii;
544                 }
545                 PageParameters params = queryParameters(null, tm.name, states, null, null, true, 1);
8d2caa 546                 entryPanel.add(new LinkPanel("milestoneName", null, tm.name, TicketsPage.class, params).setRenderBodyOnly(true));
a96110 547
JM 548                 String css;
667163 549                 String status = tm.status.name();
a96110 550                 switch (tm.status) {
JM 551                 case Open:
667163 552                     if (tm.isOverdue()) {
JM 553                         css = "aui-lozenge aui-lozenge-subtle aui-lozenge-error";
554                         status = "overdue";
555                     } else {
556                         css = "aui-lozenge aui-lozenge-subtle";
557                     }
a96110 558                     break;
JM 559                 default:
560                     css = "aui-lozenge";
561                     break;
562                 }
667163 563                 Label stateLabel = new Label("milestoneState", status);
a96110 564                 WicketUtils.setCssClass(stateLabel, css);
8d2caa 565                 entryPanel.add(stateLabel);
a96110 566
JM 567                 if (tm.due == null) {
8d2caa 568                     entryPanel.add(new Label("milestoneDue", getString("gb.notSpecified")));
a96110 569                 } else {
8d2caa 570                     entryPanel.add(WicketUtils.createDatestampLabel("milestoneDue", tm.due, getTimeZone(), getTimeUtils()));
a96110 571                 }
ce048e 572                 if (acceptingUpdates) {
8d2caa 573                     entryPanel.add(new LinkPanel("editMilestone", null, getString("gb.edit"), EditMilestonePage.class,
ce048e 574                         WicketUtils.newObjectParameter(repositoryName, tm.name)));
JM 575                 } else {
8d2caa 576                     entryPanel.add(new Label("editMilestone").setVisible(false));
JM 577                 }
578
579                 if (tm.isOpen()) {
580                     // re-load milestone with query results
581                     TicketMilestone m = app().tickets().getMilestone(getRepositoryModel(), tm.name);
582
583                     Fragment milestonePanel = new Fragment("milestonePanel", "openMilestoneFragment", this);
584                     Label label = new Label("progress");
a98ebb 585                     WicketUtils.setCssStyle(label, "width:" + m.getProgress() + "%;");
8d2caa 586                     milestonePanel.add(label);
JM 587
588                     milestonePanel.add(new LinkPanel("openTickets", null,
589                             MessageFormat.format(getString("gb.nOpenTickets"), m.getOpenTickets()),
590                             TicketsPage.class,
fdd82f 591                             queryParameters(null, tm.name, TicketsUI.openStatii, null, null, true, 1)));
8d2caa 592
JM 593                     milestonePanel.add(new LinkPanel("closedTickets", null,
594                             MessageFormat.format(getString("gb.nClosedTickets"), m.getClosedTickets()),
595                             TicketsPage.class,
fdd82f 596                             queryParameters(null, tm.name, TicketsUI.closedStatii, null, null, true, 1)));
8d2caa 597
JM 598                     milestonePanel.add(new Label("totalTickets", MessageFormat.format(getString("gb.nTotalTickets"), m.getTotalTickets())));
599                     entryPanel.add(milestonePanel);
600                 } else {
601                     entryPanel.add(new Label("milestonePanel").setVisible(false));
ce048e 602                 }
5e3521 603             }
JM 604         };
8d2caa 605         return milestonesList;
5e3521 606     }
JM 607
608     protected PageParameters queryParameters(
609             String query,
610             String milestone,
611             String[] states,
612             String assignedTo,
613             String sort,
614             boolean descending,
615             int page) {
616
617         PageParameters params = WicketUtils.newRepositoryParameter(repositoryName);
618         if (!StringUtils.isEmpty(query)) {
619             params.add("q", query);
620         }
621         if (!StringUtils.isEmpty(milestone)) {
622             params.add(Lucene.milestone.name(), milestone);
623         }
624         if (!ArrayUtils.isEmpty(states)) {
625             for (String state : states) {
626                 params.add(Lucene.status.name(), state);
627             }
628         }
629         if (!StringUtils.isEmpty(assignedTo)) {
630             params.add(Lucene.responsible.name(), assignedTo);
631         }
632         if (!StringUtils.isEmpty(sort)) {
633             params.add("sort", sort);
634         }
635         if (!descending) {
636             params.add("direction", "asc");
637         }
638         if (page > 1) {
639             params.add("pg", "" + page);
640         }
641         return params;
642     }
643
644     protected PageParameters newTicketParameter(QueryResult ticket) {
645         return WicketUtils.newObjectParameter(repositoryName, "" + ticket.number);
646     }
647
648     @Override
649     protected String getPageName() {
650         return getString("gb.tickets");
651     }
652
653     protected void buildPager(
654             final String query,
655             final String milestone,
656             final String [] states,
657             final String assignedTo,
658             final String sort,
659             final boolean desc,
660             final int page,
661             int pageSize,
662             int count,
663             int total) {
664
665         boolean showNav = total > (2 * pageSize);
666         boolean allowPrev = page > 1;
667         boolean allowNext = (pageSize * (page - 1) + count) < total;
668         add(new BookmarkablePageLink<Void>("prevLink", TicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, page - 1)).setEnabled(allowPrev).setVisible(showNav));
669         add(new BookmarkablePageLink<Void>("nextLink", TicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, page + 1)).setEnabled(allowNext).setVisible(showNav));
670
671         if (total <= pageSize) {
672             add(new Label("pageLink").setVisible(false));
673             return;
674         }
675
676         // determine page numbers to display
677         int pages = count == 0 ? 0 : ((total / pageSize) + (total % pageSize == 0 ? 0 : 1));
678         // preferred number of pagelinks
679         int segments = 5;
680         if (pages < segments) {
681             // not enough data for preferred number of page links
682             segments = pages;
683         }
684         int minpage = Math.min(Math.max(1, page - 2), pages - (segments - 1));
685         int maxpage = Math.min(pages, minpage + (segments - 1));
686         List<Integer> sequence = new ArrayList<Integer>();
687         for (int i = minpage; i <= maxpage; i++) {
688             sequence.add(i);
689         }
690
691         ListDataProvider<Integer> pagesDp = new ListDataProvider<Integer>(sequence);
692         DataView<Integer> pagesView = new DataView<Integer>("pageLink", pagesDp) {
693             private static final long serialVersionUID = 1L;
694
695             @Override
696             public void populateItem(final Item<Integer> item) {
697                 final Integer i = item.getModelObject();
698                 LinkPanel link = new LinkPanel("page", null, "" + i, TicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, i));
699                 link.setRenderBodyOnly(true);
700                 if (i == page) {
701                     WicketUtils.setCssClass(item, "active");
702                 }
703                 item.add(link);
704             }
705         };
706         add(pagesView);
707     }
708 }