# HG changeset patch # User Mike Becker # Date 1738432328 -3600 # Node ID 1dc9c405e9e2b6c378e5abf834c8e26c06ac154c # Parent d1edd8d9c8a1bde53b5412d20ae088345e9fe711 implement adding and filtering for variants relates to #491 diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 setup/postgres/psql_create_tables.sql --- a/setup/postgres/psql_create_tables.sql Thu Jan 30 21:20:27 2025 +0100 +++ b/setup/postgres/psql_create_tables.sql Sat Feb 01 18:52:08 2025 +0100 @@ -192,3 +192,11 @@ ); create unique index lpit_commit_ref_unique on lpit_commit_ref (issueid, commit_hash); + +create table lpit_issue_variant_status +( + issueid integer not null references lpit_issue (issueid), + variant integer not null references lpit_variant (id), + status issue_status not null default 'InSpecification', + primary key (issueid, variant) +); diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 setup/postgres/psql_patch_1.5.0.sql --- a/setup/postgres/psql_patch_1.5.0.sql Thu Jan 30 21:20:27 2025 +0100 +++ b/setup/postgres/psql_patch_1.5.0.sql Sat Feb 01 18:52:08 2025 +0100 @@ -16,3 +16,11 @@ ); create unique index lpit_variant_node_unique on lpit_variant (project, node); + +create table lpit_issue_variant_status +( + issueid integer not null references lpit_issue (issueid), + variant integer not null references lpit_variant (id), + status issue_status not null default 'InSpecification', + primary key (issueid, variant) +); diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Thu Jan 30 21:20:27 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Sat Feb 01 18:52:08 2025 +0100 @@ -29,6 +29,7 @@ import de.uapcore.lightpit.types.CommitRef import de.uapcore.lightpit.viewmodel.ComponentSummary import de.uapcore.lightpit.viewmodel.IssueSummary +import de.uapcore.lightpit.viewmodel.VariantSummary import de.uapcore.lightpit.viewmodel.VersionSummary interface DataAccessObject { @@ -60,7 +61,7 @@ fun updateComponent(component: Component) fun listVariants(project: Project): List - //fun listVariantSummaries(project: Project): List + fun listVariantSummaries(project: Project): List fun findVariant(id: Int): Variant? fun findVariantByNode(project: Project, node: String): Variant? fun insertVariant(variant: Variant) @@ -95,9 +96,9 @@ /** * Lists all issues for the specified [project]. * The result will only [includeDone] issues, if requested. - * When a [specificVersion] or a [specificComponent] is requested, - * the result is filtered for [version] or [component] respectively. - * In both cases null means that only issues without version or component shall be returned. + * When a [specificVersion], [specificComponent], or [specificVariant] is requested, + * the result is filtered for [version], [component], or [variant] respectively. + * In those cases null means that only issues without version/component/variant shall be returned. */ fun listIssues( project: Project, @@ -105,7 +106,9 @@ specificVersion: Boolean, version: Version?, specificComponent: Boolean, - component: Component? + component: Component?, + specificVariant: Boolean, + variant: Variant? ): List fun findIssue(id: Int): Issue? fun insertIssue(issue: Issue): Int diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Thu Jan 30 21:20:27 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Sat Feb 01 18:52:08 2025 +0100 @@ -32,6 +32,7 @@ import de.uapcore.lightpit.types.WebColor import de.uapcore.lightpit.viewmodel.ComponentSummary import de.uapcore.lightpit.viewmodel.IssueSummary +import de.uapcore.lightpit.viewmodel.VariantSummary import de.uapcore.lightpit.viewmodel.VersionSummary import org.intellij.lang.annotations.Language import java.sql.Connection @@ -394,14 +395,15 @@ setInt(1, project.id) queryAll { it.extractVariant() } } -/* + override fun listVariantSummaries(project: Project): List = withStatement( """ with issues as ( - select variant, phase, count(issueid) as total - from lpit_issue - join lpit_issue_phases using (status) + select variant, phase, count(i.issueid) as total + from lpit_issue i + join lpit_issue_variant_status vs on i.issueid = vs.issueid + join lpit_issue_phases p on p.status = vs.status group by variant, phase ), summary as ( @@ -409,28 +411,26 @@ from lpit_variant v left join issues i on v.id = i.variant ) - select c.id, project, name, node, color, ordinal, description, active, - userid, username, givenname, lastname, mail, + select v.id, project, name, node, color, ordinal, description, active, open.total as open, wip.total as wip, done.total as done - from lpit_component c - left join lpit_user on lead = userid - left join summary open on c.id = open.id and open.phase = 0 - left join summary wip on c.id = wip.id and wip.phase = 1 - left join summary done on c.id = done.id and done.phase = 2 - where c.project = ? + from lpit_variant v + left join summary open on v.id = open.id and open.phase = 0 + left join summary wip on v.id = wip.id and wip.phase = 1 + left join summary done on v.id = done.id and done.phase = 2 + where v.project = ? order by ordinal, name """.trimIndent() ) { setInt(1, project.id) queryAll { rs -> - ComponentSummary(rs.extractComponent()).apply { + VariantSummary(rs.extractVariant()).apply { issueSummary.open = rs.getInt("open") issueSummary.active = rs.getInt("wip") issueSummary.done = rs.getInt("done") } } } -*/ + override fun findVariant(id: Int): Variant? = withStatement("$variantQuery where id = ?") { setInt(1, id) @@ -619,6 +619,29 @@ left join lpit_user on userid = assignee """.trimIndent() + //language=SQL + private val issueQueryForVariant = + """ + select i.issueid, i.project, + p.projectid as project_projectid, + p.name as project_name, + p.node as project_node, + p.ordinal as project_ordinal, + p.description as project_description, + p.vcs as project_vcs, + p.repourl as project_repourl, + component, c.name as componentname, c.node as componentnode, c.color as componentcolor, + vs.status, phase, category, subject, i.description, + userid, username, givenname, lastname, mail, + created, updated, eta, affected, resolved + from lpit_issue i + join lpit_project p on i.project = projectid + join lpit_issue_variant_status vs on vs.issueid = i.issueid + join lpit_issue_phases ph on ph.status = vs.status + left join lpit_component c on component = c.id + left join lpit_user on userid = assignee + """.trimIndent() + private fun ResultSet.extractIssue(): Issue { val proj = extractProject("project_") val comp = getInt("component").let { @@ -681,15 +704,46 @@ specificVersion: Boolean, version: Version?, specificComponent: Boolean, - component: Component? - ): List = - withStatement( - """$issueQuery where i.project = ? and + component: Component?, + specificVariant: Boolean, + variant: Variant? + ): List { + // base query + var sql = if (variant != null) issueQueryForVariant else issueQuery + + // prepare a filter when only issues without a variant are requested + if (specificVariant && variant == null) { + // language=SQL + sql = """with variants_per_issue(issueid, variants) as ( + select issueid, count(variant) + from lpit_issue_variant_status + right join lpit_issue using (issueid) + group by issueid + ) $sql join variants_per_issue using (issueid) + """.trimIndent() + } + + // add component and version queries + // language=SQL + sql = """$sql where i.project = ? and (? or phase < 2) and (not ? or ? in (resolved, affected)) and (not ? or (resolved is null and affected is null)) and (not ? or component = ?) and (not ? or component is null) """.trimIndent() - ) { + + // if specific variant requested, add respective clause + if (variant != null) { + // language=SQL + sql = "$sql and vs.variant = ?" + } + + // if only issues without variants are requested, use the prepared filter + if (specificVariant && variant == null) { + // language=SQL + sql ="$sql and variants = 0" + } + + return withStatement(sql) { setInt(1, project.id) setBoolean(2, includeDone) @@ -701,8 +755,13 @@ setInt(7, component?.id ?: 0) setBoolean(8, specificComponent && component == null) + if (variant != null) { + setInt(9, variant.id) + } + queryAll { it.extractIssue() } } + } override fun findIssue(id: Int): Issue? = withStatement("$issueQuery where issueid = ?") { diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt --- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Thu Jan 30 21:20:27 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Sat Feb 01 18:52:08 2025 +0100 @@ -27,10 +27,7 @@ import de.uapcore.lightpit.* import de.uapcore.lightpit.dao.DataAccessObject -import de.uapcore.lightpit.entities.Component -import de.uapcore.lightpit.entities.Issue -import de.uapcore.lightpit.entities.Project -import de.uapcore.lightpit.entities.Version +import de.uapcore.lightpit.entities.* import de.uapcore.lightpit.logic.* import de.uapcore.lightpit.types.VcsType import de.uapcore.lightpit.types.VersionStatus @@ -62,6 +59,11 @@ get("/%project/components/-/create", this::componentForm) post("/%project/components/-/commit", this::componentCommit) + get("/%project/variants/", this::variants) + get("/%project/variants/%variant/edit", this::variantForm) + get("/%project/variants/-/create", this::variantForm) + post("/%project/variants/-/commit", this::variantCommit) + get("/%project/issues/%version/%component/%variant/%issue", this::issue) get("/%project/issues/%version/%component/%variant/%issue/edit", this::issueForm) post("/%project/issues/%version/%component/%variant/%issue/comment", this::issueComment) @@ -114,8 +116,14 @@ val version = if (path.versionInfo is OptionalPathInfo.Specific) path.versionInfo.elem else null val specificComponent = path.componentInfo !is OptionalPathInfo.All val component = if (path.componentInfo is OptionalPathInfo.Specific) path.componentInfo.elem else null + val specificVariant = path.variantInfo !is OptionalPathInfo.All + val variant = if (path.variantInfo is OptionalPathInfo.Specific) path.variantInfo.elem else null - val issues = dao.listIssues(project, filter.includeDone, specificVersion, version, specificComponent, component) + val issues = dao.listIssues(project, filter.includeDone, + specificVersion, version, + specificComponent, component, + specificVariant, variant + ) .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary)) .filter(issueFilterFunction(filter, relationsMap, http.remoteUser ?: "")) @@ -323,6 +331,62 @@ http.renderCommit("projects/${project.node}/components/") } + private fun variants(http: HttpRequest, dao: DataAccessObject) { + withPathInfo(http, dao)?.let { path -> + with(http) { + pageTitle = "${path.project.name} - ${i18n("navmenu.variants")}" + view = VariantsView( + path.projectInfo, + dao.listVariantSummaries(path.project) + ) + navigationMenu = projectNavMenu(dao.listProjects(), path) + styleSheets = listOf("projects") + javascript = "issue-overview" + render("variants") + } + } + } + + private fun variantForm(http: HttpRequest, dao: DataAccessObject) { + withPathInfo(http, dao)?.let { path -> + val variant = if (path.variantInfo is OptionalPathInfo.Specific) + path.variantInfo.elem else Variant(-1, path.project.id) + + with(http) { + view = VariantEditView(path.projectInfo, variant) + navigationMenu = projectNavMenu(dao.listProjects(), path) + styleSheets = listOf("projects") + render("variant-form") + } + } + } + + private fun variantCommit(http: HttpRequest, dao: DataAccessObject) { + val idParams = obtainIdAndProject(http, dao) ?: return + val (id, project) = idParams + + val variant = Variant(id, project.id).apply { + name = http.param("name") ?: "" + node = http.param("node") ?: "" + ordinal = http.param("ordinal")?.toIntOrNull() ?: 0 + color = WebColor(http.param("color") ?: "#000000") + description = http.param("description") + active = http.param("active", ::boolValidator, true, mutableListOf()) + // intentional defaults + if (node.isBlank()) node = name + // sanitizing + node = sanitizeNode(node) + } + + if (id < 0) { + dao.insertVariant(variant) + } else { + dao.updateVariant(variant) + } + + http.renderCommit("projects/${project.node}/variants/") + } + private fun issue(http: HttpRequest, dao: DataAccessObject) { val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) if (issue == null) { diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 src/main/resources/localization/strings.properties --- a/src/main/resources/localization/strings.properties Thu Jan 30 21:20:27 2025 +0100 +++ b/src/main/resources/localization/strings.properties Sat Feb 01 18:52:08 2025 +0100 @@ -43,6 +43,7 @@ button.remove=Remove button.save=Save button.user.create=Add Developer +button.variant.create=New Variant button.version.create=New Version button.version.edit=Edit Version commit.redirect-link=If redirection does not work, click the following link: @@ -175,6 +176,9 @@ validation.date.format=Illegal date format. validation.username.null=Username is mandatory. validation.username.unique=Username is already taken. +variant.active=Active +variant.color=Color +variant=Variant version.eol=End of Life version.latest=Latest Version version.next=Next Version diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 src/main/resources/localization/strings_de.properties --- a/src/main/resources/localization/strings_de.properties Thu Jan 30 21:20:27 2025 +0100 +++ b/src/main/resources/localization/strings_de.properties Sat Feb 01 18:52:08 2025 +0100 @@ -43,6 +43,7 @@ button.remove=Entfernen button.save=Speichern button.user.create=Neuer Entwickler +button.variant.create=Neue Variante button.version.create=Neue Version button.version.edit=Version Bearbeiten commit.redirect-link=Falls die Weiterleitung nicht klappt, klicken Sie bitte hier: @@ -175,6 +176,9 @@ validation.date.format=Datumsformat wird nicht unterst\u00fctzt. validation.username.null=Benutzername ist ein Pflichtfeld. validation.username.unique=Der Benutzername wird bereits verwendet. +variant.active=Aktiv +variant.color=Farbe +variant=Variante version.eol=Supportende version.latest=Neuste Version version.next=N\u00e4chste Version diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 src/main/webapp/WEB-INF/jsp/project-details.jsp --- a/src/main/webapp/WEB-INF/jsp/project-details.jsp Thu Jan 30 21:20:27 2025 +0100 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp Sat Feb 01 18:52:08 2025 +0100 @@ -32,6 +32,7 @@ + <%@include file="../jspf/project-header.jspf"%> diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 src/main/webapp/WEB-INF/jsp/variant-form.jsp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jsp/variant-form.jsp Sat Feb 01 18:52:08 2025 +0100 @@ -0,0 +1,92 @@ +<%-- +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + +Copyright 2021 Mike Becker. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--%> +<%@page pageEncoding="UTF-8" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + +
+ + + + + + + + + + + + + + + "> + + + + + + + + "> + + + + + + + + + + + + + + + + + +
+ + +
" />
" />
+ +
+ +
+ checked > +
+ + + + + +
+
diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 src/main/webapp/WEB-INF/jsp/variants.jsp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jsp/variants.jsp Sat Feb 01 18:52:08 2025 +0100 @@ -0,0 +1,98 @@ +<%-- +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + +Copyright 2021 Mike Becker. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--%> +<%@page pageEncoding="UTF-8" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + +<%@include file="../jspf/project-header.jspf"%> + +
+ + + +
+ +

+ + +<%@include file="../jspf/issue-summary.jspf"%> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + style="text-decoration: line-through;" + > + + + ${variantInfo.issueSummary.open}${variantInfo.issueSummary.active}${variantInfo.issueSummary.done}
+ + <%@include file="../jspf/issue-progress.jspf" %> +
\ No newline at end of file diff -r d1edd8d9c8a1 -r 1dc9c405e9e2 src/main/webapp/WEB-INF/jspf/project-header.jspf --- a/src/main/webapp/WEB-INF/jspf/project-header.jspf Thu Jan 30 21:20:27 2025 +0100 +++ b/src/main/webapp/WEB-INF/jspf/project-header.jspf Sat Feb 01 18:52:08 2025 +0100 @@ -1,6 +1,7 @@ <%-- project: Project component: Component (optional) +variant: Variant (optional) --%> + +
+
:
+
+
:
+
+
+