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

Mon, 30 Oct 2023 10:06:22 +0100

author
Mike Becker <universe@uap-core.de>
date
Mon, 30 Oct 2023 10:06:22 +0100
changeset 291
bcf05cccac6f
parent 284
671c1c8fbf1c
child 292
703591e739f4
permissions
-rw-r--r--

replace Enum.values() with Enum.entries

     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 issue: Issue,
   106     val comments: List<IssueComment>,
   107     val project: Project,
   108     val version: Version?,
   109     val component: Component?,
   110     projectIssues: List<Issue>,
   111     val currentRelations: List<IssueRelation>,
   112     /**
   113      * Optional resource key to an error message for the relation editor.
   114      */
   115     val relationError: String?,
   116     commitRefs: List<CommitRef>
   117 ) : View() {
   118     val relationTypes = RelationType.entries
   119     val linkableIssues = projectIssues.filterNot { it.id == issue.id }
   120     val commitLinks: List<CommitLink>
   122     private val parser: Parser
   123     private val renderer: HtmlRenderer
   125     init {
   126         val options = MutableDataSet()
   127             .set(SharedDataKeys.EXTENSIONS, listOf(TablesExtension.create(), StrikethroughExtension.create()))
   128         parser = Parser.builder(options).build()
   129         renderer = HtmlRenderer.builder(
   130             options
   131                 .set(HtmlRenderer.ESCAPE_HTML, true)
   132         ).build()
   134         issue.description = formatMarkdown(issue.description ?: "")
   135         for (comment in comments) {
   136             comment.commentFormatted = formatMarkdown(comment.comment)
   137         }
   139         val commitBaseUrl = project.repoUrl
   140         commitLinks = (if (commitBaseUrl == null || project.vcs == VcsType.None) emptyList() else commitRefs.map {
   141             CommitLink(buildCommitUrl(commitBaseUrl, project.vcs, it.hash), it.hash, it.message)
   142         })
   143     }
   145     private fun buildCommitUrl(baseUrl: String, vcs: VcsType, hash: String): String =
   146         with (StringBuilder(baseUrl)) {
   147             if (!endsWith("/")) append('/')
   148             when (vcs) {
   149                 VcsType.Mercurial -> append("rev/")
   150                 else -> append("commit/")
   151             }
   152             append(hash)
   153             toString()
   154         }
   156     private fun formatEmojis(text: String) = text
   157         .replace("(/)", "&#9989;")
   158         .replace("(x)", "&#10060;")
   159         .replace("(!)", "&#9889;")
   161     private fun formatMarkdown(text: String) =
   162         renderer.render(parser.parse(formatEmojis(text)))
   163 }
   165 class IssueEditView(
   166     val issue: Issue,
   167     val versions: List<Version>,
   168     val components: List<Component>,
   169     val users: List<User>,
   170     val project: Project, // TODO: allow null values to create issues from the IssuesServlet
   171     val version: Version? = null,
   172     val component: Component? = null
   173 ) : EditView() {
   175     val versionsUpcoming: List<Version>
   176     val versionsRecent: List<Version>
   178     val issueStatus = IssueStatus.entries
   179     val issueCategory = IssueCategory.entries
   181     init {
   182         val recent = mutableListOf<Version>()
   183         issue.affected?.let { recent.add(it) }
   184         val upcoming = mutableListOf<Version>()
   185         issue.resolved?.let { upcoming.add(it) }
   187         for (v in versions) {
   188             if (v.status.isReleased) {
   189                 if (v.status != VersionStatus.Deprecated) recent.add(v)
   190             } else {
   191                 upcoming.add(v)
   192             }
   193         }
   194         versionsRecent = recent.distinct()
   195         versionsUpcoming = upcoming.distinct()
   196     }
   197 }
   199 class IssueFilter(http: HttpRequest) {
   201     val issueStatus = IssueStatus.entries
   202     val issueCategory = IssueCategory.entries
   203     val sortCriteria = IssueSorter.Field.entries.flatMap { listOf(IssueSorter.Criteria(it, true), IssueSorter.Criteria(it, false)) }
   204     val flagIncludeDone = "f.0"
   205     val flagMine = "f.1"
   206     val flagBlocker = "f.2"
   208     val includeDone: Boolean = evalFlag(http, flagIncludeDone)
   209     val onlyMine: Boolean = evalFlag(http, flagMine)
   210     val onlyBlocker: Boolean = evalFlag(http, flagBlocker)
   211     val status: List<IssueStatus> = evalEnum(http, "s")
   212     val category: List<IssueCategory> = evalEnum(http, "c")
   214     val sortPrimary: IssueSorter.Criteria = evalSort(http, "primary", IssueSorter.Criteria(IssueSorter.Field.DONE))
   215     val sortSecondary: IssueSorter.Criteria = evalSort(http, "secondary", IssueSorter.Criteria(IssueSorter.Field.ETA))
   216     val sortTertiary: IssueSorter.Criteria = evalSort(http, "tertiary", IssueSorter.Criteria(IssueSorter.Field.UPDATED, false))
   218     private fun evalSort(http: HttpRequest, prio: String, defaultValue: IssueSorter.Criteria): IssueSorter.Criteria {
   219         val param = http.param("sort_$prio")
   220         if (param != null) {
   221             http.session.removeAttribute("sort_$prio")
   222             val p = param.split(".")
   223             if (p.size > 1) {
   224                 try {
   225                     http.session.setAttribute("sort_$prio", IssueSorter.Criteria(enumValueOf(p[0]), p[1].toBoolean()))
   226                 } catch (_:IllegalArgumentException) {
   227                     // ignore malfored values
   228                 }
   229             }
   230         }
   231         return http.session.getAttribute("sort_$prio") as IssueSorter.Criteria? ?: defaultValue
   232     }
   234     private fun evalFlag(http: HttpRequest, name: String): Boolean {
   235         val param = http.paramArray("filter")
   236         if (param.isNotEmpty()) {
   237             if (param.contains(name)) {
   238                 http.session.setAttribute(name, true)
   239             } else {
   240                 http.session.removeAttribute(name)
   241             }
   242         }
   243         return http.session.getAttribute(name) != null
   244     }
   246     private inline fun <reified T : Enum<T>> evalEnum(http: HttpRequest, prefix: String): List<T> {
   247         val sattr = "f.${prefix}"
   248         val param = http.paramArray("filter")
   249         if (param.isNotEmpty()) {
   250             val list = param.filter { it.startsWith("${prefix}.") }
   251                 .map { it.substring(prefix.length + 1) }
   252                 .map {
   253                     try {
   254                         // quick and very dirty validation
   255                         enumValueOf<T>(it)
   256                     } catch (_: IllegalArgumentException) {
   257                         // skip
   258                     }
   259                 }
   260             if (list.isEmpty()) {
   261                 http.session.removeAttribute(sattr)
   262             } else {
   263                 http.session.setAttribute(sattr, list.joinToString(","))
   264             }
   265         }
   267         return http.session.getAttribute(sattr)
   268             ?.toString()
   269             ?.split(",")
   270             ?.map { enumValueOf(it) }
   271             ?: emptyList()
   272     }
   273 }

mercurial