cleanup ProjectsModule

Sun, 23 Aug 2020 15:10:49 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 23 Aug 2020 15:10:49 +0200
changeset 99
a369fb1b3aa2
parent 98
5c406eef0e5c
child 100
7e3c61c340d3

cleanup ProjectsModule

src/main/java/de/uapcore/lightpit/modules/ProjectsModule.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/ProjectDetails.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetailsView.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/ProjectView.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/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/project-details.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-form.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/version-form.jsp file | annotate | diff | comparison | revisions
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sun Aug 23 15:10:49 2020 +0200
@@ -39,14 +39,13 @@
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
 import java.io.IOException;
 import java.sql.Date;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.NoSuchElementException;
-import java.util.Objects;
+import java.util.Optional;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -60,178 +59,43 @@
 
     private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class);
 
-    public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project");
-    public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected_issue");
-    public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version");
-
-    // TODO: try to get rid of this shit
-    private class SessionSelection {
-        final HttpSession session;
-        final HttpServletRequest req;
-        final DataAccessObjects dao;
-        Project project;
-        Version version;
-        Issue issue;
-
-        SessionSelection(HttpServletRequest req, DataAccessObjects dao) {
-            this.req = req;
-            this.dao = dao;
-            session = req.getSession();
-        }
-
-        void newProject() {
-            project = null;
-            version = null;
-            issue = null;
-            updateAttributes();
-            project = new Project(-1);
-            updateAttributes();
-        }
-
-        void newVersion() throws SQLException {
-            project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
-            syncProject();
-            version = null;
-            issue = null;
-            updateAttributes();
-            version = new Version(-1);
-            version.setProject(project);
-            updateAttributes();
-        }
-
-        void newIssue() throws SQLException {
-            project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
-            syncProject();
-            version = null;
-            issue = null;
-            updateAttributes();
-            issue = new Issue(-1);
-            issue.setProject(project);
-            updateAttributes();
-        }
-
-        void selectVersion(Version selectedVersion) throws SQLException {
-            issue = null;
-            version = selectedVersion;
-            if (!version.getProject().equals(project)) {
-                project = dao.getProjectDao().find(version.getProject().getId());
-            }
-            // our object contains more details
-            version.setProject(project);
-            updateAttributes();
-        }
-
-        void selectIssue(Issue selectedIssue) throws SQLException {
-            issue = selectedIssue;
-            if (!issue.getProject().equals(project)) {
-                project = dao.getProjectDao().find(issue.getProject().getId());
-            }
-            // our object contains more details
-            issue.setProject(project);
-            if (!issue.getResolvedVersions().contains(version)
-                    && !issue.getAffectedVersions().contains(version)) {
-                version = null;
-            }
-            updateAttributes();
-        }
-
-        void syncProject() throws SQLException {
-            final var projectSelection = getParameter(req, Integer.class, "pid");
-            if (projectSelection.isPresent()) {
-                final var selectedProject = dao.getProjectDao().find(projectSelection.get());
-                if (!Objects.equals(selectedProject, project)) {
-                    // reset version and issue if project changed
-                    version = null;
-                    issue = null;
-                }
-                project = selectedProject;
-            } else {
-                project = project == null ? null : dao.getProjectDao().find(project.getId());
-            }
-        }
-
-        void syncVersion() throws SQLException {
-            final var versionSelection = getParameter(req, Integer.class, "vid");
-            if (versionSelection.isPresent()) {
-                if (versionSelection.get() < 0) {
-                    version = null;
-                } else {
-                    final var selectedVersion = dao.getVersionDao().find(versionSelection.get());
-                    if (!Objects.equals(selectedVersion, version)) {
-                        issue = null;
-                    }
-                    selectVersion(selectedVersion);
-                }
-            } else {
-                version = version == null ? null : dao.getVersionDao().find(version.getId());
-            }
-        }
-
-        void syncIssue() throws SQLException {
-            final var issueSelection = getParameter(req, Integer.class, "issue");
-            if (issueSelection.isPresent()) {
-                if (issueSelection.get() < 0) {
-                    issue = null;
-                } else {
-                    final var selectedIssue = dao.getIssueDao().find(issueSelection.get());
-                    dao.getIssueDao().joinVersionInformation(selectedIssue);
-                    selectIssue(selectedIssue);
-                }
-            } else {
-                issue = issue == null ? null : dao.getIssueDao().find(issue.getId());
-            }
-        }
-
-        void sync() throws SQLException {
-            project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
-            version = (Version) session.getAttribute(SESSION_ATTR_SELECTED_VERSION);
-            issue = (Issue) session.getAttribute(SESSION_ATTR_SELECTED_ISSUE);
-
-            syncProject();
-            syncVersion();
-            syncIssue();
-
-            updateAttributes();
-        }
-
-        private void updateAttributes() {
-            session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, project);
-            session.setAttribute(SESSION_ATTR_SELECTED_VERSION, version);
-            session.setAttribute(SESSION_ATTR_SELECTED_ISSUE, issue);
-        }
-    }
+    private static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project");
+    private static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version");
+    private static final String PARAMETER_SELECTED_PROJECT = "pid";
+    private static final String PARAMETER_SELECTED_VERSION = "vid";
 
     @Override
     protected String getResourceBundleName() {
         return "localization.projects";
     }
 
-    private String queryParams(Project p, Version v, Issue i) {
-        return String.format("pid=%d&vid=%d&issue=%d",
+    private String queryParams(Project p, Version v) {
+        return String.format("pid=%d&vid=%d",
                 p == null ? -1 : p.getId(),
-                v == null ? -1 : v.getId(),
-                i == null ? -1 : i.getId()
+                v == null ? -1 : v.getId()
         );
     }
 
     /**
      * Creates the navigation menu.
      *
-     * @param projects  the list of projects
-     * @param selection the currently selected objects
-     * @param projInfo  info about the currently selected project or null
-     * @return a dynamic navigation menu trying to display as many levels as possible
+     * @param req the servlet request
+     * @param viewModel the current view model
      */
-    private List<MenuEntry> getNavMenu(List<Project> projects, SessionSelection selection, ProjectInfo projInfo) {
+    private void setNavigationMenu(HttpServletRequest req, ProjectView viewModel) {
+        final Project selectedProject = Optional.ofNullable(viewModel.getProjectInfo()).map(ProjectInfo::getProject).orElse(null);
+
         final var navigation = new ArrayList<MenuEntry>();
 
-        for (Project proj : projects) {
+        for (ProjectInfo plistInfo : viewModel.getProjectList()) {
+            final var proj = plistInfo.getProject();
             final var projEntry = new MenuEntry(
                     proj.getName(),
-                    "projects/view?pid=" + proj.getId()
+                    "projects/view?" + queryParams(proj, null)
             );
             navigation.add(projEntry);
-            if (proj.equals(selection.project)) {
+            if (proj.equals(selectedProject)) {
+                final var projInfo = viewModel.getProjectInfo();
                 projEntry.setActive(true);
 
                 // ****************
@@ -239,8 +103,8 @@
                 // ****************
                 {
                     final var entry = new MenuEntry(1,
-                            new ResourceKey("localization.projects", "menu.versions"),
-                            "projects/view?" + queryParams(proj, null, null)
+                            new ResourceKey(getResourceBundleName(), "menu.versions"),
+                            "projects/view?" + queryParams(proj, null)
                     );
                     navigation.add(entry);
                 }
@@ -248,19 +112,19 @@
                 final var level2 = new ArrayList<MenuEntry>();
                 {
                     final var entry = new MenuEntry(
-                            new ResourceKey("localization.projects", "filter.all"),
-                            "projects/view?" + queryParams(proj, null, null)
+                            new ResourceKey(getResourceBundleName(), "filter.all"),
+                            "projects/view?" + queryParams(proj, null)
                     );
-                    if (selection.version == null) entry.setActive(true);
+                    if (viewModel.getVersionFilter() == null) entry.setActive(true);
                     level2.add(entry);
                 }
 
                 for (Version version : projInfo.getVersions()) {
                     final var entry = new MenuEntry(
                             version.getName(),
-                            "projects/versions/view?" + queryParams(proj, version, null)
+                            "projects/view?" + queryParams(proj, version)
                     );
-                    if (version.equals(selection.version)) entry.setActive(true);
+                    if (version.equals(viewModel.getVersionFilter())) entry.setActive(true);
                     level2.add(entry);
                 }
 
@@ -269,69 +133,85 @@
             }
         }
 
-        return navigation;
+        setNavigationMenu(req, navigation);
+    }
+
+    private int syncParamWithSession(HttpServletRequest req, String param, String attr) {
+        final var session = req.getSession();
+        final var idParam = getParameter(req, Integer.class, param);
+        final int id;
+        if (idParam.isPresent()) {
+            id = idParam.get();
+            session.setAttribute(attr, id);
+        } else {
+            id = Optional.ofNullable(session.getAttribute(attr)).map(x->(Integer)x).orElse(-1);
+        }
+        return id;
+    }
+
+    private void populate(ProjectView viewModel, HttpServletRequest req, DataAccessObjects dao) throws SQLException {
+        final var projectDao = dao.getProjectDao();
+        final var versionDao = dao.getVersionDao();
+
+        projectDao.list().stream().map(ProjectInfo::new).forEach(viewModel.getProjectList()::add);
+
+        // Select Project
+        final int pid = syncParamWithSession(req, PARAMETER_SELECTED_PROJECT, SESSION_ATTR_SELECTED_PROJECT);
+        if (pid >= 0) {
+            final var project = projectDao.find(pid);
+            final var info = new ProjectInfo(project);
+            info.setVersions(versionDao.list(project));
+            info.setIssueSummary(projectDao.getIssueSummary(project));
+            viewModel.setProjectInfo(info);
+        }
+
+        // Select Version
+        final int vid = syncParamWithSession(req, PARAMETER_SELECTED_VERSION, SESSION_ATTR_SELECTED_VERSION);
+        if (vid >= 0) {
+            viewModel.setVersionFilter(versionDao.find(vid));
+        }
+    }
+
+    private ResponseType forwardView(HttpServletRequest req, ProjectView viewModel, String name) {
+        setViewModel(req, viewModel);
+        setContentPage(req, name);
+        setStylesheet(req, "projects");
+        setNavigationMenu(req, viewModel);
+        return ResponseType.HTML;
     }
 
     @RequestMapping(method = HttpMethod.GET)
     public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
-        final var sessionSelection = new SessionSelection(req, dao);
-        sessionSelection.sync();
+        final var viewModel = new ProjectView();
+        populate(viewModel, req, dao);
 
         final var projectDao = dao.getProjectDao();
         final var versionDao = dao.getVersionDao();
 
-        final var projectList = projectDao.list();
-
-        final var viewModel = new ProjectIndexView();
-        for (var project : projectList) {
-            final var info = new ProjectInfo(project);
-            info.setVersions(versionDao.list(project));
-            info.setIssueSummary(projectDao.getIssueSummary(project));
-            viewModel.getProjects().add(info);
+        for (var info : viewModel.getProjectList()) {
+            info.setVersions(versionDao.list(info.getProject()));
+            info.setIssueSummary(projectDao.getIssueSummary(info.getProject()));
         }
 
-        setViewModel(req, viewModel);
-        setContentPage(req, "projects");
-        setStylesheet(req, "projects");
-
-        setNavigationMenu(req, getNavMenu(projectList, sessionSelection, currentProjectInfo(dao, sessionSelection.project)));
-
-        return ResponseType.HTML;
+        return forwardView(req, viewModel, "projects");
     }
 
-    private ProjectInfo currentProjectInfo(DataAccessObjects dao, Project project) throws SQLException {
-        if (project == null) return null;
-        final var projectDao = dao.getProjectDao();
-        final var versionDao = dao.getVersionDao();
-
-        final var info = new ProjectInfo(project);
-        info.setVersions(versionDao.list(project));
-        info.setIssueSummary(projectDao.getIssueSummary(project));
-        return info;
-    }
-
-    private ProjectEditView configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
-        final var viewModel = new ProjectEditView();
-        viewModel.setProject(selection.project);
+    private void configure(ProjectEditView viewModel, Project project, DataAccessObjects dao) throws SQLException {
+        viewModel.setProject(project);
         viewModel.setUsers(dao.getUserDao().list());
-        setNavigationMenu(req, getNavMenu(dao.getProjectDao().list(), selection, currentProjectInfo(dao, selection.project)));
-        setViewModel(req, viewModel);
-        setContentPage(req, "project-form");
-        return viewModel;
     }
 
     @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
     public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
-        final var selection = new SessionSelection(req, dao);
-        if (getParameter(req, Integer.class, "pid").isEmpty()) {
-            selection.newProject();
-        } else {
-            selection.sync();
-        }
+        final var viewModel = new ProjectEditView();
+        populate(viewModel, req, dao);
 
-        configureEditForm(req, dao, selection);
+        final var project = Optional.ofNullable(viewModel.getProjectInfo())
+                .map(ProjectInfo::getProject)
+                .orElse(new Project(-1));
+        configure(viewModel, project, dao);
 
-        return ResponseType.HTML;
+        return forwardView(req, viewModel, "project-form");
     }
 
     @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
@@ -339,7 +219,7 @@
 
         Project project = new Project(-1);
         try {
-            project = new Project(getParameter(req, Integer.class, "id").orElseThrow());
+            project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
             project.setName(getParameter(req, String.class, "name").orElseThrow());
             getParameter(req, String.class, "description").ifPresent(project::setDescription);
             getParameter(req, String.class, "repoUrl").ifPresent(project::setRepoUrl);
@@ -349,99 +229,61 @@
 
             dao.getProjectDao().saveOrUpdate(project);
 
-            setRedirectLocation(req, "./projects/");
+            setRedirectLocation(req, "./projects/view?pid="+project.getId());
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
             LOG.debug("Successfully updated project {}", project.getName());
+
+            return ResponseType.HTML;
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
             LOG.warn("Form validation failure: {}", ex.getMessage());
             LOG.debug("Details:", ex);
-            final var selection = new SessionSelection(req, dao);
-            selection.project = project;
-            final var vm = configureEditForm(req, dao, selection);
-            vm.setErrorText(ex.getMessage()); // TODO: error text
+            final var viewModel = new ProjectEditView();
+            populate(viewModel, req, dao);
+            configure(viewModel, project, dao);
+            // TODO: error text
+            return forwardView(req, viewModel, "project-form");
         }
-
-        return ResponseType.HTML;
     }
 
     @RequestMapping(requestPath = "view", method = HttpMethod.GET)
     public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
-        final var selection = new SessionSelection(req, dao);
-        selection.sync();
+        final var viewModel = new ProjectDetailsView();
+        populate(viewModel, req, dao);
 
-        if (selection.project == null) {
+        if (viewModel.getProjectInfo() == null) {
             resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
             return ResponseType.NONE;
         }
 
-        final var projectDao = dao.getProjectDao();
-        final var versionDao = dao.getVersionDao();
         final var issueDao = dao.getIssueDao();
 
-        final var viewModel = new ProjectView(selection.project);
-        final var issues = issueDao.list(selection.project);
-        for (var issue : issues) issueDao.joinVersionInformation(issue);
-        viewModel.setIssues(issues);
-        // TODO: fix duplicated selection of versions (projectInfo also contains these infos)
-        viewModel.setVersions(versionDao.list(selection.project));
-        viewModel.updateVersionInfo();
-        setViewModel(req, viewModel);
-
-        setNavigationMenu(req, getNavMenu(projectDao.list(), selection, currentProjectInfo(dao, selection.project)));
-        setContentPage(req, "project-details");
-        setStylesheet(req, "projects");
-
-        return ResponseType.HTML;
-    }
-
-    @RequestMapping(requestPath = "versions/view", method = HttpMethod.GET)
-    public ResponseType viewVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
-        final var selection = new SessionSelection(req, dao);
-        selection.sync();
-        if (selection.version == null) {
-            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
-            return ResponseType.NONE;
-        }
+        final var project = viewModel.getProjectInfo().getProject();
 
-        final var projectDao = dao.getProjectDao();
-        final var issueDao = dao.getIssueDao();
-
-        final var viewModel = new VersionView(selection.version);
-        final var issues = issueDao.list(selection.version);
+        final var detailView = viewModel.getProjectDetails();
+        if (viewModel.getVersionFilter() != null) {
+            detailView.updateVersionInfo(List.of(viewModel.getVersionFilter()));
+        } else {
+            detailView.updateVersionInfo(viewModel.getProjectInfo().getVersions());
+        }
+        final var issues = issueDao.list(project);
         for (var issue : issues) issueDao.joinVersionInformation(issue);
-        viewModel.setIssues(issues);
-        setViewModel(req, viewModel);
-
-        setNavigationMenu(req, getNavMenu(projectDao.list(), selection, currentProjectInfo(dao, selection.project)));
-        setContentPage(req, "version");
-        setStylesheet(req, "projects");
+        detailView.setIssues(issues);
 
-        return ResponseType.HTML;
-    }
-
-    private VersionEditView configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
-        final var viewModel = new VersionEditView(selection.version);
-        if (selection.version.getProject() == null) {
-            viewModel.setProjects(dao.getProjectDao().list());
-        }
-        setViewModel(req, viewModel);
-        setContentPage(req, "version-form");
-        setNavigationMenu(req, getNavMenu(dao.getProjectDao().list(), selection, currentProjectInfo(dao, selection.project)));
-        return viewModel;
+        return forwardView(req, viewModel, "project-details");
     }
 
     @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET)
     public ResponseType editVersion(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
-        final var selection = new SessionSelection(req, dao);
-        if (getParameter(req, Integer.class, "vid").isEmpty()) {
-            selection.newVersion();
+        final var viewModel = new VersionEditView();
+        populate(viewModel, req, dao);
+
+        if (viewModel.getVersionFilter() == null) {
+            viewModel.setVersion(new Version(-1));
         } else {
-            selection.sync();
+            viewModel.setVersion(viewModel.getVersionFilter());
         }
 
-        configureEditVersionForm(req, dao, selection);
-
-        return ResponseType.HTML;
+        return forwardView(req, viewModel, "version-form");
     }
 
     @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST)
@@ -462,77 +304,43 @@
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
             LOG.warn("Form validation failure: {}", ex.getMessage());
             LOG.debug("Details:", ex);
-            final var selection = new SessionSelection(req, dao);
-            selection.selectVersion(version);
-            final var viewModel = configureEditVersionForm(req, dao, selection);
+            final var viewModel = new VersionEditView();
+            populate(viewModel, req, dao);
+            viewModel.setVersion(version);
             // TODO: set Error Text
+            return forwardView(req, viewModel, "version-form");
         }
 
         return ResponseType.HTML;
     }
 
-    private IssueEditView configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
-        final var viewModel = new IssueEditView(selection.issue);
-
-        if (selection.issue.getProject() == null) {
-            viewModel.setProjects(dao.getProjectDao().list());
-        } else {
-            viewModel.setVersions(dao.getVersionDao().list(selection.issue.getProject()));
-        }
+    private void configure(IssueEditView viewModel, Issue issue, DataAccessObjects dao) throws SQLException {
+        issue.setProject(viewModel.getProjectInfo().getProject());
+        viewModel.setIssue(issue);
+        viewModel.configureVersionSelectors(viewModel.getProjectInfo().getVersions());
         viewModel.setUsers(dao.getUserDao().list());
-        setViewModel(req, viewModel);
-
-        setContentPage(req, "issue-form");
-        setNavigationMenu(req, getNavMenu(dao.getProjectDao().list(), selection, currentProjectInfo(dao, selection.project)));
-        return viewModel;
-    }
-
-    @RequestMapping(requestPath = "issues/", method = HttpMethod.GET)
-    public ResponseType issues(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
-        final var selection = new SessionSelection(req, dao);
-        selection.sync();
-        if (selection.project == null) {
-            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
-            return ResponseType.NONE;
-        }
-
-        final var projectDao = dao.getProjectDao();
-        final var issueDao = dao.getIssueDao();
-
-        final var viewModel = new IssuesView();
-        viewModel.setProject(selection.project);
-        if (selection.version == null) {
-            viewModel.setIssues(issueDao.list(selection.project));
-        } else {
-            viewModel.setVersion(selection.version);
-            viewModel.setIssues(issueDao.list(selection.version));
-        }
-        setViewModel(req, viewModel);
-
-        setNavigationMenu(req, getNavMenu(projectDao.list(), selection, currentProjectInfo(dao, selection.project)));
-        setContentPage(req, "issues");
-        setStylesheet(req, "projects");
-
-        return ResponseType.HTML;
     }
 
     @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
     public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
-        final var selection = new SessionSelection(req, dao);
-        if (getParameter(req, Integer.class, "issue").isEmpty()) {
-            selection.newIssue();
+        final var viewModel = new IssueEditView();
+
+        final var issueParam = getParameter(req, Integer.class, "issue");
+        if (issueParam.isPresent()) {
+            final var issue = dao.getIssueDao().find(issueParam.get());
+            req.getSession().setAttribute(SESSION_ATTR_SELECTED_PROJECT, issue.getProject().getId());
+            populate(viewModel, req, dao);
+            configure(viewModel, issue, dao);
         } else {
-            selection.sync();
+            populate(viewModel, req, dao);
+            configure(viewModel, new Issue(-1), dao);
         }
 
-        configureEditIssueForm(req, dao, selection);
-
-        return ResponseType.HTML;
+        return forwardView(req, viewModel, "issue-form");
     }
 
     @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
     public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
-
         Issue issue = new Issue(-1);
         try {
             issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
@@ -560,16 +368,16 @@
             dao.getIssueDao().saveOrUpdate(issue);
 
             // specifying the issue parameter keeps the edited issue as menu item
-            setRedirectLocation(req, "./projects/issues/?issue=" + issue.getId());
+            setRedirectLocation(req, "./projects/view/?pid=" + issue.getProject().getId());
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
             // TODO: set request attribute with error text
             LOG.warn("Form validation failure: {}", ex.getMessage());
             LOG.debug("Details:", ex);
-            final var selection = new SessionSelection(req, dao);
-            selection.selectIssue(issue);
-            final var viewModel = configureEditIssueForm(req, dao, selection);
+            final var viewModel = new IssueEditView();
+            configure(viewModel, issue, dao);
             // TODO: set Error Text
+            return forwardView(req, viewModel, "issue-form");
         }
 
         return ResponseType.HTML;
--- a/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Sun Aug 23 15:10:49 2020 +0200
@@ -4,15 +4,15 @@
 
 import java.util.*;
 
-public class IssueEditView {
-    private final Issue issue;
+public class IssueEditView extends ProjectView {
+    private Issue issue;
 
     private List<Project> projects = Collections.emptyList();
     private Set<Version> versionsUpcoming = new HashSet<>();
     private Set<Version> versionsRecent = new HashSet<>();
     private List<User> users;
 
-    public IssueEditView(Issue issue) {
+    public void setIssue(Issue issue) {
         this.issue = issue;
     }
 
@@ -36,7 +36,7 @@
         return versionsRecent;
     }
 
-    public void setVersions(List<Version> versions) {
+    public void configureVersionSelectors(List<Version> versions) {
         versionsRecent.clear();
         versionsUpcoming.clear();
         // keep the current selection, if any
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetails.java	Sun Aug 23 15:10:49 2020 +0200
@@ -0,0 +1,57 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.Issue;
+import de.uapcore.lightpit.entities.IssueSummary;
+import de.uapcore.lightpit.entities.Version;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class ProjectDetails {
+
+    private List<VersionInfo> versionInfos = Collections.emptyList();
+
+    private List<Issue> issues = Collections.emptyList();
+    private List<Issue> issuesWithoutVersion;
+    private IssueSummary issuesWithoutVersionTotal;
+
+    public List<Issue> getIssues() {
+        return issues;
+    }
+
+    public void setIssues(List<Issue> issues) {
+        this.issues = issues;
+        issuesWithoutVersion = new ArrayList<>();
+        issuesWithoutVersionTotal = new IssueSummary();
+        for (Issue issue : issues) {
+            // we want to list all issues that do not have a target version
+            if (issue.getResolvedVersions().isEmpty()) {
+                issuesWithoutVersion.add(issue);
+                issuesWithoutVersionTotal.add(issue);
+            }
+        }
+    }
+
+    public void updateVersionInfo(Collection<Version> versions) {
+        versionInfos = new ArrayList<>();
+        for (Version version : versions) {
+            final var info = new VersionInfo(version);
+            info.collectIssues(issues);
+            versionInfos.add(info);
+        }
+    }
+
+    public List<Issue> getIssuesWithoutVersion() {
+        return issuesWithoutVersion;
+    }
+
+    public IssueSummary getIssuesWithoutVersionTotal() {
+        return issuesWithoutVersionTotal;
+    }
+
+    public List<VersionInfo> getVersionInfos() {
+        return versionInfos;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetailsView.java	Sun Aug 23 15:10:49 2020 +0200
@@ -0,0 +1,10 @@
+package de.uapcore.lightpit.viewmodel;
+
+public class ProjectDetailsView extends ProjectView {
+
+    private final ProjectDetails projectDetails = new ProjectDetails();
+
+    public ProjectDetails getProjectDetails() {
+        return projectDetails;
+    }
+}
--- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java	Sun Aug 23 15:10:49 2020 +0200
@@ -5,7 +5,7 @@
 
 import java.util.List;
 
-public class ProjectEditView {
+public class ProjectEditView extends ProjectView {
 
     private Project project;
     private List<User> users;
--- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectIndexView.java	Sat Aug 22 18:34:36 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-package de.uapcore.lightpit.viewmodel;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class ProjectIndexView {
-
-    private List<ProjectInfo> projects = new ArrayList<>();
-
-    public List<ProjectInfo> getProjects() {
-        return projects;
-    }
-
-    public void setProjects(List<ProjectInfo> projects) {
-        this.projects = projects;
-    }
-}
--- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java	Sun Aug 23 15:10:49 2020 +0200
@@ -1,82 +1,33 @@
 package de.uapcore.lightpit.viewmodel;
 
-import de.uapcore.lightpit.entities.Issue;
-import de.uapcore.lightpit.entities.IssueSummary;
-import de.uapcore.lightpit.entities.Project;
 import de.uapcore.lightpit.entities.Version;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 public class ProjectView {
 
-    private final Project project;
-    private List<Version> versions = Collections.emptyList();
-    private List<Issue> issues = Collections.emptyList();
+    private final List<ProjectInfo> projectList = new ArrayList<>();
+    private ProjectInfo projectInfo;
+    private Version versionFilter;
 
-    private IssueSummary issuesTotal;
-    private List<Issue> issuesWithoutVersion;
-    private IssueSummary issuesWithoutVersionTotal;
-    private List<VersionInfo> versionInfos = Collections.emptyList();
-
-    public ProjectView(Project project) {
-        this.project = project;
+    public List<ProjectInfo> getProjectList() {
+        return projectList;
     }
 
-    public Project getProject() {
-        return project;
-    }
-
-    public List<Issue> getIssues() {
-        return issues;
+    public ProjectInfo getProjectInfo() {
+        return projectInfo;
     }
 
-    public void setIssues(List<Issue> issues) {
-        this.issues = issues;
-        issuesTotal = new IssueSummary();
-        issuesWithoutVersion = new ArrayList<>();
-        issuesWithoutVersionTotal = new IssueSummary();
-        for (Issue issue : issues) {
-            issuesTotal.add(issue);
-            // we want to list all issues that do not have a target version
-            if (issue.getResolvedVersions().isEmpty()) {
-                issuesWithoutVersion.add(issue);
-                issuesWithoutVersionTotal.add(issue);
-            }
-        }
-    }
-
-    public List<Version> getVersions() {
-        return versions;
-    }
-
-    public void setVersions(List<Version> versions) {
-        this.versions = versions;
+    public void setProjectInfo(ProjectInfo projectInfo) {
+        this.projectInfo = projectInfo;
     }
 
-    public void updateVersionInfo() {
-        versionInfos = new ArrayList<>();
-        for (Version version : versions) {
-            final var info = new VersionInfo(version);
-            info.collectIssues(issues);
-            versionInfos.add(info);
-        }
-    }
-
-    public IssueSummary getIssuesTotal() {
-        return issuesTotal;
+    public Version getVersionFilter() {
+        return versionFilter;
     }
 
-    public List<Issue> getIssuesWithoutVersion() {
-        return issuesWithoutVersion;
-    }
-
-    public IssueSummary getIssuesWithoutVersionTotal() {
-        return issuesWithoutVersionTotal;
-    }
-
-    public List<VersionInfo> getVersionInfos() {
-        return versionInfos;
+    public void setVersionFilter(Version versionFilter) {
+        this.versionFilter = versionFilter;
     }
 }
--- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java	Sun Aug 23 15:10:49 2020 +0200
@@ -1,18 +1,13 @@
 package de.uapcore.lightpit.viewmodel;
 
-import de.uapcore.lightpit.entities.Project;
 import de.uapcore.lightpit.entities.Version;
 import de.uapcore.lightpit.entities.VersionStatus;
 
-import java.util.Collections;
-import java.util.List;
-
-public class VersionEditView {
-    private final Version version;
-    private List<Project> projects = Collections.emptyList();
+public class VersionEditView extends ProjectView {
+    private Version version;
     private String errorText;
 
-    public VersionEditView(Version version) {
+    public void setVersion(Version version) {
         this.version = version;
     }
 
@@ -20,14 +15,6 @@
         return version;
     }
 
-    public List<Project> getProjects() {
-        return projects;
-    }
-
-    public void setProjects(List<Project> projects) {
-        this.projects = projects;
-    }
-
     public VersionStatus[] getVersionStatus() {
         return VersionStatus.values();
     }
--- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionView.java	Sat Aug 22 18:34:36 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-package de.uapcore.lightpit.viewmodel;
-
-import de.uapcore.lightpit.entities.Issue;
-import de.uapcore.lightpit.entities.Version;
-
-import java.util.List;
-
-public class VersionView {
-
-    private final VersionInfo versionInfo;
-
-    public VersionView(Version version) {
-        this.versionInfo = new VersionInfo(version);
-    }
-
-    public VersionInfo getVersionInfo() {
-        return versionInfo;
-    }
-
-    public void setIssues(List<Issue> issues) {
-        versionInfo.collectIssues(issues);
-    }
-}
--- a/src/main/resources/localization/projects.properties	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/resources/localization/projects.properties	Sun Aug 23 15:10:49 2020 +0200
@@ -70,7 +70,7 @@
 version.status.Deprecated=Deprecated
 
 
-issue.without-version=Issues w/o Assigned Version
+issue.without-version=No Assigned Version
 issue.project=Project
 issue.subject=Subject
 issue.description=Description
--- a/src/main/resources/localization/projects_de.properties	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/resources/localization/projects_de.properties	Sun Aug 23 15:10:49 2020 +0200
@@ -69,7 +69,7 @@
 version.status.LTS=Langzeitsupport
 version.status.Deprecated=Veraltet
 
-issue.without-version=Vorg\u00e4nge ohne Version
+issue.without-version=Keine Version zugeordnet
 issue.project=Projekt
 issue.subject=Thema
 issue.description=Beschreibung
--- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Sun Aug 23 15:10:49 2020 +0200
@@ -148,7 +148,7 @@
                 <input type="hidden" name="id" value="${issue.id}"/>
                 <c:choose>
                     <c:when test="${not empty issue.project}">
-                        <c:set var="cancelUrl">./projects/issues/?pid=${issue.project.id}</c:set>
+                        <c:set var="cancelUrl">./projects/view?pid=${issue.project.id}</c:set>
                     </c:when>
                     <c:otherwise>
                         <c:set var="cancelUrl">./projects/</c:set>
--- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sun Aug 23 15:10:49 2020 +0200
@@ -28,9 +28,9 @@
 <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
 
-<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectView" scope="request" />
+<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectDetailsView" scope="request" />
 
-<c:set var="project" scope="page" value="${viewmodel.project}"/>
+<c:set var="project" scope="page" value="${viewmodel.projectInfo.project}"/>
 <%@include file="../jspf/project-header.jsp"%>
 
 <div id="tool-area">
@@ -40,27 +40,34 @@
 
 <h2><fmt:message key="progress" /></h2>
 
-<c:set var="summary" value="${viewmodel.issuesTotal}" />
+<c:set var="summary" value="${viewmodel.projectInfo.issueSummary}" />
 <%@include file="../jspf/issue-summary.jsp"%>
 
 <h2><fmt:message key="issue.without-version" /></h2>
 
-<c:set var="issues" value="${viewmodel.issuesWithoutVersion}"/>
-<c:set var="summary" value="${viewmodel.issuesWithoutVersionTotal}" />
+<c:set var="issues" value="${viewmodel.projectDetails.issuesWithoutVersion}"/>
+<c:set var="summary" value="${viewmodel.projectDetails.issuesWithoutVersionTotal}" />
 <%@include file="../jspf/issue-summary.jsp"%>
 <%@include file="../jspf/issue-list.jsp"%>
 
-<c:forEach var="versionInfo" items="${viewmodel.versionInfos}">
+<c:forEach var="versionInfo" items="${viewmodel.projectDetails.versionInfos}">
     <h2>
         <fmt:message key="version.label" /> <c:out value="${versionInfo.version.name}" /> - <fmt:message key="version.status.${versionInfo.version.status}"/>
-        (<a href="./projects/versions/view?vid=${versionInfo.version.id}"><fmt:message key="version.open" /></a>)
     </h2>
 
     <h3><fmt:message key="issues.resolved"/> </h3>
     <c:set var="summary" value="${versionInfo.resolvedTotal}"/>
-    <c:set var="issues" value="${versionInfo.resolved}"/>
     <%@include file="../jspf/issue-summary.jsp"%>
+    <c:set var="issues" value="${versionInfo.resolved}"/>
     <c:if test="${not empty issues}">
-    <%@include file="../jspf/issue-list.jsp"%>
+        <%@include file="../jspf/issue-list.jsp"%>
+    </c:if>
+
+    <h3><fmt:message key="issues.reported"/> </h3>
+    <c:set var="summary" value="${versionInfo.reportedTotal}"/>
+    <%@include file="../jspf/issue-summary.jsp"%>
+    <c:set var="issues" value="${versionInfo.reported}"/>
+    <c:if test="${not empty issues}">
+        <%@include file="../jspf/issue-list.jsp"%>
     </c:if>
 </c:forEach>
\ No newline at end of file
--- a/src/main/webapp/WEB-INF/jsp/project-form.jsp	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/project-form.jsp	Sun Aug 23 15:10:49 2020 +0200
@@ -67,7 +67,7 @@
         <tfoot>
         <tr>
             <td colspan="2">
-                <input type="hidden" name="id" value="${project.id}"/>
+                <input type="hidden" name="pid" value="${project.id}"/>
                 <a href="./projects/" class="button">
                     <fmt:message bundle="${lightpit_bundle}" key="button.cancel"/>
                 </a>
--- a/src/main/webapp/WEB-INF/jsp/projects.jsp	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/projects.jsp	Sun Aug 23 15:10:49 2020 +0200
@@ -28,9 +28,9 @@
 <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
 
-<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectIndexView" scope="request"/>
+<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectView" scope="request"/>
 
-<c:if test="${empty viewmodel.projects}">
+<c:if test="${empty viewmodel.projectList}">
     <div class="info-box">
         <fmt:message key="no-projects"/>
     </div>
@@ -40,7 +40,7 @@
     <a href="./projects/edit" class="button"><fmt:message key="button.create"/></a>
 </div>
 
-<c:if test="${not empty viewmodel.projects}">
+<c:if test="${not empty viewmodel.projectList}">
     <table id="project-list" class="datatable medskip">
         <colgroup>
             <col>
@@ -65,7 +65,7 @@
         </tr>
         </thead>
         <tbody>
-        <c:forEach var="projectInfo" items="${viewmodel.projects}">
+        <c:forEach var="projectInfo" items="${viewmodel.projectList}">
             <c:set var="project" scope="page" value="${projectInfo.project}"/>
             <tr class="nowrap">
                 <td style="width: 2em;"><a href="./projects/edit?pid=${project.id}">&#x270e;</a></td>
@@ -79,12 +79,12 @@
                 </td>
                 <td class="hright">
                     <c:if test="${not empty projectInfo.latestVersion}">
-                        <a href="./projects/versions/view?vid=${projectInfo.latestVersion.id}"><c:out value="${projectInfo.latestVersion.name}"/></a>
+                        <a href="./projects/view?pid=${project.id}&vid=${projectInfo.latestVersion.id}"><c:out value="${projectInfo.latestVersion.name}"/></a>
                     </c:if>
                 </td>
                 <td class="hright">
                     <c:if test="${not empty projectInfo.nextVersion}">
-                        <a href="./projects/versions/view?vid=${projectInfo.nextVersion.id}"><c:out value="${projectInfo.nextVersion.name}"/></a>
+                        <a href="./projects/view?pid=${project.id}&vid=${projectInfo.nextVersion.id}"><c:out value="${projectInfo.nextVersion.name}"/></a>
                     </c:if>
                 </td>
                 <td class="hright">${projectInfo.issueSummary.open}</td>
--- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sat Aug 22 18:34:36 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sun Aug 23 15:10:49 2020 +0200
@@ -41,21 +41,8 @@
         <tr>
             <th><fmt:message key="version.project"/></th>
             <td>
-                <c:choose>
-                    <c:when test="${not empty version.project}">
-                        <c:out value="${version.project.name}" />
-                        <input type="hidden" name="pid" value="${version.project.id}" />
-                    </c:when>
-                    <c:otherwise>
-                        <select name="pid" required>
-                            <c:forEach var="project" items="${viewmodel.projects}">
-                                <option value="${project.id}">
-                                    <c:out value="${project.name}" />
-                                </option>
-                            </c:forEach>
-                        </select>
-                    </c:otherwise>
-                </c:choose>
+                <c:out value="${version.project.name}" />
+                <input type="hidden" name="pid" value="${version.project.id}" />
             </td>
         </tr>
         <tr>
@@ -85,15 +72,7 @@
         <tr>
             <td colspan="2">
                 <input type="hidden" name="id" value="${version.id}"/>
-                <c:choose>
-                    <c:when test="${not empty version.project}">
-                        <c:set var="cancelUrl">./projects/view?pid=${version.project.id}</c:set>
-                    </c:when>
-                    <c:otherwise>
-                        <c:set var="cancelUrl">./projects/</c:set>
-                    </c:otherwise>
-                </c:choose>
-                <a href="${cancelUrl}" class="button">
+                <a href="./projects/view?pid=${version.project.id}" class="button">
                     <fmt:message bundle="${lightpit_bundle}" key="button.cancel"/>
                 </a>
                 <button type="submit"><fmt:message bundle="${lightpit_bundle}" key="button.okay"/></button>

mercurial