Sun, 24 May 2020 15:30:43 +0200
adds project overview page
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Sat May 23 14:13:09 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Sun May 24 15:30:43 2020 +0200 @@ -282,6 +282,13 @@ protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) { final String paramValue = req.getParameter(name); if (paramValue == null) return Optional.empty(); + if (clazz.equals(Boolean.class)) { + if (paramValue.toLowerCase().equals("false") || paramValue.equals("0")) { + return Optional.of((T)Boolean.FALSE); + } else { + return Optional.of((T)Boolean.TRUE); + } + } if (clazz.equals(String.class)) return Optional.of((T) paramValue); if (java.sql.Date.class.isAssignableFrom(clazz)) { try {
--- a/src/main/java/de/uapcore/lightpit/dao/VersionDao.java Sat May 23 14:13:09 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/VersionDao.java Sun May 24 15:30:43 2020 +0200 @@ -30,6 +30,7 @@ import de.uapcore.lightpit.entities.Project; import de.uapcore.lightpit.entities.Version; +import de.uapcore.lightpit.entities.VersionStatistics; import java.sql.SQLException; import java.util.List; @@ -44,4 +45,31 @@ * @throws SQLException on any kind of SQL error */ List<Version> list(Project project) throws SQLException; + + /** + * Retrieves statistics about issues that arose in a version. + * + * @param version the version + * @return version statistics + * @throws SQLException on any kind of SQL error + */ + VersionStatistics statsOpenedIssues(Version version) throws SQLException; + + /** + * Retrieves statistics about issues that are scheduled for a version. + * + * @param version the version + * @return version statistics + * @throws SQLException on any kind of SQL error + */ + VersionStatistics statsScheduledIssues(Version version) throws SQLException; + + /** + * Retrieves statistics about issues that are resolved in a version. + * + * @param version the version + * @return version statistics + * @throws SQLException on any kind of SQL error + */ + VersionStatistics statsResolvedIssues(Version version) throws SQLException; }
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java Sat May 23 14:13:09 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java Sun May 24 15:30:43 2020 +0200 @@ -29,9 +29,7 @@ package de.uapcore.lightpit.dao.postgres; import de.uapcore.lightpit.dao.VersionDao; -import de.uapcore.lightpit.entities.Project; -import de.uapcore.lightpit.entities.Version; -import de.uapcore.lightpit.entities.VersionStatus; +import de.uapcore.lightpit.entities.*; import java.sql.Connection; import java.sql.PreparedStatement; @@ -44,13 +42,14 @@ public final class PGVersionDao implements VersionDao { private final PreparedStatement insert, update, list, find; + private final PreparedStatement issuesAffected, issuesScheduled, issuesResolved; public PGVersionDao(Connection connection) throws SQLException { list = connection.prepareStatement( "select versionid, project, name, ordinal, status " + "from lpit_version " + "where project = ? " + - "order by ordinal, lower(name)"); + "order by ordinal desc, lower(name) desc"); find = connection.prepareStatement( "select versionid, project, name, ordinal, status " + @@ -63,6 +62,28 @@ update = connection.prepareStatement( "update lpit_version set name = ?, ordinal = ?, status = ?::version_status where versionid = ?" ); + + issuesAffected = connection.prepareStatement( + "select category, status, count(*) as issuecount " + + "from lpit_issue_affected_version " + + "join lpit_issue using (issueid) " + + "where versionid = ? " + + "group by category, status" + ); + issuesScheduled = connection.prepareStatement( + "select category, status, count(*) as issuecount " + + "from lpit_issue_scheduled_version " + + "join lpit_issue using (issueid) " + + "where versionid = ? " + + "group by category, status" + ); + issuesResolved = connection.prepareStatement( + "select category, status, count(*) as issuecount " + + "from lpit_issue_resolved_version " + + "join lpit_issue using (issueid) " + + "where versionid = ? " + + "group by category, status" + ); } private Version mapColumns(ResultSet result) throws SQLException { @@ -74,6 +95,20 @@ return version; } + private VersionStatistics versionStatistics(Version version, PreparedStatement stmt) throws SQLException { + stmt.setInt(1, version.getId()); + final var result = stmt.executeQuery(); + final var stats = new VersionStatistics(version); + while (result.next()) { + stats.setIssueCount( + IssueCategory.valueOf(result.getString("category")), + IssueStatus.valueOf(result.getString("status")), + result.getInt("issuecount") + ); + } + return stats; + } + @Override public void save(Version instance) throws SQLException { Objects.requireNonNull(instance.getName()); @@ -121,4 +156,19 @@ } } } + + @Override + public VersionStatistics statsOpenedIssues(Version version) throws SQLException { + return versionStatistics(version, issuesAffected); + } + + @Override + public VersionStatistics statsScheduledIssues(Version version) throws SQLException { + return versionStatistics(version, issuesScheduled); + } + + @Override + public VersionStatistics statsResolvedIssues(Version version) throws SQLException { + return versionStatistics(version, issuesResolved); + } }
--- a/src/main/java/de/uapcore/lightpit/entities/IssueStatus.java Sat May 23 14:13:09 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/entities/IssueStatus.java Sun May 24 15:30:43 2020 +0200 @@ -29,12 +29,27 @@ package de.uapcore.lightpit.entities; public enum IssueStatus { - InSpecification, - ToDo, - Scheduled, - InProgress, - InReview, - Done, - Rejected, - Withdrawn + InSpecification(0), + ToDo(0), + Scheduled(1), + InProgress(1), + InReview(1), + Done(2), + Rejected(2), + Withdrawn(2), + Duplicate(2); + + private int phase; + + IssueStatus(int phase) { + this.phase = phase; + } + + public int getPhase() { + return phase; + } + + public static int phaseCount() { + return 3; + } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/entities/VersionStatistics.java Sun May 24 15:30:43 2020 +0200 @@ -0,0 +1,101 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2018 Mike Becker. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +package de.uapcore.lightpit.entities; + +public class VersionStatistics { + + private final Version version; + private int[][] issueCount; + + private int[] rowTotals = null; + private int[] columnTotals = null; + private int total = -1; + + public VersionStatistics(Version version) { + this.version = version; + issueCount = new int[IssueCategory.values().length][IssueStatus.values().length]; + } + + public Version getVersion() { + return version; + } + + public void setIssueCount(IssueCategory category, IssueStatus status, int count) { + issueCount[category.ordinal()][status.ordinal()] = count; + total = -1; + rowTotals = columnTotals = null; + } + + public int[][] getIssueCount() { + return issueCount; + } + + public int[] getRowTotals() { + if (rowTotals != null) return rowTotals; + final int cn = IssueCategory.values().length; + final int sn = IssueStatus.values().length; + final var totals = new int[cn]; + for (int i = 0 ; i < cn ; i++) { + totals[i] = 0; + for (int j = 0 ; j < sn ; j++) { + totals[i] += issueCount[i][j]; + } + } + return rowTotals = totals; + } + + public int[] getColumnTotals() { + if (columnTotals != null) return columnTotals; + final int cn = IssueCategory.values().length; + final int sn = IssueStatus.values().length; + final var totals = new int[sn]; + for (int i = 0 ; i < sn ; i++) { + totals[i] = 0; + for (int j = 0 ; j < cn ; j++) { + totals[i] += issueCount[j][i]; + } + } + return columnTotals = totals; + } + + public int getTotal() { + if (this.total >= 0) { + return this.total; + } + int total = 0; + final int cn = IssueCategory.values().length; + final int sn = IssueStatus.values().length; + for (int i = 0 ; i < sn ; i++) { + for (int j = 0 ; j < cn ; j++) { + total += issueCount[j][i]; + } + } + return this.total = total; + } +}
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Sat May 23 14:13:09 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Sun May 24 15:30:43 2020 +0200 @@ -57,9 +57,10 @@ private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class); - public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected-project"); - public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected-issue"); - public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected-version"); + public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project"); + public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected_issue"); + public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version"); + public static final String SESSION_ATTR_HIDE_ZEROS = fqn(ProjectsModule.class, "stats_hide_zeros"); private class SessionSelection { final HttpSession session; @@ -128,15 +129,40 @@ } } + private void setAttributeHideZeros(HttpServletRequest req) { + final Boolean value; + final var param = getParameter(req, Boolean.class, "reduced"); + if (param.isPresent()) { + value = param.get(); + req.getSession().setAttribute(SESSION_ATTR_HIDE_ZEROS, value); + } else { + final var sessionValue = req.getSession().getAttribute(SESSION_ATTR_HIDE_ZEROS); + if (sessionValue != null) { + value = (Boolean) sessionValue; + } else { + value = false; + req.getSession().setAttribute(SESSION_ATTR_HIDE_ZEROS, value); + } + } + req.setAttribute("statsHideZeros", value); + } + @Override protected String getResourceBundleName() { return "localization.projects"; } + + private static final int BREADCRUMB_LEVEL_ROOT = 0; + private static final int BREADCRUMB_LEVEL_PROJECT = 1; + private static final int BREADCRUMB_LEVEL_VERSION = 2; + private static final int BREADCRUMB_LEVEL_ISSUE_LIST = 3; + private static final int BREADCRUMB_LEVEL_ISSUE = 4; + /** * Creates the breadcrumb menu. * - * @param level the current active level (0: root, 1: project, 2: version, 3: issue) + * @param level the current active level (0: root, 1: project, 2: version, 3: issue list, 4: issue) * @param sessionSelection the currently selected objects * @return a dynamic breadcrumb menu trying to display as many levels as possible */ @@ -147,7 +173,7 @@ entry = new MenuEntry(new ResourceKey("localization.lightpit", "menu.projects"), "projects/"); breadcrumbs.add(entry); - if (level == 0) entry.setActive(true); + if (level == BREADCRUMB_LEVEL_ROOT) entry.setActive(true); if (sessionSelection.project != null) { if (sessionSelection.project.getId() < 0) { @@ -157,7 +183,7 @@ entry = new MenuEntry(sessionSelection.project.getName(), "projects/view?pid=" + sessionSelection.project.getId()); } - if (level == 1) entry.setActive(true); + if (level == BREADCRUMB_LEVEL_PROJECT) entry.setActive(true); breadcrumbs.add(entry); } @@ -170,15 +196,19 @@ // TODO: change link to issue overview for that version "projects/versions/edit?id=" + sessionSelection.version.getId()); } - if (level == 2) entry.setActive(true); + if (level == BREADCRUMB_LEVEL_VERSION) entry.setActive(true); + breadcrumbs.add(entry); + } + + if (sessionSelection.project != null) { + entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"), + // TODO: maybe also add selected version + "projects/issues/?pid=" + sessionSelection.project.getId()); + if (level == BREADCRUMB_LEVEL_ISSUE_LIST) entry.setActive(true); breadcrumbs.add(entry); } if (sessionSelection.issue != null) { - entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"), - // TODO: change link to a separate issue view (maybe depending on the selected version) - "projects/view?pid=" + sessionSelection.issue.getProject().getId()); - breadcrumbs.add(entry); if (sessionSelection.issue.getId() < 0) { entry = new MenuEntry(new ResourceKey("localization.projects", "button.issue.create"), "projects/issues/edit"); @@ -187,7 +217,7 @@ // TODO: maybe change link to a view rather than directly opening the editor "projects/issues/edit?id=" + sessionSelection.issue.getId()); } - if (level == 3) entry.setActive(true); + if (level == BREADCRUMB_LEVEL_ISSUE) entry.setActive(true); breadcrumbs.add(entry); } @@ -202,7 +232,7 @@ setContentPage(req, "projects"); setStylesheet(req, "projects"); - setBreadcrumbs(req, getBreadcrumbs(0, sessionSelection)); + setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ROOT, sessionSelection)); return ResponseType.HTML; } @@ -211,7 +241,7 @@ req.setAttribute("project", selection.project); req.setAttribute("users", dao.getUserDao().list()); setContentPage(req, "project-form"); - setBreadcrumbs(req, getBreadcrumbs(1, selection)); + setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection)); } @RequestMapping(requestPath = "edit", method = HttpMethod.GET) @@ -253,29 +283,60 @@ } @RequestMapping(requestPath = "view", method = HttpMethod.GET) - public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { + public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException { final var sessionSelection = new SessionSelection(req, dao); + if (sessionSelection.project == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected."); + return ResponseType.NONE; + } - req.setAttribute("versions", dao.getVersionDao().list(sessionSelection.project)); - req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project)); + final var versionDao = dao.getVersionDao(); + final var versions = versionDao.list(sessionSelection.project); + final var statsAffected = new ArrayList<VersionStatistics>(); + final var statsScheduled = new ArrayList<VersionStatistics>(); + final var statsResolved = new ArrayList<VersionStatistics>(); + for (Version version : versions) { + statsAffected.add(versionDao.statsOpenedIssues(version)); + statsScheduled.add(versionDao.statsScheduledIssues(version)); + statsResolved.add(versionDao.statsResolvedIssues(version)); + } - setBreadcrumbs(req, getBreadcrumbs(1, sessionSelection)); + setAttributeHideZeros(req); + + req.setAttribute("versions", versions); + req.setAttribute("statsAffected", statsAffected); + req.setAttribute("statsScheduled", statsScheduled); + req.setAttribute("statsResolved", statsResolved); + + req.setAttribute("issueStatusEnum", IssueStatus.values()); + req.setAttribute("issueCategoryEnum", IssueCategory.values()); + + setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, sessionSelection)); setContentPage(req, "project-details"); + setStylesheet(req, "projects"); return ResponseType.HTML; } private void configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { + final var versionDao = dao.getVersionDao(); req.setAttribute("projects", dao.getProjectDao().list()); req.setAttribute("version", selection.version); req.setAttribute("versionStatusEnum", VersionStatus.values()); + req.setAttribute("issueStatusEnum", IssueStatus.values()); + req.setAttribute("issueCategoryEnum", IssueCategory.values()); + req.setAttribute("statsAffected", versionDao.statsOpenedIssues(selection.version)); + req.setAttribute("statsScheduled", versionDao.statsScheduledIssues(selection.version)); + req.setAttribute("statsResolved", versionDao.statsResolvedIssues(selection.version)); + setAttributeHideZeros(req); + setContentPage(req, "version-form"); - setBreadcrumbs(req, getBreadcrumbs(2, selection)); + setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection)); } @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET) - public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { + public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { final var sessionSelection = new SessionSelection(req, dao); sessionSelection.selectVersion(findByParameter(req, Integer.class, "id", dao.getVersionDao()::find) @@ -286,7 +347,7 @@ } @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST) - public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { + public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { final var sessionSelection = new SessionSelection(req, dao); var version = new Version(-1, sessionSelection.project); @@ -320,11 +381,28 @@ req.setAttribute("users", dao.getUserDao().list()); setContentPage(req, "issue-form"); - setBreadcrumbs(req, getBreadcrumbs(3, selection)); + setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE, selection)); + } + + @RequestMapping(requestPath = "issues/", method = HttpMethod.GET) + public ResponseType issues(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException { + final var sessionSelection = new SessionSelection(req, dao); + if (sessionSelection.project == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected."); + return ResponseType.NONE; + } + + req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project)); + + setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, sessionSelection)); + setContentPage(req, "issues"); + setStylesheet(req, "projects"); + + return ResponseType.HTML; } @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET) - public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { + public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { final var sessionSelection = new SessionSelection(req, dao); sessionSelection.selectIssue(findByParameter(req, Integer.class, "id", @@ -335,7 +413,7 @@ } @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST) - public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { + public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { final var sessionSelection = new SessionSelection(req, dao); Issue issue = new Issue(-1, sessionSelection.project);
--- a/src/main/resources/localization/projects.properties Sat May 23 14:13:09 2020 +0200 +++ b/src/main/resources/localization/projects.properties Sun May 24 15:30:43 2020 +0200 @@ -26,6 +26,10 @@ button.create=New Project button.version.create=New Version button.issue.create=New Issue +button.issue.list=Show Issues + +button.stats.hidezeros=Reduced View +button.stats.showzeros=Full View no-projects=Welcome to LightPIT. Start off by creating a new project! @@ -45,12 +49,18 @@ placeholder.null-owner=Unassigned placeholder.null-assignee=Unassigned +version.label=Version version.status.Future=Future version.status.Unreleased=Unreleased version.status.Released=Released version.status.LTS=LTS version.status.Deprecated=Deprecated +version.statistics.affected=Affected by Issues +version.statistics.scheduled=Scheduled Issues +version.statistics.resolved=Resolved Issues +version.statistics.total=Total + thead.issue.project=Project thead.issue.subject=Subject thead.issue.description=Description @@ -81,3 +91,4 @@ issue.status.Done=Done issue.status.Rejected=Rejected issue.status.Withdrawn=Withdrawn +issue.status.Duplicate=Duplicate
--- a/src/main/resources/localization/projects_de.properties Sat May 23 14:13:09 2020 +0200 +++ b/src/main/resources/localization/projects_de.properties Sun May 24 15:30:43 2020 +0200 @@ -26,6 +26,10 @@ button.create=Neues Projekt button.version.create=Neue Version button.issue.create=Neuer Vorgang +button.issue.list=Vorg\u00e4nge + +button.stats.hidezeros=Reduzierte Ansicht +button.stats.showzeros=Komplettansicht no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes! @@ -45,12 +49,18 @@ placeholder.null-owner=Nicht Zugewiesen placeholder.null-assignee=Niemandem +version.label=Version version.status.Future=Geplant version.status.Unreleased=Unver\u00f6ffentlicht version.status.Released=Ver\u00f6ffentlicht version.status.LTS=Langzeitsupport version.status.Deprecated=Veraltet +version.statistics.affected=Betroffen von Vorg\u00e4ngen +version.statistics.scheduled=Geplante Vorg\u00e4nge +version.statistics.resolved=Gel\u00f6ste Vorg\u00e4nge +version.statistics.total=Summe + thead.issue.project=Projekt thead.issue.subject=Thema thead.issue.description=Beschreibung @@ -81,3 +91,4 @@ issue.status.Done=Erledigt issue.status.Rejected=Zur\u00fcckgewiesen issue.status.Withdrawn=Zur\u00fcckgezogen +issue.status.Duplicate=Duplikat
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jsp/issues.jsp Sun May 24 15:30:43 2020 +0200 @@ -0,0 +1,85 @@ +<%-- +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + +Copyright 2018 Mike Becker. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--%> +<%@page pageEncoding="UTF-8" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + +<jsp:useBean id="issues" type="java.util.List<de.uapcore.lightpit.entities.Issue>" scope="request"/> + +<div id="tool-area"> + <div> + <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a> + </div> +</div> + +<table id="issue-list" class="datatable medskip"> + <thead> + <tr> + <th><fmt:message key="thead.issue.subject"/></th> + <th><fmt:message key="thead.issue.assignee"/></th> + <th><fmt:message key="thead.issue.category"/></th> + <th><fmt:message key="thead.issue.status"/></th> + <th><fmt:message key="thead.issue.created"/></th> + <th><fmt:message key="thead.issue.updated"/></th> + <th><fmt:message key="thead.issue.eta"/></th> + </tr> + </thead> + <tbody> + <c:forEach var="issue" items="${issues}"> + <tr> + <td> + <a href="./projects/issues/edit?id=${issue.id}"> + <c:out value="${issue.subject}" /> + </a> + </td> + <td> + <c:if test="${not empty issue.assignee}"> + <c:out value="${issue.assignee.shortDisplayname}" /> + </c:if> + <c:if test="${empty issue.assignee}"> + <fmt:message key="placeholder.null-assignee" /> + </c:if> + </td> + <td> + <fmt:message key="issue.category.${issue.category}" /> + </td> + <td> + <fmt:message key="issue.status.${issue.status}" /> + </td> + <td> + <fmt:formatDate value="${issue.created}" type="BOTH"/> + </td> + <td> + <fmt:formatDate value="${issue.updated}" type="BOTH"/> + </td> + <td> + <fmt:formatDate value="${issue.eta}" /> + </td> + </tr> + </c:forEach> + </tbody> +</table>
--- a/src/main/webapp/WEB-INF/jsp/project-details.jsp Sat May 23 14:13:09 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp Sun May 24 15:30:43 2020 +0200 @@ -25,87 +25,46 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --%> <%@page pageEncoding="UTF-8" %> -<%@page import="de.uapcore.lightpit.modules.ProjectsModule" %> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<c:set scope="page" var="selectedProject" value="${sessionScope[ProjectsModule.SESSION_ATTR_SELECTED_PROJECT]}"/> - <jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" scope="request"/> -<jsp:useBean id="issues" type="java.util.List<de.uapcore.lightpit.entities.Issue>" scope="request"/> +<jsp:useBean id="statsAffected" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/> +<jsp:useBean id="statsScheduled" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/> +<jsp:useBean id="statsResolved" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/> +<jsp:useBean id="issueStatusEnum" type="de.uapcore.lightpit.entities.IssueStatus[]" scope="request"/> +<jsp:useBean id="issueCategoryEnum" type="de.uapcore.lightpit.entities.IssueCategory[]" scope="request"/> +<jsp:useBean id="statsHideZeros" type="java.lang.Boolean" scope="request"/> <div id="tool-area"> <a href="./projects/versions/edit" class="button"><fmt:message key="button.version.create"/></a> <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a> + <a href="./projects/issues/" class="button"><fmt:message key="button.issue.list"/></a> + <c:if test="${not statsHideZeros}"> + <a href="./projects/view?reduced=1" class="button"><fmt:message key="button.stats.hidezeros"/></a> + </c:if> + <c:if test="${statsHideZeros}"> + <a href="./projects/view?reduced=0" class="button"><fmt:message key="button.stats.showzeros"/></a> + </c:if> </div> -<c:if test="${not empty versions}"> - <table id="version-list" class="datatable medskip"> - <thead> - <tr> - <th></th> - <th><fmt:message key="thead.version.name"/></th> - <th><fmt:message key="thead.version.status"/></th> - </tr> - </thead> - <tbody> - <c:forEach var="version" items="${versions}"> - <tr class="nowrap"> - <td style="width: 2em;"><a href="./projects/versions/edit?id=${version.id}">✎</a> - </td> - <td><c:out value="${version.name}"/></td> - <td><fmt:message key="version.status.${version.status}"/></td> - </tr> - </c:forEach> - </tbody> - </table> -</c:if> +<div id="version-stats"> +<c:forEach var="version" items="${versions}" varStatus="iter"> + <h2> + <fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/> + <a href="./projects/versions/edit?id=${version.id}">✎</a> + </h2> -<table id="issue-list" class="datatable medskip"> - <thead> - <tr> - <th><fmt:message key="thead.issue.subject"/></th> - <th><fmt:message key="thead.issue.assignee"/></th> - <th><fmt:message key="thead.issue.category"/></th> - <th><fmt:message key="thead.issue.status"/></th> - <th><fmt:message key="thead.issue.created"/></th> - <th><fmt:message key="thead.issue.updated"/></th> - <th><fmt:message key="thead.issue.eta"/></th> - <!-- TODO: add other information --> - </tr> - </thead> - <tbody> - <c:forEach var="issue" items="${issues}"> - <tr> - <td> - <a href="./projects/issues/edit?id=${issue.id}"> - <c:out value="${issue.subject}" /> - </a> - </td> - <td> - <c:if test="${not empty issue.assignee}"> - <c:out value="${issue.assignee.shortDisplayname}" /> - </c:if> - <c:if test="${empty issue.assignee}"> - <fmt:message key="placeholder.null-assignee" /> - </c:if> - </td> - <td> - <fmt:message key="issue.category.${issue.category}" /> - </td> - <td> - <fmt:message key="issue.status.${issue.status}" /> - </td> - <td> - <fmt:formatDate value="${issue.created}" type="BOTH"/> - </td> - <td> - <fmt:formatDate value="${issue.updated}" type="BOTH"/> - </td> - <td> - <fmt:formatDate value="${issue.eta}" /> - </td> - </tr> - </c:forEach> - </tbody> -</table> + <h3><fmt:message key="version.statistics.affected" /></h3> + <c:set var="stats" value="${statsAffected[iter.index]}" /> + <%@include file="../jspf/version-stats.jsp" %> + + <h3><fmt:message key="version.statistics.scheduled" /></h3> + <c:set var="stats" value="${statsScheduled[iter.index]}" /> + <%@include file="../jspf/version-stats.jsp" %> + + <h3><fmt:message key="version.statistics.resolved" /></h3> + <c:set var="stats" value="${statsResolved[iter.index]}" /> + <%@include file="../jspf/version-stats.jsp" %> +</c:forEach> +</div>
--- a/src/main/webapp/WEB-INF/jsp/version-form.jsp Sat May 23 14:13:09 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp Sun May 24 15:30:43 2020 +0200 @@ -32,6 +32,11 @@ <jsp:useBean id="version" type="de.uapcore.lightpit.entities.Version" scope="request"/> <jsp:useBean id="versionStatusEnum" type="de.uapcore.lightpit.entities.VersionStatus[]" scope="request"/> +<jsp:useBean id="statsAffected" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/> +<jsp:useBean id="statsScheduled" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/> +<jsp:useBean id="statsResolved" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/> +<jsp:useBean id="statsHideZeros" type="java.lang.Boolean" scope="request"/> + <form action="./projects/versions/commit" method="post"> <table class="formtable" style="width: 35ch"> <colgroup> @@ -95,3 +100,15 @@ </tfoot> </table> </form> + +<h3><fmt:message key="version.statistics.affected" /></h3> +<c:set var="stats" value="${statsAffected}" /> +<%@include file="../jspf/version-stats.jsp" %> + +<h3><fmt:message key="version.statistics.scheduled" /></h3> +<c:set var="stats" value="${statsScheduled}" /> +<%@include file="../jspf/version-stats.jsp" %> + +<h3><fmt:message key="version.statistics.resolved" /></h3> +<c:set var="stats" value="${statsResolved}" /> +<%@include file="../jspf/version-stats.jsp" %> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jspf/version-stats.jsp Sun May 24 15:30:43 2020 +0200 @@ -0,0 +1,54 @@ +<%@ taglib uri = "http://java.sun.com/jsp/jstl/functions" prefix = "fn" %> + +<table class="datatable"> + <c:if test="${statsHideZeros}"> + <c:set var="visibleColumns" value="0"/> + <c:forEach var="idx" begin="0" end="${fn:length(issueStatusEnum)-1}"> + <c:set var="visibleColumns" value="${visibleColumns + (stats.columnTotals[idx] eq 0 ? 0 :1)}"/> + </c:forEach> + </c:if> + <c:if test="${not statsHideZeros}"> + <c:set var="visibleColumns" value="${fn:length(issueStatusEnum)}" /> + </c:if> + <c:set var="colwidth"><fmt:formatNumber value="${100/(visibleColumns+2)}" maxFractionDigits="0" /></c:set> + <colgroup> + <c:forEach var="idx" begin="1" end="${visibleColumns+2}"> + <col width="${colwidth}%"> + </c:forEach> + </colgroup> + <thead> + <tr> + <th></th> + <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter"> + <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}"> + <th class="hcenter"><fmt:message key="issue.status.${issueStatus}"/></th> + </c:if> + </c:forEach> + <th class="hcenter"><fmt:message key="version.statistics.total"/> </th> + </tr> + </thead> + <tbody> + <c:forEach var="issueCategory" items="${issueCategoryEnum}" varStatus="categoryIter"> + <c:if test="${not statsHideZeros or stats.rowTotals[categoryIter.index] gt 0}"> + <tr> + <th><fmt:message key="issue.category.${issueCategory}" /></th> + <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter"> + <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}"> + <td>${stats.issueCount[categoryIter.index][statusIter.index]}</td> + </c:if> + </c:forEach> + <td>${stats.rowTotals[categoryIter.index]}</td> + </tr> + </c:if> + </c:forEach> + <tr> + <th><fmt:message key="version.statistics.total"/> </th> + <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter"> + <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}"> + <td>${stats.columnTotals[statusIter.index]}</td> + </c:if> + </c:forEach> + <td>${stats.total}</td> + </tr> + </tbody> +</table> \ No newline at end of file
--- a/src/main/webapp/lightpit.css Sat May 23 14:13:09 2020 +0200 +++ b/src/main/webapp/lightpit.css Sun May 24 15:30:43 2020 +0200 @@ -120,7 +120,7 @@ width: auto; border-style: solid; border-width: 1pt; - border-color: black; + border-color: silver; border-collapse: collapse; } @@ -178,6 +178,10 @@ text-align: center; } +.hright { + text-align: right; +} + .smalltext { font-size: smaller; }
--- a/src/main/webapp/projects.css Sat May 23 14:13:09 2020 +0200 +++ b/src/main/webapp/projects.css Sun May 24 15:30:43 2020 +0200 @@ -29,4 +29,12 @@ #issue-list td { white-space: nowrap; +} + +#version-stats h2 { + white-space: nowrap; +} + +#version-stats td { + text-align: right; } \ No newline at end of file