adds the ability to create and edit issues

Fri, 22 May 2020 21:23:57 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 22 May 2020 21:23:57 +0200
changeset 75
33b6843fdf8a
parent 74
91d1fc2a3a14
child 76
82f71fb1758a

adds the ability to create and edit issues

setup/postgres/psql_create_tables.sql file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/GenericDao.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/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/PGUserDao.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/User.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/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/UsersModule.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/user-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/version-form.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/setup/postgres/psql_create_tables.sql	Fri May 22 17:26:27 2020 +0200
     1.2 +++ b/setup/postgres/psql_create_tables.sql	Fri May 22 21:23:57 2020 +0200
     1.3 @@ -10,7 +10,7 @@
     1.4  );
     1.5  
     1.6  create table lpit_project (
     1.7 -    id              serial          primary key,
     1.8 +    projectid       serial          primary key,
     1.9      name            varchar(20)     not null unique,
    1.10      description     varchar(200),
    1.11      repoUrl         varchar(50),
    1.12 @@ -26,8 +26,8 @@
    1.13  );
    1.14  
    1.15  create table lpit_version (
    1.16 -    id              serial          primary key,
    1.17 -    project         integer         not null references lpit_project(id),
    1.18 +    versionid       serial          primary key,
    1.19 +    project         integer         not null references lpit_project(projectid),
    1.20      name            varchar(20)     not null,
    1.21      ordinal         integer         not null default 0,
    1.22      status          version_status  not null default 'Future'
    1.23 @@ -53,15 +53,34 @@
    1.24  );
    1.25  
    1.26  create table lpit_issue (
    1.27 -    id              serial          primary key,
    1.28 -    project         integer         not null references lpit_project(id),
    1.29 +    issueid         serial          primary key,
    1.30 +    project         integer         not null references lpit_project(projectid),
    1.31      status          issue_status    not null default 'InSpecification',
    1.32      category        issue_category  not null default 'Feature',
    1.33      subject         varchar(20)     not null,
    1.34      description     text,
    1.35 -    version_plan    integer         references lpit_version(id),
    1.36 -    version_done    integer         references lpit_version(id),
    1.37 +    assignee        integer         references lpit_user(userid),
    1.38      created         timestamp       with time zone not null default now(),
    1.39      updated         timestamp       with time zone not null default now(),
    1.40      eta             date
    1.41  );
    1.42 +
    1.43 +create table lpit_issue_affected_version (
    1.44 +    issueid         integer         references lpit_issue(issueid),
    1.45 +    versionid       integer         references lpit_version(versionid),
    1.46 +    primary key (issueid, versionid)
    1.47 +);
    1.48 +
    1.49 +create table lpit_issue_scheduled_version (
    1.50 +    issueid         integer         references lpit_issue(issueid),
    1.51 +    versionid       integer         references lpit_version(versionid),
    1.52 +    primary key (issueid, versionid)
    1.53 +);
    1.54 +
    1.55 +create table lpit_issue_resolved_version (
    1.56 +    issueid         integer         references lpit_issue(issueid),
    1.57 +    versionid       integer         references lpit_version(versionid),
    1.58 +    primary key (issueid, versionid)
    1.59 +);
    1.60 +
    1.61 +
     2.1 --- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Fri May 22 17:26:27 2020 +0200
     2.2 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Fri May 22 21:23:57 2020 +0200
     2.3 @@ -295,6 +295,13 @@
     2.4          final String paramValue = req.getParameter(name);
     2.5          if (paramValue == null) return Optional.empty();
     2.6          if (clazz.equals(String.class)) return Optional.of((T) paramValue);
     2.7 +        if (java.sql.Date.class.isAssignableFrom(clazz)) {
     2.8 +            try {
     2.9 +                return Optional.of((T)java.sql.Date.valueOf(paramValue));
    2.10 +            } catch (IllegalArgumentException ex) {
    2.11 +                return Optional.empty();
    2.12 +            }
    2.13 +        }
    2.14          try {
    2.15              final Constructor<T> ctor = clazz.getConstructor(String.class);
    2.16              return Optional.of(ctor.newInstance(paramValue));
     3.1 --- a/src/main/java/de/uapcore/lightpit/dao/GenericDao.java	Fri May 22 17:26:27 2020 +0200
     3.2 +++ b/src/main/java/de/uapcore/lightpit/dao/GenericDao.java	Fri May 22 21:23:57 2020 +0200
     3.3 @@ -34,6 +34,7 @@
     3.4  
     3.5      /**
     3.6       * Finds an entity by its integer ID.
     3.7 +     * It is not guaranteed that referenced entities are automatically joined.
     3.8       *
     3.9       * @param id the id
    3.10       * @return the enity or null if there is no such entity
    3.11 @@ -43,6 +44,7 @@
    3.12  
    3.13      /**
    3.14       * Inserts an instance into database.
    3.15 +     * It is not guaranteed that generated fields will be updated in the instance.
    3.16       *
    3.17       * @param instance the instance to insert
    3.18       * @throws SQLException on any kind of SQL errors
     4.1 --- a/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Fri May 22 17:26:27 2020 +0200
     4.2 +++ b/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Fri May 22 21:23:57 2020 +0200
     4.3 @@ -38,10 +38,31 @@
     4.4  
     4.5      /**
     4.6       * Lists all issues for the specified project.
     4.7 +     * This is not guaranteed to contain version information.
     4.8 +     * Use {@link #joinVersionInformation(Issue)} to obtain this information for a specific issue.
     4.9       *
    4.10       * @param project the project
    4.11       * @return a list of issues
    4.12       * @throws SQLException on any kind of SQL error
    4.13       */
    4.14      List<Issue> list(Project project) throws SQLException;
    4.15 +
    4.16 +    /**
    4.17 +     * Saves an instances to the database.
    4.18 +     * Implementations of this DAO must guarantee that the generated ID is stored in the instance.
    4.19 +     *
    4.20 +     * @param instance the instance to insert
    4.21 +     * @throws SQLException on any kind of SQL error
    4.22 +     * @see Issue#setId(int)
    4.23 +     */
    4.24 +    @Override
    4.25 +    void save(Issue instance) throws SQLException;
    4.26 +
    4.27 +    /**
    4.28 +     * Retrieves the affected, scheduled and resolved versions for the specified issue.
    4.29 +     *
    4.30 +     * @param issue the issue to join the information for
    4.31 +     * @throws SQLException on any kind of SQL error
    4.32 +     */
    4.33 +    void joinVersionInformation(Issue issue) throws SQLException;
    4.34  }
     5.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Fri May 22 17:26:27 2020 +0200
     5.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Fri May 22 21:23:57 2020 +0200
     5.3 @@ -43,63 +43,92 @@
     5.4  
     5.5  public final class PGIssueDao implements IssueDao {
     5.6  
     5.7 -    private final PreparedStatement insert, update, list, find;
     5.8 +    private final PreparedStatement insert, update, list, find, affectedVersions, scheduledVersions, resolvedVersions;
     5.9  
    5.10      public PGIssueDao(Connection connection) throws SQLException {
    5.11          list = connection.prepareStatement(
    5.12 -                "select issue.id, issue.project, issue.status, issue.category, issue.subject, issue.description, " +
    5.13 -                        "vplan.id, vplan.name, vdone.id, vdone.name, " +
    5.14 -                        "issue.created, issue.updated, issue.eta " +
    5.15 -                        "from lpit_issue issue " +
    5.16 -                        "left join lpit_version vplan on vplan.id = version_plan " +
    5.17 -                        "left join lpit_version vdone on vdone.id = version_done " +
    5.18 -                        "where issue.project = ? ");
    5.19 +                "select issueid, project, status, category, subject, description, " +
    5.20 +                        "userid, username, givenname, lastname, mail, " +
    5.21 +                        "created, updated, eta " +
    5.22 +                        "from lpit_issue " +
    5.23 +                        "left join lpit_user on userid = assignee " +
    5.24 +                        "where project = ? ");
    5.25  
    5.26          find = connection.prepareStatement(
    5.27 -                "select issue.id, issue.project, issue.status, issue.category, issue.subject, issue.description, " +
    5.28 -                        "vplan.id, vplan.name, vdone.id, vdone.name, " +
    5.29 -                        "issue.created, issue.updated, issue.eta " +
    5.30 -                        "from lpit_issue issue " +
    5.31 -                        "left join lpit_version vplan on vplan.id = version_plan " +
    5.32 -                        "left join lpit_version vdone on vdone.id = version_done " +
    5.33 -                        "where issue.id = ? ");
    5.34 +                "select issueid, project, status, category, subject, description, " +
    5.35 +                        "userid, username, givenname, lastname, mail, " +
    5.36 +                        "created, updated, eta " +
    5.37 +                        "from lpit_issue " +
    5.38 +                        "left join lpit_user on userid = assignee " +
    5.39 +                        "where issueid = ? ");
    5.40  
    5.41          insert = connection.prepareStatement(
    5.42 -                "insert into lpit_issue (project, status, category, subject, description, version_plan, version_done, eta) " +
    5.43 -                        "values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?)"
    5.44 +                "insert into lpit_issue (project, status, category, subject, description, assignee, eta) " +
    5.45 +                        "values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?) returning issueid"
    5.46          );
    5.47          update = connection.prepareStatement(
    5.48                  "update lpit_issue set updated = now(), status = ?::issue_status, category = ?::issue_category, " +
    5.49 -                        "subject = ?, description = ?, version_plan = ?, version_done = ?, eta = ? where id = ?"
    5.50 +                        "subject = ?, description = ?, assignee = ?, eta = ? where issueid = ?"
    5.51 +        );
    5.52 +
    5.53 +        affectedVersions = connection.prepareStatement(
    5.54 +                "select v.versionid, v.name, v.status, v.ordinal " +
    5.55 +                        "from lpit_version v join lpit_issue_affected_version using (versionid) " +
    5.56 +                        "where issueid = ? " +
    5.57 +                        "order by v.ordinal, v.name"
    5.58 +        );
    5.59 +
    5.60 +        scheduledVersions = connection.prepareStatement(
    5.61 +                "select v.versionid, v.name, v.status, v.ordinal " +
    5.62 +                        "from lpit_version v join lpit_issue_scheduled_version using (versionid) " +
    5.63 +                        "where issueid = ? " +
    5.64 +                        "order by v.ordinal, v.name"
    5.65 +        );
    5.66 +
    5.67 +        resolvedVersions = connection.prepareStatement(
    5.68 +                "select v.versionid, v.name, v.status, v.ordinal " +
    5.69 +                        "from lpit_version v join lpit_issue_resolved_version using (versionid) " +
    5.70 +                        "where issueid = ? " +
    5.71 +                        "order by v.ordinal, v.name"
    5.72          );
    5.73      }
    5.74  
    5.75 -    private Version obtainVersion(ResultSet result, Project project, String prefix) throws SQLException {
    5.76 -        final int vplan = result.getInt(prefix + "id");
    5.77 -        if (vplan > 0) {
    5.78 -            final var ver = new Version(vplan, project);
    5.79 -            ver.setName(result.getString(prefix + "name"));
    5.80 -            return ver;
    5.81 +    private User obtainAssignee(ResultSet result) throws SQLException {
    5.82 +        final int id = result.getInt("userid");
    5.83 +        if (id != 0) {
    5.84 +            final var user = new User(id);
    5.85 +            user.setUsername(result.getString("username"));
    5.86 +            user.setGivenname(result.getString("givenname"));
    5.87 +            user.setLastname(result.getString("lastname"));
    5.88 +            user.setMail(result.getString("mail"));
    5.89 +            return user;
    5.90          } else {
    5.91              return null;
    5.92          }
    5.93      }
    5.94  
    5.95 -    public Issue mapColumns(ResultSet result) throws SQLException {
    5.96 -        final var project = new Project(result.getInt("issue.project"));
    5.97 -        final var issue = new Issue(result.getInt("issue.id"), project);
    5.98 -        issue.setStatus(IssueStatus.valueOf(result.getString("issue.status")));
    5.99 -        issue.setCategory(IssueCategory.valueOf(result.getString("issue.category")));
   5.100 -        issue.setSubject(result.getString("issue.subject"));
   5.101 -        issue.setDescription(result.getString("issue.description"));
   5.102 -        issue.setScheduledVersion(obtainVersion(result, project, "vplan."));
   5.103 -        issue.setResolvedVersion(obtainVersion(result, project, "vdone."));
   5.104 -        issue.setCreated(result.getTimestamp("issue.created"));
   5.105 -        issue.setUpdated(result.getTimestamp("issue.updated"));
   5.106 -        issue.setEta(result.getDate("issue.eta"));
   5.107 +    private Issue mapColumns(ResultSet result) throws SQLException {
   5.108 +        final var project = new Project(result.getInt("project"));
   5.109 +        final var issue = new Issue(result.getInt("issueid"), project);
   5.110 +        issue.setStatus(IssueStatus.valueOf(result.getString("status")));
   5.111 +        issue.setCategory(IssueCategory.valueOf(result.getString("category")));
   5.112 +        issue.setSubject(result.getString("subject"));
   5.113 +        issue.setDescription(result.getString("description"));
   5.114 +        issue.setAssignee(obtainAssignee(result));
   5.115 +        issue.setCreated(result.getTimestamp("created"));
   5.116 +        issue.setUpdated(result.getTimestamp("updated"));
   5.117 +        issue.setEta(result.getDate("eta"));
   5.118          return issue;
   5.119      }
   5.120  
   5.121 +    private Version mapVersion(ResultSet result, Project project) throws SQLException {
   5.122 +        final var version = new Version(result.getInt("v.versionid"), project);
   5.123 +        version.setName(result.getString("v.name"));
   5.124 +        version.setOrdinal(result.getInt("v.ordinal"));
   5.125 +        version.setStatus(VersionStatus.valueOf(result.getString("v.status")));
   5.126 +        return version;
   5.127 +    }
   5.128 +
   5.129      @Override
   5.130      public void save(Issue instance) throws SQLException {
   5.131          Objects.requireNonNull(instance.getSubject());
   5.132 @@ -109,36 +138,38 @@
   5.133          insert.setString(3, instance.getCategory().name());
   5.134          insert.setString(4, instance.getSubject());
   5.135          setStringOrNull(insert, 5, instance.getDescription());
   5.136 -        setForeignKeyOrNull(insert, 6, instance.getScheduledVersion(), Version::getId);
   5.137 -        setForeignKeyOrNull(insert, 7, instance.getResolvedVersion(), Version::getId);
   5.138 -        setDateOrNull(insert, 8, instance.getEta());
   5.139 -        insert.executeUpdate();
   5.140 +        setForeignKeyOrNull(insert, 6, instance.getAssignee(), User::getId);
   5.141 +        setDateOrNull(insert, 7, instance.getEta());
   5.142 +        // insert and retrieve the ID
   5.143 +        final var rs = insert.executeQuery();
   5.144 +        rs.next();
   5.145 +        instance.setId(rs.getInt(1));
   5.146      }
   5.147  
   5.148      @Override
   5.149      public boolean update(Issue instance) throws SQLException {
   5.150 +        if (instance.getId() < 0) return false;
   5.151          Objects.requireNonNull(instance.getSubject());
   5.152          update.setString(1, instance.getStatus().name());
   5.153          update.setString(2, instance.getCategory().name());
   5.154          update.setString(3, instance.getSubject());
   5.155          setStringOrNull(update, 4, instance.getDescription());
   5.156 -        setForeignKeyOrNull(update, 5, instance.getScheduledVersion(), Version::getId);
   5.157 -        setForeignKeyOrNull(update, 6, instance.getResolvedVersion(), Version::getId);
   5.158 -        setDateOrNull(update, 7, instance.getEta());
   5.159 -        update.setInt(8, instance.getId());
   5.160 +        setForeignKeyOrNull(update, 5, instance.getAssignee(), User::getId);
   5.161 +        setDateOrNull(update, 6, instance.getEta());
   5.162 +        update.setInt(7, instance.getId());
   5.163          return update.executeUpdate() > 0;
   5.164      }
   5.165  
   5.166      @Override
   5.167      public List<Issue> list(Project project) throws SQLException {
   5.168          list.setInt(1, project.getId());
   5.169 -        List<Issue> versions = new ArrayList<>();
   5.170 +        List<Issue> issues = new ArrayList<>();
   5.171          try (var result = list.executeQuery()) {
   5.172              while (result.next()) {
   5.173 -                versions.add(mapColumns(result));
   5.174 +                issues.add(mapColumns(result));
   5.175              }
   5.176          }
   5.177 -        return versions;
   5.178 +        return issues;
   5.179      }
   5.180  
   5.181      @Override
   5.182 @@ -152,4 +183,23 @@
   5.183              }
   5.184          }
   5.185      }
   5.186 +
   5.187 +    private List<Version> listVersions(PreparedStatement stmt, Issue issue) throws SQLException {
   5.188 +        stmt.setInt(1, issue.getId());
   5.189 +        List<Version> versions = new ArrayList<>();
   5.190 +        try (var result = stmt.executeQuery()) {
   5.191 +            while (result.next()) {
   5.192 +                versions.add(mapVersion(result, issue.getProject()));
   5.193 +            }
   5.194 +        }
   5.195 +        return versions;
   5.196 +    }
   5.197 +
   5.198 +    @Override
   5.199 +    public void joinVersionInformation(Issue issue) throws SQLException {
   5.200 +        Objects.requireNonNull(issue.getProject());
   5.201 +        issue.setAffectedVersions(listVersions(affectedVersions, issue));
   5.202 +        issue.setScheduledVersions(listVersions(scheduledVersions, issue));
   5.203 +        issue.setResolvedVersions(listVersions(resolvedVersions, issue));
   5.204 +    }
   5.205  }
     6.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Fri May 22 17:26:27 2020 +0200
     6.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Fri May 22 21:23:57 2020 +0200
     6.3 @@ -49,29 +49,29 @@
     6.4  
     6.5      public PGProjectDao(Connection connection) throws SQLException {
     6.6          list = connection.prepareStatement(
     6.7 -                "select id, name, description, repourl, " +
     6.8 +                "select projectid, name, description, repourl, " +
     6.9                          "userid, username, lastname, givenname, mail " +
    6.10                          "from lpit_project " +
    6.11                          "left join lpit_user owner on lpit_project.owner = owner.userid " +
    6.12                          "order by name");
    6.13  
    6.14          find = connection.prepareStatement(
    6.15 -                "select id, name, description, repourl, " +
    6.16 +                "select projectid, name, description, repourl, " +
    6.17                          "userid, username, lastname, givenname, mail " +
    6.18                          "from lpit_project " +
    6.19                          "left join lpit_user owner on lpit_project.owner = owner.userid " +
    6.20 -                        "where id = ?");
    6.21 +                        "where projectid = ?");
    6.22  
    6.23          insert = connection.prepareStatement(
    6.24                  "insert into lpit_project (name, description, repourl, owner) values (?, ?, ?, ?)"
    6.25          );
    6.26          update = connection.prepareStatement(
    6.27 -                "update lpit_project set name = ?, description = ?, repourl = ?, owner = ? where id = ?"
    6.28 +                "update lpit_project set name = ?, description = ?, repourl = ?, owner = ? where projectid = ?"
    6.29          );
    6.30      }
    6.31  
    6.32      public Project mapColumns(ResultSet result) throws SQLException {
    6.33 -        final var proj = new Project(result.getInt("id"));
    6.34 +        final var proj = new Project(result.getInt("projectid"));
    6.35          proj.setName(result.getString("name"));
    6.36          proj.setDescription(result.getString("description"));
    6.37          proj.setRepoUrl(result.getString("repourl"));
    6.38 @@ -101,6 +101,7 @@
    6.39  
    6.40      @Override
    6.41      public boolean update(Project instance) throws SQLException {
    6.42 +        if (instance.getId() < 0) return false;
    6.43          Objects.requireNonNull(instance.getName());
    6.44          update.setString(1, instance.getName());
    6.45          setStringOrNull(update, 2, instance.getDescription());
     7.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java	Fri May 22 17:26:27 2020 +0200
     7.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java	Fri May 22 21:23:57 2020 +0200
     7.3 @@ -58,7 +58,7 @@
     7.4          update = connection.prepareStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?");
     7.5      }
     7.6  
     7.7 -    public User mapColumns(ResultSet result) throws SQLException {
     7.8 +    private User mapColumns(ResultSet result) throws SQLException {
     7.9          final int id = result.getInt("userid");
    7.10          if (id == 0) return null;
    7.11          final var user = new User(id);
    7.12 @@ -81,6 +81,7 @@
    7.13  
    7.14      @Override
    7.15      public boolean update(User instance) throws SQLException {
    7.16 +        if (instance.getId() < 0) return false;
    7.17          setStringOrNull(update, 1, instance.getLastname());
    7.18          setStringOrNull(update, 2, instance.getGivenname());
    7.19          setStringOrNull(update, 3, instance.getMail());
     8.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Fri May 22 17:26:27 2020 +0200
     8.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Fri May 22 21:23:57 2020 +0200
     8.3 @@ -47,26 +47,27 @@
     8.4  
     8.5      public PGVersionDao(Connection connection) throws SQLException {
     8.6          list = connection.prepareStatement(
     8.7 -                "select id, project, name, ordinal, status " +
     8.8 +                "select versionid, project, name, ordinal, status " +
     8.9                          "from lpit_version " +
    8.10                          "where project = ? " +
    8.11                          "order by ordinal, lower(name)");
    8.12  
    8.13          find = connection.prepareStatement(
    8.14 -                "select id, project, name, ordinal, status " +
    8.15 +                "select versionid, project, name, ordinal, status " +
    8.16                          "from lpit_version " +
    8.17 -                        "where id = ?");
    8.18 +                        "where versionid = ?");
    8.19  
    8.20          insert = connection.prepareStatement(
    8.21                  "insert into lpit_version (project, name, ordinal, status) values (?, ?, ?, ?::version_status)"
    8.22          );
    8.23          update = connection.prepareStatement(
    8.24 -                "update lpit_version set name = ?, ordinal = ?, status = ?::version_status where id = ?"
    8.25 +                "update lpit_version set name = ?, ordinal = ?, status = ?::version_status where versionid = ?"
    8.26          );
    8.27      }
    8.28  
    8.29 -    public Version mapColumns(ResultSet result) throws SQLException {
    8.30 -        final var version = new Version(result.getInt("id"), new Project(result.getInt("project")));
    8.31 +    private Version mapColumns(ResultSet result) throws SQLException {
    8.32 +        final var project = new Project(result.getInt("project"));
    8.33 +        final var version = new Version(result.getInt("versionid"), project);
    8.34          version.setName(result.getString("name"));
    8.35          version.setOrdinal(result.getInt("ordinal"));
    8.36          version.setStatus(VersionStatus.valueOf(result.getString("status")));
    8.37 @@ -86,6 +87,7 @@
    8.38  
    8.39      @Override
    8.40      public boolean update(Version instance) throws SQLException {
    8.41 +        if (instance.getId() < 0) return false;
    8.42          Objects.requireNonNull(instance.getName());
    8.43          update.setString(1, instance.getName());
    8.44          update.setInt(2, instance.getOrdinal());
    8.45 @@ -100,7 +102,9 @@
    8.46          List<Version> versions = new ArrayList<>();
    8.47          try (var result = list.executeQuery()) {
    8.48              while (result.next()) {
    8.49 -                versions.add(mapColumns(result));
    8.50 +                final var v = mapColumns(result);
    8.51 +                v.setProject(project);
    8.52 +                versions.add(v);
    8.53              }
    8.54          }
    8.55          return versions;
     9.1 --- a/src/main/java/de/uapcore/lightpit/entities/Issue.java	Fri May 22 17:26:27 2020 +0200
     9.2 +++ b/src/main/java/de/uapcore/lightpit/entities/Issue.java	Fri May 22 21:23:57 2020 +0200
     9.3 @@ -30,12 +30,14 @@
     9.4  
     9.5  import java.sql.Date;
     9.6  import java.sql.Timestamp;
     9.7 +import java.time.Instant;
     9.8 +import java.util.Collections;
     9.9  import java.util.List;
    9.10  import java.util.Objects;
    9.11  
    9.12  public final class Issue {
    9.13  
    9.14 -    private final int id;
    9.15 +    private int id;
    9.16      private final Project project;
    9.17  
    9.18      private IssueStatus status;
    9.19 @@ -43,13 +45,14 @@
    9.20  
    9.21      private String subject;
    9.22      private String description;
    9.23 +    private User assignee;
    9.24  
    9.25 -    private List<Version> affectedVersions;
    9.26 -    private Version scheduledVersion;
    9.27 -    private Version resolvedVersion;
    9.28 +    private List<Version> affectedVersions = Collections.emptyList();
    9.29 +    private List<Version> scheduledVersions = Collections.emptyList();
    9.30 +    private List<Version> resolvedVersions = Collections.emptyList();
    9.31  
    9.32 -    private Timestamp created;
    9.33 -    private Timestamp updated;
    9.34 +    private Timestamp created = Timestamp.from(Instant.now());
    9.35 +    private Timestamp updated = Timestamp.from(Instant.now());
    9.36      private Date eta;
    9.37  
    9.38      public Issue(int id, Project project) {
    9.39 @@ -61,6 +64,14 @@
    9.40          return id;
    9.41      }
    9.42  
    9.43 +    /**
    9.44 +     * Should only be used by a DAO to store the generated ID.
    9.45 +     * @param id the freshly generated ID returned from the database after insert
    9.46 +     */
    9.47 +    public void setId(int id) {
    9.48 +        this.id = id;
    9.49 +    }
    9.50 +
    9.51      public Project getProject() {
    9.52          return project;
    9.53      }
    9.54 @@ -97,6 +108,14 @@
    9.55          this.description = description;
    9.56      }
    9.57  
    9.58 +    public User getAssignee() {
    9.59 +        return assignee;
    9.60 +    }
    9.61 +
    9.62 +    public void setAssignee(User assignee) {
    9.63 +        this.assignee = assignee;
    9.64 +    }
    9.65 +
    9.66      public List<Version> getAffectedVersions() {
    9.67          return affectedVersions;
    9.68      }
    9.69 @@ -105,20 +124,20 @@
    9.70          this.affectedVersions = affectedVersions;
    9.71      }
    9.72  
    9.73 -    public Version getScheduledVersion() {
    9.74 -        return scheduledVersion;
    9.75 +    public List<Version> getScheduledVersions() {
    9.76 +        return scheduledVersions;
    9.77      }
    9.78  
    9.79 -    public void setScheduledVersion(Version scheduledVersion) {
    9.80 -        this.scheduledVersion = scheduledVersion;
    9.81 +    public void setScheduledVersions(List<Version> scheduledVersions) {
    9.82 +        this.scheduledVersions = scheduledVersions;
    9.83      }
    9.84  
    9.85 -    public Version getResolvedVersion() {
    9.86 -        return resolvedVersion;
    9.87 +    public List<Version> getResolvedVersions() {
    9.88 +        return resolvedVersions;
    9.89      }
    9.90  
    9.91 -    public void setResolvedVersion(Version resolvedVersion) {
    9.92 -        this.resolvedVersion = resolvedVersion;
    9.93 +    public void setResolvedVersions(List<Version> resolvedVersions) {
    9.94 +        this.resolvedVersions = resolvedVersions;
    9.95      }
    9.96  
    9.97      public Timestamp getCreated() {
    10.1 --- a/src/main/java/de/uapcore/lightpit/entities/User.java	Fri May 22 17:26:27 2020 +0200
    10.2 +++ b/src/main/java/de/uapcore/lightpit/entities/User.java	Fri May 22 21:23:57 2020 +0200
    10.3 @@ -80,21 +80,26 @@
    10.4          this.lastname = lastname;
    10.5      }
    10.6  
    10.7 -    public String getDisplayname() {
    10.8 +    public String getShortDisplayname() {
    10.9          StringBuilder dn = new StringBuilder();
   10.10          if (givenname != null)
   10.11              dn.append(givenname);
   10.12          dn.append(' ');
   10.13          if (lastname != null)
   10.14              dn.append(lastname);
   10.15 -        dn.append(' ');
   10.16 -        if (mail != null && !mail.isBlank()) {
   10.17 -            dn.append("<" + mail + ">");
   10.18 -        }
   10.19          final var str = dn.toString().trim();
   10.20          return str.isBlank() ? username : str;
   10.21      }
   10.22  
   10.23 +    public String getDisplayname() {
   10.24 +        final String sdn = getShortDisplayname();
   10.25 +        if (mail != null && !mail.isBlank()) {
   10.26 +            return sdn + " <" + mail + ">";
   10.27 +        } else {
   10.28 +            return sdn;
   10.29 +        }
   10.30 +    }
   10.31 +
   10.32      @Override
   10.33      public boolean equals(Object o) {
   10.34          if (this == o) return true;
    11.1 --- a/src/main/java/de/uapcore/lightpit/entities/Version.java	Fri May 22 17:26:27 2020 +0200
    11.2 +++ b/src/main/java/de/uapcore/lightpit/entities/Version.java	Fri May 22 21:23:57 2020 +0200
    11.3 @@ -33,7 +33,7 @@
    11.4  public final class Version implements Comparable<Version> {
    11.5  
    11.6      private final int id;
    11.7 -    private final Project project;
    11.8 +    private Project project;
    11.9      private String name;
   11.10      /**
   11.11       * If we do not want versions to be ordered lexicographically we may specify an order.
   11.12 @@ -50,6 +50,10 @@
   11.13          return id;
   11.14      }
   11.15  
   11.16 +    public void setProject(Project project) {
   11.17 +        this.project = project;
   11.18 +    }
   11.19 +
   11.20      public Project getProject() {
   11.21          return project;
   11.22      }
    12.1 --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Fri May 22 17:26:27 2020 +0200
    12.2 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Fri May 22 21:23:57 2020 +0200
    12.3 @@ -38,12 +38,14 @@
    12.4  import javax.servlet.annotation.WebServlet;
    12.5  import javax.servlet.http.HttpServletRequest;
    12.6  import javax.servlet.http.HttpServletResponse;
    12.7 +import javax.servlet.http.HttpSession;
    12.8  import java.io.IOException;
    12.9 +import java.sql.Date;
   12.10  import java.sql.SQLException;
   12.11  import java.util.ArrayList;
   12.12  import java.util.List;
   12.13  import java.util.NoSuchElementException;
   12.14 -import java.util.Optional;
   12.15 +import java.util.Objects;
   12.16  
   12.17  import static de.uapcore.lightpit.Functions.fqn;
   12.18  
   12.19 @@ -61,32 +63,85 @@
   12.20      private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class);
   12.21  
   12.22      public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected-project");
   12.23 +    public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected-issue");
   12.24 +    public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected-version");
   12.25  
   12.26 -    private Project getSelectedProject(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
   12.27 -        final var projectDao = dao.getProjectDao();
   12.28 -        final var session = req.getSession();
   12.29 -        final var projectSelection = getParameter(req, Integer.class, "pid");
   12.30 -        final Project selectedProject;
   12.31 -        if (projectSelection.isPresent()) {
   12.32 -            selectedProject = projectDao.find(projectSelection.get());
   12.33 -        } else {
   12.34 -            final var sessionProject = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
   12.35 -            selectedProject = sessionProject == null ? null : projectDao.find(sessionProject.getId());
   12.36 +    private class SessionSelection {
   12.37 +        final HttpSession session;
   12.38 +        Project project;
   12.39 +        Version version;
   12.40 +        Issue issue;
   12.41 +
   12.42 +        SessionSelection(HttpServletRequest req, Project project) {
   12.43 +            this.session = req.getSession();
   12.44 +            this.project = project;
   12.45 +            version = null;
   12.46 +            issue = null;
   12.47 +            updateAttributes();
   12.48          }
   12.49 -        session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, selectedProject);
   12.50 -        return selectedProject;
   12.51 +
   12.52 +        SessionSelection(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
   12.53 +            this.session = req.getSession();
   12.54 +            final var issueDao = dao.getIssueDao();
   12.55 +            final var projectDao = dao.getProjectDao();
   12.56 +            final var issueSelection = getParameter(req, Integer.class, "issue");
   12.57 +            if (issueSelection.isPresent()) {
   12.58 +                issue = issueDao.find(issueSelection.get());
   12.59 +            } else {
   12.60 +                final var issue = (Issue) session.getAttribute(SESSION_ATTR_SELECTED_ISSUE);
   12.61 +                this.issue = issue == null ? null : issueDao.find(issue.getId());
   12.62 +            }
   12.63 +            if (issue != null) {
   12.64 +                version = null; // show the issue globally
   12.65 +                project = projectDao.find(issue.getProject().getId());
   12.66 +            }
   12.67 +
   12.68 +            final var projectSelection = getParameter(req, Integer.class, "pid");
   12.69 +            if (projectSelection.isPresent()) {
   12.70 +                final var selectedProject = projectDao.find(projectSelection.get());
   12.71 +                if (!Objects.equals(selectedProject, project)) {
   12.72 +                    // reset version and issue if project changed
   12.73 +                    version = null;
   12.74 +                    issue = null;
   12.75 +                }
   12.76 +                project = selectedProject;
   12.77 +            } else {
   12.78 +                final var sessionProject = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
   12.79 +                project = sessionProject == null ? null : projectDao.find(sessionProject.getId());
   12.80 +            }
   12.81 +            updateAttributes();
   12.82 +        }
   12.83 +
   12.84 +        void selectVersion(Version version) {
   12.85 +            if (!version.getProject().equals(project)) throw new AssertionError("Nice, you implemented a bug!");
   12.86 +            this.version = version;
   12.87 +            this.issue = null;
   12.88 +            updateAttributes();
   12.89 +        }
   12.90 +
   12.91 +        void selectIssue(Issue issue) {
   12.92 +            if (!issue.getProject().equals(project)) throw new AssertionError("Nice, you implemented a bug!");
   12.93 +            this.issue = issue;
   12.94 +            this.version = null;
   12.95 +            updateAttributes();
   12.96 +        }
   12.97 +
   12.98 +        void updateAttributes() {
   12.99 +            session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, project);
  12.100 +            session.setAttribute(SESSION_ATTR_SELECTED_VERSION, version);
  12.101 +            session.setAttribute(SESSION_ATTR_SELECTED_ISSUE, issue);
  12.102 +        }
  12.103      }
  12.104  
  12.105  
  12.106      /**
  12.107       * Creates the breadcrumb menu.
  12.108       *
  12.109 -     * @param level           the current active level
  12.110 -     * @param selectedProject the selected project, if any, or null
  12.111 +     * @param level           the current active level (0: root, 1: project, 2: version, 3: issue)
  12.112 +     * @param sessionSelection the currently selected objects
  12.113       * @return a dynamic breadcrumb menu trying to display as many levels as possible
  12.114       */
  12.115 -    private List<MenuEntry> getBreadcrumbs(int level,
  12.116 -                                           Project selectedProject) {
  12.117 +    private List<MenuEntry> getBreadcrumbs(int level, SessionSelection sessionSelection) {
  12.118          MenuEntry entry;
  12.119  
  12.120          final var breadcrumbs = new ArrayList<MenuEntry>();
  12.121 @@ -95,52 +150,77 @@
  12.122          breadcrumbs.add(entry);
  12.123          if (level == 0) entry.setActive(true);
  12.124  
  12.125 -        if (selectedProject == null)
  12.126 -            return breadcrumbs;
  12.127 +        if (sessionSelection.project != null) {
  12.128 +            if (sessionSelection.project.getId() < 0) {
  12.129 +                entry = new MenuEntry(new ResourceKey("localization.projects", "button.create"),
  12.130 +                        "projects/edit", 1);
  12.131 +            } else {
  12.132 +                entry = new MenuEntry(sessionSelection.project.getName(),
  12.133 +                        "projects/view?pid=" + sessionSelection.project.getId(), 1);
  12.134 +            }
  12.135 +            if (level == 1) entry.setActive(true);
  12.136 +            breadcrumbs.add(entry);
  12.137 +        }
  12.138  
  12.139 -        entry = new MenuEntry(selectedProject.getName(),
  12.140 -                "projects/view?pid=" + selectedProject.getId(), 1);
  12.141 -        if (level == 1) entry.setActive(true);
  12.142 +        if (sessionSelection.version != null) {
  12.143 +            if (sessionSelection.version.getId() < 0) {
  12.144 +                entry = new MenuEntry(new ResourceKey("localization.projects", "button.version.create"),
  12.145 +                        "projects/versions/edit", 2);
  12.146 +            } else {
  12.147 +                entry = new MenuEntry(sessionSelection.version.getName(),
  12.148 +                        // TODO: change link to issue overview for that version
  12.149 +                        "projects/versions/edit?id=" + sessionSelection.version.getId(), 2);
  12.150 +            }
  12.151 +            if (level == 2) entry.setActive(true);
  12.152 +            breadcrumbs.add(entry);
  12.153 +        }
  12.154  
  12.155 -        breadcrumbs.add(entry);
  12.156 +        if (sessionSelection.issue != null) {
  12.157 +            entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"),
  12.158 +                    // TODO: change link to a separate issue view (maybe depending on the selected version)
  12.159 +                    "projects/view?pid=" + sessionSelection.issue.getProject().getId(), 3);
  12.160 +            breadcrumbs.add(entry);
  12.161 +            if (sessionSelection.issue.getId() < 0) {
  12.162 +                entry = new MenuEntry(new ResourceKey("localization.projects", "button.issue.create"),
  12.163 +                        "projects/issues/edit", 2);
  12.164 +            } else {
  12.165 +                entry = new MenuEntry("#" + sessionSelection.issue.getId(),
  12.166 +                        // TODO: maybe change link to a view rather than directly opening the editor
  12.167 +                        "projects/issues/edit?id=" + sessionSelection.issue.getId(), 4);
  12.168 +            }
  12.169 +            if (level == 3) entry.setActive(true);
  12.170 +            breadcrumbs.add(entry);
  12.171 +        }
  12.172 +
  12.173          return breadcrumbs;
  12.174      }
  12.175  
  12.176      @RequestMapping(method = HttpMethod.GET)
  12.177      public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
  12.178 -
  12.179 +        final var sessionSelection = new SessionSelection(req, dao);
  12.180          final var projectList = dao.getProjectDao().list();
  12.181          req.setAttribute("projects", projectList);
  12.182          setContentPage(req, "projects");
  12.183          setStylesheet(req, "projects");
  12.184  
  12.185 -        final var selectedProject = getSelectedProject(req, dao);
  12.186 -        setBreadcrumbs(req, getBreadcrumbs(0, selectedProject));
  12.187 +        setBreadcrumbs(req, getBreadcrumbs(0, sessionSelection));
  12.188  
  12.189          return ResponseType.HTML;
  12.190      }
  12.191  
  12.192 -    private void configureEditForm(HttpServletRequest req, DataAccessObjects dao, Optional<Project> project) throws SQLException {
  12.193 -        if (project.isPresent()) {
  12.194 -            req.setAttribute("project", project.get());
  12.195 -            setBreadcrumbs(req, getBreadcrumbs(1, project.get()));
  12.196 -        } else {
  12.197 -            req.setAttribute("project", new Project(-1));
  12.198 -            setBreadcrumbs(req, getBreadcrumbs(0, null));
  12.199 -        }
  12.200 -
  12.201 +    private void configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
  12.202 +        req.setAttribute("project", selection.project);
  12.203          req.setAttribute("users", dao.getUserDao().list());
  12.204          setContentPage(req, "project-form");
  12.205 +        setBreadcrumbs(req, getBreadcrumbs(1, selection));
  12.206      }
  12.207  
  12.208      @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
  12.209      public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
  12.210 +        final var selection = new SessionSelection(req, findByParameter(req, Integer.class, "id",
  12.211 +                dao.getProjectDao()::find).orElse(new Project(-1)));
  12.212  
  12.213 -        Optional<Project> project = findByParameter(req, Integer.class, "id", dao.getProjectDao()::find);
  12.214 -        configureEditForm(req, dao, project);
  12.215 -        if (project.isPresent()) {
  12.216 -            req.getSession().setAttribute(SESSION_ATTR_SELECTED_PROJECT, project.get());
  12.217 -        }
  12.218 +        configureEditForm(req, dao, selection);
  12.219  
  12.220          return ResponseType.HTML;
  12.221      }
  12.222 @@ -148,7 +228,7 @@
  12.223      @RequestMapping(requestPath = "commit", method = HttpMethod.POST)
  12.224      public ResponseType commit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
  12.225  
  12.226 -        Project project = null;
  12.227 +        Project project = new Project(-1);
  12.228          try {
  12.229              project = new Project(getParameter(req, Integer.class, "id").orElseThrow());
  12.230              project.setName(getParameter(req, String.class, "name").orElseThrow());
  12.231 @@ -163,11 +243,11 @@
  12.232              setRedirectLocation(req, "./projects/");
  12.233              setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
  12.234              LOG.debug("Successfully updated project {}", project.getName());
  12.235 -        } catch (NoSuchElementException | NumberFormatException | SQLException ex) {
  12.236 +        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
  12.237              // TODO: set request attribute with error text
  12.238              LOG.warn("Form validation failure: {}", ex.getMessage());
  12.239              LOG.debug("Details:", ex);
  12.240 -            configureEditForm(req, dao, Optional.ofNullable(project));
  12.241 +            configureEditForm(req, dao, new SessionSelection(req, project));
  12.242          }
  12.243  
  12.244          return ResponseType.HTML;
  12.245 @@ -175,123 +255,133 @@
  12.246  
  12.247      @RequestMapping(requestPath = "view", method = HttpMethod.GET)
  12.248      public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
  12.249 -        final var selectedProject = getSelectedProject(req, dao);
  12.250 -        if (selectedProject == null) {
  12.251 -            resp.sendError(HttpServletResponse.SC_FORBIDDEN);
  12.252 -            return ResponseType.NONE;
  12.253 -        }
  12.254 +        final var sessionSelection = new SessionSelection(req, dao);
  12.255  
  12.256 -        req.setAttribute("versions", dao.getVersionDao().list(selectedProject));
  12.257 -        req.setAttribute("issues", dao.getIssueDao().list(selectedProject));
  12.258 +        req.setAttribute("versions", dao.getVersionDao().list(sessionSelection.project));
  12.259 +        req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project));
  12.260  
  12.261 -        // TODO: add more levels depending on last visited location
  12.262 -        setBreadcrumbs(req, getBreadcrumbs(1, selectedProject));
  12.263 -
  12.264 +        setBreadcrumbs(req, getBreadcrumbs(1, sessionSelection));
  12.265          setContentPage(req, "project-details");
  12.266  
  12.267          return ResponseType.HTML;
  12.268      }
  12.269  
  12.270 -    private void configureEditVersionForm(HttpServletRequest req, Optional<Version> version, Project selectedProject) {
  12.271 -        req.setAttribute("version", version.orElse(new Version(-1, selectedProject)));
  12.272 +    private void configureEditVersionForm(HttpServletRequest req, SessionSelection selection) {
  12.273 +        req.setAttribute("version", selection.version);
  12.274          req.setAttribute("versionStatusEnum", VersionStatus.values());
  12.275  
  12.276          setContentPage(req, "version-form");
  12.277 +        setBreadcrumbs(req, getBreadcrumbs(2, selection));
  12.278      }
  12.279  
  12.280      @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET)
  12.281      public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
  12.282 -        final var selectedProject = getSelectedProject(req, dao);
  12.283 -        if (selectedProject == null) {
  12.284 +        final var sessionSelection = new SessionSelection(req, dao);
  12.285 +        if (sessionSelection.project == null) {
  12.286 +            // TODO: remove this bullshit and only retrieve the object from session if we are creating a fresh version
  12.287              resp.sendError(HttpServletResponse.SC_FORBIDDEN);
  12.288              return ResponseType.NONE;
  12.289          }
  12.290  
  12.291 -        configureEditVersionForm(req,
  12.292 -                findByParameter(req, Integer.class, "id", dao.getVersionDao()::find),
  12.293 -                selectedProject);
  12.294 +        sessionSelection.selectVersion(findByParameter(req, Integer.class, "id", dao.getVersionDao()::find)
  12.295 +                .orElse(new Version(-1, sessionSelection.project)));
  12.296 +        configureEditVersionForm(req, sessionSelection);
  12.297  
  12.298          return ResponseType.HTML;
  12.299      }
  12.300  
  12.301      @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST)
  12.302      public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
  12.303 -        final var selectedProject = getSelectedProject(req, dao);
  12.304 -        if (selectedProject == null) {
  12.305 +        final var sessionSelection = new SessionSelection(req, dao);
  12.306 +        if (sessionSelection.project == null) {
  12.307 +            // TODO: remove this bullshit and retrieve project id from hidden field
  12.308              resp.sendError(HttpServletResponse.SC_FORBIDDEN);
  12.309              return ResponseType.NONE;
  12.310          }
  12.311  
  12.312 -        Version version = null;
  12.313 +        var version = new Version(-1, sessionSelection.project);
  12.314          try {
  12.315 -            version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), selectedProject);
  12.316 +            version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), sessionSelection.project);
  12.317              version.setName(getParameter(req, String.class, "name").orElseThrow());
  12.318              getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal);
  12.319              version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
  12.320              dao.getVersionDao().saveOrUpdate(version);
  12.321  
  12.322 -            setRedirectLocation(req, "./projects/versions/");
  12.323 +            // specifying the pid parameter will purposely reset the session selected version!
  12.324 +            setRedirectLocation(req, "./projects/view?pid="+sessionSelection.project.getId());
  12.325              setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
  12.326 -            LOG.debug("Successfully updated version {} for project {}", version.getName(), selectedProject.getName());
  12.327 -        } catch (NoSuchElementException | NumberFormatException | SQLException ex) {
  12.328 +            LOG.debug("Successfully updated version {} for project {}", version.getName(), sessionSelection.project.getName());
  12.329 +        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
  12.330              // TODO: set request attribute with error text
  12.331              LOG.warn("Form validation failure: {}", ex.getMessage());
  12.332              LOG.debug("Details:", ex);
  12.333 -            configureEditVersionForm(req, Optional.ofNullable(version), selectedProject);
  12.334 +            sessionSelection.selectVersion(version);
  12.335 +            configureEditVersionForm(req, sessionSelection);
  12.336          }
  12.337  
  12.338          return ResponseType.HTML;
  12.339      }
  12.340  
  12.341 -    private void configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, Optional<Issue> issue, Project selectedProject) throws SQLException {
  12.342 -
  12.343 -        req.setAttribute("issue", issue.orElse(new Issue(-1, selectedProject)));
  12.344 +    private void configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
  12.345 +        req.setAttribute("issue", selection.issue);
  12.346          req.setAttribute("issueStatusEnum", IssueStatus.values());
  12.347          req.setAttribute("issueCategoryEnum", IssueCategory.values());
  12.348 -        req.setAttribute("versions", dao.getVersionDao().list(selectedProject));
  12.349 +        req.setAttribute("versions", dao.getVersionDao().list(selection.project));
  12.350 +        req.setAttribute("users", dao.getUserDao().list());
  12.351  
  12.352          setContentPage(req, "issue-form");
  12.353 +        setBreadcrumbs(req, getBreadcrumbs(3, selection));
  12.354      }
  12.355  
  12.356      @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
  12.357      public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
  12.358 -        final var selectedProject = getSelectedProject(req, dao);
  12.359 -        if (selectedProject == null) {
  12.360 +        final var sessionSelection = new SessionSelection(req, dao);
  12.361 +        if (sessionSelection.project == null) {
  12.362 +            // TODO: remove this bullshit and only retrieve the object from session if we are creating a fresh issue
  12.363              resp.sendError(HttpServletResponse.SC_FORBIDDEN);
  12.364              return ResponseType.NONE;
  12.365          }
  12.366  
  12.367 -        configureEditIssueForm(req, dao,
  12.368 -                findByParameter(req, Integer.class, "id", dao.getIssueDao()::find),
  12.369 -                selectedProject);
  12.370 +        sessionSelection.selectIssue(findByParameter(req, Integer.class, "id",
  12.371 +                dao.getIssueDao()::find).orElse(new Issue(-1, sessionSelection.project)));
  12.372 +        configureEditIssueForm(req, dao, sessionSelection);
  12.373  
  12.374          return ResponseType.HTML;
  12.375      }
  12.376  
  12.377      @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
  12.378      public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
  12.379 -        final var selectedProject = getSelectedProject(req, dao);
  12.380 -        if (selectedProject == null) {
  12.381 +        // TODO: remove this bullshit and store the project ID as hidden field
  12.382 +        final var sessionSelection = new SessionSelection(req, dao);
  12.383 +        if (sessionSelection.project == null) {
  12.384              resp.sendError(HttpServletResponse.SC_FORBIDDEN);
  12.385              return ResponseType.NONE;
  12.386          }
  12.387  
  12.388 -        Issue issue = null;
  12.389 +        Issue issue = new Issue(-1, sessionSelection.project);
  12.390          try {
  12.391 -            issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), selectedProject);
  12.392 -
  12.393 -            // TODO: implement
  12.394 -
  12.395 +            issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), sessionSelection.project);
  12.396 +            getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory);
  12.397 +            getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus);
  12.398 +            issue.setSubject(getParameter(req, String.class, "subject").orElseThrow());
  12.399 +            getParameter(req, Integer.class, "assignee").map(
  12.400 +                    userid -> userid >= 0 ? new User(userid) : null
  12.401 +            ).ifPresent(issue::setAssignee);
  12.402 +            getParameter(req, String.class, "description").ifPresent(issue::setDescription);
  12.403 +            getParameter(req, Date.class, "eta").ifPresent(issue::setEta);
  12.404              dao.getIssueDao().saveOrUpdate(issue);
  12.405  
  12.406 -            setRedirectLocation(req, "./projects/issues/");
  12.407 +            // TODO: redirect to issue overview
  12.408 +            // specifying the issue parameter keeps the edited issue as breadcrumb
  12.409 +            setRedirectLocation(req, "./projects/view?issue="+issue.getId());
  12.410              setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
  12.411 -            LOG.debug("Successfully updated issue {} for project {}", issue.getId(), selectedProject.getName());
  12.412 -        } catch (NoSuchElementException | NumberFormatException | SQLException ex) {
  12.413 +            LOG.debug("Successfully updated issue {} for project {}", issue.getId(), sessionSelection.project.getName());
  12.414 +        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
  12.415              // TODO: set request attribute with error text
  12.416              LOG.warn("Form validation failure: {}", ex.getMessage());
  12.417              LOG.debug("Details:", ex);
  12.418 -            configureEditIssueForm(req, dao, Optional.ofNullable(issue), selectedProject);
  12.419 +            sessionSelection.selectIssue(issue);
  12.420 +            configureEditIssueForm(req, dao, sessionSelection);
  12.421          }
  12.422  
  12.423          return ResponseType.HTML;
    13.1 --- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Fri May 22 17:26:27 2020 +0200
    13.2 +++ b/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Fri May 22 21:23:57 2020 +0200
    13.3 @@ -91,7 +91,7 @@
    13.4              setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
    13.5  
    13.6              LOG.debug("Successfully updated user {}", user.getUsername());
    13.7 -        } catch (NoSuchElementException | NumberFormatException | SQLException ex) {
    13.8 +        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
    13.9              // TODO: set request attribute with error text
   13.10              req.setAttribute("user", user);
   13.11              setContentPage(req, "user-form");
    14.1 --- a/src/main/resources/localization/projects.properties	Fri May 22 17:26:27 2020 +0200
    14.2 +++ b/src/main/resources/localization/projects.properties	Fri May 22 21:23:57 2020 +0200
    14.3 @@ -29,6 +29,8 @@
    14.4  
    14.5  no-projects=Welcome to LightPIT. Start off by creating a new project!
    14.6  
    14.7 +menu.issues=Issues
    14.8 +
    14.9  thead.name=Name
   14.10  thead.description=Description
   14.11  thead.repoUrl=Repository
   14.12 @@ -40,6 +42,7 @@
   14.13  tooltip.ordinal=Use to override lexicographic ordering.
   14.14  
   14.15  placeholder.null-owner=Unassigned
   14.16 +placeholder.null-assignee=Unassigned
   14.17  
   14.18  version.status.Future=Future
   14.19  version.status.Unreleased=Unreleased
   14.20 @@ -48,8 +51,31 @@
   14.21  version.status.Deprecated=Deprecated
   14.22  
   14.23  thead.issue.subject=Subject
   14.24 +thead.issue.description=Description
   14.25 +thead.issue.assignee=Assignee
   14.26 +thead.issue.affected-version=Affected Version
   14.27 +thead.issue.affected-versions=Affected Versions
   14.28 +thead.issue.scheduled-version=Scheduled for Version
   14.29 +thead.issue.scheduled-versions=Scheduled for Versions
   14.30 +thead.issue.resolved-version=Resolved in Version
   14.31 +thead.issue.resolved-versions=Resolved in Versions
   14.32  thead.issue.category=Category
   14.33  thead.issue.status=Status
   14.34  thead.issue.created=Created
   14.35  thead.issue.updated=Updated
   14.36  thead.issue.eta=ETA
   14.37 +
   14.38 +issue.category.Feature=Feature
   14.39 +issue.category.Improvement=Improvement
   14.40 +issue.category.Bug=Bug
   14.41 +issue.category.Task=Task
   14.42 +issue.category.Test=Test
   14.43 +
   14.44 +issue.status.InSpecification=Specification
   14.45 +issue.status.ToDo=To Do
   14.46 +issue.status.Scheduled=Scheduled
   14.47 +issue.status.InProgress=In Progress
   14.48 +issue.status.InReview=Review
   14.49 +issue.status.Done=Done
   14.50 +issue.status.Rejected=Rejected
   14.51 +issue.status.Withdrawn=Withdrawn
    15.1 --- a/src/main/resources/localization/projects_de.properties	Fri May 22 17:26:27 2020 +0200
    15.2 +++ b/src/main/resources/localization/projects_de.properties	Fri May 22 21:23:57 2020 +0200
    15.3 @@ -29,6 +29,8 @@
    15.4  
    15.5  no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes!
    15.6  
    15.7 +menu.issues=Vorg\u00e4nge
    15.8 +
    15.9  thead.name=Name
   15.10  thead.description=Beschreibung
   15.11  thead.repoUrl=Repository
   15.12 @@ -40,6 +42,7 @@
   15.13  tooltip.ordinal=\u00dcbersteuert die lexikographische Sortierung.
   15.14  
   15.15  placeholder.null-owner=Nicht Zugewiesen
   15.16 +placeholder.null-assignee=Niemandem
   15.17  
   15.18  version.status.Future=Geplant
   15.19  version.status.Unreleased=Unver\u00f6ffentlicht
   15.20 @@ -48,8 +51,31 @@
   15.21  version.status.Deprecated=Veraltet
   15.22  
   15.23  thead.issue.subject=Thema
   15.24 +thead.issue.description=Beschreibung
   15.25 +thead.issue.assignee=Zugewiesen
   15.26 +thead.issue.affected-version=Betroffene Version
   15.27 +thead.issue.affected-versions=Betroffene Versionen
   15.28 +thead.issue.scheduled-version=Zielversion
   15.29 +thead.issue.scheduled-versions=Zielversionen
   15.30 +thead.issue.resolved-version=Gel\u00f6st in Version
   15.31 +thead.issue.resolved-versions=Gel\u00f6st in Versionen
   15.32  thead.issue.category=Kategorie
   15.33  thead.issue.status=Status
   15.34  thead.issue.created=Erstellt
   15.35  thead.issue.updated=Aktualisiert
   15.36  thead.issue.eta=Zieldatum
   15.37 +
   15.38 +issue.category.Feature=Feature
   15.39 +issue.category.Improvement=Verbesserung
   15.40 +issue.category.Bug=Fehler
   15.41 +issue.category.Task=Aufgabe
   15.42 +issue.category.Test=Test
   15.43 +
   15.44 +issue.status.InSpecification=Spezifikation
   15.45 +issue.status.ToDo=Zu Erledigen
   15.46 +issue.status.Scheduled=Geplant
   15.47 +issue.status.InProgress=In Arbeit
   15.48 +issue.status.InReview=Im Review
   15.49 +issue.status.Done=Erledigt
   15.50 +issue.status.Rejected=Zur\u00fcckgewiesen
   15.51 +issue.status.Withdrawn=Zur\u00fcckgezogen
    16.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    16.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Fri May 22 21:23:57 2020 +0200
    16.3 @@ -0,0 +1,162 @@
    16.4 +<%--
    16.5 +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
    16.6 +
    16.7 +Copyright 2018 Mike Becker. All rights reserved.
    16.8 +
    16.9 +Redistribution and use in source and binary forms, with or without
   16.10 +modification, are permitted provided that the following conditions are met:
   16.11 +
   16.12 +1. Redistributions of source code must retain the above copyright
   16.13 +notice, this list of conditions and the following disclaimer.
   16.14 +
   16.15 +2. Redistributions in binary form must reproduce the above copyright
   16.16 +notice, this list of conditions and the following disclaimer in the
   16.17 +documentation and/or other materials provided with the distribution.
   16.18 +
   16.19 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   16.20 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   16.21 +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   16.22 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   16.23 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   16.24 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   16.25 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   16.26 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   16.27 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   16.28 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   16.29 +--%>
   16.30 +<%@page pageEncoding="UTF-8" %>
   16.31 +<%@page import="de.uapcore.lightpit.Constants" %>
   16.32 +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   16.33 +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   16.34 +
   16.35 +<c:set scope="page" var="moduleInfo" value="${requestScope[Constants.REQ_ATTR_MODULE_INFO]}"/>
   16.36 +
   16.37 +<jsp:useBean id="issue" type="de.uapcore.lightpit.entities.Issue" scope="request"/>
   16.38 +<jsp:useBean id="issueStatusEnum" type="de.uapcore.lightpit.entities.IssueStatus[]" scope="request"/>
   16.39 +<jsp:useBean id="issueCategoryEnum" type="de.uapcore.lightpit.entities.IssueCategory[]" scope="request"/>
   16.40 +<jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" scope="request"/>
   16.41 +<jsp:useBean id="users" type="java.util.List<de.uapcore.lightpit.entities.User>" scope="request"/>
   16.42 +
   16.43 +<form action="./${moduleInfo.modulePath}/issues/commit" method="post">
   16.44 +    <table class="formtable">
   16.45 +        <colgroup>
   16.46 +            <col>
   16.47 +            <col style="width: 75ch">
   16.48 +        </colgroup>
   16.49 +        <tbody>
   16.50 +        <tr>
   16.51 +            <th><fmt:message key="thead.issue.category"/></th>
   16.52 +            <td>
   16.53 +                <select name="category">
   16.54 +                    <c:forEach var="category" items="${issueCategoryEnum}">
   16.55 +                        <option
   16.56 +                                <c:if test="${category eq issue.category}">selected</c:if>
   16.57 +                                value="${category}">
   16.58 +                            <fmt:message key="issue.category.${category}" />
   16.59 +                        </option>
   16.60 +                    </c:forEach>
   16.61 +                </select>
   16.62 +            </td>
   16.63 +        </tr>
   16.64 +        <tr>
   16.65 +            <th><fmt:message key="thead.issue.status"/></th>
   16.66 +            <td>
   16.67 +                <select name="status">
   16.68 +                    <c:forEach var="status" items="${issueStatusEnum}">
   16.69 +                        <option
   16.70 +                                <c:if test="${status eq issue.status}">selected</c:if>
   16.71 +                                value="${status}">
   16.72 +                                <fmt:message key="issue.status.${status}" />
   16.73 +                        </option>
   16.74 +                    </c:forEach>
   16.75 +                </select>
   16.76 +            </td>
   16.77 +        </tr>
   16.78 +        <tr>
   16.79 +            <th><fmt:message key="thead.issue.subject"/></th>
   16.80 +            <td><input name="subject" type="text" maxlength="20" required value="<c:out value="${issue.subject}"/>" /></td>
   16.81 +        </tr>
   16.82 +        <tr>
   16.83 +            <th class="vtop"><fmt:message key="thead.issue.description"/></th>
   16.84 +            <td>
   16.85 +                <textarea name="description"><c:out value="${issue.description}"/></textarea>
   16.86 +            </td>
   16.87 +        </tr>
   16.88 +        <tr>
   16.89 +            <th><fmt:message key="thead.issue.assignee"/></th>
   16.90 +            <td>
   16.91 +                <select name="assignee">
   16.92 +                    <option value="-1"><fmt:message key="placeholder.null-assignee"/></option>
   16.93 +                    <c:forEach var="user" items="${users}">
   16.94 +                        <option
   16.95 +                                <c:if test="${not empty issue.assignee and user eq issue.assignee}">selected</c:if>
   16.96 +                                value="${user.id}"><c:out value="${user.displayname}"/></option>
   16.97 +                    </c:forEach>
   16.98 +                </select>
   16.99 +            </td>
  16.100 +        </tr>
  16.101 +        <tr>
  16.102 +            <th>
  16.103 +                <c:choose>
  16.104 +                    <c:when test="${issue.affectedVersions.size() gt 1}">
  16.105 +                        <fmt:message key="thead.issue.affected-versions"/>
  16.106 +                    </c:when>
  16.107 +                    <c:otherwise>
  16.108 +                        <fmt:message key="thead.issue.affected-version"/>
  16.109 +                    </c:otherwise>
  16.110 +                </c:choose>
  16.111 +            </th>
  16.112 +            <td>TODO</td>
  16.113 +        </tr>
  16.114 +        <tr>
  16.115 +            <th>
  16.116 +                <c:choose>
  16.117 +                    <c:when test="${issue.scheduledVersions.size() gt 1}">
  16.118 +                        <fmt:message key="thead.issue.scheduled-versions"/>
  16.119 +                    </c:when>
  16.120 +                    <c:otherwise>
  16.121 +                        <fmt:message key="thead.issue.scheduled-version"/>
  16.122 +                    </c:otherwise>
  16.123 +                </c:choose>
  16.124 +            </th>
  16.125 +            <td>TODO</td>
  16.126 +        </tr>
  16.127 +        <tr>
  16.128 +            <th>
  16.129 +                <c:choose>
  16.130 +                    <c:when test="${issue.resolvedVersions.size() gt 1}">
  16.131 +                        <fmt:message key="thead.issue.resolved-versions"/>
  16.132 +                    </c:when>
  16.133 +                    <c:otherwise>
  16.134 +                        <fmt:message key="thead.issue.resolved-version"/>
  16.135 +                    </c:otherwise>
  16.136 +                </c:choose>
  16.137 +            </th>
  16.138 +            <td>TODO</td>
  16.139 +        </tr>
  16.140 +        <tr>
  16.141 +            <th><fmt:message key="thead.issue.eta"/></th>
  16.142 +            <td><input name="eta" type="date" value="<fmt:formatDate value="${issue.eta}" pattern="YYYY-MM-dd" />" /> </td>
  16.143 +        </tr>
  16.144 +        <tr>
  16.145 +            <th><fmt:message key="thead.issue.created"/></th>
  16.146 +            <td><fmt:formatDate value="${issue.created}" /></td>
  16.147 +        </tr>
  16.148 +        <tr>
  16.149 +            <th><fmt:message key="thead.issue.updated"/></th>
  16.150 +            <td><fmt:formatDate value="${issue.updated}" /></td>
  16.151 +        </tr>
  16.152 +        </tbody>
  16.153 +        <tfoot>
  16.154 +        <tr>
  16.155 +            <td colspan="2">
  16.156 +                <input type="hidden" name="id" value="${issue.id}"/>
  16.157 +                <a href="./${moduleInfo.modulePath}/view?pid=${issue.project.id}" class="button">
  16.158 +                    <fmt:message bundle="${lightpit_bundle}" key="button.cancel"/>
  16.159 +                </a>
  16.160 +                <button type="submit"><fmt:message bundle="${lightpit_bundle}" key="button.okay"/></button>
  16.161 +            </td>
  16.162 +        </tr>
  16.163 +        </tfoot>
  16.164 +    </table>
  16.165 +</form>
    17.1 --- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Fri May 22 17:26:27 2020 +0200
    17.2 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Fri May 22 21:23:57 2020 +0200
    17.3 @@ -66,8 +66,8 @@
    17.4  <table id="issue-list" class="datatable medskip">
    17.5      <thead>
    17.6      <tr>
    17.7 -        <th></th>
    17.8          <th><fmt:message key="thead.issue.subject"/></th>
    17.9 +        <th><fmt:message key="thead.issue.assignee"/></th>
   17.10          <th><fmt:message key="thead.issue.category"/></th>
   17.11          <th><fmt:message key="thead.issue.status"/></th>
   17.12          <th><fmt:message key="thead.issue.created"/></th>
   17.13 @@ -76,5 +76,38 @@
   17.14          <!-- TODO: add other information -->
   17.15      </tr>
   17.16      </thead>
   17.17 -    <!-- TODO: add actual list -->
   17.18 +    <tbody>
   17.19 +    <c:forEach var="issue" items="${issues}">
   17.20 +        <tr>
   17.21 +            <td>
   17.22 +                <a href="./projects/issues/edit?id=${issue.id}">
   17.23 +                <c:out value="${issue.subject}" />
   17.24 +                </a>
   17.25 +            </td>
   17.26 +            <td>
   17.27 +                <c:if test="${not empty issue.assignee}">
   17.28 +                    <c:out value="${issue.assignee.shortDisplayname}" />
   17.29 +                </c:if>
   17.30 +                <c:if test="${empty issue.assignee}">
   17.31 +                    <fmt:message key="placeholder.null-assignee" />
   17.32 +                </c:if>
   17.33 +            </td>
   17.34 +            <td>
   17.35 +                <fmt:message key="issue.category.${issue.category}" />
   17.36 +            </td>
   17.37 +            <td>
   17.38 +                <fmt:message key="issue.status.${issue.status}" />
   17.39 +            </td>
   17.40 +            <td>
   17.41 +                <fmt:formatDate value="${issue.created}" type="BOTH"/>
   17.42 +            </td>
   17.43 +            <td>
   17.44 +                <fmt:formatDate value="${issue.updated}" type="BOTH"/>
   17.45 +            </td>
   17.46 +            <td>
   17.47 +                <fmt:formatDate value="${issue.eta}" />
   17.48 +            </td>
   17.49 +        </tr>
   17.50 +    </c:forEach>
   17.51 +    </tbody>
   17.52  </table>
    18.1 --- a/src/main/webapp/WEB-INF/jsp/project-form.jsp	Fri May 22 17:26:27 2020 +0200
    18.2 +++ b/src/main/webapp/WEB-INF/jsp/project-form.jsp	Fri May 22 21:23:57 2020 +0200
    18.3 @@ -43,15 +43,15 @@
    18.4          <tbody>
    18.5          <tr>
    18.6              <th><fmt:message key="thead.name"/></th>
    18.7 -            <td><input name="name" type="text" maxlength="20" required value="${project.name}"/></td>
    18.8 +            <td><input name="name" type="text" maxlength="20" required value="<c:out value="${project.name}"/>" /></td>
    18.9          </tr>
   18.10          <tr>
   18.11              <th class="vtop"><fmt:message key="thead.description"/></th>
   18.12 -            <td><input type="text" name="description" maxlength="200" value="${project.description}"/></td>
   18.13 +            <td><input type="text" name="description" maxlength="200" value="<c:out value="${project.description}"/>" /></td>
   18.14          </tr>
   18.15          <tr>
   18.16              <th><fmt:message key="thead.repoUrl"/></th>
   18.17 -            <td><input name="repoUrl" type="url" maxlength="50" value="${project.repoUrl}"/></td>
   18.18 +            <td><input name="repoUrl" type="url" maxlength="50" value="<c:out value="${project.repoUrl}"/>" /></td>
   18.19          </tr>
   18.20          <tr>
   18.21              <th><fmt:message key="thead.owner"/></th>
   18.22 @@ -60,7 +60,7 @@
   18.23                      <option value="-1"><fmt:message key="placeholder.null-owner"/></option>
   18.24                      <c:forEach var="user" items="${users}">
   18.25                          <option
   18.26 -                                <c:if test="${not empty project.owner and user.id eq project.owner.id}">selected</c:if>
   18.27 +                                <c:if test="${not empty project.owner and user eq project.owner}">selected</c:if>
   18.28                                  value="${user.id}"><c:out value="${user.displayname}"/></option>
   18.29                      </c:forEach>
   18.30                  </select>
    19.1 --- a/src/main/webapp/WEB-INF/jsp/user-form.jsp	Fri May 22 17:26:27 2020 +0200
    19.2 +++ b/src/main/webapp/WEB-INF/jsp/user-form.jsp	Fri May 22 21:23:57 2020 +0200
    19.3 @@ -42,20 +42,20 @@
    19.4          <tbody>
    19.5          <tr>
    19.6              <th><fmt:message key="thead.username"/></th>
    19.7 -            <td><input name="username" type="text" maxlength="50" required value="${user.username}"
    19.8 +            <td><input name="username" type="text" maxlength="50" required value="<c:out value="${user.username}"/>"
    19.9                         <c:if test="${user.id ge 0}">readonly</c:if> /></td>
   19.10          </tr>
   19.11          <tr>
   19.12              <th><fmt:message key="thead.givenname"/></th>
   19.13 -            <td><input name="givenname" type="text" maxlength="50" value="${user.givenname}"/></td>
   19.14 +            <td><input name="givenname" type="text" maxlength="50" value="<c:out value="${user.givenname}"/>" /></td>
   19.15          </tr>
   19.16          <tr>
   19.17              <th><fmt:message key="thead.lastname"/></th>
   19.18 -            <td><input name="lastname" type="text" maxlength="50" value="${user.lastname}"/></td>
   19.19 +            <td><input name="lastname" type="text" maxlength="50" value="<c:out value="${user.lastname}"/>" /></td>
   19.20          </tr>
   19.21          <tr>
   19.22              <th><fmt:message key="thead.mail"/></th>
   19.23 -            <td><input name="mail" type="email" maxlength="50" value="${user.mail}"/></td>
   19.24 +            <td><input name="mail" type="email" maxlength="50" value="<c:out value="${user.mail}"/>" /></td>
   19.25          </tr>
   19.26          </tbody>
   19.27          <tfoot>
    20.1 --- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Fri May 22 17:26:27 2020 +0200
    20.2 +++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Fri May 22 21:23:57 2020 +0200
    20.3 @@ -44,7 +44,7 @@
    20.4          <tbody>
    20.5          <tr>
    20.6              <th><fmt:message key="thead.version.name"/></th>
    20.7 -            <td><input name="name" type="text" maxlength="20" required value="${version.name}"/></td>
    20.8 +            <td><input name="name" type="text" maxlength="20" required value="<c:out value="${version.name}"/>" /></td>
    20.9          </tr>
   20.10          <tr>
   20.11              <th><fmt:message key="thead.version.status"/></th>
   20.12 @@ -69,8 +69,9 @@
   20.13          <tr>
   20.14              <td colspan="2">
   20.15                  <input type="hidden" name="id" value="${version.id}"/>
   20.16 -                <a href="./${moduleInfo.modulePath}/versions/" class="button"><fmt:message bundle="${lightpit_bundle}"
   20.17 -                                                                                           key="button.cancel"/></a>
   20.18 +                <a href="./${moduleInfo.modulePath}/view?pid=${version.project.id}" class="button">
   20.19 +                    <fmt:message bundle="${lightpit_bundle}" key="button.cancel"/>
   20.20 +                </a>
   20.21                  <button type="submit"><fmt:message bundle="${lightpit_bundle}" key="button.okay"/></button>
   20.22              </td>
   20.23          </tr>
    21.1 --- a/src/main/webapp/lightpit.css	Fri May 22 17:26:27 2020 +0200
    21.2 +++ b/src/main/webapp/lightpit.css	Fri May 22 21:23:57 2020 +0200
    21.3 @@ -158,6 +158,10 @@
    21.4      width: 100%;
    21.5  }
    21.6  
    21.7 +table.formtable input[type=date] {
    21.8 +    width: auto;
    21.9 +}
   21.10 +
   21.11  table.formtable tfoot td {
   21.12      text-align: right;
   21.13  }
    22.1 --- a/src/main/webapp/projects.css	Fri May 22 17:26:27 2020 +0200
    22.2 +++ b/src/main/webapp/projects.css	Fri May 22 21:23:57 2020 +0200
    22.3 @@ -26,3 +26,7 @@
    22.4   * POSSIBILITY OF SUCH DAMAGE.
    22.5   *
    22.6   */
    22.7 +
    22.8 +#issue-list td {
    22.9 +    white-space: nowrap;
   22.10 +}
   22.11 \ No newline at end of file

mercurial