James Moger
2015-11-22 ed552ba47c02779c270ffd62841d6d1048dade70
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)));
f5d568 275         
PM 276         add(new BookmarkablePageLink<Void>("maintenanceQuery", TicketsPage.class,
277                 queryParameters(
278                         Lucene.type.matches(TicketModel.Type.Maintenance.name()),
279                         milestoneParam,
280                         statiiParam,
281                         assignedToParam,
282                         sortBy,
283                         desc,
284                         1)));
5e3521 285
JM 286         add(new BookmarkablePageLink<Void>("resetQuery", TicketsPage.class,
287                 queryParameters(
288                         null,
289                         milestoneParam,
fdd82f 290                         TicketsUI.openStatii,
5e3521 291                         null,
JM 292                         null,
293                         true,
294                         1)));
295
296         if (isAuthenticated) {
297             add(new Label("userDivider"));
298             add(new BookmarkablePageLink<Void>("createdQuery", TicketsPage.class,
299                     queryParameters(
300                             Lucene.createdby.matches(user.username),
301                             milestoneParam,
302                             statiiParam,
303                             assignedToParam,
304                             sortBy,
305                             desc,
306                             1)));
307
308             add(new BookmarkablePageLink<Void>("watchedQuery", TicketsPage.class,
309                     queryParameters(
310                             Lucene.watchedby.matches(user.username),
311                             milestoneParam,
312                             statiiParam,
313                             assignedToParam,
314                             sortBy,
315                             desc,
316                             1)));
317             add(new BookmarkablePageLink<Void>("mentionsQuery", TicketsPage.class,
318                     queryParameters(
319                             Lucene.mentions.matches(user.username),
320                             milestoneParam,
321                             statiiParam,
322                             assignedToParam,
323                             sortBy,
324                             desc,
325                             1)));
326         } else {
327             add(new Label("userDivider").setVisible(false));
328             add(new Label("createdQuery").setVisible(false));
329             add(new Label("watchedQuery").setVisible(false));
330             add(new Label("mentionsQuery").setVisible(false));
331         }
332
333         Set<TicketQuery> dynamicQueries = new TreeSet<TicketQuery>();
334         for (TicketLabel label : app().tickets().getLabels(getRepositoryModel())) {
335             String q = QueryBuilder.q(Lucene.labels.matches(label.name)).build();
336             dynamicQueries.add(new TicketQuery(label.name, q).color(label.color));
337         }
338
339         for (QueryResult ticket : results) {
340             if (!StringUtils.isEmpty(ticket.topic)) {
341                 String q = QueryBuilder.q(Lucene.topic.matches(ticket.topic)).build();
342                 dynamicQueries.add(new TicketQuery(ticket.topic, q));
343             }
344
345             if (!ArrayUtils.isEmpty(ticket.labels)) {
346                 for (String label : ticket.labels) {
347                     String q = QueryBuilder.q(Lucene.labels.matches(label)).build();
348                     dynamicQueries.add(new TicketQuery(label, q));
349                 }
350             }
351         }
352
353         if (dynamicQueries.size() == 0) {
354             add(new Label("dynamicQueries").setVisible(false));
355         } else {
356             Fragment fragment = new Fragment("dynamicQueries", "dynamicQueriesFragment", this);
357             ListDataProvider<TicketQuery> dynamicQueriesDp = new ListDataProvider<TicketQuery>(new ArrayList<TicketQuery>(dynamicQueries));
358             DataView<TicketQuery> dynamicQueriesList = new DataView<TicketQuery>("dynamicQuery", dynamicQueriesDp) {
359                 private static final long serialVersionUID = 1L;
360
361                 @Override
362                 public void populateItem(final Item<TicketQuery> item) {
363                     final TicketQuery tq = item.getModelObject();
364                     Component swatch = new Label("swatch", "&nbsp;").setEscapeModelStrings(false);
365                     if (StringUtils.isEmpty(tq.color)) {
366                         // calculate a color
367                         tq.color = StringUtils.getColor(tq.name);
368                     }
369                     String background = MessageFormat.format("background-color:{0};", tq.color);
370                     swatch.add(new SimpleAttributeModifier("style", background));
371                     item.add(swatch);
372                     if (activeQuery.contains(tq.query)) {
373                         // selected
374                         String q = QueryBuilder.q(activeQuery).remove(tq.query).build();
375                         PageParameters params = queryParameters(q, milestoneParam, statiiParam, assignedToParam, sortBy, desc, 1);
376                         item.add(new LinkPanel("link", "active", tq.name, TicketsPage.class, params).setRenderBodyOnly(true));
377                         Label checked = new Label("checked");
378                         WicketUtils.setCssClass(checked, "iconic-o-x");
379                         item.add(checked);
380                         item.add(new SimpleAttributeModifier("style", background));
381                     } else {
382                         // unselected
383                         String q = QueryBuilder.q(queryParam).toSubquery().and(tq.query).build();
384                         PageParameters params = queryParameters(q, milestoneParam, statiiParam, assignedToParam, sortBy, desc, 1);
385                         item.add(new LinkPanel("link", null, tq.name, TicketsPage.class, params).setRenderBodyOnly(true));
386                         item.add(new Label("checked").setVisible(false));
387                     }
388                 }
389             };
390             fragment.add(dynamicQueriesList);
391             add(fragment);
392         }
393
394         // states
395         if (ArrayUtils.isEmpty(statiiParam)) {
396             add(new Label("selectedStatii", getString("gb.all")));
397         } else {
398             add(new Label("selectedStatii", StringUtils.flattenStrings(Arrays.asList(statiiParam), ",")));
399         }
fdd82f 400         add(new BookmarkablePageLink<Void>("openTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.openStatii, assignedToParam, sortBy, desc, 1)));
JM 401         add(new BookmarkablePageLink<Void>("closedTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.closedStatii, assignedToParam, sortBy, desc, 1)));
5e3521 402         add(new BookmarkablePageLink<Void>("allTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, null, assignedToParam, sortBy, desc, 1)));
JM 403
404         // by status
706251 405         List<Status> statii = new ArrayList<Status>(Arrays.asList(Status.values()));
JM 406         statii.remove(Status.Closed);
5e3521 407         ListDataProvider<Status> resolutionsDp = new ListDataProvider<Status>(statii);
JM 408         DataView<Status> statiiLinks = new DataView<Status>("statii", resolutionsDp) {
409             private static final long serialVersionUID = 1L;
410
411             @Override
412             public void populateItem(final Item<Status> item) {
413                 final Status status = item.getModelObject();
414                 PageParameters p = queryParameters(queryParam, milestoneParam, new String [] { status.name().toLowerCase() }, assignedToParam, sortBy, desc, 1);
fdd82f 415                 String css = TicketsUI.getStatusClass(status);
5e3521 416                 item.add(new LinkPanel("statusLink", css, status.toString(), TicketsPage.class, p).setRenderBodyOnly(true));
JM 417             }
418         };
419         add(statiiLinks);
420
421         // responsible filter
422         List<TicketResponsible> responsibles = new ArrayList<TicketResponsible>();
423         for (RegistrantAccessPermission perm : app().repositories().getUserAccessPermissions(getRepositoryModel())) {
424             if (perm.permission.atLeast(AccessPermission.PUSH)) {
425                 UserModel u = app().users().getUserModel(perm.registrant);
426                 responsibles.add(new TicketResponsible(u));
427             }
428         }
429         Collections.sort(responsibles);
430         responsibles.add(0, any);
431
432         TicketResponsible currentResponsible = null;
433         for (TicketResponsible u : responsibles) {
434             if (u.username.equals(assignedToParam)) {
435                 currentResponsible = u;
436                 break;
437             }
438         }
439
440         add(new Label("currentResponsible", currentResponsible == null ? "" : currentResponsible.displayname));
441         ListDataProvider<TicketResponsible> responsibleDp = new ListDataProvider<TicketResponsible>(responsibles);
442         DataView<TicketResponsible> responsibleMenu = new DataView<TicketResponsible>("responsible", responsibleDp) {
443             private static final long serialVersionUID = 1L;
444
445             @Override
446             public void populateItem(final Item<TicketResponsible> item) {
447                 final TicketResponsible u = item.getModelObject();
448                 PageParameters params = queryParameters(queryParam, milestoneParam, statiiParam, u.username, sortBy, desc, 1);
449                 item.add(new LinkPanel("responsibleLink", null, u.displayname, TicketsPage.class, params).setRenderBodyOnly(true));
450             }
451         };
452         add(responsibleMenu);
453         PageParameters resetResponsibleParams = queryParameters(queryParam, milestoneParam, statiiParam, null, sortBy, desc, 1);
454         add(new BookmarkablePageLink<Void>("resetResponsible", TicketsPage.class, resetResponsibleParams));
455
456         List<TicketSort> sortChoices = new ArrayList<TicketSort>();
457         sortChoices.add(new TicketSort(getString("gb.sortNewest"), Lucene.created.name(), true));
458         sortChoices.add(new TicketSort(getString("gb.sortOldest"), Lucene.created.name(), false));
459         sortChoices.add(new TicketSort(getString("gb.sortMostRecentlyUpdated"), Lucene.updated.name(), true));
460         sortChoices.add(new TicketSort(getString("gb.sortLeastRecentlyUpdated"), Lucene.updated.name(), false));
461         sortChoices.add(new TicketSort(getString("gb.sortMostComments"), Lucene.comments.name(), true));
462         sortChoices.add(new TicketSort(getString("gb.sortLeastComments"), Lucene.comments.name(), false));
463         sortChoices.add(new TicketSort(getString("gb.sortMostPatchsetRevisions"), Lucene.patchsets.name(), true));
464         sortChoices.add(new TicketSort(getString("gb.sortLeastPatchsetRevisions"), Lucene.patchsets.name(), false));
465         sortChoices.add(new TicketSort(getString("gb.sortMostVotes"), Lucene.votes.name(), true));
466         sortChoices.add(new TicketSort(getString("gb.sortLeastVotes"), Lucene.votes.name(), false));
f9c78c 467         sortChoices.add(new TicketSort(getString("gb.sortHighestPriority"), Lucene.priority.name(), true));
PM 468         sortChoices.add(new TicketSort(getString("gb.sortLowestPriority"), Lucene.priority.name(), false));
469         sortChoices.add(new TicketSort(getString("gb.sortHighestSeverity"), Lucene.severity.name(), true));
470         sortChoices.add(new TicketSort(getString("gb.sortLowestSeverity"), Lucene.severity.name(), false));
471         
5e3521 472         TicketSort currentSort = sortChoices.get(0);
JM 473         for (TicketSort ts : sortChoices) {
474             if (ts.sortBy.equals(sortBy) && desc == ts.desc) {
475                 currentSort = ts;
476                 break;
477             }
478         }
479         add(new Label("currentSort", currentSort.name));
480
481         ListDataProvider<TicketSort> sortChoicesDp = new ListDataProvider<TicketSort>(sortChoices);
482         DataView<TicketSort> sortMenu = new DataView<TicketSort>("sort", sortChoicesDp) {
483             private static final long serialVersionUID = 1L;
484
485             @Override
486             public void populateItem(final Item<TicketSort> item) {
487                 final TicketSort ts = item.getModelObject();
488                 PageParameters params = queryParameters(queryParam, milestoneParam, statiiParam, assignedToParam, ts.sortBy, ts.desc, 1);
489                 item.add(new LinkPanel("sortLink", null, ts.name, TicketsPage.class, params).setRenderBodyOnly(true));
490             }
491         };
492         add(sortMenu);
493
494
495         // paging links
496         buildPager(queryParam, milestoneParam, statiiParam, assignedToParam, sortBy, desc, page, pageSize, results.size(), totalResults);
497
fdd82f 498         add(new TicketListPanel("ticketList", results, false, false));
5e3521 499
ce048e 500         // new milestone link
JM 501         RepositoryModel repositoryModel = getRepositoryModel();
502         final boolean acceptingUpdates = app().tickets().isAcceptingTicketUpdates(repositoryModel)
503                  && user != null && user.canAdmin(getRepositoryModel());
504         if (acceptingUpdates) {
505             add(new LinkPanel("newMilestone", null, getString("gb.newMilestone"),
506                 NewMilestonePage.class, WicketUtils.newRepositoryParameter(repositoryName)));
507         } else {
508             add(new Label("newMilestone").setVisible(false));
509         }
667163 510
ce048e 511         // milestones list
8d2caa 512         List<TicketMilestone> openMilestones = new ArrayList<TicketMilestone>();
JM 513         List<TicketMilestone> closedMilestones = new ArrayList<TicketMilestone>();
514         for (TicketMilestone milestone : app().tickets().getMilestones(repositoryModel)) {
515             if (milestone.isOpen()) {
516                 openMilestones.add(milestone);
517             } else {
518                 closedMilestones.add(milestone);
519             }
520         }
521         Collections.sort(openMilestones, new Comparator<TicketMilestone>() {
667163 522             @Override
JM 523             public int compare(TicketMilestone o1, TicketMilestone o2) {
524                 return o2.due.compareTo(o1.due);
525             }
526         });
8d2caa 527
JM 528         Collections.sort(closedMilestones, new Comparator<TicketMilestone>() {
529             @Override
530             public int compare(TicketMilestone o1, TicketMilestone o2) {
531                 return o2.due.compareTo(o1.due);
532             }
533         });
534
535         DataView<TicketMilestone> openMilestonesList = milestoneList("openMilestonesList", openMilestones, acceptingUpdates);
536         add(openMilestonesList);
537
538         DataView<TicketMilestone> closedMilestonesList = milestoneList("closedMilestonesList", closedMilestones, acceptingUpdates);
539         add(closedMilestonesList);
540     }
541
542     protected DataView<TicketMilestone> milestoneList(String wicketId, List<TicketMilestone> milestones, final boolean acceptingUpdates) {
543         ListDataProvider<TicketMilestone> milestonesDp = new ListDataProvider<TicketMilestone>(milestones);
544         DataView<TicketMilestone> milestonesList = new DataView<TicketMilestone>(wicketId, milestonesDp) {
5e3521 545             private static final long serialVersionUID = 1L;
JM 546
547             @Override
548             public void populateItem(final Item<TicketMilestone> item) {
8d2caa 549                 Fragment entryPanel = new Fragment("entryPanel", "milestoneListFragment", this);
JM 550                 item.add(entryPanel);
551
5e3521 552                 final TicketMilestone tm = item.getModelObject();
d2d5fc 553                 String [] states;
JM 554                 if (tm.isOpen()) {
555                     states = TicketsUI.openStatii;
556                 } else {
557                     states = TicketsUI.closedStatii;
558                 }
559                 PageParameters params = queryParameters(null, tm.name, states, null, null, true, 1);
8d2caa 560                 entryPanel.add(new LinkPanel("milestoneName", null, tm.name, TicketsPage.class, params).setRenderBodyOnly(true));
a96110 561
JM 562                 String css;
667163 563                 String status = tm.status.name();
a96110 564                 switch (tm.status) {
JM 565                 case Open:
667163 566                     if (tm.isOverdue()) {
JM 567                         css = "aui-lozenge aui-lozenge-subtle aui-lozenge-error";
568                         status = "overdue";
569                     } else {
570                         css = "aui-lozenge aui-lozenge-subtle";
571                     }
a96110 572                     break;
JM 573                 default:
574                     css = "aui-lozenge";
575                     break;
576                 }
667163 577                 Label stateLabel = new Label("milestoneState", status);
a96110 578                 WicketUtils.setCssClass(stateLabel, css);
8d2caa 579                 entryPanel.add(stateLabel);
a96110 580
JM 581                 if (tm.due == null) {
8d2caa 582                     entryPanel.add(new Label("milestoneDue", getString("gb.notSpecified")));
a96110 583                 } else {
8d2caa 584                     entryPanel.add(WicketUtils.createDatestampLabel("milestoneDue", tm.due, getTimeZone(), getTimeUtils()));
a96110 585                 }
ce048e 586                 if (acceptingUpdates) {
8d2caa 587                     entryPanel.add(new LinkPanel("editMilestone", null, getString("gb.edit"), EditMilestonePage.class,
ce048e 588                         WicketUtils.newObjectParameter(repositoryName, tm.name)));
JM 589                 } else {
8d2caa 590                     entryPanel.add(new Label("editMilestone").setVisible(false));
JM 591                 }
592
593                 if (tm.isOpen()) {
594                     // re-load milestone with query results
595                     TicketMilestone m = app().tickets().getMilestone(getRepositoryModel(), tm.name);
596
597                     Fragment milestonePanel = new Fragment("milestonePanel", "openMilestoneFragment", this);
598                     Label label = new Label("progress");
a98ebb 599                     WicketUtils.setCssStyle(label, "width:" + m.getProgress() + "%;");
8d2caa 600                     milestonePanel.add(label);
JM 601
602                     milestonePanel.add(new LinkPanel("openTickets", null,
603                             MessageFormat.format(getString("gb.nOpenTickets"), m.getOpenTickets()),
604                             TicketsPage.class,
fdd82f 605                             queryParameters(null, tm.name, TicketsUI.openStatii, null, null, true, 1)));
8d2caa 606
JM 607                     milestonePanel.add(new LinkPanel("closedTickets", null,
608                             MessageFormat.format(getString("gb.nClosedTickets"), m.getClosedTickets()),
609                             TicketsPage.class,
fdd82f 610                             queryParameters(null, tm.name, TicketsUI.closedStatii, null, null, true, 1)));
8d2caa 611
JM 612                     milestonePanel.add(new Label("totalTickets", MessageFormat.format(getString("gb.nTotalTickets"), m.getTotalTickets())));
613                     entryPanel.add(milestonePanel);
614                 } else {
615                     entryPanel.add(new Label("milestonePanel").setVisible(false));
ce048e 616                 }
5e3521 617             }
JM 618         };
8d2caa 619         return milestonesList;
5e3521 620     }
JM 621
622     protected PageParameters queryParameters(
623             String query,
624             String milestone,
625             String[] states,
626             String assignedTo,
627             String sort,
628             boolean descending,
629             int page) {
630
631         PageParameters params = WicketUtils.newRepositoryParameter(repositoryName);
632         if (!StringUtils.isEmpty(query)) {
633             params.add("q", query);
634         }
635         if (!StringUtils.isEmpty(milestone)) {
636             params.add(Lucene.milestone.name(), milestone);
637         }
638         if (!ArrayUtils.isEmpty(states)) {
639             for (String state : states) {
640                 params.add(Lucene.status.name(), state);
641             }
642         }
643         if (!StringUtils.isEmpty(assignedTo)) {
644             params.add(Lucene.responsible.name(), assignedTo);
645         }
646         if (!StringUtils.isEmpty(sort)) {
647             params.add("sort", sort);
648         }
649         if (!descending) {
650             params.add("direction", "asc");
651         }
652         if (page > 1) {
653             params.add("pg", "" + page);
654         }
655         return params;
656     }
657
658     protected PageParameters newTicketParameter(QueryResult ticket) {
659         return WicketUtils.newObjectParameter(repositoryName, "" + ticket.number);
660     }
661
662     @Override
663     protected String getPageName() {
664         return getString("gb.tickets");
665     }
666
667     protected void buildPager(
668             final String query,
669             final String milestone,
670             final String [] states,
671             final String assignedTo,
672             final String sort,
673             final boolean desc,
674             final int page,
675             int pageSize,
676             int count,
677             int total) {
678
679         boolean showNav = total > (2 * pageSize);
680         boolean allowPrev = page > 1;
681         boolean allowNext = (pageSize * (page - 1) + count) < total;
682         add(new BookmarkablePageLink<Void>("prevLink", TicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, page - 1)).setEnabled(allowPrev).setVisible(showNav));
683         add(new BookmarkablePageLink<Void>("nextLink", TicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, page + 1)).setEnabled(allowNext).setVisible(showNav));
684
685         if (total <= pageSize) {
686             add(new Label("pageLink").setVisible(false));
687             return;
688         }
689
690         // determine page numbers to display
691         int pages = count == 0 ? 0 : ((total / pageSize) + (total % pageSize == 0 ? 0 : 1));
692         // preferred number of pagelinks
693         int segments = 5;
694         if (pages < segments) {
695             // not enough data for preferred number of page links
696             segments = pages;
697         }
698         int minpage = Math.min(Math.max(1, page - 2), pages - (segments - 1));
699         int maxpage = Math.min(pages, minpage + (segments - 1));
700         List<Integer> sequence = new ArrayList<Integer>();
701         for (int i = minpage; i <= maxpage; i++) {
702             sequence.add(i);
703         }
704
705         ListDataProvider<Integer> pagesDp = new ListDataProvider<Integer>(sequence);
706         DataView<Integer> pagesView = new DataView<Integer>("pageLink", pagesDp) {
707             private static final long serialVersionUID = 1L;
708
709             @Override
710             public void populateItem(final Item<Integer> item) {
711                 final Integer i = item.getModelObject();
712                 LinkPanel link = new LinkPanel("page", null, "" + i, TicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, i));
713                 link.setRenderBodyOnly(true);
714                 if (i == page) {
715                     WicketUtils.setCssClass(item, "active");
716                 }
717                 item.add(link);
718             }
719         };
720         add(pagesView);
721     }
722 }