improves issue overview and adds progress information

Mon, 01 Jun 2020 14:46:58 +0200

author
Mike Becker <universe@uap-core.de>
date
Mon, 01 Jun 2020 14:46:58 +0200
changeset 86
0a658e53177c
parent 85
3d16ad54b3dc
child 87
501addad452b

improves issue overview and adds progress information

pom.xml file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/Constants.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/IssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/ProjectDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/VersionDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Issue.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/IssueSummary.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Project.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Version.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/LanguageModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/UsersModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/IssuesView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/LanguageView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectIndexView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectInfo.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/UsersEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/UsersView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionInfo.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionView.java file | annotate | diff | comparison | revisions
src/main/resources/localization/projects.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/projects_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issues.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/language.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-details.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/projects.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/site.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/user-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/users.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/version-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/version.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-list.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-summary.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/project-header.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/version-stats.jsp file | annotate | diff | comparison | revisions
src/main/webapp/lightpit.css file | annotate | diff | comparison | revisions
src/main/webapp/projects.css file | annotate | diff | comparison | revisions
     1.1 --- a/pom.xml	Sat May 30 18:12:38 2020 +0200
     1.2 +++ b/pom.xml	Mon Jun 01 14:46:58 2020 +0200
     1.3 @@ -4,7 +4,7 @@
     1.4      <modelVersion>4.0.0</modelVersion>
     1.5      <groupId>de.uapcore</groupId>
     1.6      <artifactId>lightpit</artifactId>
     1.7 -    <version>0.1-SNAPSHOT</version>
     1.8 +    <version>0.2</version>
     1.9      <packaging>war</packaging>
    1.10      <properties>
    1.11          <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     2.1 --- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Sat May 30 18:12:38 2020 +0200
     2.2 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Mon Jun 01 14:46:58 2020 +0200
     2.3 @@ -88,6 +88,7 @@
     2.4  
     2.5      /**
     2.6       * Returns the name of the resource bundle associated with this servlet.
     2.7 +     *
     2.8       * @return the resource bundle base name
     2.9       */
    2.10      protected abstract String getResourceBundleName();
    2.11 @@ -266,6 +267,17 @@
    2.12      }
    2.13  
    2.14      /**
    2.15 +     * Sets the view model object.
    2.16 +     * The type must match the expected type in the JSP file.
    2.17 +     *
    2.18 +     * @param req       the servlet request object
    2.19 +     * @param viewModel the view model object
    2.20 +     */
    2.21 +    public void setViewModel(HttpServletRequest req, Object viewModel) {
    2.22 +        req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel);
    2.23 +    }
    2.24 +
    2.25 +    /**
    2.26       * Obtains a request parameter of the specified type.
    2.27       * The specified type must have a single-argument constructor accepting a string to perform conversion.
    2.28       * The constructor of the specified type may throw an exception on conversion failures.
    2.29 @@ -281,7 +293,7 @@
    2.30              final String[] paramValues = req.getParameterValues(name);
    2.31              int len = paramValues == null ? 0 : paramValues.length;
    2.32              final var array = (T) Array.newInstance(clazz.getComponentType(), len);
    2.33 -            for (int i = 0 ; i < len ; i++) {
    2.34 +            for (int i = 0; i < len; i++) {
    2.35                  try {
    2.36                      final Constructor<?> ctor = clazz.getComponentType().getConstructor(String.class);
    2.37                      Array.set(array, i, ctor.newInstance(paramValues[i]));
     3.1 --- a/src/main/java/de/uapcore/lightpit/Constants.java	Sat May 30 18:12:38 2020 +0200
     3.2 +++ b/src/main/java/de/uapcore/lightpit/Constants.java	Mon Jun 01 14:46:58 2020 +0200
     3.3 @@ -92,6 +92,11 @@
     3.4      public static final String REQ_ATTR_CONTENT_PAGE = fqn(AbstractLightPITServlet.class, "content-page");
     3.5  
     3.6      /**
     3.7 +     * Key for the view model object (the type depends on the rendered site).
     3.8 +     */
     3.9 +    public static final String REQ_ATTR_VIEWMODEL = "viewmodel";
    3.10 +
    3.11 +    /**
    3.12       * Key for the name of the additional stylesheet used by a module.
    3.13       */
    3.14      public static final String REQ_ATTR_STYLESHEET = fqn(AbstractLightPITServlet.class, "extraCss");
     4.1 --- a/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Sat May 30 18:12:38 2020 +0200
     4.2 +++ b/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Mon Jun 01 14:46:58 2020 +0200
     4.3 @@ -30,6 +30,7 @@
     4.4  
     4.5  import de.uapcore.lightpit.entities.Issue;
     4.6  import de.uapcore.lightpit.entities.Project;
     4.7 +import de.uapcore.lightpit.entities.Version;
     4.8  
     4.9  import java.sql.SQLException;
    4.10  import java.util.List;
    4.11 @@ -48,6 +49,15 @@
    4.12      List<Issue> list(Project project) throws SQLException;
    4.13  
    4.14      /**
    4.15 +     * Lists all issues that are somehow related to the specified version.
    4.16 +     *
    4.17 +     * @param version the version
    4.18 +     * @return a list of issues
    4.19 +     * @throws SQLException on any kind of SQL error
    4.20 +     */
    4.21 +    List<Issue> list(Version version) throws SQLException;
    4.22 +
    4.23 +    /**
    4.24       * Saves an instances to the database.
    4.25       * Implementations of this DAO must guarantee that the generated ID is stored in the instance.
    4.26       *
     5.1 --- a/src/main/java/de/uapcore/lightpit/dao/ProjectDao.java	Sat May 30 18:12:38 2020 +0200
     5.2 +++ b/src/main/java/de/uapcore/lightpit/dao/ProjectDao.java	Mon Jun 01 14:46:58 2020 +0200
     5.3 @@ -28,6 +28,7 @@
     5.4   */
     5.5  package de.uapcore.lightpit.dao;
     5.6  
     5.7 +import de.uapcore.lightpit.entities.IssueSummary;
     5.8  import de.uapcore.lightpit.entities.Project;
     5.9  
    5.10  import java.sql.SQLException;
    5.11 @@ -35,4 +36,6 @@
    5.12  
    5.13  public interface ProjectDao extends GenericDao<Project> {
    5.14      List<Project> list() throws SQLException;
    5.15 +
    5.16 +    IssueSummary getIssueSummary(Project project) throws SQLException;
    5.17  }
     6.1 --- a/src/main/java/de/uapcore/lightpit/dao/VersionDao.java	Sat May 30 18:12:38 2020 +0200
     6.2 +++ b/src/main/java/de/uapcore/lightpit/dao/VersionDao.java	Mon Jun 01 14:46:58 2020 +0200
     6.3 @@ -30,7 +30,6 @@
     6.4  
     6.5  import de.uapcore.lightpit.entities.Project;
     6.6  import de.uapcore.lightpit.entities.Version;
     6.7 -import de.uapcore.lightpit.entities.VersionStatistics;
     6.8  
     6.9  import java.sql.SQLException;
    6.10  import java.util.List;
    6.11 @@ -45,31 +44,4 @@
    6.12       * @throws SQLException on any kind of SQL error
    6.13       */
    6.14      List<Version> list(Project project) throws SQLException;
    6.15 -
    6.16 -    /**
    6.17 -     * Retrieves statistics about issues that arose in a version.
    6.18 -     *
    6.19 -     * @param version the version
    6.20 -     * @return version statistics
    6.21 -     * @throws SQLException on any kind of SQL error
    6.22 -     */
    6.23 -    VersionStatistics statsOpenedIssues(Version version) throws SQLException;
    6.24 -
    6.25 -    /**
    6.26 -     * Retrieves statistics about issues that are scheduled for a version.
    6.27 -     *
    6.28 -     * @param version the version
    6.29 -     * @return version statistics
    6.30 -     * @throws SQLException on any kind of SQL error
    6.31 -     */
    6.32 -    VersionStatistics statsScheduledIssues(Version version) throws SQLException;
    6.33 -
    6.34 -    /**
    6.35 -     * Retrieves statistics about issues that are resolved in a version.
    6.36 -     *
    6.37 -     * @param version the version
    6.38 -     * @return version statistics
    6.39 -     * @throws SQLException on any kind of SQL error
    6.40 -     */
    6.41 -    VersionStatistics statsResolvedIssues(Version version) throws SQLException;
    6.42  }
     7.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Sat May 30 18:12:38 2020 +0200
     7.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Mon Jun 01 14:46:58 2020 +0200
     7.3 @@ -43,7 +43,7 @@
     7.4  
     7.5  public final class PGIssueDao implements IssueDao {
     7.6  
     7.7 -    private final PreparedStatement insert, update, list, find;
     7.8 +    private final PreparedStatement insert, update, list, listForVersion, find;
     7.9      private final PreparedStatement affectedVersions, scheduledVersions, resolvedVersions;
    7.10      private final PreparedStatement clearAffected, clearScheduled, clearResolved;
    7.11      private final PreparedStatement insertAffected, insertScheduled, insertResolved;
    7.12 @@ -56,7 +56,24 @@
    7.13                          "from lpit_issue i " +
    7.14                          "left join lpit_project p on project = projectid " +
    7.15                          "left join lpit_user on userid = assignee " +
    7.16 -                        "where project = ? ");
    7.17 +                        "where project = ? "+
    7.18 +                        "order by eta asc, updated desc");
    7.19 +
    7.20 +        listForVersion = connection.prepareStatement(
    7.21 +                "with issue_version as ( "+
    7.22 +                        "select issueid, versionid from lpit_issue_affected_version union "+
    7.23 +                        "select issueid, versionid from lpit_issue_scheduled_version union "+
    7.24 +                        "select issueid, versionid from lpit_issue_resolved_version) "+
    7.25 +                        "select issueid, project, p.name as projectname, status, category, subject, i.description, " +
    7.26 +                        "userid, username, givenname, lastname, mail, " +
    7.27 +                        "created, updated, eta " +
    7.28 +                        "from lpit_issue i " +
    7.29 +                        "join issue_version using (issueid) "+
    7.30 +                        "left join lpit_project p on project = projectid " +
    7.31 +                        "left join lpit_user on userid = assignee " +
    7.32 +                        "where versionid = ? "+
    7.33 +                        "order by eta asc, updated desc"
    7.34 +        );
    7.35  
    7.36          find = connection.prepareStatement(
    7.37                  "select issueid, project, p.name as projectname, status, category, subject, i.description, " +
    7.38 @@ -121,7 +138,8 @@
    7.39      private Issue mapColumns(ResultSet result) throws SQLException {
    7.40          final var project = new Project(result.getInt("project"));
    7.41          project.setName(result.getString("projectname"));
    7.42 -        final var issue = new Issue(result.getInt("issueid"), project);
    7.43 +        final var issue = new Issue(result.getInt("issueid"));
    7.44 +        issue.setProject(project);
    7.45          issue.setStatus(IssueStatus.valueOf(result.getString("status")));
    7.46          issue.setCategory(IssueCategory.valueOf(result.getString("category")));
    7.47          issue.setSubject(result.getString("subject"));
    7.48 @@ -133,8 +151,8 @@
    7.49          return issue;
    7.50      }
    7.51  
    7.52 -    private Version mapVersion(ResultSet result, Project project) throws SQLException {
    7.53 -        final var version = new Version(result.getInt("versionid"), project);
    7.54 +    private Version mapVersion(ResultSet result) throws SQLException {
    7.55 +        final var version = new Version(result.getInt("versionid"));
    7.56          version.setName(result.getString("name"));
    7.57          version.setOrdinal(result.getInt("ordinal"));
    7.58          version.setStatus(VersionStatus.valueOf(result.getString("status")));
    7.59 @@ -203,11 +221,10 @@
    7.60          }
    7.61      }
    7.62  
    7.63 -    @Override
    7.64 -    public List<Issue> list(Project project) throws SQLException {
    7.65 -        list.setInt(1, project.getId());
    7.66 +    private List<Issue> list(PreparedStatement query, int arg) throws SQLException {
    7.67 +        query.setInt(1, arg);
    7.68          List<Issue> issues = new ArrayList<>();
    7.69 -        try (var result = list.executeQuery()) {
    7.70 +        try (var result = query.executeQuery()) {
    7.71              while (result.next()) {
    7.72                  issues.add(mapColumns(result));
    7.73              }
    7.74 @@ -216,6 +233,16 @@
    7.75      }
    7.76  
    7.77      @Override
    7.78 +    public List<Issue> list(Project project) throws SQLException {
    7.79 +        return list(list, project.getId());
    7.80 +    }
    7.81 +
    7.82 +    @Override
    7.83 +    public List<Issue> list(Version version) throws SQLException {
    7.84 +        return list(listForVersion, version.getId());
    7.85 +    }
    7.86 +
    7.87 +    @Override
    7.88      public Issue find(int id) throws SQLException {
    7.89          find.setInt(1, id);
    7.90          try (var result = find.executeQuery()) {
    7.91 @@ -232,7 +259,7 @@
    7.92          List<Version> versions = new ArrayList<>();
    7.93          try (var result = stmt.executeQuery()) {
    7.94              while (result.next()) {
    7.95 -                versions.add(mapVersion(result, issue.getProject()));
    7.96 +                versions.add(mapVersion(result));
    7.97              }
    7.98          }
    7.99          return versions;
     8.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Sat May 30 18:12:38 2020 +0200
     8.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Mon Jun 01 14:46:58 2020 +0200
     8.3 @@ -29,6 +29,7 @@
     8.4  package de.uapcore.lightpit.dao.postgres;
     8.5  
     8.6  import de.uapcore.lightpit.dao.ProjectDao;
     8.7 +import de.uapcore.lightpit.entities.IssueSummary;
     8.8  import de.uapcore.lightpit.entities.Project;
     8.9  import de.uapcore.lightpit.entities.User;
    8.10  
    8.11 @@ -98,24 +99,26 @@
    8.12          return proj;
    8.13      }
    8.14  
    8.15 -    private void mapIssueSummary(Project proj) throws SQLException {
    8.16 -        issue_summary.setInt(1, proj.getId());
    8.17 +    public IssueSummary getIssueSummary(Project project) throws SQLException {
    8.18 +        issue_summary.setInt(1, project.getId());
    8.19          final var result = issue_summary.executeQuery();
    8.20 +        final var summary = new IssueSummary();
    8.21          while (result.next()) {
    8.22              final var phase = result.getInt("phase");
    8.23              final var total = result.getInt("total");
    8.24              switch(phase) {
    8.25                  case 0:
    8.26 -                    proj.setOpenIssues(total);
    8.27 +                    summary.setOpen(total);
    8.28                      break;
    8.29                  case 1:
    8.30 -                    proj.setActiveIssues(total);
    8.31 +                    summary.setActive(total);
    8.32                      break;
    8.33                  case 2:
    8.34 -                    proj.setDoneIssues(total);
    8.35 +                    summary.setDone(total);
    8.36                      break;
    8.37              }
    8.38          }
    8.39 +        return summary;
    8.40      }
    8.41  
    8.42      @Override
    8.43 @@ -146,7 +149,6 @@
    8.44          try (var result = list.executeQuery()) {
    8.45              while (result.next()) {
    8.46                  final var project = mapColumns(result);
    8.47 -                mapIssueSummary(project);
    8.48                  projects.add(project);
    8.49              }
    8.50          }
    8.51 @@ -159,7 +161,6 @@
    8.52          try (var result = find.executeQuery()) {
    8.53              if (result.next()) {
    8.54                  final var project = mapColumns(result);
    8.55 -                mapIssueSummary(project);
    8.56                  return project;
    8.57              } else {
    8.58                  return null;
     9.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Sat May 30 18:12:38 2020 +0200
     9.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Mon Jun 01 14:46:58 2020 +0200
     9.3 @@ -29,7 +29,9 @@
     9.4  package de.uapcore.lightpit.dao.postgres;
     9.5  
     9.6  import de.uapcore.lightpit.dao.VersionDao;
     9.7 -import de.uapcore.lightpit.entities.*;
     9.8 +import de.uapcore.lightpit.entities.Project;
     9.9 +import de.uapcore.lightpit.entities.Version;
    9.10 +import de.uapcore.lightpit.entities.VersionStatus;
    9.11  
    9.12  import java.sql.Connection;
    9.13  import java.sql.PreparedStatement;
    9.14 @@ -42,7 +44,6 @@
    9.15  public final class PGVersionDao implements VersionDao {
    9.16  
    9.17      private final PreparedStatement insert, update, list, find;
    9.18 -    private final PreparedStatement issuesAffected, issuesScheduled, issuesResolved;
    9.19  
    9.20      public PGVersionDao(Connection connection) throws SQLException {
    9.21          list = connection.prepareStatement(
    9.22 @@ -64,54 +65,19 @@
    9.23          update = connection.prepareStatement(
    9.24                  "update lpit_version set name = ?, ordinal = ?, status = ?::version_status where versionid = ?"
    9.25          );
    9.26 -
    9.27 -        issuesAffected = connection.prepareStatement(
    9.28 -                "select category, status, count(*) as issuecount " +
    9.29 -                        "from lpit_issue_affected_version " +
    9.30 -                        "join lpit_issue using (issueid) " +
    9.31 -                        "where versionid = ? " +
    9.32 -                        "group by category, status"
    9.33 -        );
    9.34 -        issuesScheduled = connection.prepareStatement(
    9.35 -                "select category, status, count(*) as issuecount " +
    9.36 -                        "from lpit_issue_scheduled_version " +
    9.37 -                        "join lpit_issue using (issueid) " +
    9.38 -                        "where versionid = ? " +
    9.39 -                        "group by category, status"
    9.40 -        );
    9.41 -        issuesResolved = connection.prepareStatement(
    9.42 -                "select category, status, count(*) as issuecount " +
    9.43 -                        "from lpit_issue_resolved_version " +
    9.44 -                        "join lpit_issue using (issueid) " +
    9.45 -                        "where versionid = ? " +
    9.46 -                        "group by category, status"
    9.47 -        );
    9.48      }
    9.49  
    9.50      private Version mapColumns(ResultSet result) throws SQLException {
    9.51          final var project = new Project(result.getInt("project"));
    9.52          project.setName(result.getString("projectname"));
    9.53 -        final var version = new Version(result.getInt("versionid"), project);
    9.54 +        final var version = new Version(result.getInt("versionid"));
    9.55 +        version.setProject(project);
    9.56          version.setName(result.getString("name"));
    9.57          version.setOrdinal(result.getInt("ordinal"));
    9.58          version.setStatus(VersionStatus.valueOf(result.getString("status")));
    9.59          return version;
    9.60      }
    9.61  
    9.62 -    private VersionStatistics versionStatistics(Version version, PreparedStatement stmt) throws SQLException {
    9.63 -        stmt.setInt(1, version.getId());
    9.64 -        final var result = stmt.executeQuery();
    9.65 -        final var stats = new VersionStatistics(version);
    9.66 -        while (result.next()) {
    9.67 -            stats.setIssueCount(
    9.68 -                    IssueCategory.valueOf(result.getString("category")),
    9.69 -                    IssueStatus.valueOf(result.getString("status")),
    9.70 -                    result.getInt("issuecount")
    9.71 -            );
    9.72 -        }
    9.73 -        return stats;
    9.74 -    }
    9.75 -
    9.76      @Override
    9.77      public void save(Version instance) throws SQLException {
    9.78          Objects.requireNonNull(instance.getName());
    9.79 @@ -159,19 +125,4 @@
    9.80              }
    9.81          }
    9.82      }
    9.83 -
    9.84 -    @Override
    9.85 -    public VersionStatistics statsOpenedIssues(Version version) throws SQLException {
    9.86 -        return versionStatistics(version, issuesAffected);
    9.87 -    }
    9.88 -
    9.89 -    @Override
    9.90 -    public VersionStatistics statsScheduledIssues(Version version) throws SQLException {
    9.91 -        return versionStatistics(version, issuesScheduled);
    9.92 -    }
    9.93 -
    9.94 -    @Override
    9.95 -    public VersionStatistics statsResolvedIssues(Version version) throws SQLException {
    9.96 -        return versionStatistics(version, issuesResolved);
    9.97 -    }
    9.98  }
    10.1 --- a/src/main/java/de/uapcore/lightpit/entities/Issue.java	Sat May 30 18:12:38 2020 +0200
    10.2 +++ b/src/main/java/de/uapcore/lightpit/entities/Issue.java	Mon Jun 01 14:46:58 2020 +0200
    10.3 @@ -38,7 +38,7 @@
    10.4  public final class Issue {
    10.5  
    10.6      private int id;
    10.7 -    private final Project project;
    10.8 +    private Project project;
    10.9  
   10.10      private IssueStatus status;
   10.11      private IssueCategory category;
   10.12 @@ -55,9 +55,8 @@
   10.13      private Timestamp updated = Timestamp.from(Instant.now());
   10.14      private Date eta;
   10.15  
   10.16 -    public Issue(int id, Project project) {
   10.17 +    public Issue(int id) {
   10.18          this.id = id;
   10.19 -        this.project = project;
   10.20      }
   10.21  
   10.22      public int getId() {
   10.23 @@ -72,6 +71,10 @@
   10.24          this.id = id;
   10.25      }
   10.26  
   10.27 +    public void setProject(Project project) {
   10.28 +        this.project = project;
   10.29 +    }
   10.30 +
   10.31      public Project getProject() {
   10.32          return project;
   10.33      }
    11.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    11.2 +++ b/src/main/java/de/uapcore/lightpit/entities/IssueSummary.java	Mon Jun 01 14:46:58 2020 +0200
    11.3 @@ -0,0 +1,67 @@
    11.4 +package de.uapcore.lightpit.entities;
    11.5 +
    11.6 +public class IssueSummary {
    11.7 +    private int open = 0;
    11.8 +    private int active = 0;
    11.9 +    private int done = 0;
   11.10 +
   11.11 +    public int getOpen() {
   11.12 +        return open;
   11.13 +    }
   11.14 +
   11.15 +    public void setOpen(int open) {
   11.16 +        this.open = open;
   11.17 +    }
   11.18 +
   11.19 +    public int getActive() {
   11.20 +        return active;
   11.21 +    }
   11.22 +
   11.23 +    public void setActive(int active) {
   11.24 +        this.active = active;
   11.25 +    }
   11.26 +
   11.27 +    public int getDone() {
   11.28 +        return done;
   11.29 +    }
   11.30 +
   11.31 +    public void setDone(int done) {
   11.32 +        this.done = done;
   11.33 +    }
   11.34 +
   11.35 +    public int getTotal() {
   11.36 +        return open+active+done;
   11.37 +    }
   11.38 +
   11.39 +    public int getOpenPercent() {
   11.40 +        return 100-getActivePercent()-getDonePercent();
   11.41 +    }
   11.42 +
   11.43 +    public int getActivePercent() {
   11.44 +        int total = getTotal();
   11.45 +        return total > 0 ? 100*active/total : 0;
   11.46 +    }
   11.47 +
   11.48 +    public int getDonePercent() {
   11.49 +        int total = getTotal();
   11.50 +        return total > 0 ? 100*done/total : 0;
   11.51 +    }
   11.52 +
   11.53 +    /**
   11.54 +     * Adds the specified issue to the summary by increming the respective counter.
   11.55 +     * @param issue the issue
   11.56 +     */
   11.57 +    public void add(Issue issue) {
   11.58 +        switch (issue.getStatus().getPhase()) {
   11.59 +            case 0:
   11.60 +                open++;
   11.61 +                break;
   11.62 +            case 1:
   11.63 +                active++;
   11.64 +                break;
   11.65 +            case 2:
   11.66 +                done++;
   11.67 +                break;
   11.68 +        }
   11.69 +    }
   11.70 +}
    12.1 --- a/src/main/java/de/uapcore/lightpit/entities/Project.java	Sat May 30 18:12:38 2020 +0200
    12.2 +++ b/src/main/java/de/uapcore/lightpit/entities/Project.java	Mon Jun 01 14:46:58 2020 +0200
    12.3 @@ -38,10 +38,6 @@
    12.4      private String repoUrl;
    12.5      private User owner;
    12.6  
    12.7 -    private int openIssues;
    12.8 -    private int activeIssues;
    12.9 -    private int doneIssues;
   12.10 -
   12.11      public Project(int id) {
   12.12          this.id = id;
   12.13      }
   12.14 @@ -82,30 +78,6 @@
   12.15          this.owner = owner;
   12.16      }
   12.17  
   12.18 -    public int getOpenIssues() {
   12.19 -        return openIssues;
   12.20 -    }
   12.21 -
   12.22 -    public void setOpenIssues(int openIssues) {
   12.23 -        this.openIssues = openIssues;
   12.24 -    }
   12.25 -
   12.26 -    public int getActiveIssues() {
   12.27 -        return activeIssues;
   12.28 -    }
   12.29 -
   12.30 -    public void setActiveIssues(int activeIssues) {
   12.31 -        this.activeIssues = activeIssues;
   12.32 -    }
   12.33 -
   12.34 -    public int getDoneIssues() {
   12.35 -        return doneIssues;
   12.36 -    }
   12.37 -
   12.38 -    public void setDoneIssues(int doneIssues) {
   12.39 -        this.doneIssues = doneIssues;
   12.40 -    }
   12.41 -
   12.42      @Override
   12.43      public boolean equals(Object o) {
   12.44          if (this == o) return true;
    13.1 --- a/src/main/java/de/uapcore/lightpit/entities/Version.java	Sat May 30 18:12:38 2020 +0200
    13.2 +++ b/src/main/java/de/uapcore/lightpit/entities/Version.java	Mon Jun 01 14:46:58 2020 +0200
    13.3 @@ -41,9 +41,8 @@
    13.4      private int ordinal = 0;
    13.5      private VersionStatus status = VersionStatus.Future;
    13.6  
    13.7 -    public Version(int id, Project project) {
    13.8 +    public Version(int id) {
    13.9          this.id = id;
   13.10 -        this.project = project;
   13.11      }
   13.12  
   13.13      public int getId() {
    14.1 --- a/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java	Sat May 30 18:12:38 2020 +0200
    14.2 +++ b/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java	Mon Jun 01 14:46:58 2020 +0200
    14.3 @@ -29,6 +29,7 @@
    14.4  package de.uapcore.lightpit.modules;
    14.5  
    14.6  import de.uapcore.lightpit.*;
    14.7 +import de.uapcore.lightpit.viewmodel.LanguageView;
    14.8  import org.slf4j.Logger;
    14.9  import org.slf4j.LoggerFactory;
   14.10  
   14.11 @@ -86,9 +87,12 @@
   14.12      @RequestMapping(method = HttpMethod.GET)
   14.13      public ResponseType handle(HttpServletRequest req) {
   14.14  
   14.15 -        req.setAttribute("languages", languages);
   14.16 -        req.setAttribute("browserLanguage", req.getLocale());
   14.17 +        final var viewModel = new LanguageView();
   14.18 +        viewModel.setLanguages(languages);
   14.19 +        viewModel.setBrowserLanguage(req.getLocale());
   14.20 +        viewModel.setCurrentLanguage((Locale)req.getSession().getAttribute(Constants.SESSION_ATTR_LANGUAGE));
   14.21  
   14.22 +        setViewModel(req, viewModel);
   14.23          setStylesheet(req, "language");
   14.24          setContentPage(req, "language");
   14.25          return ResponseType.HTML;
    15.1 --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sat May 30 18:12:38 2020 +0200
    15.2 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Mon Jun 01 14:46:58 2020 +0200
    15.3 @@ -32,6 +32,7 @@
    15.4  import de.uapcore.lightpit.*;
    15.5  import de.uapcore.lightpit.dao.DataAccessObjects;
    15.6  import de.uapcore.lightpit.entities.*;
    15.7 +import de.uapcore.lightpit.viewmodel.*;
    15.8  import org.slf4j.Logger;
    15.9  import org.slf4j.LoggerFactory;
   15.10  
   15.11 @@ -42,7 +43,10 @@
   15.12  import java.io.IOException;
   15.13  import java.sql.Date;
   15.14  import java.sql.SQLException;
   15.15 -import java.util.*;
   15.16 +import java.util.ArrayList;
   15.17 +import java.util.List;
   15.18 +import java.util.NoSuchElementException;
   15.19 +import java.util.Objects;
   15.20  import java.util.stream.Collectors;
   15.21  import java.util.stream.Stream;
   15.22  
   15.23 @@ -59,41 +63,81 @@
   15.24      public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project");
   15.25      public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected_issue");
   15.26      public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version");
   15.27 -    public static final String SESSION_ATTR_HIDE_ZEROS = fqn(ProjectsModule.class, "stats_hide_zeros");
   15.28  
   15.29      private class SessionSelection {
   15.30          final HttpSession session;
   15.31 +        final HttpServletRequest req;
   15.32 +        final DataAccessObjects dao;
   15.33          Project project;
   15.34          Version version;
   15.35          Issue issue;
   15.36  
   15.37 -        SessionSelection(HttpServletRequest req, Project project) {
   15.38 -            this.session = req.getSession();
   15.39 -            this.project = project;
   15.40 +        SessionSelection(HttpServletRequest req, DataAccessObjects dao) {
   15.41 +            this.req = req;
   15.42 +            this.dao = dao;
   15.43 +            session = req.getSession();
   15.44 +        }
   15.45 +
   15.46 +        void newProject() {
   15.47 +            project = null;
   15.48              version = null;
   15.49              issue = null;
   15.50              updateAttributes();
   15.51 +            project = new Project(-1);
   15.52 +            updateAttributes();
   15.53          }
   15.54  
   15.55 -        SessionSelection(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
   15.56 -            this.session = req.getSession();
   15.57 -            final var issueDao = dao.getIssueDao();
   15.58 -            final var projectDao = dao.getProjectDao();
   15.59 -            final var issueSelection = getParameter(req, Integer.class, "issue");
   15.60 -            if (issueSelection.isPresent()) {
   15.61 -                issue = issueDao.find(issueSelection.get());
   15.62 -            } else {
   15.63 -                final var issue = (Issue) session.getAttribute(SESSION_ATTR_SELECTED_ISSUE);
   15.64 -                this.issue = issue == null ? null : issueDao.find(issue.getId());
   15.65 +        void newVersion() throws SQLException {
   15.66 +            project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
   15.67 +            syncProject();
   15.68 +            version = null;
   15.69 +            issue = null;
   15.70 +            updateAttributes();
   15.71 +            version = new Version(-1);
   15.72 +            version.setProject(project);
   15.73 +            updateAttributes();
   15.74 +        }
   15.75 +
   15.76 +        void newIssue() throws SQLException {
   15.77 +            project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
   15.78 +            syncProject();
   15.79 +            version = null;
   15.80 +            issue = null;
   15.81 +            updateAttributes();
   15.82 +            issue = new Issue(-1);
   15.83 +            issue.setProject(project);
   15.84 +            updateAttributes();
   15.85 +        }
   15.86 +
   15.87 +        void selectVersion(Version selectedVersion) throws SQLException {
   15.88 +            issue = null;
   15.89 +            version = selectedVersion;
   15.90 +            if (!version.getProject().equals(project)) {
   15.91 +                project = dao.getProjectDao().find(version.getProject().getId());
   15.92              }
   15.93 -            if (issue != null) {
   15.94 -                version = null; // show the issue globally
   15.95 -                project = projectDao.find(issue.getProject().getId());
   15.96 +            // our object contains more details
   15.97 +            version.setProject(project);
   15.98 +            updateAttributes();
   15.99 +        }
  15.100 +
  15.101 +        void selectIssue(Issue selectedIssue) throws SQLException {
  15.102 +            issue = selectedIssue;
  15.103 +            if (!issue.getProject().equals(project)) {
  15.104 +                project = dao.getProjectDao().find(issue.getProject().getId());
  15.105              }
  15.106 +            // our object contains more details
  15.107 +            issue.setProject(project);
  15.108 +            if (!issue.getResolvedVersions().contains(version) && !issue.getScheduledVersions().contains(version)
  15.109 +                    && !issue.getAffectedVersions().contains(version)) {
  15.110 +                version = null;
  15.111 +            }
  15.112 +            updateAttributes();
  15.113 +        }
  15.114  
  15.115 +        void syncProject() throws SQLException {
  15.116              final var projectSelection = getParameter(req, Integer.class, "pid");
  15.117              if (projectSelection.isPresent()) {
  15.118 -                final var selectedProject = projectDao.find(projectSelection.get());
  15.119 +                final var selectedProject = dao.getProjectDao().find(projectSelection.get());
  15.120                  if (!Objects.equals(selectedProject, project)) {
  15.121                      // reset version and issue if project changed
  15.122                      version = null;
  15.123 @@ -101,51 +145,57 @@
  15.124                  }
  15.125                  project = selectedProject;
  15.126              } else {
  15.127 -                final var sessionProject = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
  15.128 -                project = sessionProject == null ? null : projectDao.find(sessionProject.getId());
  15.129 +                project = project == null ? null : dao.getProjectDao().find(project.getId());
  15.130              }
  15.131 +        }
  15.132 +
  15.133 +        void syncVersion() throws SQLException {
  15.134 +            final var versionSelection = getParameter(req, Integer.class, "vid");
  15.135 +            if (versionSelection.isPresent()) {
  15.136 +                if (versionSelection.get() < 0) {
  15.137 +                    version = null;
  15.138 +                } else {
  15.139 +                    final var selectedVersion = dao.getVersionDao().find(versionSelection.get());
  15.140 +                    if (!Objects.equals(selectedVersion, version)) {
  15.141 +                        issue = null;
  15.142 +                    }
  15.143 +                    selectVersion(selectedVersion);
  15.144 +                }
  15.145 +            } else {
  15.146 +                version = version == null ? null : dao.getVersionDao().find(version.getId());
  15.147 +            }
  15.148 +        }
  15.149 +
  15.150 +        void syncIssue() throws SQLException {
  15.151 +            final var issueSelection = getParameter(req, Integer.class, "issue");
  15.152 +            if (issueSelection.isPresent()) {
  15.153 +                final var selectedIssue = dao.getIssueDao().find(issueSelection.get());
  15.154 +                dao.getIssueDao().joinVersionInformation(selectedIssue);
  15.155 +                selectIssue(selectedIssue);
  15.156 +            } else {
  15.157 +                issue = issue == null ? null : dao.getIssueDao().find(issue.getId());
  15.158 +            }
  15.159 +        }
  15.160 +
  15.161 +        void sync() throws SQLException {
  15.162 +            project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
  15.163 +            version = (Version) session.getAttribute(SESSION_ATTR_SELECTED_VERSION);
  15.164 +            issue = (Issue) session.getAttribute(SESSION_ATTR_SELECTED_ISSUE);
  15.165 +
  15.166 +            syncProject();
  15.167 +            syncVersion();
  15.168 +            syncIssue();
  15.169 +
  15.170              updateAttributes();
  15.171          }
  15.172  
  15.173 -        void selectVersion(Version version) {
  15.174 -            this.project = version.getProject();
  15.175 -            this.version = version;
  15.176 -            this.issue = null;
  15.177 -            updateAttributes();
  15.178 -        }
  15.179 -
  15.180 -        void selectIssue(Issue issue) {
  15.181 -            this.project = issue.getProject();
  15.182 -            this.issue = issue;
  15.183 -            this.version = null;
  15.184 -            updateAttributes();
  15.185 -        }
  15.186 -
  15.187 -        void updateAttributes() {
  15.188 +        private void updateAttributes() {
  15.189              session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, project);
  15.190              session.setAttribute(SESSION_ATTR_SELECTED_VERSION, version);
  15.191              session.setAttribute(SESSION_ATTR_SELECTED_ISSUE, issue);
  15.192          }
  15.193      }
  15.194  
  15.195 -    private void setAttributeHideZeros(HttpServletRequest req) {
  15.196 -        final Boolean value;
  15.197 -        final var param = getParameter(req, Boolean.class, "reduced");
  15.198 -        if (param.isPresent()) {
  15.199 -            value = param.get();
  15.200 -            req.getSession().setAttribute(SESSION_ATTR_HIDE_ZEROS, value);
  15.201 -        } else {
  15.202 -            final var sessionValue = req.getSession().getAttribute(SESSION_ATTR_HIDE_ZEROS);
  15.203 -            if (sessionValue != null) {
  15.204 -                value = (Boolean) sessionValue;
  15.205 -            } else {
  15.206 -                value = false;
  15.207 -                req.getSession().setAttribute(SESSION_ATTR_HIDE_ZEROS, value);
  15.208 -            }
  15.209 -        }
  15.210 -        req.setAttribute("statsHideZeros", value);
  15.211 -    }
  15.212 -
  15.213      @Override
  15.214      protected String getResourceBundleName() {
  15.215          return "localization.projects";
  15.216 @@ -162,10 +212,10 @@
  15.217       * Creates the breadcrumb menu.
  15.218       *
  15.219       * @param level           the current active level (0: root, 1: project, 2: version, 3: issue list, 4: issue)
  15.220 -     * @param sessionSelection the currently selected objects
  15.221 +     * @param selection the currently selected objects
  15.222       * @return a dynamic breadcrumb menu trying to display as many levels as possible
  15.223       */
  15.224 -    private List<MenuEntry> getBreadcrumbs(int level, SessionSelection sessionSelection) {
  15.225 +    private List<MenuEntry> getBreadcrumbs(int level, SessionSelection selection) {
  15.226          MenuEntry entry;
  15.227  
  15.228          final var breadcrumbs = new ArrayList<MenuEntry>();
  15.229 @@ -174,47 +224,49 @@
  15.230          breadcrumbs.add(entry);
  15.231          if (level == BREADCRUMB_LEVEL_ROOT) entry.setActive(true);
  15.232  
  15.233 -        if (sessionSelection.project != null) {
  15.234 -            if (sessionSelection.project.getId() < 0) {
  15.235 +        if (selection.project != null) {
  15.236 +            if (selection.project.getId() < 0) {
  15.237                  entry = new MenuEntry(new ResourceKey("localization.projects", "button.create"),
  15.238                          "projects/edit");
  15.239              } else {
  15.240 -                entry = new MenuEntry(sessionSelection.project.getName(),
  15.241 -                        "projects/view?pid=" + sessionSelection.project.getId());
  15.242 +                entry = new MenuEntry(selection.project.getName(),
  15.243 +                        "projects/view?pid=" + selection.project.getId());
  15.244              }
  15.245              if (level == BREADCRUMB_LEVEL_PROJECT) entry.setActive(true);
  15.246              breadcrumbs.add(entry);
  15.247          }
  15.248  
  15.249 -        if (sessionSelection.version != null) {
  15.250 -            if (sessionSelection.version.getId() < 0) {
  15.251 +        if (selection.version != null) {
  15.252 +            if (selection.version.getId() < 0) {
  15.253                  entry = new MenuEntry(new ResourceKey("localization.projects", "button.version.create"),
  15.254                          "projects/versions/edit");
  15.255              } else {
  15.256 -                entry = new MenuEntry(sessionSelection.version.getName(),
  15.257 -                        // TODO: change link to issue overview for that version
  15.258 -                        "projects/versions/edit?id=" + sessionSelection.version.getId());
  15.259 +                entry = new MenuEntry(selection.version.getName(),
  15.260 +                        "projects/versions/view?vid=" + selection.version.getId());
  15.261              }
  15.262              if (level == BREADCRUMB_LEVEL_VERSION) entry.setActive(true);
  15.263              breadcrumbs.add(entry);
  15.264          }
  15.265  
  15.266 -        if (sessionSelection.project != null) {
  15.267 +        if (selection.project != null) {
  15.268 +            String path = "projects/issues/?pid=" + selection.project.getId();
  15.269 +            if (selection.version != null) {
  15.270 +                path += "&vid="+selection.version.getId();
  15.271 +            }
  15.272              entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"),
  15.273 -                    // TODO: maybe also add selected version
  15.274 -                    "projects/issues/?pid=" + sessionSelection.project.getId());
  15.275 +                    path);
  15.276              if (level == BREADCRUMB_LEVEL_ISSUE_LIST) entry.setActive(true);
  15.277              breadcrumbs.add(entry);
  15.278          }
  15.279  
  15.280 -        if (sessionSelection.issue != null) {
  15.281 -            if (sessionSelection.issue.getId() < 0) {
  15.282 +        if (selection.issue != null) {
  15.283 +            if (selection.issue.getId() < 0) {
  15.284                  entry = new MenuEntry(new ResourceKey("localization.projects", "button.issue.create"),
  15.285                          "projects/issues/edit");
  15.286              } else {
  15.287 -                entry = new MenuEntry("#" + sessionSelection.issue.getId(),
  15.288 +                entry = new MenuEntry("#" + selection.issue.getId(),
  15.289                          // TODO: maybe change link to a view rather than directly opening the editor
  15.290 -                        "projects/issues/edit?id=" + sessionSelection.issue.getId());
  15.291 +                        "projects/issues/edit?issue=" + selection.issue.getId());
  15.292              }
  15.293              if (level == BREADCRUMB_LEVEL_ISSUE) entry.setActive(true);
  15.294              breadcrumbs.add(entry);
  15.295 @@ -226,8 +278,22 @@
  15.296      @RequestMapping(method = HttpMethod.GET)
  15.297      public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
  15.298          final var sessionSelection = new SessionSelection(req, dao);
  15.299 -        final var projectList = dao.getProjectDao().list();
  15.300 -        req.setAttribute("projects", projectList);
  15.301 +        sessionSelection.sync();
  15.302 +
  15.303 +        final var projectDao = dao.getProjectDao();
  15.304 +        final var versionDao = dao.getVersionDao();
  15.305 +
  15.306 +        final var projectList = projectDao.list();
  15.307 +
  15.308 +        final var viewModel = new ProjectIndexView();
  15.309 +        for (var project : projectList) {
  15.310 +            final var info = new ProjectInfo(project);
  15.311 +            info.setVersions(versionDao.list(project));
  15.312 +            info.setIssueSummary(projectDao.getIssueSummary(project));
  15.313 +            viewModel.getProjects().add(info);
  15.314 +        }
  15.315 +
  15.316 +        setViewModel(req, viewModel);
  15.317          setContentPage(req, "projects");
  15.318          setStylesheet(req, "projects");
  15.319  
  15.320 @@ -236,17 +302,24 @@
  15.321          return ResponseType.HTML;
  15.322      }
  15.323  
  15.324 -    private void configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
  15.325 -        req.setAttribute("project", selection.project);
  15.326 -        req.setAttribute("users", dao.getUserDao().list());
  15.327 +    private ProjectEditView configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
  15.328 +        final var viewModel = new ProjectEditView();
  15.329 +        viewModel.setProject(selection.project);
  15.330 +        viewModel.setUsers(dao.getUserDao().list());
  15.331 +        setViewModel(req, viewModel);
  15.332          setContentPage(req, "project-form");
  15.333          setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection));
  15.334 +        return viewModel;
  15.335      }
  15.336  
  15.337      @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
  15.338      public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
  15.339 -        final var selection = new SessionSelection(req, findByParameter(req, Integer.class, "id",
  15.340 -                dao.getProjectDao()::find).orElse(new Project(-1)));
  15.341 +        final var selection = new SessionSelection(req, dao);
  15.342 +        if (getParameter(req, Integer.class, "pid").isEmpty()) {
  15.343 +            selection.newProject();
  15.344 +        } else {
  15.345 +            selection.sync();
  15.346 +        }
  15.347  
  15.348          configureEditForm(req, dao, selection);
  15.349  
  15.350 @@ -272,10 +345,12 @@
  15.351              setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
  15.352              LOG.debug("Successfully updated project {}", project.getName());
  15.353          } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
  15.354 -            // TODO: set request attribute with error text
  15.355              LOG.warn("Form validation failure: {}", ex.getMessage());
  15.356              LOG.debug("Details:", ex);
  15.357 -            configureEditForm(req, dao, new SessionSelection(req, project));
  15.358 +            final var selection = new SessionSelection(req, dao);
  15.359 +            selection.project = project;
  15.360 +            final var vm = configureEditForm(req, dao, selection);
  15.361 +            vm.setErrorText(ex.getMessage()); // TODO: error text
  15.362          }
  15.363  
  15.364          return ResponseType.HTML;
  15.365 @@ -283,127 +358,143 @@
  15.366  
  15.367      @RequestMapping(requestPath = "view", method = HttpMethod.GET)
  15.368      public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
  15.369 -        final var sessionSelection = new SessionSelection(req, dao);
  15.370 -        if (sessionSelection.project == null) {
  15.371 +        final var selection = new SessionSelection(req, dao);
  15.372 +        selection.sync();
  15.373 +
  15.374 +        if (selection.project == null) {
  15.375              resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
  15.376              return ResponseType.NONE;
  15.377          }
  15.378  
  15.379          final var versionDao = dao.getVersionDao();
  15.380 -        final var versions = versionDao.list(sessionSelection.project);
  15.381 -        final var statsAffected = new ArrayList<VersionStatistics>();
  15.382 -        final var statsScheduled = new ArrayList<VersionStatistics>();
  15.383 -        final var statsResolved = new ArrayList<VersionStatistics>();
  15.384 -        for (Version version : versions) {
  15.385 -            statsAffected.add(versionDao.statsOpenedIssues(version));
  15.386 -            statsScheduled.add(versionDao.statsScheduledIssues(version));
  15.387 -            statsResolved.add(versionDao.statsResolvedIssues(version));
  15.388 -        }
  15.389 +        final var issueDao = dao.getIssueDao();
  15.390  
  15.391 -        setAttributeHideZeros(req);
  15.392 +        final var viewModel = new ProjectView(selection.project);
  15.393 +        final var issues = issueDao.list(selection.project);
  15.394 +        for (var issue : issues) issueDao.joinVersionInformation(issue);
  15.395 +        viewModel.setIssues(issues);
  15.396 +        viewModel.setVersions(versionDao.list(selection.project));
  15.397 +        viewModel.updateVersionInfo();
  15.398 +        setViewModel(req, viewModel);
  15.399  
  15.400 -        req.setAttribute("project", sessionSelection.project);
  15.401 -        req.setAttribute("versions", versions);
  15.402 -        req.setAttribute("statsAffected", statsAffected);
  15.403 -        req.setAttribute("statsScheduled", statsScheduled);
  15.404 -        req.setAttribute("statsResolved", statsResolved);
  15.405 -
  15.406 -        req.setAttribute("issueStatusEnum", IssueStatus.values());
  15.407 -        req.setAttribute("issueCategoryEnum", IssueCategory.values());
  15.408 -
  15.409 -        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, sessionSelection));
  15.410 +        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection));
  15.411          setContentPage(req, "project-details");
  15.412          setStylesheet(req, "projects");
  15.413  
  15.414          return ResponseType.HTML;
  15.415      }
  15.416  
  15.417 -    private void configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
  15.418 -        final var versionDao = dao.getVersionDao();
  15.419 -        req.setAttribute("projects", dao.getProjectDao().list());
  15.420 -        req.setAttribute("version", selection.version);
  15.421 -        req.setAttribute("versionStatusEnum", VersionStatus.values());
  15.422 +    @RequestMapping(requestPath = "versions/view", method = HttpMethod.GET)
  15.423 +    public ResponseType viewVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
  15.424 +        final var selection = new SessionSelection(req, dao);
  15.425 +        selection.sync();
  15.426 +        if (selection.version == null) {
  15.427 +            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
  15.428 +            return ResponseType.NONE;
  15.429 +        }
  15.430  
  15.431 -        req.setAttribute("issueStatusEnum", IssueStatus.values());
  15.432 -        req.setAttribute("issueCategoryEnum", IssueCategory.values());
  15.433 -        req.setAttribute("statsAffected", versionDao.statsOpenedIssues(selection.version));
  15.434 -        req.setAttribute("statsScheduled", versionDao.statsScheduledIssues(selection.version));
  15.435 -        req.setAttribute("statsResolved", versionDao.statsResolvedIssues(selection.version));
  15.436 -        setAttributeHideZeros(req);
  15.437 +        final var issueDao = dao.getIssueDao();
  15.438 +        final var viewModel = new VersionView(selection.version);
  15.439 +        final var issues = issueDao.list(selection.version);
  15.440 +        for (var issue : issues) issueDao.joinVersionInformation(issue);
  15.441 +        viewModel.setIssues(issues);
  15.442 +        setViewModel(req, viewModel);
  15.443  
  15.444 +        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection));
  15.445 +        setContentPage(req, "version");
  15.446 +        setStylesheet(req, "projects");
  15.447 +
  15.448 +        return ResponseType.HTML;
  15.449 +    }
  15.450 +
  15.451 +    private VersionEditView configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
  15.452 +        final var viewModel = new VersionEditView(selection.version);
  15.453 +        if (selection.version.getProject() == null) {
  15.454 +            viewModel.setProjects(dao.getProjectDao().list());
  15.455 +        }
  15.456 +        setViewModel(req, viewModel);
  15.457          setContentPage(req, "version-form");
  15.458          setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection));
  15.459 +        return viewModel;
  15.460      }
  15.461  
  15.462      @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET)
  15.463 -    public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
  15.464 -        final var sessionSelection = new SessionSelection(req, dao);
  15.465 +    public ResponseType editVersion(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
  15.466 +        final var selection = new SessionSelection(req, dao);
  15.467 +        if (getParameter(req, Integer.class, "vid").isEmpty()) {
  15.468 +            selection.newVersion();
  15.469 +        } else {
  15.470 +            selection.sync();
  15.471 +        }
  15.472  
  15.473 -        sessionSelection.selectVersion(findByParameter(req, Integer.class, "id", dao.getVersionDao()::find)
  15.474 -                .orElse(new Version(-1, sessionSelection.project)));
  15.475 -        configureEditVersionForm(req, dao, sessionSelection);
  15.476 +        configureEditVersionForm(req, dao, selection);
  15.477  
  15.478          return ResponseType.HTML;
  15.479      }
  15.480  
  15.481      @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST)
  15.482      public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
  15.483 -        final var sessionSelection = new SessionSelection(req, dao);
  15.484  
  15.485 -        var version = new Version(-1, sessionSelection.project);
  15.486 +        var version = new Version(-1);
  15.487          try {
  15.488 -            version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), sessionSelection.project);
  15.489 +            version = new Version(getParameter(req, Integer.class, "id").orElseThrow());
  15.490 +            version.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
  15.491              version.setName(getParameter(req, String.class, "name").orElseThrow());
  15.492              getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal);
  15.493              version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
  15.494              dao.getVersionDao().saveOrUpdate(version);
  15.495  
  15.496              // specifying the pid parameter will purposely reset the session selected version!
  15.497 -            setRedirectLocation(req, "./projects/view?pid="+sessionSelection.project.getId());
  15.498 +            setRedirectLocation(req, "./projects/view?pid="+version.getProject().getId());
  15.499              setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
  15.500 -            LOG.debug("Successfully updated version {} for project {}", version.getName(), sessionSelection.project.getName());
  15.501          } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
  15.502 -            // TODO: set request attribute with error text
  15.503              LOG.warn("Form validation failure: {}", ex.getMessage());
  15.504              LOG.debug("Details:", ex);
  15.505 -            sessionSelection.selectVersion(version);
  15.506 -            configureEditVersionForm(req, dao, sessionSelection);
  15.507 +            final var selection = new SessionSelection(req, dao);
  15.508 +            selection.selectVersion(version);
  15.509 +            final var viewModel = configureEditVersionForm(req, dao, selection);
  15.510 +            // TODO: set Error Text
  15.511          }
  15.512  
  15.513          return ResponseType.HTML;
  15.514      }
  15.515  
  15.516 -    private void configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
  15.517 +    private IssueEditView configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
  15.518 +        final var viewModel = new IssueEditView(selection.issue);
  15.519  
  15.520 -        if (selection.issue.getProject() == null || selection.issue.getProject().getId() < 0) {
  15.521 -            req.setAttribute("projects", dao.getProjectDao().list());
  15.522 -            req.setAttribute("versions", Collections.<Version>emptyList());
  15.523 +        if (selection.issue.getProject() == null) {
  15.524 +            viewModel.setProjects(dao.getProjectDao().list());
  15.525          } else {
  15.526 -            req.setAttribute("projects", Collections.<Project>emptyList());
  15.527 -            req.setAttribute("versions", dao.getVersionDao().list(selection.issue.getProject()));
  15.528 +            viewModel.setVersions(dao.getVersionDao().list(selection.issue.getProject()));
  15.529          }
  15.530 -
  15.531 -        dao.getIssueDao().joinVersionInformation(selection.issue);
  15.532 -        req.setAttribute("issue", selection.issue);
  15.533 -        req.setAttribute("issueStatusEnum", IssueStatus.values());
  15.534 -        req.setAttribute("issueCategoryEnum", IssueCategory.values());
  15.535 -        req.setAttribute("users", dao.getUserDao().list());
  15.536 +        viewModel.setUsers(dao.getUserDao().list());
  15.537 +        setViewModel(req, viewModel);
  15.538  
  15.539          setContentPage(req, "issue-form");
  15.540          setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE, selection));
  15.541 +        return viewModel;
  15.542      }
  15.543  
  15.544      @RequestMapping(requestPath = "issues/", method = HttpMethod.GET)
  15.545      public ResponseType issues(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
  15.546 -        final var sessionSelection = new SessionSelection(req, dao);
  15.547 -        if (sessionSelection.project == null) {
  15.548 +        final var selection = new SessionSelection(req, dao);
  15.549 +        selection.sync();
  15.550 +        if (selection.project == null) {
  15.551              resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
  15.552              return ResponseType.NONE;
  15.553          }
  15.554  
  15.555 -        req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project));
  15.556 +        final var viewModel = new IssuesView();
  15.557 +        viewModel.setProject(selection.project);
  15.558 +        if (selection.version == null) {
  15.559 +            viewModel.setIssues(dao.getIssueDao().list(selection.project));
  15.560 +        } else {
  15.561 +            viewModel.setVersion(selection.version);
  15.562 +            viewModel.setIssues(dao.getIssueDao().list(selection.version));
  15.563 +        }
  15.564 +        setViewModel(req, viewModel);
  15.565  
  15.566 -        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, sessionSelection));
  15.567 +        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, selection));
  15.568          setContentPage(req, "issues");
  15.569          setStylesheet(req, "projects");
  15.570  
  15.571 @@ -412,22 +503,25 @@
  15.572  
  15.573      @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
  15.574      public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
  15.575 -        final var sessionSelection = new SessionSelection(req, dao);
  15.576 +        final var selection = new SessionSelection(req, dao);
  15.577 +        if (getParameter(req, Integer.class, "issue").isEmpty()) {
  15.578 +            selection.newIssue();
  15.579 +        } else {
  15.580 +            selection.sync();
  15.581 +        }
  15.582  
  15.583 -        sessionSelection.selectIssue(findByParameter(req, Integer.class, "id",
  15.584 -                dao.getIssueDao()::find).orElse(new Issue(-1, sessionSelection.project)));
  15.585 -        configureEditIssueForm(req, dao, sessionSelection);
  15.586 +        configureEditIssueForm(req, dao, selection);
  15.587  
  15.588          return ResponseType.HTML;
  15.589      }
  15.590  
  15.591      @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
  15.592      public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
  15.593 -        final var sessionSelection = new SessionSelection(req, dao);
  15.594  
  15.595 -        Issue issue = new Issue(-1, sessionSelection.project);
  15.596 +        Issue issue = new Issue(-1);
  15.597          try {
  15.598 -            issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), sessionSelection.project);
  15.599 +            issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
  15.600 +            issue.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
  15.601              getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory);
  15.602              getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus);
  15.603              issue.setSubject(getParameter(req, String.class, "subject").orElseThrow());
  15.604 @@ -440,17 +534,17 @@
  15.605              getParameter(req, Integer[].class, "affected")
  15.606                      .map(Stream::of)
  15.607                      .map(stream ->
  15.608 -                        stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList())
  15.609 +                        stream.map(Version::new).collect(Collectors.toList())
  15.610                      ).ifPresent(issue::setAffectedVersions);
  15.611              getParameter(req, Integer[].class, "scheduled")
  15.612                      .map(Stream::of)
  15.613                      .map(stream ->
  15.614 -                            stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList())
  15.615 +                            stream.map(Version::new).collect(Collectors.toList())
  15.616                      ).ifPresent(issue::setScheduledVersions);
  15.617              getParameter(req, Integer[].class, "resolved")
  15.618                      .map(Stream::of)
  15.619                      .map(stream ->
  15.620 -                            stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList())
  15.621 +                            stream.map(Version::new).collect(Collectors.toList())
  15.622                      ).ifPresent(issue::setResolvedVersions);
  15.623  
  15.624              dao.getIssueDao().saveOrUpdate(issue);
  15.625 @@ -458,13 +552,14 @@
  15.626              // specifying the issue parameter keeps the edited issue as breadcrumb
  15.627              setRedirectLocation(req, "./projects/issues/?issue="+issue.getId());
  15.628              setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
  15.629 -            LOG.debug("Successfully updated issue {} for project {}", issue.getId(), sessionSelection.project.getName());
  15.630          } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
  15.631              // TODO: set request attribute with error text
  15.632              LOG.warn("Form validation failure: {}", ex.getMessage());
  15.633              LOG.debug("Details:", ex);
  15.634 -            sessionSelection.selectIssue(issue);
  15.635 -            configureEditIssueForm(req, dao, sessionSelection);
  15.636 +            final var selection = new SessionSelection(req, dao);
  15.637 +            selection.selectIssue(issue);
  15.638 +            final var viewModel = configureEditIssueForm(req, dao, selection);
  15.639 +            // TODO: set Error Text
  15.640          }
  15.641  
  15.642          return ResponseType.HTML;
    16.1 --- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Sat May 30 18:12:38 2020 +0200
    16.2 +++ b/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Mon Jun 01 14:46:58 2020 +0200
    16.3 @@ -32,6 +32,8 @@
    16.4  import de.uapcore.lightpit.*;
    16.5  import de.uapcore.lightpit.dao.DataAccessObjects;
    16.6  import de.uapcore.lightpit.entities.User;
    16.7 +import de.uapcore.lightpit.viewmodel.UsersEditView;
    16.8 +import de.uapcore.lightpit.viewmodel.UsersView;
    16.9  import org.slf4j.Logger;
   16.10  import org.slf4j.LoggerFactory;
   16.11  
   16.12 @@ -57,7 +59,9 @@
   16.13      public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
   16.14          final var userDao = dao.getUserDao();
   16.15  
   16.16 -        req.setAttribute("users", userDao.list());
   16.17 +        final var viewModel = new UsersView();
   16.18 +        viewModel.setUsers(userDao.list());
   16.19 +        setViewModel(req, viewModel);
   16.20          setContentPage(req, "users");
   16.21  
   16.22          return ResponseType.HTML;
   16.23 @@ -66,9 +70,11 @@
   16.24      @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
   16.25      public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
   16.26  
   16.27 -        req.setAttribute("user", findByParameter(req, Integer.class, "id",
   16.28 +        final var viewModel = new UsersEditView();
   16.29 +        viewModel.setUser(findByParameter(req, Integer.class, "id",
   16.30                  dao.getUserDao()::find).orElse(new User(-1)));
   16.31  
   16.32 +        setViewModel(req, viewModel);
   16.33          setContentPage(req, "user-form");
   16.34  
   16.35          return ResponseType.HTML;
   16.36 @@ -92,8 +98,10 @@
   16.37  
   16.38              LOG.debug("Successfully updated user {}", user.getUsername());
   16.39          } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
   16.40 -            // TODO: set request attribute with error text
   16.41 -            req.setAttribute("user", user);
   16.42 +            final var viewModel = new UsersEditView();
   16.43 +            viewModel.setUser(user);
   16.44 +            // TODO: viewModel.setErrorText()
   16.45 +            setViewModel(req, viewModel);
   16.46              setContentPage(req, "user-form");
   16.47              LOG.warn("Form validation failure: {}", ex.getMessage());
   16.48              LOG.debug("Details:", ex);
    17.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    17.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Mon Jun 01 14:46:58 2020 +0200
    17.3 @@ -0,0 +1,54 @@
    17.4 +package de.uapcore.lightpit.viewmodel;
    17.5 +
    17.6 +import de.uapcore.lightpit.entities.*;
    17.7 +
    17.8 +import java.util.Collections;
    17.9 +import java.util.List;
   17.10 +
   17.11 +public class IssueEditView {
   17.12 +    private final Issue issue;
   17.13 +
   17.14 +    private List<Project> projects = Collections.emptyList();
   17.15 +    private List<Version> versions = Collections.emptyList();
   17.16 +    private List<User> users;
   17.17 +
   17.18 +    public IssueEditView(Issue issue) {
   17.19 +        this.issue = issue;
   17.20 +    }
   17.21 +
   17.22 +    public Issue getIssue() {
   17.23 +        return issue;
   17.24 +    }
   17.25 +
   17.26 +    public List<Project> getProjects() {
   17.27 +        return projects;
   17.28 +    }
   17.29 +
   17.30 +    public void setProjects(List<Project> projects) {
   17.31 +        this.projects = projects;
   17.32 +    }
   17.33 +
   17.34 +    public List<Version> getVersions() {
   17.35 +        return versions;
   17.36 +    }
   17.37 +
   17.38 +    public void setVersions(List<Version> versions) {
   17.39 +        this.versions = versions;
   17.40 +    }
   17.41 +
   17.42 +    public List<User> getUsers() {
   17.43 +        return users;
   17.44 +    }
   17.45 +
   17.46 +    public void setUsers(List<User> users) {
   17.47 +        this.users = users;
   17.48 +    }
   17.49 +
   17.50 +    public IssueStatus[] getIssueStatus() {
   17.51 +        return IssueStatus.values();
   17.52 +    }
   17.53 +
   17.54 +    public IssueCategory[] getIssueCategory() {
   17.55 +        return IssueCategory.values();
   17.56 +    }
   17.57 +}
    18.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    18.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssuesView.java	Mon Jun 01 14:46:58 2020 +0200
    18.3 @@ -0,0 +1,37 @@
    18.4 +package de.uapcore.lightpit.viewmodel;
    18.5 +
    18.6 +import de.uapcore.lightpit.entities.Issue;
    18.7 +import de.uapcore.lightpit.entities.Project;
    18.8 +import de.uapcore.lightpit.entities.Version;
    18.9 +
   18.10 +import java.util.List;
   18.11 +
   18.12 +public class IssuesView {
   18.13 +    private List<Issue> issues;
   18.14 +    private Project project;
   18.15 +    private Version version;
   18.16 +
   18.17 +    public List<Issue> getIssues() {
   18.18 +        return issues;
   18.19 +    }
   18.20 +
   18.21 +    public void setIssues(List<Issue> issues) {
   18.22 +        this.issues = issues;
   18.23 +    }
   18.24 +
   18.25 +    public Version getVersion() {
   18.26 +        return version;
   18.27 +    }
   18.28 +
   18.29 +    public void setVersion(Version version) {
   18.30 +        this.version = version;
   18.31 +    }
   18.32 +
   18.33 +    public Project getProject() {
   18.34 +        return project;
   18.35 +    }
   18.36 +
   18.37 +    public void setProject(Project project) {
   18.38 +        this.project = project;
   18.39 +    }
   18.40 +}
    19.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    19.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/LanguageView.java	Mon Jun 01 14:46:58 2020 +0200
    19.3 @@ -0,0 +1,36 @@
    19.4 +package de.uapcore.lightpit.viewmodel;
    19.5 +
    19.6 +import java.util.List;
    19.7 +import java.util.Locale;
    19.8 +
    19.9 +public class LanguageView {
   19.10 +
   19.11 +    private List<Locale> languages;
   19.12 +    private Locale browserLanguage;
   19.13 +    private Locale currentLanguage;
   19.14 +
   19.15 +
   19.16 +    public List<Locale> getLanguages() {
   19.17 +        return languages;
   19.18 +    }
   19.19 +
   19.20 +    public void setLanguages(List<Locale> languages) {
   19.21 +        this.languages = languages;
   19.22 +    }
   19.23 +
   19.24 +    public Locale getBrowserLanguage() {
   19.25 +        return browserLanguage;
   19.26 +    }
   19.27 +
   19.28 +    public void setBrowserLanguage(Locale browserLanguage) {
   19.29 +        this.browserLanguage = browserLanguage;
   19.30 +    }
   19.31 +
   19.32 +    public Locale getCurrentLanguage() {
   19.33 +        return currentLanguage;
   19.34 +    }
   19.35 +
   19.36 +    public void setCurrentLanguage(Locale currentLanguage) {
   19.37 +        this.currentLanguage = currentLanguage;
   19.38 +    }
   19.39 +}
    20.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    20.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java	Mon Jun 01 14:46:58 2020 +0200
    20.3 @@ -0,0 +1,37 @@
    20.4 +package de.uapcore.lightpit.viewmodel;
    20.5 +
    20.6 +import de.uapcore.lightpit.entities.Project;
    20.7 +import de.uapcore.lightpit.entities.User;
    20.8 +
    20.9 +import java.util.List;
   20.10 +
   20.11 +public class ProjectEditView {
   20.12 +
   20.13 +    private Project project;
   20.14 +    private List<User> users;
   20.15 +    private String errorText;
   20.16 +
   20.17 +    public Project getProject() {
   20.18 +        return project;
   20.19 +    }
   20.20 +
   20.21 +    public void setProject(Project project) {
   20.22 +        this.project = project;
   20.23 +    }
   20.24 +
   20.25 +    public List<User> getUsers() {
   20.26 +        return users;
   20.27 +    }
   20.28 +
   20.29 +    public void setUsers(List<User> users) {
   20.30 +        this.users = users;
   20.31 +    }
   20.32 +
   20.33 +    public String getErrorText() {
   20.34 +        return errorText;
   20.35 +    }
   20.36 +
   20.37 +    public void setErrorText(String errorText) {
   20.38 +        this.errorText = errorText;
   20.39 +    }
   20.40 +}
    21.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    21.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectIndexView.java	Mon Jun 01 14:46:58 2020 +0200
    21.3 @@ -0,0 +1,17 @@
    21.4 +package de.uapcore.lightpit.viewmodel;
    21.5 +
    21.6 +import java.util.ArrayList;
    21.7 +import java.util.List;
    21.8 +
    21.9 +public class ProjectIndexView {
   21.10 +
   21.11 +    private List<ProjectInfo> projects = new ArrayList<>();
   21.12 +
   21.13 +    public List<ProjectInfo> getProjects() {
   21.14 +        return projects;
   21.15 +    }
   21.16 +
   21.17 +    public void setProjects(List<ProjectInfo> projects) {
   21.18 +        this.projects = projects;
   21.19 +    }
   21.20 +}
    22.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    22.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectInfo.java	Mon Jun 01 14:46:58 2020 +0200
    22.3 @@ -0,0 +1,58 @@
    22.4 +package de.uapcore.lightpit.viewmodel;
    22.5 +
    22.6 +import de.uapcore.lightpit.entities.IssueSummary;
    22.7 +import de.uapcore.lightpit.entities.Project;
    22.8 +import de.uapcore.lightpit.entities.Version;
    22.9 +import de.uapcore.lightpit.entities.VersionStatus;
   22.10 +
   22.11 +import java.util.Collections;
   22.12 +import java.util.List;
   22.13 +
   22.14 +public class ProjectInfo {
   22.15 +
   22.16 +    private final Project project;
   22.17 +    private List<Version> versions = Collections.emptyList();
   22.18 +    private IssueSummary issueSummary = new IssueSummary();
   22.19 +
   22.20 +    public ProjectInfo(Project project) {
   22.21 +        this.project = project;
   22.22 +    }
   22.23 +
   22.24 +    public Project getProject() {
   22.25 +        return project;
   22.26 +    }
   22.27 +
   22.28 +    public List<Version> getVersions() {
   22.29 +        return versions;
   22.30 +    }
   22.31 +
   22.32 +    public void setVersions(List<Version> versions) {
   22.33 +        this.versions = versions;
   22.34 +    }
   22.35 +
   22.36 +    public Version getLatestVersion() {
   22.37 +        for (var v : versions) {
   22.38 +            if (v.getStatus().ordinal() >= VersionStatus.Released.ordinal())
   22.39 +                return v;
   22.40 +        }
   22.41 +        return null;
   22.42 +    }
   22.43 +
   22.44 +    public Version getNextVersion() {
   22.45 +        Version next = null;
   22.46 +        for (var v : versions) {
   22.47 +            if (v.getStatus().ordinal() >= VersionStatus.Released.ordinal())
   22.48 +                break;
   22.49 +            next = v;
   22.50 +        }
   22.51 +        return next;
   22.52 +    }
   22.53 +
   22.54 +    public IssueSummary getIssueSummary() {
   22.55 +        return issueSummary;
   22.56 +    }
   22.57 +
   22.58 +    public void setIssueSummary(IssueSummary issueSummary) {
   22.59 +        this.issueSummary = issueSummary;
   22.60 +    }
   22.61 +}
    23.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    23.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java	Mon Jun 01 14:46:58 2020 +0200
    23.3 @@ -0,0 +1,81 @@
    23.4 +package de.uapcore.lightpit.viewmodel;
    23.5 +
    23.6 +import de.uapcore.lightpit.entities.Issue;
    23.7 +import de.uapcore.lightpit.entities.IssueSummary;
    23.8 +import de.uapcore.lightpit.entities.Project;
    23.9 +import de.uapcore.lightpit.entities.Version;
   23.10 +
   23.11 +import java.util.ArrayList;
   23.12 +import java.util.Collections;
   23.13 +import java.util.List;
   23.14 +
   23.15 +public class ProjectView {
   23.16 +
   23.17 +    private final Project project;
   23.18 +    private List<Version> versions = Collections.emptyList();
   23.19 +    private List<Issue> issues = Collections.emptyList();
   23.20 +
   23.21 +    private IssueSummary issuesTotal;
   23.22 +    private List<Issue> issuesWithoutVersion;
   23.23 +    private IssueSummary issuesWithoutVersionTotal;
   23.24 +    private List<VersionInfo> versionInfos = Collections.emptyList();
   23.25 +
   23.26 +    public ProjectView(Project project) {
   23.27 +        this.project = project;
   23.28 +    }
   23.29 +
   23.30 +    public Project getProject() {
   23.31 +        return project;
   23.32 +    }
   23.33 +
   23.34 +    public List<Issue> getIssues() {
   23.35 +        return issues;
   23.36 +    }
   23.37 +
   23.38 +    public void setIssues(List<Issue> issues) {
   23.39 +        this.issues = issues;
   23.40 +        issuesTotal = new IssueSummary();
   23.41 +        issuesWithoutVersion = new ArrayList<>();
   23.42 +        issuesWithoutVersionTotal = new IssueSummary();
   23.43 +        for (Issue issue : issues) {
   23.44 +            issuesTotal.add(issue);
   23.45 +            if (issue.getResolvedVersions().isEmpty() && issue.getScheduledVersions().isEmpty() && issue.getResolvedVersions().isEmpty()) {
   23.46 +                issuesWithoutVersion.add(issue);
   23.47 +                issuesWithoutVersionTotal.add(issue);
   23.48 +            }
   23.49 +        }
   23.50 +    }
   23.51 +
   23.52 +    public List<Version> getVersions() {
   23.53 +        return versions;
   23.54 +    }
   23.55 +
   23.56 +    public void setVersions(List<Version> versions) {
   23.57 +        this.versions = versions;
   23.58 +    }
   23.59 +
   23.60 +    public void updateVersionInfo() {
   23.61 +        versionInfos = new ArrayList<>();
   23.62 +        for (Version version : versions) {
   23.63 +            final var info = new VersionInfo(version);
   23.64 +            info.collectIssues(issues);
   23.65 +            versionInfos.add(info);
   23.66 +        }
   23.67 +    }
   23.68 +
   23.69 +    public IssueSummary getIssuesTotal() {
   23.70 +        return issuesTotal;
   23.71 +    }
   23.72 +
   23.73 +    public List<Issue> getIssuesWithoutVersion() {
   23.74 +        return issuesWithoutVersion;
   23.75 +    }
   23.76 +
   23.77 +    public IssueSummary getIssuesWithoutVersionTotal() {
   23.78 +        return issuesWithoutVersionTotal;
   23.79 +    }
   23.80 +
   23.81 +    public List<VersionInfo> getVersionInfos() {
   23.82 +        return versionInfos;
   23.83 +    }
   23.84 +}
    24.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    24.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/UsersEditView.java	Mon Jun 01 14:46:58 2020 +0200
    24.3 @@ -0,0 +1,24 @@
    24.4 +package de.uapcore.lightpit.viewmodel;
    24.5 +
    24.6 +import de.uapcore.lightpit.entities.User;
    24.7 +
    24.8 +public class UsersEditView {
    24.9 +    private User user;
   24.10 +    private String errorText;
   24.11 +
   24.12 +    public User getUser() {
   24.13 +        return user;
   24.14 +    }
   24.15 +
   24.16 +    public void setUser(User user) {
   24.17 +        this.user = user;
   24.18 +    }
   24.19 +
   24.20 +    public String getErrorText() {
   24.21 +        return errorText;
   24.22 +    }
   24.23 +
   24.24 +    public void setErrorText(String errorText) {
   24.25 +        this.errorText = errorText;
   24.26 +    }
   24.27 +}
    25.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    25.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/UsersView.java	Mon Jun 01 14:46:58 2020 +0200
    25.3 @@ -0,0 +1,17 @@
    25.4 +package de.uapcore.lightpit.viewmodel;
    25.5 +
    25.6 +import de.uapcore.lightpit.entities.User;
    25.7 +
    25.8 +import java.util.List;
    25.9 +
   25.10 +public class UsersView {
   25.11 +    private List<User> users;
   25.12 +
   25.13 +    public List<User> getUsers() {
   25.14 +        return users;
   25.15 +    }
   25.16 +
   25.17 +    public void setUsers(List<User> users) {
   25.18 +        this.users = users;
   25.19 +    }
   25.20 +}
    26.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    26.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java	Mon Jun 01 14:46:58 2020 +0200
    26.3 @@ -0,0 +1,42 @@
    26.4 +package de.uapcore.lightpit.viewmodel;
    26.5 +
    26.6 +import de.uapcore.lightpit.entities.Project;
    26.7 +import de.uapcore.lightpit.entities.Version;
    26.8 +import de.uapcore.lightpit.entities.VersionStatus;
    26.9 +
   26.10 +import java.util.Collections;
   26.11 +import java.util.List;
   26.12 +
   26.13 +public class VersionEditView {
   26.14 +    private final Version version;
   26.15 +    private List<Project> projects = Collections.emptyList();
   26.16 +    private String errorText;
   26.17 +
   26.18 +    public VersionEditView(Version version) {
   26.19 +        this.version = version;
   26.20 +    }
   26.21 +
   26.22 +    public Version getVersion() {
   26.23 +        return version;
   26.24 +    }
   26.25 +
   26.26 +    public List<Project> getProjects() {
   26.27 +        return projects;
   26.28 +    }
   26.29 +
   26.30 +    public void setProjects(List<Project> projects) {
   26.31 +        this.projects = projects;
   26.32 +    }
   26.33 +
   26.34 +    public VersionStatus[] getVersionStatus() {
   26.35 +        return VersionStatus.values();
   26.36 +    }
   26.37 +
   26.38 +    public String getErrorText() {
   26.39 +        return errorText;
   26.40 +    }
   26.41 +
   26.42 +    public void setErrorText(String errorText) {
   26.43 +        this.errorText = errorText;
   26.44 +    }
   26.45 +}
    27.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    27.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionInfo.java	Mon Jun 01 14:46:58 2020 +0200
    27.3 @@ -0,0 +1,82 @@
    27.4 +package de.uapcore.lightpit.viewmodel;
    27.5 +
    27.6 +import de.uapcore.lightpit.entities.Issue;
    27.7 +import de.uapcore.lightpit.entities.IssueSummary;
    27.8 +import de.uapcore.lightpit.entities.Version;
    27.9 +
   27.10 +import java.util.ArrayList;
   27.11 +import java.util.List;
   27.12 +
   27.13 +public class VersionInfo {
   27.14 +
   27.15 +    private final Version version;
   27.16 +
   27.17 +    private final IssueSummary reportedTotal = new IssueSummary();
   27.18 +    private final IssueSummary scheduledTotal = new IssueSummary();
   27.19 +    private final IssueSummary resolvedTotal = new IssueSummary();
   27.20 +
   27.21 +    private final List<Issue> reported = new ArrayList<>();
   27.22 +    private final List<Issue> scheduled = new ArrayList<>();
   27.23 +    private final List<Issue> resolved = new ArrayList<>();
   27.24 +
   27.25 +    public VersionInfo(Version version) {
   27.26 +        this.version = version;
   27.27 +    }
   27.28 +
   27.29 +    public Version getVersion() {
   27.30 +        return version;
   27.31 +    }
   27.32 +
   27.33 +    public void addReported(Issue issue) {
   27.34 +        reportedTotal.add(issue);
   27.35 +        reported.add(issue);
   27.36 +    }
   27.37 +
   27.38 +    public void addScheduled(Issue issue) {
   27.39 +        scheduledTotal.add(issue);
   27.40 +        scheduled.add(issue);
   27.41 +    }
   27.42 +
   27.43 +    public void addResolved(Issue issue) {
   27.44 +        resolvedTotal.add(issue);
   27.45 +        resolved.add(issue);
   27.46 +    }
   27.47 +
   27.48 +    public IssueSummary getReportedTotal() {
   27.49 +        return reportedTotal;
   27.50 +    }
   27.51 +
   27.52 +    public IssueSummary getScheduledTotal() {
   27.53 +        return scheduledTotal;
   27.54 +    }
   27.55 +
   27.56 +    public IssueSummary getResolvedTotal() {
   27.57 +        return resolvedTotal;
   27.58 +    }
   27.59 +
   27.60 +    public List<Issue> getReported() {
   27.61 +        return reported;
   27.62 +    }
   27.63 +
   27.64 +    public List<Issue> getScheduled() {
   27.65 +        return scheduled;
   27.66 +    }
   27.67 +
   27.68 +    public List<Issue> getResolved() {
   27.69 +        return resolved;
   27.70 +    }
   27.71 +
   27.72 +    public void collectIssues(List<Issue> issues) {
   27.73 +        for (Issue issue : issues) {
   27.74 +            if (issue.getAffectedVersions().contains(version)) {
   27.75 +                addReported(issue);
   27.76 +            }
   27.77 +            if (issue.getScheduledVersions().contains(version)) {
   27.78 +                addScheduled(issue);
   27.79 +            }
   27.80 +            if (issue.getResolvedVersions().contains(version)) {
   27.81 +                addResolved(issue);
   27.82 +            }
   27.83 +        }
   27.84 +    }
   27.85 +}
    28.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    28.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionView.java	Mon Jun 01 14:46:58 2020 +0200
    28.3 @@ -0,0 +1,23 @@
    28.4 +package de.uapcore.lightpit.viewmodel;
    28.5 +
    28.6 +import de.uapcore.lightpit.entities.Issue;
    28.7 +import de.uapcore.lightpit.entities.Version;
    28.8 +
    28.9 +import java.util.List;
   28.10 +
   28.11 +public class VersionView {
   28.12 +
   28.13 +    private final VersionInfo versionInfo;
   28.14 +
   28.15 +    public VersionView(Version version) {
   28.16 +        this.versionInfo = new VersionInfo(version);
   28.17 +    }
   28.18 +
   28.19 +    public VersionInfo getVersionInfo() {
   28.20 +        return versionInfo;
   28.21 +    }
   28.22 +
   28.23 +    public void setIssues(List<Issue> issues) {
   28.24 +        versionInfo.collectIssues(issues);
   28.25 +    }
   28.26 +}
    29.1 --- a/src/main/resources/localization/projects.properties	Sat May 30 18:12:38 2020 +0200
    29.2 +++ b/src/main/resources/localization/projects.properties	Mon Jun 01 14:46:58 2020 +0200
    29.3 @@ -26,7 +26,7 @@
    29.4  button.create=New Project
    29.5  button.version.create=New Version
    29.6  button.issue.create=New Issue
    29.7 -button.issue.list=Show Issues
    29.8 +button.issue.all=All Issues
    29.9  
   29.10  button.stats.hidezeros=Reduced View
   29.11  button.stats.showzeros=Full View
   29.12 @@ -39,9 +39,18 @@
   29.13  description=Description
   29.14  repoUrl=Repository
   29.15  owner=Project Lead
   29.16 +version.latest=Latest Version
   29.17 +version.next=Next Version
   29.18 +
   29.19 +progress=Overall Progress
   29.20 +
   29.21  issues.open=Open
   29.22  issues.active=In Progress
   29.23  issues.done=Done
   29.24 +issues.total=Total
   29.25 +issues.reported=Reported Issues
   29.26 +issues.scheduled=Scheduled Issues
   29.27 +issues.resolved=Resolved Issues
   29.28  
   29.29  version.project=Project
   29.30  version.name=Version
   29.31 @@ -59,11 +68,8 @@
   29.32  version.status.LTS=LTS
   29.33  version.status.Deprecated=Deprecated
   29.34  
   29.35 -version.statistics.affected=Affected by Issues
   29.36 -version.statistics.scheduled=Scheduled Issues
   29.37 -version.statistics.resolved=Resolved Issues
   29.38 -version.statistics.total=Total
   29.39  
   29.40 +issue.without-version=Issues w/o Assigned Version
   29.41  issue.project=Project
   29.42  issue.subject=Subject
   29.43  issue.description=Description
    30.1 --- a/src/main/resources/localization/projects_de.properties	Sat May 30 18:12:38 2020 +0200
    30.2 +++ b/src/main/resources/localization/projects_de.properties	Mon Jun 01 14:46:58 2020 +0200
    30.3 @@ -26,7 +26,7 @@
    30.4  button.create=Neues Projekt
    30.5  button.version.create=Neue Version
    30.6  button.issue.create=Neuer Vorgang
    30.7 -button.issue.list=Vorg\u00e4nge
    30.8 +button.issue.all=Alle Vorg\u00e4nge
    30.9  
   30.10  button.stats.hidezeros=Reduzierte Ansicht
   30.11  button.stats.showzeros=Komplettansicht
   30.12 @@ -39,9 +39,18 @@
   30.13  description=Beschreibung
   30.14  repoUrl=Repository
   30.15  owner=Projektleitung
   30.16 +version.latest=Neuste Version
   30.17 +version.next=N\u00e4chste Version
   30.18 +
   30.19 +progress=Gesamtfortschritt
   30.20 +
   30.21  issues.open=Offen
   30.22  issues.active=In Arbeit
   30.23  issues.done=Erledigt
   30.24 +issues.reported=Er\u00f6ffnete Vorg\u00e4nge
   30.25 +issues.scheduled=Geplante Vorg\u00e4nge 
   30.26 +issues.resolved=Gel\u00f6ste Vorg\u00e4nge
   30.27 +issues.total=Summe
   30.28  
   30.29  version.project=Projekt
   30.30  version.name=Version
   30.31 @@ -59,11 +68,7 @@
   30.32  version.status.LTS=Langzeitsupport
   30.33  version.status.Deprecated=Veraltet
   30.34  
   30.35 -version.statistics.affected=Betroffen von Vorg\u00e4ngen
   30.36 -version.statistics.scheduled=Geplante Vorg\u00e4nge 
   30.37 -version.statistics.resolved=Gel\u00f6ste Vorg\u00e4nge
   30.38 -version.statistics.total=Summe
   30.39 -
   30.40 +issue.without-version=Vorg\u00e4nge ohne Version
   30.41  issue.project=Projekt
   30.42  issue.subject=Thema
   30.43  issue.description=Beschreibung
    31.1 --- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Sat May 30 18:12:38 2020 +0200
    31.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Mon Jun 01 14:46:58 2020 +0200
    31.3 @@ -28,12 +28,9 @@
    31.4  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    31.5  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    31.6  
    31.7 -<jsp:useBean id="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request" />
    31.8 -<jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" scope="request" />
    31.9 -<jsp:useBean id="issue" type="de.uapcore.lightpit.entities.Issue" scope="request"/>
   31.10 -<jsp:useBean id="issueStatusEnum" type="de.uapcore.lightpit.entities.IssueStatus[]" scope="request"/>
   31.11 -<jsp:useBean id="issueCategoryEnum" type="de.uapcore.lightpit.entities.IssueCategory[]" scope="request"/>
   31.12 -<jsp:useBean id="users" type="java.util.List<de.uapcore.lightpit.entities.User>" scope="request"/>
   31.13 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssueEditView" scope="request"/>
   31.14 +<c:set var="issue" scope="page" value="${viewmodel.issue}" />
   31.15 +<c:set var="versions" value="${viewmodel.versions}" />
   31.16  
   31.17  <form action="./projects/issues/commit" method="post">
   31.18      <table class="formtable">
   31.19 @@ -45,26 +42,28 @@
   31.20          <tr>
   31.21              <th><fmt:message key="issue.project"/></th>
   31.22              <td>
   31.23 -                <c:if test="${issue.project.id ge 0}">
   31.24 -                    <c:out value="${issue.project.name}" />
   31.25 -                    <input type="hidden" name="pid" value="${issue.project.id}" />
   31.26 -                </c:if>
   31.27 -                <c:if test="${empty issue.project or issue.project.id lt 0}">
   31.28 -                <select name="pid" required>
   31.29 -                    <c:forEach var="project" items="${projects}">
   31.30 -                        <option value="${project.id}">
   31.31 -                            <c:out value="${project.name}" />
   31.32 -                        </option>
   31.33 -                    </c:forEach>
   31.34 -                </select>
   31.35 -                </c:if>
   31.36 +                <c:choose>
   31.37 +                    <c:when test="${not empty issue.project}">
   31.38 +                        <c:out value="${issue.project.name}" />
   31.39 +                        <input type="hidden" name="pid" value="${issue.project.id}" />
   31.40 +                    </c:when>
   31.41 +                    <c:otherwise>
   31.42 +                        <select name="pid" required>
   31.43 +                            <c:forEach var="project" items="${viewmodel.projects}">
   31.44 +                                <option value="${project.id}">
   31.45 +                                    <c:out value="${project.name}" />
   31.46 +                                </option>
   31.47 +                            </c:forEach>
   31.48 +                        </select>
   31.49 +                    </c:otherwise>
   31.50 +                </c:choose>
   31.51              </td>
   31.52          </tr>
   31.53          <tr>
   31.54              <th><fmt:message key="issue.category"/></th>
   31.55              <td>
   31.56                  <select name="category">
   31.57 -                    <c:forEach var="category" items="${issueCategoryEnum}">
   31.58 +                    <c:forEach var="category" items="${viewmodel.issueCategory}">
   31.59                          <option
   31.60                                  <c:if test="${category eq issue.category}">selected</c:if>
   31.61                                  value="${category}">
   31.62 @@ -78,7 +77,7 @@
   31.63              <th><fmt:message key="issue.status"/></th>
   31.64              <td>
   31.65                  <select name="status">
   31.66 -                    <c:forEach var="status" items="${issueStatusEnum}">
   31.67 +                    <c:forEach var="status" items="${viewmodel.issueStatus}">
   31.68                          <option
   31.69                                  <c:if test="${status eq issue.status}">selected</c:if>
   31.70                                  value="${status}">
   31.71 @@ -95,7 +94,7 @@
   31.72          <tr>
   31.73              <th class="vtop"><fmt:message key="issue.description"/></th>
   31.74              <td>
   31.75 -                <textarea name="description"><c:out value="${issue.description}"/></textarea>
   31.76 +                <textarea name="description" rows="10"><c:out value="${issue.description}"/></textarea>
   31.77              </td>
   31.78          </tr>
   31.79          <tr>
   31.80 @@ -103,7 +102,7 @@
   31.81              <td>
   31.82                  <select name="assignee">
   31.83                      <option value="-1"><fmt:message key="placeholder.null-assignee"/></option>
   31.84 -                    <c:forEach var="user" items="${users}">
   31.85 +                    <c:forEach var="user" items="${viewmodel.users}">
   31.86                          <option
   31.87                                  <c:if test="${not empty issue.assignee and user eq issue.assignee}">selected</c:if>
   31.88                                  value="${user.id}"><c:out value="${user.displayname}"/></option>
   31.89 @@ -155,7 +154,7 @@
   31.90              <td colspan="2">
   31.91                  <input type="hidden" name="id" value="${issue.id}"/>
   31.92                  <c:choose>
   31.93 -                    <c:when test="${not empty issue.project and issue.project.id ge 0}">
   31.94 +                    <c:when test="${not empty issue.project}">
   31.95                          <c:set var="cancelUrl">./projects/issues/?pid=${issue.project.id}</c:set>
   31.96                      </c:when>
   31.97                      <c:otherwise>
    32.1 --- a/src/main/webapp/WEB-INF/jsp/issues.jsp	Sat May 30 18:12:38 2020 +0200
    32.2 +++ b/src/main/webapp/WEB-INF/jsp/issues.jsp	Mon Jun 01 14:46:58 2020 +0200
    32.3 @@ -28,60 +28,26 @@
    32.4  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    32.5  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    32.6  
    32.7 -<jsp:useBean id="issues" type="java.util.List<de.uapcore.lightpit.entities.Issue>" scope="request"/>
    32.8 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssuesView" scope="request"/>
    32.9 +<c:set var="project" scope="page" value="${viewmodel.project}"/>
   32.10 +<c:set var="version" scope="page" value="${viewmodel.version}"/>
   32.11 +<%@include file="../jspf/project-header.jsp"%>
   32.12 +
   32.13 +<c:if test="${not empty version}">
   32.14 +    <h2>
   32.15 +        <fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/>
   32.16 +        <a href="./projects/versions/edit?vid=${version.id}">&#x270e;</a>
   32.17 +    </h2>
   32.18 +</c:if>
   32.19  
   32.20  <div id="tool-area">
   32.21      <div>
   32.22          <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a>
   32.23 +        <c:if test="${not empty version}">
   32.24 +            <a href="./projects/issues/?pid=${project.id}&vid=-1" class="button"><fmt:message key="button.issue.all"/></a>
   32.25 +        </c:if>
   32.26      </div>
   32.27  </div>
   32.28  
   32.29 -<table id="issue-list" class="datatable medskip">
   32.30 -    <thead>
   32.31 -    <tr>
   32.32 -        <th><fmt:message key="issue.subject"/></th>
   32.33 -        <th><fmt:message key="issue.assignee"/></th>
   32.34 -        <th><fmt:message key="issue.category"/></th>
   32.35 -        <th><fmt:message key="issue.status"/></th>
   32.36 -        <th><fmt:message key="issue.created"/></th>
   32.37 -        <th><fmt:message key="issue.updated"/></th>
   32.38 -        <th><fmt:message key="issue.eta"/></th>
   32.39 -    </tr>
   32.40 -    </thead>
   32.41 -    <tbody>
   32.42 -    <c:forEach var="issue" items="${issues}">
   32.43 -        <tr>
   32.44 -            <td>
   32.45 -                <span class="phase-${issue.status.phase}">
   32.46 -                    <a href="./projects/issues/edit?id=${issue.id}">
   32.47 -                        <c:out value="${issue.subject}" />
   32.48 -                    </a>
   32.49 -                </span>
   32.50 -            </td>
   32.51 -            <td>
   32.52 -                <c:if test="${not empty issue.assignee}">
   32.53 -                    <c:out value="${issue.assignee.shortDisplayname}" />
   32.54 -                </c:if>
   32.55 -                <c:if test="${empty issue.assignee}">
   32.56 -                    <fmt:message key="placeholder.null-assignee" />
   32.57 -                </c:if>
   32.58 -            </td>
   32.59 -            <td>
   32.60 -                <fmt:message key="issue.category.${issue.category}" />
   32.61 -            </td>
   32.62 -            <td>
   32.63 -                <fmt:message key="issue.status.${issue.status}" />
   32.64 -            </td>
   32.65 -            <td>
   32.66 -                <fmt:formatDate value="${issue.created}" type="BOTH"/>
   32.67 -            </td>
   32.68 -            <td>
   32.69 -                <fmt:formatDate value="${issue.updated}" type="BOTH"/>
   32.70 -            </td>
   32.71 -            <td>
   32.72 -                <fmt:formatDate value="${issue.eta}" />
   32.73 -            </td>
   32.74 -        </tr>
   32.75 -    </c:forEach>
   32.76 -    </tbody>
   32.77 -</table>
   32.78 +<c:set var="issues" value="${viewmodel.issues}"/>
   32.79 +<%@include file="../jspf/issue-list.jsp"%>
   32.80 \ No newline at end of file
    33.1 --- a/src/main/webapp/WEB-INF/jsp/language.jsp	Sat May 30 18:12:38 2020 +0200
    33.2 +++ b/src/main/webapp/WEB-INF/jsp/language.jsp	Mon Jun 01 14:46:58 2020 +0200
    33.3 @@ -25,23 +25,19 @@
    33.4  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
    33.5  --%>
    33.6  <%@page pageEncoding="UTF-8" %>
    33.7 -<%@page import="de.uapcore.lightpit.Constants" %>
    33.8  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    33.9  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   33.10  
   33.11 -<jsp:useBean id="languages" type="java.util.List<java.util.Locale>" scope="request"/>
   33.12 -<jsp:useBean id="browserLanguage" type="java.util.Locale" scope="request"/>
   33.13 -
   33.14 -<c:set scope="page" var="currentLanguage" value="${sessionScope[Constants.SESSION_ATTR_LANGUAGE]}"/>
   33.15 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.LanguageView" scope="request"/>
   33.16  
   33.17  <form method="POST" id="lang-selector">
   33.18 -    <c:forEach items="${languages}" var="l">
   33.19 +    <c:forEach items="${viewmodel.languages}" var="l">
   33.20          <label>
   33.21              <input type="radio" name="language" value="${l.language}"
   33.22 -                   <c:if test="${l.language eq currentLanguage.language}">checked</c:if>/>
   33.23 +                   <c:if test="${l.language eq viewmodel.currentLanguage.language}">checked</c:if>/>
   33.24                  ${l.displayLanguage}
   33.25 -            (${l.getDisplayLanguage(currentLanguage)}
   33.26 -            <c:if test="${not empty browserLanguage and l.language eq browserLanguage.language}"><c:set
   33.27 +            (${l.getDisplayLanguage(viewmodel.currentLanguage)}
   33.28 +            <c:if test="${not empty viewmodel.browserLanguage and l.language eq viewmodel.browserLanguage.language}"><c:set
   33.29                      var="browserLanguagePresent" value="true"/>&nbsp;-&nbsp;<fmt:message key="browserLanguage"/></c:if>)
   33.30          </label>
   33.31      </c:forEach>
    34.1 --- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sat May 30 18:12:38 2020 +0200
    34.2 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Mon Jun 01 14:46:58 2020 +0200
    34.3 @@ -28,66 +28,46 @@
    34.4  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    34.5  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    34.6  
    34.7 -<jsp:useBean id="project" type="de.uapcore.lightpit.entities.Project" scope="request" />
    34.8 -<jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" scope="request"/>
    34.9 -<jsp:useBean id="statsAffected" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/>
   34.10 -<jsp:useBean id="statsScheduled" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/>
   34.11 -<jsp:useBean id="statsResolved" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/>
   34.12 -<jsp:useBean id="issueStatusEnum" type="de.uapcore.lightpit.entities.IssueStatus[]" scope="request"/>
   34.13 -<jsp:useBean id="issueCategoryEnum" type="de.uapcore.lightpit.entities.IssueCategory[]" scope="request"/>
   34.14 -<jsp:useBean id="statsHideZeros" type="java.lang.Boolean" scope="request"/>
   34.15 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectView" scope="request" />
   34.16  
   34.17 -<div id="project-attributes">
   34.18 -    <div class="row">
   34.19 -        <div class="caption"><fmt:message key="name"/>:</div>
   34.20 -        <div><c:out value="${project.name}"/></div>
   34.21 -        <div class="caption"><fmt:message key="description"/>:</div>
   34.22 -        <div><c:out value="${project.description}"/></div>
   34.23 -    </div>
   34.24 -    <div class="row">
   34.25 -        <div class="caption"><fmt:message key="owner"/>:</div>
   34.26 -        <div>
   34.27 -            <c:if test="${not empty project.owner}"><c:out value="${project.owner.displayname}"/></c:if>
   34.28 -        </div>
   34.29 -        <div class="caption"><fmt:message key="repoUrl"/>:</div>
   34.30 -        <div>
   34.31 -            <c:if test="${not empty project.repoUrl}">
   34.32 -                <a target="_blank" href="<c:out value="${project.repoUrl}"/>"><c:out
   34.33 -                        value="${project.repoUrl}"/></a>
   34.34 -            </c:if>
   34.35 -        </div>
   34.36 -    </div>
   34.37 -</div>
   34.38 +<c:set var="project" scope="page" value="${viewmodel.project}"/>
   34.39 +<%@include file="../jspf/project-header.jsp"%>
   34.40  
   34.41  <div id="tool-area">
   34.42      <a href="./projects/versions/edit" class="button"><fmt:message key="button.version.create"/></a>
   34.43 -    <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a>
   34.44 -    <a href="./projects/issues/" class="button"><fmt:message key="button.issue.list"/></a>
   34.45 -    <c:if test="${not statsHideZeros}">
   34.46 -    <a href="./projects/view?reduced=1" class="button"><fmt:message key="button.stats.hidezeros"/></a>
   34.47 -    </c:if>
   34.48 -    <c:if test="${statsHideZeros}">
   34.49 -    <a href="./projects/view?reduced=0" class="button"><fmt:message key="button.stats.showzeros"/></a>
   34.50 -    </c:if>
   34.51 +    <a href="./projects/issues/edit?pid=${project.id}" class="button"><fmt:message key="button.issue.create"/></a>
   34.52  </div>
   34.53  
   34.54 -<div id="version-stats">
   34.55 -<c:forEach var="version" items="${versions}" varStatus="iter">
   34.56 +<h2><fmt:message key="progress" /></h2>
   34.57 +
   34.58 +<c:set var="summary" value="${viewmodel.issuesTotal}" />
   34.59 +<%@include file="../jspf/issue-summary.jsp"%>
   34.60 +
   34.61 +<h2><fmt:message key="issue.without-version" /></h2>
   34.62 +
   34.63 +<c:set var="issues" value="${viewmodel.issuesWithoutVersion}"/>
   34.64 +<c:set var="summary" value="${viewmodel.issuesWithoutVersionTotal}" />
   34.65 +<%@include file="../jspf/issue-summary.jsp"%>
   34.66 +<%@include file="../jspf/issue-list.jsp"%>
   34.67 +
   34.68 +<c:forEach var="versionInfo" items="${viewmodel.versionInfos}">
   34.69      <h2>
   34.70 -        <fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/>
   34.71 -        <a href="./projects/versions/edit?id=${version.id}">&#x270e;</a>
   34.72 +        <fmt:message key="version.label" /> <c:out value="${versionInfo.version.name}" /> - <fmt:message key="version.status.${versionInfo.version.status}"/>
   34.73 +        (<a href="./projects/versions/view?vid=${versionInfo.version.id}">open</a>)
   34.74      </h2>
   34.75  
   34.76 -    <h3><fmt:message key="version.statistics.affected" /></h3>
   34.77 -    <c:set var="stats" value="${statsAffected[iter.index]}" />
   34.78 -    <%@include file="../jspf/version-stats.jsp" %>
   34.79 +    <h3><fmt:message key="issues.reported"/> </h3>
   34.80 +    <c:set var="summary" value="${versionInfo.reportedTotal}"/>
   34.81 +    <c:set var="issues" value="${versionInfo.reported}"/>
   34.82 +    <%@include file="../jspf/issue-summary.jsp"%>
   34.83  
   34.84 -    <h3><fmt:message key="version.statistics.scheduled" /></h3>
   34.85 -    <c:set var="stats" value="${statsScheduled[iter.index]}" />
   34.86 -    <%@include file="../jspf/version-stats.jsp" %>
   34.87 +    <h3><fmt:message key="issues.scheduled"/> </h3>
   34.88 +    <c:set var="summary" value="${versionInfo.scheduledTotal}"/>
   34.89 +    <c:set var="issues" value="${versionInfo.scheduled}"/>
   34.90 +    <%@include file="../jspf/issue-summary.jsp"%>
   34.91  
   34.92 -    <h3><fmt:message key="version.statistics.resolved" /></h3>
   34.93 -    <c:set var="stats" value="${statsResolved[iter.index]}" />
   34.94 -    <%@include file="../jspf/version-stats.jsp" %>
   34.95 -</c:forEach>
   34.96 -</div>
   34.97 +    <h3><fmt:message key="issues.resolved"/> </h3>
   34.98 +    <c:set var="summary" value="${versionInfo.resolvedTotal}"/>
   34.99 +    <c:set var="issues" value="${versionInfo.resolved}"/>
  34.100 +    <%@include file="../jspf/issue-summary.jsp"%>
  34.101 +</c:forEach>
  34.102 \ No newline at end of file
    35.1 --- a/src/main/webapp/WEB-INF/jsp/projects.jsp	Sat May 30 18:12:38 2020 +0200
    35.2 +++ b/src/main/webapp/WEB-INF/jsp/projects.jsp	Mon Jun 01 14:46:58 2020 +0200
    35.3 @@ -25,15 +25,12 @@
    35.4  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    35.5  --%>
    35.6  <%@page pageEncoding="UTF-8" %>
    35.7 -<%@page import="de.uapcore.lightpit.modules.ProjectsModule" %>
    35.8  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    35.9  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   35.10  
   35.11 -<c:set scope="page" var="selectedProject" value="${sessionScope[ProjectsModule.SESSION_ATTR_SELECTED_PROJECT]}"/>
   35.12 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectIndexView" scope="request"/>
   35.13  
   35.14 -<jsp:useBean id="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request"/>
   35.15 -
   35.16 -<c:if test="${empty projects}">
   35.17 +<c:if test="${empty viewmodel.projects}">
   35.18      <div class="info-box">
   35.19          <fmt:message key="no-projects"/>
   35.20      </div>
   35.21 @@ -43,30 +40,35 @@
   35.22      <a href="./projects/edit" class="button"><fmt:message key="button.create"/></a>
   35.23  </div>
   35.24  
   35.25 -<c:if test="${not empty projects}">
   35.26 +<c:if test="${not empty viewmodel.projects}">
   35.27      <table id="project-list" class="datatable medskip">
   35.28          <colgroup>
   35.29              <col>
   35.30              <col width="20%">
   35.31              <col width="50%">
   35.32 -            <col width="10%">
   35.33 -            <col width="10%">
   35.34 -            <col width="10%">
   35.35 +            <col width="6%">
   35.36 +            <col width="6%">
   35.37 +            <col width="6%">
   35.38 +            <col width="6%">
   35.39 +            <col width="6%">
   35.40          </colgroup>
   35.41          <thead>
   35.42          <tr>
   35.43              <th></th>
   35.44              <th><fmt:message key="name"/></th>
   35.45              <th><fmt:message key="repoUrl"/></th>
   35.46 -            <th><fmt:message key="issues.open"/></th>
   35.47 -            <th><fmt:message key="issues.active"/></th>
   35.48 -            <th><fmt:message key="issues.done"/></th>
   35.49 +            <th class="hcenter"><fmt:message key="version.latest"/></th>
   35.50 +            <th class="hcenter"><fmt:message key="version.next"/></th>
   35.51 +            <th class="hcenter"><fmt:message key="issues.open"/></th>
   35.52 +            <th class="hcenter"><fmt:message key="issues.active"/></th>
   35.53 +            <th class="hcenter"><fmt:message key="issues.done"/></th>
   35.54          </tr>
   35.55          </thead>
   35.56          <tbody>
   35.57 -        <c:forEach var="project" items="${projects}">
   35.58 +        <c:forEach var="projectInfo" items="${viewmodel.projects}">
   35.59 +            <c:set var="project" scope="page" value="${projectInfo.project}"/>
   35.60              <tr class="nowrap">
   35.61 -                <td style="width: 2em;"><a href="./projects/edit?id=${project.id}">&#x270e;</a></td>
   35.62 +                <td style="width: 2em;"><a href="./projects/edit?pid=${project.id}">&#x270e;</a></td>
   35.63                  <td><a href="./projects/view?pid=${project.id}"><c:out value="${project.name}"/></a>
   35.64                  </td>
   35.65                  <td>
   35.66 @@ -75,9 +77,19 @@
   35.67                                  value="${project.repoUrl}"/></a>
   35.68                      </c:if>
   35.69                  </td>
   35.70 -                <td>${project.openIssues}</td>
   35.71 -                <td>${project.activeIssues}</td>
   35.72 -                <td>${project.doneIssues}</td>
   35.73 +                <td class="hright">
   35.74 +                    <c:if test="${not empty projectInfo.latestVersion}">
   35.75 +                        <a href="./projects/versions/view?vid=${projectInfo.latestVersion.id}"><c:out value="${projectInfo.latestVersion.name}"/></a>
   35.76 +                    </c:if>
   35.77 +                </td>
   35.78 +                <td class="hright">
   35.79 +                    <c:if test="${not empty projectInfo.nextVersion}">
   35.80 +                        <a href="./projects/versions/view?vid=${projectInfo.nextVersion.id}"><c:out value="${projectInfo.nextVersion.name}"/></a>
   35.81 +                    </c:if>
   35.82 +                </td>
   35.83 +                <td class="hright">${projectInfo.issueSummary.open}</td>
   35.84 +                <td class="hright">${projectInfo.issueSummary.active}</td>
   35.85 +                <td class="hright">${projectInfo.issueSummary.done}</td>
   35.86              </tr>
   35.87          </c:forEach>
   35.88          </tbody>
    36.1 --- a/src/main/webapp/WEB-INF/jsp/site.jsp	Sat May 30 18:12:38 2020 +0200
    36.2 +++ b/src/main/webapp/WEB-INF/jsp/site.jsp	Mon Jun 01 14:46:58 2020 +0200
    36.3 @@ -26,7 +26,6 @@
    36.4  --%>
    36.5  <%@page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" %>
    36.6  <%@page import="de.uapcore.lightpit.Constants" %>
    36.7 -<%@page import="de.uapcore.lightpit.modules.ProjectsModule" %>
    36.8  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    36.9  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   36.10  <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
   36.11 @@ -60,9 +59,6 @@
   36.12      <fmt:setLocale scope="request" value="${sessionScope[Constants.SESSION_ATTR_LANGUAGE]}"/>
   36.13  </c:if>
   36.14  
   36.15 -<%-- Selected project, if any --%>
   36.16 -<c:set scope="page" var="selectedProject" value="${sessionScope[ProjectsModule.SESSION_ATTR_SELECTED_PROJECT]}"/>
   36.17 -
   36.18  <%-- Load resource bundles --%>
   36.19  <fmt:setBundle scope="request" basename="${bundleName}"/>
   36.20  <fmt:setBundle scope="request" var="lightpit_bundle" basename="localization.lightpit"/>
    37.1 --- a/src/main/webapp/WEB-INF/jsp/user-form.jsp	Sat May 30 18:12:38 2020 +0200
    37.2 +++ b/src/main/webapp/WEB-INF/jsp/user-form.jsp	Mon Jun 01 14:46:58 2020 +0200
    37.3 @@ -28,7 +28,8 @@
    37.4  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    37.5  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    37.6  
    37.7 -<jsp:useBean id="user" type="de.uapcore.lightpit.entities.User" scope="request"/>
    37.8 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.UsersEditView" scope="request"/>
    37.9 +<c:set var="user" scope="page" value="${viewmodel.user}" />
   37.10  
   37.11  <form action="./teams/commit" method="post">
   37.12      <table class="formtable">
    38.1 --- a/src/main/webapp/WEB-INF/jsp/users.jsp	Sat May 30 18:12:38 2020 +0200
    38.2 +++ b/src/main/webapp/WEB-INF/jsp/users.jsp	Mon Jun 01 14:46:58 2020 +0200
    38.3 @@ -28,9 +28,9 @@
    38.4  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    38.5  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    38.6  
    38.7 -<jsp:useBean id="users" type="java.util.List<de.uapcore.lightpit.entities.User>" scope="request"/>
    38.8 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.UsersView" scope="request"/>
    38.9  
   38.10 -<c:if test="${empty users}">
   38.11 +<c:if test="${empty viewmodel.users}">
   38.12      <div class="info-box">
   38.13          <fmt:message key="no-users"/>
   38.14      </div>
   38.15 @@ -40,7 +40,7 @@
   38.16      <a href="./teams/edit" class="button"><fmt:message key="button.create"/></a>
   38.17  </div>
   38.18  
   38.19 -<c:if test="${not empty users}">
   38.20 +<c:if test="${not empty viewmodel.users}">
   38.21      <table class="datatable medskip">
   38.22          <thead>
   38.23          <tr>
   38.24 @@ -49,7 +49,7 @@
   38.25          </tr>
   38.26          </thead>
   38.27          <tbody>
   38.28 -        <c:forEach var="user" items="${users}">
   38.29 +        <c:forEach var="user" items="${viewmodel.users}">
   38.30              <tr>
   38.31                  <td><a href="./teams/edit?id=${user.id}">&#x270e;</a></td>
   38.32                  <td><c:out value="${user.displayname}"/></td>
    39.1 --- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sat May 30 18:12:38 2020 +0200
    39.2 +++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Mon Jun 01 14:46:58 2020 +0200
    39.3 @@ -28,14 +28,8 @@
    39.4  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    39.5  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    39.6  
    39.7 -<jsp:useBean id="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request" />
    39.8 -<jsp:useBean id="version" type="de.uapcore.lightpit.entities.Version" scope="request"/>
    39.9 -<jsp:useBean id="versionStatusEnum" type="de.uapcore.lightpit.entities.VersionStatus[]" scope="request"/>
   39.10 -
   39.11 -<jsp:useBean id="statsAffected" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/>
   39.12 -<jsp:useBean id="statsScheduled" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/>
   39.13 -<jsp:useBean id="statsResolved" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/>
   39.14 -<jsp:useBean id="statsHideZeros" type="java.lang.Boolean" scope="request"/>
   39.15 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.VersionEditView" scope="request" />
   39.16 +<c:set var="version" scope="page" value="${viewmodel.version}"/>
   39.17  
   39.18  <form action="./projects/versions/commit" method="post">
   39.19      <table class="formtable" style="width: 35ch">
   39.20 @@ -47,19 +41,21 @@
   39.21          <tr>
   39.22              <th><fmt:message key="version.project"/></th>
   39.23              <td>
   39.24 -                <c:if test="${version.project.id ge 0}">
   39.25 -                    <c:out value="${version.project.name}" />
   39.26 -                    <input type="hidden" name="pid" value="${version.project.id}" />
   39.27 -                </c:if>
   39.28 -                <c:if test="${empty version.project or version.project.id lt 0}">
   39.29 -                    <select name="pid" required>
   39.30 -                        <c:forEach var="project" items="${projects}">
   39.31 -                            <option value="${project.id}">
   39.32 -                                <c:out value="${project.name}" />
   39.33 -                            </option>
   39.34 -                        </c:forEach>
   39.35 -                    </select>
   39.36 -                </c:if>
   39.37 +                <c:choose>
   39.38 +                    <c:when test="${not empty version.project}">
   39.39 +                        <c:out value="${version.project.name}" />
   39.40 +                        <input type="hidden" name="pid" value="${version.project.id}" />
   39.41 +                    </c:when>
   39.42 +                    <c:otherwise>
   39.43 +                        <select name="pid" required>
   39.44 +                            <c:forEach var="project" items="${viewmodel.projects}">
   39.45 +                                <option value="${project.id}">
   39.46 +                                    <c:out value="${project.name}" />
   39.47 +                                </option>
   39.48 +                            </c:forEach>
   39.49 +                        </select>
   39.50 +                    </c:otherwise>
   39.51 +                </c:choose>
   39.52              </td>
   39.53          </tr>
   39.54          <tr>
   39.55 @@ -70,7 +66,7 @@
   39.56              <th><fmt:message key="version.status"/></th>
   39.57              <td>
   39.58                  <select name="status" required>
   39.59 -                    <c:forEach var="elem" items="${versionStatusEnum}">
   39.60 +                    <c:forEach var="elem" items="${viewmodel.versionStatus}">
   39.61                          <option
   39.62                                  <c:if test="${elem eq version.status}">selected</c:if> value="${elem}"><fmt:message
   39.63                                  key="version.status.${elem}"/></option>
   39.64 @@ -90,7 +86,7 @@
   39.65              <td colspan="2">
   39.66                  <input type="hidden" name="id" value="${version.id}"/>
   39.67                  <c:choose>
   39.68 -                    <c:when test="${not empty version.project and version.project.id ge 0}">
   39.69 +                    <c:when test="${not empty version.project}">
   39.70                          <c:set var="cancelUrl">./projects/view?pid=${version.project.id}</c:set>
   39.71                      </c:when>
   39.72                      <c:otherwise>
    40.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    40.2 +++ b/src/main/webapp/WEB-INF/jsp/version.jsp	Mon Jun 01 14:46:58 2020 +0200
    40.3 @@ -0,0 +1,65 @@
    40.4 +<%--
    40.5 +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
    40.6 +
    40.7 +Copyright 2018 Mike Becker. All rights reserved.
    40.8 +
    40.9 +Redistribution and use in source and binary forms, with or without
   40.10 +modification, are permitted provided that the following conditions are met:
   40.11 +
   40.12 +1. Redistributions of source code must retain the above copyright
   40.13 +notice, this list of conditions and the following disclaimer.
   40.14 +
   40.15 +2. Redistributions in binary form must reproduce the above copyright
   40.16 +notice, this list of conditions and the following disclaimer in the
   40.17 +documentation and/or other materials provided with the distribution.
   40.18 +
   40.19 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   40.20 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   40.21 +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   40.22 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   40.23 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   40.24 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   40.25 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   40.26 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   40.27 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   40.28 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   40.29 +--%>
   40.30 +<%@page pageEncoding="UTF-8" %>
   40.31 +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   40.32 +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   40.33 +
   40.34 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.VersionView" scope="request"/>
   40.35 +<c:set var="version" scope="page" value="${viewmodel.versionInfo.version}"/>
   40.36 +
   40.37 +<c:set var="project" scope="page" value="${version.project}"/>
   40.38 +<%@include file="../jspf/project-header.jsp"%>
   40.39 +
   40.40 +
   40.41 +<div id="tool-area">
   40.42 +    <a href="./projects/issues/edit?pid=${project.id}" class="button"><fmt:message key="button.issue.create"/></a>
   40.43 +</div>
   40.44 +
   40.45 +<h2>
   40.46 +<fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/>
   40.47 +<a href="./projects/versions/edit?vid=${version.id}">&#x270e;</a>
   40.48 +</h2>
   40.49 +
   40.50 +<h3><fmt:message key="issues.reported"/> </h3>
   40.51 +
   40.52 +<c:set var="summary" value="${viewmodel.versionInfo.reportedTotal}"/>
   40.53 +<c:set var="issues" value="${viewmodel.versionInfo.reported}"/>
   40.54 +<%@include file="../jspf/issue-summary.jsp"%>
   40.55 +<%@include file="../jspf/issue-list.jsp"%>
   40.56 +
   40.57 +<h3><fmt:message key="issues.scheduled"/> </h3>
   40.58 +<c:set var="summary" value="${viewmodel.versionInfo.scheduledTotal}"/>
   40.59 +<c:set var="issues" value="${viewmodel.versionInfo.scheduled}"/>
   40.60 +<%@include file="../jspf/issue-summary.jsp"%>
   40.61 +<%@include file="../jspf/issue-list.jsp"%>
   40.62 +
   40.63 +<h3><fmt:message key="issues.resolved"/> </h3>
   40.64 +<c:set var="summary" value="${viewmodel.versionInfo.resolvedTotal}"/>
   40.65 +<c:set var="issues" value="${viewmodel.versionInfo.resolved}"/>
   40.66 +<%@include file="../jspf/issue-summary.jsp"%>
   40.67 +<%@include file="../jspf/issue-list.jsp"%>
   40.68 +
    41.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    41.2 +++ b/src/main/webapp/WEB-INF/jspf/issue-list.jsp	Mon Jun 01 14:46:58 2020 +0200
    41.3 @@ -0,0 +1,53 @@
    41.4 +<%--
    41.5 +issues: List<Issue>
    41.6 +--%>
    41.7 +
    41.8 +<table class="fullwidth datatable medskip">
    41.9 +    <thead>
   41.10 +    <tr>
   41.11 +        <th><fmt:message key="issue.subject"/></th>
   41.12 +        <th><fmt:message key="issue.assignee"/></th>
   41.13 +        <th><fmt:message key="issue.category"/></th>
   41.14 +        <th><fmt:message key="issue.status"/></th>
   41.15 +        <th><fmt:message key="issue.created"/></th>
   41.16 +        <th><fmt:message key="issue.updated"/></th>
   41.17 +        <th><fmt:message key="issue.eta"/></th>
   41.18 +    </tr>
   41.19 +    </thead>
   41.20 +    <tbody>
   41.21 +    <c:forEach var="issue" items="${issues}">
   41.22 +        <tr>
   41.23 +            <td>
   41.24 +                <span class="phase-${issue.status.phase}">
   41.25 +                    <a href="./projects/issues/edit?issue=${issue.id}">
   41.26 +                        <c:out value="${issue.subject}" />
   41.27 +                    </a>
   41.28 +                </span>
   41.29 +            </td>
   41.30 +            <td>
   41.31 +                <c:if test="${not empty issue.assignee}">
   41.32 +                    <c:out value="${issue.assignee.shortDisplayname}" />
   41.33 +                </c:if>
   41.34 +                <c:if test="${empty issue.assignee}">
   41.35 +                    <fmt:message key="placeholder.null-assignee" />
   41.36 +                </c:if>
   41.37 +            </td>
   41.38 +            <td>
   41.39 +                <fmt:message key="issue.category.${issue.category}" />
   41.40 +            </td>
   41.41 +            <td>
   41.42 +                <fmt:message key="issue.status.${issue.status}" />
   41.43 +            </td>
   41.44 +            <td>
   41.45 +                <fmt:formatDate value="${issue.created}" type="BOTH"/>
   41.46 +            </td>
   41.47 +            <td>
   41.48 +                <fmt:formatDate value="${issue.updated}" type="BOTH"/>
   41.49 +            </td>
   41.50 +            <td>
   41.51 +                <fmt:formatDate value="${issue.eta}" />
   41.52 +            </td>
   41.53 +        </tr>
   41.54 +    </c:forEach>
   41.55 +    </tbody>
   41.56 +</table>
    42.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    42.2 +++ b/src/main/webapp/WEB-INF/jspf/issue-summary.jsp	Mon Jun 01 14:46:58 2020 +0200
    42.3 @@ -0,0 +1,20 @@
    42.4 +<%--
    42.5 +summary: IssueSummary
    42.6 +--%>
    42.7 +
    42.8 +<div class="issue-summary">
    42.9 +    <div class="row">
   42.10 +        <div class="caption"><fmt:message key="issues.open"/>:</div>
   42.11 +        <div><c:out value="${summary.open}"/></div>
   42.12 +        <div class="caption"><fmt:message key="issues.active"/>:</div>
   42.13 +        <div><c:out value="${summary.active}"/></div>
   42.14 +        <div class="caption"><fmt:message key="issues.done"/>:</div>
   42.15 +        <div><c:out value="${summary.done}"/></div>
   42.16 +    </div>
   42.17 +</div>
   42.18 +
   42.19 +<div class="issue-progress-bar">
   42.20 +    <div class="open" style="width: ${summary.openPercent}%"></div>
   42.21 +    <div class="active" style="width: ${summary.activePercent}%"></div>
   42.22 +    <div class="done" style="width: ${summary.donePercent}%"></div>
   42.23 +</div>
   42.24 \ No newline at end of file
    43.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    43.2 +++ b/src/main/webapp/WEB-INF/jspf/project-header.jsp	Mon Jun 01 14:46:58 2020 +0200
    43.3 @@ -0,0 +1,24 @@
    43.4 +<%--
    43.5 +project: Project
    43.6 +--%>
    43.7 +<div class="project-attributes">
    43.8 +    <div class="row">
    43.9 +        <div class="caption"><fmt:message key="name"/>:</div>
   43.10 +        <div><c:out value="${project.name}"/></div>
   43.11 +        <div class="caption"><fmt:message key="description"/>:</div>
   43.12 +        <div><c:out value="${project.description}"/></div>
   43.13 +    </div>
   43.14 +    <div class="row">
   43.15 +        <div class="caption"><fmt:message key="owner"/>:</div>
   43.16 +        <div>
   43.17 +            <c:if test="${not empty project.owner}"><c:out value="${project.owner.displayname}"/></c:if>
   43.18 +        </div>
   43.19 +        <div class="caption"><fmt:message key="repoUrl"/>:</div>
   43.20 +        <div>
   43.21 +            <c:if test="${not empty project.repoUrl}">
   43.22 +                <a target="_blank" href="<c:out value="${project.repoUrl}"/>"><c:out
   43.23 +                        value="${project.repoUrl}"/></a>
   43.24 +            </c:if>
   43.25 +        </div>
   43.26 +    </div>
   43.27 +</div>
    44.1 --- a/src/main/webapp/WEB-INF/jspf/version-stats.jsp	Sat May 30 18:12:38 2020 +0200
    44.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    44.3 @@ -1,54 +0,0 @@
    44.4 -<%@ taglib uri = "http://java.sun.com/jsp/jstl/functions" prefix = "fn" %>
    44.5 -
    44.6 -<table class="datatable">
    44.7 -    <c:if test="${statsHideZeros}">
    44.8 -        <c:set var="visibleColumns" value="0"/>
    44.9 -        <c:forEach var="idx" begin="0" end="${fn:length(issueStatusEnum)-1}">
   44.10 -            <c:set var="visibleColumns" value="${visibleColumns + (stats.columnTotals[idx] eq 0 ? 0 :1)}"/>
   44.11 -        </c:forEach>
   44.12 -    </c:if>
   44.13 -    <c:if test="${not statsHideZeros}">
   44.14 -        <c:set var="visibleColumns" value="${fn:length(issueStatusEnum)}" />
   44.15 -    </c:if>
   44.16 -    <c:set var="colwidth"><fmt:formatNumber value="${100/(visibleColumns+2)}" maxFractionDigits="0" /></c:set>
   44.17 -    <colgroup>
   44.18 -        <c:forEach var="idx" begin="1" end="${visibleColumns+2}">
   44.19 -        <col width="${colwidth}%">
   44.20 -        </c:forEach>
   44.21 -    </colgroup>
   44.22 -    <thead>
   44.23 -        <tr>
   44.24 -            <th></th>
   44.25 -            <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter">
   44.26 -                <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}">
   44.27 -                <th class="hcenter"><fmt:message key="issue.status.${issueStatus}"/></th>
   44.28 -                </c:if>
   44.29 -            </c:forEach>
   44.30 -            <th class="hcenter"><fmt:message key="version.statistics.total"/> </th>
   44.31 -        </tr>
   44.32 -    </thead>
   44.33 -    <tbody>
   44.34 -    <c:forEach var="issueCategory" items="${issueCategoryEnum}" varStatus="categoryIter">
   44.35 -        <c:if test="${not statsHideZeros or stats.rowTotals[categoryIter.index] gt 0}">
   44.36 -        <tr>
   44.37 -        <th><fmt:message key="issue.category.${issueCategory}" /></th>
   44.38 -        <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter">
   44.39 -            <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}">
   44.40 -            <td>${stats.issueCount[categoryIter.index][statusIter.index]}</td>
   44.41 -            </c:if>
   44.42 -        </c:forEach>
   44.43 -        <td>${stats.rowTotals[categoryIter.index]}</td>
   44.44 -        </tr>
   44.45 -        </c:if>
   44.46 -    </c:forEach>
   44.47 -    <tr>
   44.48 -        <th><fmt:message key="version.statistics.total"/> </th>
   44.49 -        <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter">
   44.50 -            <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}">
   44.51 -            <td>${stats.columnTotals[statusIter.index]}</td>
   44.52 -            </c:if>
   44.53 -        </c:forEach>
   44.54 -        <td>${stats.total}</td>
   44.55 -    </tr>
   44.56 -    </tbody>
   44.57 -</table>
   44.58 \ No newline at end of file
    45.1 --- a/src/main/webapp/lightpit.css	Sat May 30 18:12:38 2020 +0200
    45.2 +++ b/src/main/webapp/lightpit.css	Mon Jun 01 14:46:58 2020 +0200
    45.3 @@ -117,7 +117,6 @@
    45.4  }
    45.5  
    45.6  table.datatable {
    45.7 -    width: auto;
    45.8      border-style: solid;
    45.9      border-width: 1pt;
   45.10      border-color: silver;
    46.1 --- a/src/main/webapp/projects.css	Sat May 30 18:12:38 2020 +0200
    46.2 +++ b/src/main/webapp/projects.css	Mon Jun 01 14:46:58 2020 +0200
    46.3 @@ -27,21 +27,12 @@
    46.4   *
    46.5   */
    46.6  
    46.7 -#issue-list td {
    46.8 -    white-space: nowrap;
    46.9 +.project-attributes, .issue-summary {
   46.10 +    display: table;
   46.11  }
   46.12  
   46.13 -#version-stats h2 {
   46.14 -    white-space: nowrap;
   46.15 -}
   46.16 -
   46.17 -#version-stats td {
   46.18 -    text-align: right;
   46.19 -}
   46.20 -
   46.21 -#project-attributes {
   46.22 +.project-attributes {
   46.23      margin-bottom: 2em;
   46.24 -    display: table;
   46.25  }
   46.26  
   46.27  .row {
   46.28 @@ -63,3 +54,28 @@
   46.29  span.phase-2 {
   46.30      text-decoration: line-through;
   46.31  }
   46.32 +
   46.33 +.issue-progress-bar {
   46.34 +    display: flex;
   46.35 +    position: relative;
   46.36 +    width: 100ex;
   46.37 +    height: 2em;
   46.38 +    border-style: inset;
   46.39 +    border-width: 2pt;
   46.40 +    border-color: #6060cc;
   46.41 +}
   46.42 +
   46.43 +.issue-progress-bar .open {
   46.44 +    height: 100%;
   46.45 +    background: steelblue;
   46.46 +}
   46.47 +
   46.48 +.issue-progress-bar .active {
   46.49 +    height: 100%;
   46.50 +    background: gold;
   46.51 +}
   46.52 +
   46.53 +.issue-progress-bar .done {
   46.54 +    height: 100%;
   46.55 +    background: green;
   46.56 +}

mercurial