src/main/kotlin/de/uapcore/lightpit/viewmodel/Issues.kt

Mon, 30 Oct 2023 14:44:36 +0100

author
Mike Becker <universe@uap-core.de>
date
Mon, 30 Oct 2023 14:44:36 +0100
changeset 292
703591e739f4
parent 291
bcf05cccac6f
permissions
-rw-r--r--

add possibility to show issues w/o version or component - fixes #335

     1 /*
     2  * Copyright 2021 Mike Becker. All rights reserved.
     3  *
     4  * Redistribution and use in source and binary forms, with or without
     5  * modification, are permitted provided that the following conditions are met:
     6  *
     7  * 1. Redistributions of source code must retain the above copyright
     8  * notice, this list of conditions and the following disclaimer.
     9  *
    10  * 2. Redistributions in binary form must reproduce the above copyright
    11  * notice, this list of conditions and the following disclaimer in the
    12  * documentation and/or other materials provided with the distribution.
    13  *
    14  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    15  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    17  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    18  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    19  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    20  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    21  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    22  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    24  */
    26 package de.uapcore.lightpit.viewmodel
    28 import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
    29 import com.vladsch.flexmark.ext.tables.TablesExtension
    30 import com.vladsch.flexmark.html.HtmlRenderer
    31 import com.vladsch.flexmark.parser.Parser
    32 import com.vladsch.flexmark.util.data.MutableDataSet
    33 import com.vladsch.flexmark.util.data.SharedDataKeys
    34 import de.uapcore.lightpit.HttpRequest
    35 import de.uapcore.lightpit.entities.*
    36 import de.uapcore.lightpit.types.*
    37 import kotlin.math.roundToInt
    39 class IssueSorter(private vararg val criteria: Criteria) : Comparator<Issue> {
    40     enum class Field {
    41         DONE, PHASE, STATUS, CATEGORY, ETA, UPDATED, CREATED;
    43         val resourceKey: String by lazy {
    44             if (this == DONE) "issue.filter.sort.done"
    45             else if (this == PHASE) "issue.filter.sort.phase"
    46             else "issue.${this.name.lowercase()}"
    47         }
    48     }
    50     data class Criteria(val field: Field, val asc: Boolean = true) {
    51         override fun toString(): String {
    52             return "$field.$asc"
    53         }
    54     }
    56     override fun compare(left: Issue, right: Issue): Int {
    57         if (left == right) {
    58             return 0
    59         }
    60         for (c in criteria) {
    61             val result = when (c.field) {
    62                 Field.PHASE -> left.status.phase.compareTo(right.status.phase)
    63                 Field.DONE -> (left.status.phase == IssueStatusPhase.Done).compareTo(right.status.phase == IssueStatusPhase.Done)
    64                 Field.STATUS -> left.status.compareTo(right.status)
    65                 Field.CATEGORY -> left.category.compareTo(right.category)
    66                 Field.ETA -> left.compareEtaTo(right.eta)
    67                 Field.UPDATED -> left.updated.compareTo(right.updated)
    68                 Field.CREATED -> left.created.compareTo(right.created)
    69             }
    70             if (result != 0) {
    71                 return if (c.asc) result else -result
    72             }
    73         }
    74         return 0
    75     }
    76 }
    78 class IssueSummary {
    79     var open = 0
    80     var active = 0
    81     var done = 0
    83     val total get() = open + active + done
    85     val openPercent get() = 100 - activePercent - donePercent
    86     val activePercent get() = if (total > 0) (100f * active / total).roundToInt() else 0
    87     val donePercent get() = if (total > 0) (100f * done / total).roundToInt() else 100
    89     /**
    90      * Adds the specified issue to the summary by incrementing the respective counter.
    91      * @param issue the issue
    92      */
    93     fun add(issue: Issue) {
    94         when (issue.status.phase) {
    95             IssueStatusPhase.Open -> open++
    96             IssueStatusPhase.WorkInProgress -> active++
    97             IssueStatusPhase.Done -> done++
    98         }
    99     }
   100 }
   102 data class CommitLink(val url: String, val hash: String, val message: String)
   104 class IssueDetailView(
   105     val pathInfos: PathInfos,
   106     val issue: Issue,
   107     val comments: List<IssueComment>,
   108     val project: Project,
   109     projectIssues: List<Issue>,
   110     val currentRelations: List<IssueRelation>,
   111     /**
   112      * Optional resource key to an error message for the relation editor.
   113      */
   114     val relationError: String?,
   115     commitRefs: List<CommitRef>
   116 ) : View() {
   117     val relationTypes = RelationType.entries
   118     val linkableIssues = projectIssues.filterNot { it.id == issue.id }
   119     val commitLinks: List<CommitLink>
   121     private val parser: Parser
   122     private val renderer: HtmlRenderer
   124     init {
   125         val options = MutableDataSet()
   126             .set(SharedDataKeys.EXTENSIONS, listOf(TablesExtension.create(), StrikethroughExtension.create()))
   127         parser = Parser.builder(options).build()
   128         renderer = HtmlRenderer.builder(
   129             options
   130                 .set(HtmlRenderer.ESCAPE_HTML, true)
   131         ).build()
   133         issue.description = formatMarkdown(issue.description ?: "")
   134         for (comment in comments) {
   135             comment.commentFormatted = formatMarkdown(comment.comment)
   136         }
   138         val commitBaseUrl = project.repoUrl
   139         commitLinks = (if (commitBaseUrl == null || project.vcs == VcsType.None) emptyList() else commitRefs.map {
   140             CommitLink(buildCommitUrl(commitBaseUrl, project.vcs, it.hash), it.hash, it.message)
   141         })
   142     }
   144     private fun buildCommitUrl(baseUrl: String, vcs: VcsType, hash: String): String =
   145         with (StringBuilder(baseUrl)) {
   146             if (!endsWith("/")) append('/')
   147             when (vcs) {
   148                 VcsType.Mercurial -> append("rev/")
   149                 else -> append("commit/")
   150             }
   151             append(hash)
   152             toString()
   153         }
   155     private fun formatEmojis(text: String) = text
   156         .replace("(/)", "&#9989;")
   157         .replace("(x)", "&#10060;")
   158         .replace("(!)", "&#9889;")
   160     private fun formatMarkdown(text: String) =
   161         renderer.render(parser.parse(formatEmojis(text)))
   162 }
   164 class IssueEditView(
   165     val issue: Issue,
   166     val versions: List<Version>,
   167     val components: List<Component>,
   168     val users: List<User>,
   169     val project: Project, // TODO: allow null values to create issues from the IssuesServlet
   170     val pathInfos: PathInfos
   171 ) : EditView() {
   173     val versionsUpcoming: List<Version>
   174     val versionsRecent: List<Version>
   176     val issueStatus = IssueStatus.entries
   177     val issueCategory = IssueCategory.entries
   179     init {
   180         val recent = mutableListOf<Version>()
   181         issue.affected?.let { recent.add(it) }
   182         val upcoming = mutableListOf<Version>()
   183         issue.resolved?.let { upcoming.add(it) }
   185         for (v in versions) {
   186             if (v.status.isReleased) {
   187                 if (v.status != VersionStatus.Deprecated) recent.add(v)
   188             } else {
   189                 upcoming.add(v)
   190             }
   191         }
   192         versionsRecent = recent.distinct()
   193         versionsUpcoming = upcoming.distinct()
   194     }
   195 }
   197 class IssueFilter(http: HttpRequest) {
   199     val issueStatus = IssueStatus.entries
   200     val issueCategory = IssueCategory.entries
   201     val sortCriteria = IssueSorter.Field.entries.flatMap { listOf(IssueSorter.Criteria(it, true), IssueSorter.Criteria(it, false)) }
   202     val flagIncludeDone = "f.0"
   203     val flagMine = "f.1"
   204     val flagBlocker = "f.2"
   206     val includeDone: Boolean = evalFlag(http, flagIncludeDone)
   207     val onlyMine: Boolean = evalFlag(http, flagMine)
   208     val onlyBlocker: Boolean = evalFlag(http, flagBlocker)
   209     val status: List<IssueStatus> = evalEnum(http, "s")
   210     val category: List<IssueCategory> = evalEnum(http, "c")
   212     val sortPrimary: IssueSorter.Criteria = evalSort(http, "primary", IssueSorter.Criteria(IssueSorter.Field.DONE))
   213     val sortSecondary: IssueSorter.Criteria = evalSort(http, "secondary", IssueSorter.Criteria(IssueSorter.Field.ETA))
   214     val sortTertiary: IssueSorter.Criteria = evalSort(http, "tertiary", IssueSorter.Criteria(IssueSorter.Field.UPDATED, false))
   216     private fun evalSort(http: HttpRequest, prio: String, defaultValue: IssueSorter.Criteria): IssueSorter.Criteria {
   217         val param = http.param("sort_$prio")
   218         if (param != null) {
   219             http.session.removeAttribute("sort_$prio")
   220             val p = param.split(".")
   221             if (p.size > 1) {
   222                 try {
   223                     http.session.setAttribute("sort_$prio", IssueSorter.Criteria(enumValueOf(p[0]), p[1].toBoolean()))
   224                 } catch (_:IllegalArgumentException) {
   225                     // ignore malfored values
   226                 }
   227             }
   228         }
   229         return http.session.getAttribute("sort_$prio") as IssueSorter.Criteria? ?: defaultValue
   230     }
   232     private fun evalFlag(http: HttpRequest, name: String): Boolean {
   233         val param = http.paramArray("filter")
   234         if (param.isNotEmpty()) {
   235             if (param.contains(name)) {
   236                 http.session.setAttribute(name, true)
   237             } else {
   238                 http.session.removeAttribute(name)
   239             }
   240         }
   241         return http.session.getAttribute(name) != null
   242     }
   244     private inline fun <reified T : Enum<T>> evalEnum(http: HttpRequest, prefix: String): List<T> {
   245         val sattr = "f.${prefix}"
   246         val param = http.paramArray("filter")
   247         if (param.isNotEmpty()) {
   248             val list = param.filter { it.startsWith("${prefix}.") }
   249                 .map { it.substring(prefix.length + 1) }
   250                 .map {
   251                     try {
   252                         // quick and very dirty validation
   253                         enumValueOf<T>(it)
   254                     } catch (_: IllegalArgumentException) {
   255                         // skip
   256                     }
   257                 }
   258             if (list.isEmpty()) {
   259                 http.session.removeAttribute(sattr)
   260             } else {
   261                 http.session.setAttribute(sattr, list.joinToString(","))
   262             }
   263         }
   265         return http.session.getAttribute(sattr)
   266             ?.toString()
   267             ?.split(",")
   268             ?.map { enumValueOf(it) }
   269             ?: emptyList()
   270     }
   271 }

mercurial