Mon, 05 Aug 2024 19:38:47 +0200
fix removing filter not working
fixes #407
/* * 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. */ package de.uapcore.lightpit.servlet 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.logic.* import de.uapcore.lightpit.types.VcsType import de.uapcore.lightpit.types.VersionStatus import de.uapcore.lightpit.types.WebColor import de.uapcore.lightpit.types.parseCommitRefs import de.uapcore.lightpit.viewmodel.* import jakarta.servlet.annotation.WebServlet import java.sql.Date @WebServlet(urlPatterns = ["/projects/*"]) class ProjectServlet : AbstractServlet() { init { get("/", this::projects) get("/%project", this::project) get("/%project/issues/%version/%component/", this::project) get("/%project/edit", this::projectForm) get("/-/create", this::projectForm) post("/-/commit", this::projectCommit) post("/%project/vcs/analyze", this::vcsAnalyze) get("/%project/versions/", this::versions) get("/%project/versions/%version/edit", this::versionForm) get("/%project/versions/-/create", this::versionForm) post("/%project/versions/-/commit", this::versionCommit) get("/%project/components/", this::components) get("/%project/components/%component/edit", this::componentForm) get("/%project/components/-/create", this::componentForm) post("/%project/components/-/commit", this::componentCommit) get("/%project/issues/%version/%component/%issue", this::issue) get("/%project/issues/%version/%component/%issue/edit", this::issueForm) post("/%project/issues/%version/%component/%issue/comment", this::issueComment) post("/%project/issues/%version/%component/%issue/relation", this::issueRelation) get("/%project/issues/%version/%component/%issue/removeRelation", this::issueRemoveRelation) get("/%project/issues/%version/%component/-/create", this::issueForm) post("/%project/issues/%version/%component/-/commit", this::issueCommit) } private fun projects(http: HttpRequest, dao: DataAccessObject) { val projects = dao.listProjects() val projectInfos = projects.map { ProjectInfo( project = it, versions = dao.listVersions(it), components = emptyList(), // not required in this view issueSummary = dao.collectIssueSummary(it) ) } with(http) { view = ProjectsView(projectInfos) navigationMenu = projectNavMenu(projects) styleSheets = listOf("projects") render("projects") } } private fun sanitizeNode(name: String): String { val san = name.replace(Regex("[/\\\\]"), "-") return if (san.startsWith(".")) { "v$san" } else { san } } private fun project(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let {path -> val project = path.projectInfo.project val filter = IssueFilter(http, dao) val needRelationsMap = filter.onlyBlocker val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap() val specificVersion = path.versionInfo !is OptionalPathInfo.All 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 issues = dao.listIssues(project, filter.includeDone, specificVersion, version, specificComponent, component) .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary)) .filter(issueFilterFunction(filter, relationsMap, http.remoteUser ?: "<Anonymous>")) with(http) { pageTitle = project.name view = ProjectDetails(path, issues, filter) navigationMenu = projectNavMenu(dao.listProjects(), path) styleSheets = listOf("projects") javascript = "issue-overview" render("project-details") } } } private fun projectForm(http: HttpRequest, dao: DataAccessObject) { http.styleSheets = listOf("projects") if (!http.pathParams.containsKey("project")) { http.view = ProjectEditView(Project(-1), dao.listUsers()) http.navigationMenu = projectNavMenu(dao.listProjects()) http.render("project-form") } else { withPathInfo(http, dao)?.let { path -> http.view = ProjectEditView(path.projectInfo.project, dao.listUsers()) http.navigationMenu = projectNavMenu(dao.listProjects(), path) http.render("project-form") } } } private fun projectCommit(http: HttpRequest, dao: DataAccessObject) { val project = Project(http.param("id")?.toIntOrNull() ?: -1).apply { name = http.param("name") ?: "" node = http.param("node") ?: "" description = http.param("description") ?: "" ordinal = http.param("ordinal")?.toIntOrNull() ?: 0 repoUrl = http.param("repoUrl") ?: "" vcs = VcsType.valueOf(http.param("vcs") ?: "None") owner = (http.param("owner")?.toIntOrNull() ?: -1).let { if (it < 0) null else dao.findUser(it) } // intentional defaults if (node.isBlank()) node = name // sanitizing node = sanitizeNode(node) } if (project.id < 0) { dao.insertProject(project) } else { dao.updateProject(project) } http.renderCommit("projects/${project.node}") } private fun vcsAnalyze(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let { path -> // if analysis is not configured, reject the request if (path.projectInfo.project.vcs == VcsType.None) { http.response.sendError(404) return } // obtain the list of issues for this project to filter cross-project references val knownIds = dao.listIssues(path.projectInfo.project, true).map { it.id } // read the provided commit log and merge only the refs that relate issues from the current project dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) }) } } private fun versions(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let { path -> with(http) { pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.versions")}" view = VersionsView( path.projectInfo, dao.listVersionSummaries(path.projectInfo.project) ) navigationMenu = projectNavMenu(dao.listProjects(), path) styleSheets = listOf("projects") javascript = "issue-overview" render("versions") } } } private fun versionForm(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let { path -> val version = if (path.versionInfo is OptionalPathInfo.Specific) path.versionInfo.elem else Version(-1, path.projectInfo.project.id) with(http) { view = VersionEditView(path.projectInfo, version) navigationMenu = projectNavMenu(dao.listProjects(), path) styleSheets = listOf("projects") render("version-form") } } } private fun obtainIdAndProject(http: HttpRequest, dao: DataAccessObject): Pair<Int, Project>? { val id = http.param("id")?.toIntOrNull() val projectid = http.param("projectid")?.toIntOrNull() ?: -1 val project = dao.findProject(projectid) return if (id == null || project == null) { http.response.sendError(400) null } else { Pair(id, project) } } private fun versionCommit(http: HttpRequest, dao: DataAccessObject) { val idParams = obtainIdAndProject(http, dao) ?: return val (id, project) = idParams val version = Version(id, project.id).apply { name = http.param("name") ?: "" node = http.param("node") ?: "" ordinal = http.param("ordinal")?.toIntOrNull() ?: 0 status = http.param("status")?.let(VersionStatus::valueOf) ?: VersionStatus.Future // TODO: process error messages eol = http.param("eol", ::dateOptValidator, null, mutableListOf()) release = http.param("release", ::dateOptValidator, null, mutableListOf()) // intentional defaults if (node.isBlank()) node = name // sanitizing node = sanitizeNode(node) } // sanitize eol and release date if (version.status.isEndOfLife) { if (version.eol == null) version.eol = Date(System.currentTimeMillis()) } else if (version.status.isReleased) { if (version.release == null) version.release = Date(System.currentTimeMillis()) } if (id < 0) { dao.insertVersion(version) } else { dao.updateVersion(version) } http.renderCommit("projects/${project.node}/versions/") } private fun components(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let { path -> with(http) { pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.components")}" view = ComponentsView( path.projectInfo, dao.listComponentSummaries(path.projectInfo.project) ) navigationMenu = projectNavMenu(dao.listProjects(), path) styleSheets = listOf("projects") javascript = "issue-overview" render("components") } } } private fun componentForm(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let { path -> val component = if (path.componentInfo is OptionalPathInfo.Specific) path.componentInfo.elem else Component(-1, path.projectInfo.project.id) with(http) { view = ComponentEditView(path.projectInfo, component, dao.listUsers()) navigationMenu = projectNavMenu(dao.listProjects(), path) styleSheets = listOf("projects") render("component-form") } } } private fun componentCommit(http: HttpRequest, dao: DataAccessObject) { val idParams = obtainIdAndProject(http, dao) ?: return val (id, project) = idParams val component = Component(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") // TODO: process error message active = http.param("active", ::boolValidator, true, mutableListOf()) lead = (http.param("lead")?.toIntOrNull() ?: -1).let { if (it < 0) null else dao.findUser(it) } // intentional defaults if (node.isBlank()) node = name // sanitizing node = sanitizeNode(node) } if (id < 0) { dao.insertComponent(component) } else { dao.updateComponent(component) } http.renderCommit("projects/${project.node}/components/") } private fun issue(http: HttpRequest, dao: DataAccessObject) { val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) if (issue == null) { http.response.sendError(404) return } withPathInfo(http, dao)?.let { path -> renderIssueView(http, dao, issue, path) } } private fun issueForm(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let { path -> val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) ?: Issue( -1, path.projectInfo.project, ) // for new issues set some defaults if (issue.id < 0) { // pre-select component, if available in the path info if (path.componentInfo is OptionalPathInfo.Specific) { issue.component = path.componentInfo.elem } // pre-select version, if available in the path info if (path.versionInfo is OptionalPathInfo.Specific) { if (path.versionInfo.elem.status.isReleased) { issue.affected = path.versionInfo.elem } else { issue.resolved = path.versionInfo.elem } } } with(http) { view = IssueEditView( issue, path.projectInfo.versions, path.projectInfo.components, dao.listUsers(), path.projectInfo.project, path ) navigationMenu = projectNavMenu(dao.listProjects(), path) styleSheets = listOf("projects") javascript = "issue-editor" render("issue-form") } } } private fun issueComment(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let {path -> commitIssueComment(http, dao, path) } } private fun issueCommit(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.run { val issue = Issue( http.param("id")?.toIntOrNull() ?: -1, projectInfo.project ).applyFormData(http, dao) val openId = if (issue.id < 0) { val id = dao.insertIssue(issue) dao.insertHistoryEvent(issue, id) id } else { val reference = dao.findIssue(issue.id) if (reference == null) { http.response.sendError(404) return } processIssueForm(issue, reference, http, dao) issue.id } if (http.param("more") != null) { http.renderCommit("${issuesHref}-/create") } else if (http.param("save") != null) { http.renderCommit("${issuesHref}${openId}") } else { http.renderCommit(issuesHref) } } } private fun issueRelation(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let {path -> addIssueRelation(http, dao, path) } } private fun issueRemoveRelation(http: HttpRequest, dao: DataAccessObject) { withPathInfo(http, dao)?.let {path -> removeIssueRelation(http, dao, path) } } }