Fri, 22 May 2020 21:23:57 +0200
adds the ability to create and edit issues
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