change rss feed to display the issue history

Sat, 09 Oct 2021 20:05:39 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 09 Oct 2021 20:05:39 +0200
changeset 235
4258b9e010ae
parent 234
d71bc6db42ef
child 236
819c5178b6fe

change rss feed to display the issue history

TODO: diffs and comments

src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/entities/IssueHistoryEntry.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt file | annotate | diff | comparison | revisions
src/main/resources/localization/strings.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/strings_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issues-feed.jsp file | annotate | diff | comparison | revisions
--- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Sat Oct 09 17:46:12 2021 +0200
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Sat Oct 09 20:05:39 2021 +0200
@@ -81,4 +81,6 @@
 
     fun insertHistoryEvent(issue: Issue, newId: Int = 0)
     fun insertHistoryEvent(issueComment: IssueComment, newId: Int = 0)
+
+    fun listIssueHistory(projectId: Int, days: Int): List<IssueHistoryEntry>
 }
--- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Sat Oct 09 17:46:12 2021 +0200
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Sat Oct 09 20:05:39 2021 +0200
@@ -696,4 +696,43 @@
     }
 
     //</editor-fold>
+
+    //<editor-fold desc="Issue History">
+
+    override fun listIssueHistory(projectId: Int, days: Int) =
+        withStatement(
+            """
+                select evt.*, evtdata.*
+                from lpit_issue_history_event evt
+                join lpit_issue using (issueid)
+                join lpit_issue_history_data evtdata using (eventid)
+                where project = ?
+                and time > now() - (? * interval '1' day) 
+                order by time desc
+            """.trimIndent()
+        ) {
+            setInt(1, projectId)
+            setInt(2, days)
+            queryAll { rs->
+                with(rs) {
+                    IssueHistoryEntry(
+                        getTimestamp("time"),
+                        getEnum("type"),
+                        IssueHistoryData(getInt("issueid"),
+                            component = getString("component") ?: "",
+                            status = getEnum("status"),
+                            category = getEnum("category"),
+                            subject = getString("subject"),
+                            description = getString("description") ?: "",
+                            assignee = getString("assignee") ?: "",
+                            eta = getDate("eta"),
+                            affected = getString("affected") ?: "",
+                            resolved = getString("resolved") ?: ""
+                        )
+                    )
+                }
+            }
+        }
+
+    //</editor-fold>
 }
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/IssueHistoryEntry.kt	Sat Oct 09 20:05:39 2021 +0200
@@ -0,0 +1,47 @@
+/*
+ * 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.entities
+
+import de.uapcore.lightpit.types.IssueCategory
+import de.uapcore.lightpit.types.IssueHistoryType
+import de.uapcore.lightpit.types.IssueStatus
+import java.sql.Date
+import java.sql.Timestamp
+
+class IssueHistoryData(
+    val id: Int,
+    val component: String,
+    val status: IssueStatus,
+    val category: IssueCategory,
+    val subject: String,
+    val description: String,
+    val assignee: String,
+    val eta: Date?,
+    val affected: String,
+    val resolved: String,
+)
+
+class IssueHistoryEntry(val time: Timestamp, val type: IssueHistoryType, val data: IssueHistoryData)
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt	Sat Oct 09 17:46:12 2021 +0200
+++ b/src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt	Sat Oct 09 20:05:39 2021 +0200
@@ -28,10 +28,12 @@
 import de.uapcore.lightpit.AbstractServlet
 import de.uapcore.lightpit.HttpRequest
 import de.uapcore.lightpit.dao.DataAccessObject
-import de.uapcore.lightpit.util.IssueFilter
-import de.uapcore.lightpit.util.IssueSorter
-import de.uapcore.lightpit.util.SpecificFilter
+import de.uapcore.lightpit.entities.IssueHistoryData
+import de.uapcore.lightpit.entities.IssueHistoryEntry
+import de.uapcore.lightpit.viewmodel.IssueDiff
 import de.uapcore.lightpit.viewmodel.IssueFeed
+import de.uapcore.lightpit.viewmodel.IssueFeedEntry
+import java.text.SimpleDateFormat
 import javax.servlet.annotation.WebServlet
 
 @WebServlet(urlPatterns = ["/feed/*"])
@@ -41,6 +43,43 @@
         get("/%project/issues.rss", this::issues)
     }
 
+    private fun String.convertLF() = replace("\r", "").replace("\n", "<br>")
+
+    private fun fullContent(issue: IssueHistoryData) = IssueDiff(
+        issue.id,
+        issue.subject,
+        issue.component,
+        issue.status.name,
+        issue.category.name,
+        issue.subject,
+        issue.description.convertLF(),
+        issue.assignee,
+        issue.eta?.let { SimpleDateFormat("dd.MM.yyyy").format(it) } ?: "",
+        issue.affected,
+        issue.resolved
+    )
+
+    private fun diffContent(cur: IssueHistoryData, next: IssueHistoryData): IssueDiff {
+        val prev = fullContent(next)
+        val diff = fullContent(cur)
+        // TODO: compute and apply diff
+        return diff
+    }
+
+    /**
+     * Generates the feed entries.
+     * Assumes that [historyEntry] is already sorted by timestamp (descending).
+     */
+    private fun generateFeedEntries(historyEntry: List<IssueHistoryEntry>) =
+        if (historyEntry.isEmpty()) emptyList()
+        else historyEntry.zipWithNext().map { (cur, next) ->
+            IssueFeedEntry(
+                cur.time, cur.type, diffContent(cur.data, next.data)
+            )
+        }.plus(
+            historyEntry.last().let { IssueFeedEntry(it.time, it.type, fullContent(it.data)) }
+        )
+
     private fun issues(http: HttpRequest, dao: DataAccessObject) {
         val project = http.pathParams["project"]?.let { dao.findProjectByNode(it) }
         if (project == null) {
@@ -48,10 +87,12 @@
             return
         }
 
-        // TODO: add a timestamp filter (e.g. last 30 days)
-        val issues = dao.listIssues(IssueFilter(SpecificFilter(project))).sortedWith(IssueSorter.DEFAULT_ISSUE_SORTER)
+        val days = http.param("days")?.toIntOrNull() ?: 30
 
-        http.view = IssueFeed(project, issues)
+        val issueHistory = dao.listIssueHistory(project.id, days)
+        // TODO: add comment history depending on parameter
+
+        http.view = IssueFeed(project, generateFeedEntries(issueHistory))
         http.renderFeed("issues-feed")
     }
 }
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt	Sat Oct 09 17:46:12 2021 +0200
+++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt	Sat Oct 09 20:05:39 2021 +0200
@@ -25,14 +25,35 @@
 
 package de.uapcore.lightpit.viewmodel
 
-import de.uapcore.lightpit.entities.Issue
 import de.uapcore.lightpit.entities.Project
+import de.uapcore.lightpit.types.IssueHistoryType
 import java.sql.Timestamp
 import java.time.Instant
 
+class IssueDiff(
+    val id: Int,
+    val currentSubject: String,
+    var component: String,
+    var status: String,
+    var category: String,
+    var subject: String,
+    var description: String,
+    var assignee: String,
+    var eta: String,
+    var affected: String,
+    var resolved: String,
+)
+
+class IssueFeedEntry(
+    val time: Timestamp,
+    val type: IssueHistoryType,
+    val issue: IssueDiff
+)
+
 class IssueFeed(
     val project: Project,
-    val issues: List<Issue>
+    val entries: List<IssueFeedEntry>
 ) : View() {
-    val lastModified = issues.map(Issue::updated).maxOrNull() ?: Timestamp.from(Instant.now())
+    val lastModified: Timestamp =
+        entries.map(IssueFeedEntry::time).maxOrNull() ?: Timestamp.from(Instant.now())
 }
\ No newline at end of file
--- a/src/main/resources/localization/strings.properties	Sat Oct 09 17:46:12 2021 +0200
+++ b/src/main/resources/localization/strings.properties	Sat Oct 09 20:05:39 2021 +0200
@@ -59,10 +59,12 @@
 error.message = Server Message
 error.returnLink = Return to
 error.timestamp = Timestamp
-feed.issues.created=Issue has been created.
 feed.issues.description=Feed about recently updated issues.
 feed.issues.title=LightPIT Issues
-feed.issues.updated=Issue has been updated.
+feed.issues.type.New=New
+feed.issues.type.Update=Update
+feed.issues.type.NewComment=Comment
+feed.issues.type.UpdateComment=Comment Update
 feed=Feed
 issue.affected-versions=Affected
 issue.assignee=Assignee
--- a/src/main/resources/localization/strings_de.properties	Sat Oct 09 17:46:12 2021 +0200
+++ b/src/main/resources/localization/strings_de.properties	Sat Oct 09 20:05:39 2021 +0200
@@ -58,10 +58,12 @@
 error.message = Server Nachricht
 error.returnLink = Kehre zurück zu
 error.timestamp = Zeitstempel
-feed.issues.created=Vorgang wurde erstellt.
 feed.issues.description=Feed \u00fcber k\u00fcrzlich aktualisierte Vorg\u00e4nge.
 feed.issues.title=LightPIT Vorg\u00e4nge
-feed.issues.updated=Vorgang wurde aktualisiert.
+feed.issues.type.New=Neu
+feed.issues.type.Update=Update
+feed.issues.type.NewComment=Kommentar
+feed.issues.type.UpdateComment=Kommentar Update
 feed=Feed
 issue.affected-versions=Betroffen
 issue.assignee=Zugewiesen
--- a/src/main/webapp/WEB-INF/jsp/issues-feed.jsp	Sat Oct 09 17:46:12 2021 +0200
+++ b/src/main/webapp/WEB-INF/jsp/issues-feed.jsp	Sat Oct 09 20:05:39 2021 +0200
@@ -34,21 +34,27 @@
     <pubDate><fmt:formatDate value="${viewmodel.lastModified}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate>
     <lastBuildDate><fmt:formatDate value="${viewmodel.lastModified}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></lastBuildDate>
 
-    <c:forEach items="${viewmodel.issues}" var="issue">
+    <c:forEach items="${viewmodel.entries}" var="entry">
+        <c:set var="issue" value="${entry.issue}"/>
         <item>
-            <title><c:if test="${not empty issue.component}"><c:out value="${issue.component.name}"/> - </c:if><c:out value="${issue.subject}"/></title>
-            <description><c:choose>
-                <c:when test="${issue.created eq issue.updated}">
-                    <fmt:message key="feed.issues.created"/>
-                </c:when>
-                <c:otherwise>
-                    <fmt:message key="feed.issues.updated"/>
-                </c:otherwise>
-            </c:choose></description>
+            <title>[<fmt:message key="feed.issues.type.${entry.type}"/>] #${issue.id} - <c:out value="${issue.currentSubject}"/></title>
+            <description><![CDATA[
+                <h1>#${issue.id} - <c:out value="${issue.subject}"/></h1>
+                <div><b><fmt:message key="component"/></b>: ${issue.component}</div>
+                <div><b><fmt:message key="issue.category"/></b>: ${issue.category}</div>
+                <div><b><fmt:message key="issue.status"/></b>: ${issue.status}</div>
+                <div><b><fmt:message key="issue.resolved-versions"/></b>: ${issue.resolved}</div>
+                <div><b><fmt:message key="issue.affected-versions"/></b>: ${issue.affected}</div>
+                <div><b><fmt:message key="issue.assignee"/></b>: ${issue.assignee}</div>
+                <div><b><fmt:message key="issue.eta"/></b>: ${issue.eta}</div>
+                <h2><fmt:message key="issue.description"/></h2>
+                ${issue.description}
+            ]]></description>
             <category><fmt:message key="issue.category.${issue.category}"/></category>
-            <link>${baseHref}projects/${issue.project.node}/issues/-/${empty issue.component ? '-' : issue.component.node}/${issue.id}</link>
-            <guid isPermaLink="true">${baseHref}projects/${issue.project.node}/issues/-/-/${issue.id}</guid>
-            <pubDate><fmt:formatDate value="${issue.updated}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate>
+            <c:set var="link" value="${baseHref}projects/${viewmodel.project.node}/issues/-/-/${issue.id}"/>
+            <link>${link}</link>
+            <guid isPermaLink="true">${link}</guid>
+            <pubDate><fmt:formatDate value="${entry.time}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate>
         </item>
     </c:forEach>
 </channel>

mercurial