#109 add comment history

Sat, 27 Nov 2021 13:03:57 +0100

author
Mike Becker <universe@uap-core.de>
date
Sat, 27 Nov 2021 13:03:57 +0100
changeset 242
b7f3e972b13c
parent 241
1ca4f27cefe8
child 243
a1c2611b02fc

#109 add comment history

setup/postgres/psql_create_tables.sql file | annotate | diff | comparison | revisions
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/servlet/ProjectServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issues-feed.jsp file | annotate | diff | comparison | revisions
--- a/setup/postgres/psql_create_tables.sql	Sat Nov 27 12:12:20 2021 +0100
+++ b/setup/postgres/psql_create_tables.sql	Sat Nov 27 13:03:57 2021 +0100
@@ -112,6 +112,7 @@
 (
     eventid serial primary key,
     issueid integer                  not null references lpit_issue (issueid) on delete cascade,
+    subject text                     not null,
     time    timestamp with time zone not null default now(),
     type    issue_history_event      not null
 );
@@ -122,7 +123,6 @@
     component         text,
     status            issue_status   not null,
     category          issue_category not null,
-    subject           text           not null,
     description       text,
     assignee          text,
     eta               date,
--- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Sat Nov 27 12:12:20 2021 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Sat Nov 27 13:03:57 2021 +0100
@@ -80,7 +80,7 @@
     fun updateComment(issueComment: IssueComment)
 
     fun insertHistoryEvent(issue: Issue, newId: Int = 0)
-    fun insertHistoryEvent(issueComment: IssueComment, newId: Int = 0)
+    fun insertHistoryEvent(issue: Issue, issueComment: IssueComment, newId: Int = 0)
 
     /**
      * Lists the issue history of the project with [projectId] for the past [days].
--- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Sat Nov 27 12:12:20 2021 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Sat Nov 27 13:03:57 2021 +0100
@@ -597,28 +597,27 @@
         val issueid = if (newId > 0) newId else issue.id
 
         val eventid =
-            withStatement("insert into lpit_issue_history_event(issueid, type) values (?,?::issue_history_event) returning eventid") {
+            withStatement("insert into lpit_issue_history_event(issueid, subject, type) values (?,?,?::issue_history_event) returning eventid") {
                 setInt(1, issueid)
-                setEnum(2, type)
+                setString(2, issue.subject)
+                setEnum(3, type)
                 querySingle { it.getInt(1) }!!
             }
         withStatement(
             """
-            insert into lpit_issue_history_data (component, status, category, subject, description, assignee, assignee_username, eta, affected, resolved, eventid)
-            values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?, ?, ?)
+            insert into lpit_issue_history_data (component, status, category, description, assignee, eta, affected, resolved, eventid)
+            values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?)
             """.trimIndent()
         ) {
             setStringOrNull(1, issue.component?.name)
             setEnum(2, issue.status)
             setEnum(3, issue.category)
-            setString(4, issue.subject)
-            setStringOrNull(5, issue.description)
-            setStringOrNull(6, issue.assignee?.shortDisplayname)
-            setStringOrNull(7, issue.assignee?.username)
-            setDateOrNull(8, issue.eta)
-            setStringOrNull(9, issue.affected?.name)
-            setStringOrNull(10, issue.resolved?.name)
-            setInt(11, eventid)
+            setStringOrNull(4, issue.description)
+            setStringOrNull(5, issue.assignee?.shortDisplayname)
+            setDateOrNull(6, issue.eta)
+            setStringOrNull(7, issue.affected?.name)
+            setStringOrNull(8, issue.resolved?.name)
+            setInt(9, eventid)
             executeUpdate()
         }
     }
@@ -678,17 +677,18 @@
     }
 
 
-    override fun insertHistoryEvent(issueComment: IssueComment, newId: Int) {
+    override fun insertHistoryEvent(issue: Issue, issueComment: IssueComment, newId: Int) {
         val type = if (newId > 0) IssueHistoryType.NewComment else IssueHistoryType.UpdateComment
         val commentid = if (newId > 0) newId else issueComment.id
 
         val eventid =
-            withStatement("insert into lpit_issue_history_event(issueid, type) values (?,?::issue_history_event) returning eventid") {
+            withStatement("insert into lpit_issue_history_event(issueid, subject, type) values (?,?,?::issue_history_event) returning eventid") {
                 setInt(1, issueComment.issueid)
-                setEnum(2, type)
+                setString(1, issue.subject)
+                setEnum(3, type)
                 querySingle { it.getInt(1) }!!
             }
-        withStatement("insert into lpit_issue_comment_history (commentid, eventid, comment) values (?,?,?)") {
+        withStatement("insert into lpit_issue_comment_history (commentid, eventid, comment) values (?,?,?,?)") {
             setInt(1, commentid)
             setInt(2, eventid)
             setString(3, issueComment.comment)
@@ -718,20 +718,19 @@
             queryAll { rs->
                 with(rs) {
                     IssueHistoryEntry(
-                        getTimestamp("time"),
-                        getEnum("type"),
-                        getString("current_assignee"),
-                        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") ?: ""
-                        )
+                        subject = getString("subject"),
+                        time = getTimestamp("time"),
+                        type = getEnum("type"),
+                        currentAssignee = getString("current_assignee"),
+                        issueid = getInt("issueid"),
+                        component = getString("component") ?: "",
+                        status = getEnum("status"),
+                        category = getEnum("category"),
+                        description = getString("description") ?: "",
+                        assignee = getString("assignee") ?: "",
+                        eta = getDate("eta"),
+                        affected = getString("affected") ?: "",
+                        resolved = getString("resolved") ?: ""
                     )
                 }
             }
@@ -755,13 +754,13 @@
             queryAll { rs->
                 with(rs) {
                     IssueCommentHistoryEntry(
-                        getTimestamp("time"),
-                        getEnum("type"),
-                        getString("current_assignee"),
-                        IssueCommentHistoryData(
-                            getInt("commentid"),
-                            getString("comment")
-                        )
+                        subject = getString("subject"),
+                        time = getTimestamp("time"),
+                        type = getEnum("type"),
+                        currentAssignee = getString("current_assignee"),
+                        issueid = getInt("issueid"),
+                        commentid = getInt("commentid"),
+                        comment = getString("comment")
                     )
                 }
             }
--- a/src/main/kotlin/de/uapcore/lightpit/entities/IssueHistoryEntry.kt	Sat Nov 27 12:12:20 2021 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/entities/IssueHistoryEntry.kt	Sat Nov 27 13:03:57 2021 +0100
@@ -31,12 +31,15 @@
 import java.sql.Date
 import java.sql.Timestamp
 
-class IssueHistoryData(
-    val id: Int,
+class IssueHistoryEntry(
+    val subject: String,
+    val time: Timestamp,
+    val type: IssueHistoryType,
+    val currentAssignee: String?,
+    val issueid: Int,
     val component: String,
     val status: IssueStatus,
     val category: IssueCategory,
-    val subject: String,
     val description: String,
     val assignee: String,
     val eta: Date?,
@@ -44,21 +47,12 @@
     val resolved: String,
 )
 
-class IssueCommentHistoryData(
-    val commentid: Int,
-    val comment: String
-)
-
-class IssueHistoryEntry(
+class IssueCommentHistoryEntry(
+    val subject: String,
     val time: Timestamp,
     val type: IssueHistoryType,
     val currentAssignee: String?,
-    val data: IssueHistoryData
-)
-
-class IssueCommentHistoryEntry(
-    val time: Timestamp,
-    val type: IssueHistoryType,
-    val currentAssignee: String?,
-    val data: IssueCommentHistoryData
+    val issueid: Int,
+    val commentid: Int,
+    val comment: String,
 )
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt	Sat Nov 27 12:12:20 2021 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt	Sat Nov 27 13:03:57 2021 +0100
@@ -30,8 +30,10 @@
 import de.uapcore.lightpit.AbstractServlet
 import de.uapcore.lightpit.HttpRequest
 import de.uapcore.lightpit.dao.DataAccessObject
-import de.uapcore.lightpit.entities.IssueHistoryData
+import de.uapcore.lightpit.entities.IssueCommentHistoryEntry
 import de.uapcore.lightpit.entities.IssueHistoryEntry
+import de.uapcore.lightpit.types.IssueHistoryType
+import de.uapcore.lightpit.viewmodel.CommentDiff
 import de.uapcore.lightpit.viewmodel.IssueDiff
 import de.uapcore.lightpit.viewmodel.IssueFeed
 import de.uapcore.lightpit.viewmodel.IssueFeedEntry
@@ -46,8 +48,36 @@
         get("/%project/issues.rss", this::issues)
     }
 
-    private fun fullContent(issue: IssueHistoryData) = IssueDiff(
-        issue.id,
+    val diffGenerator by lazyOf(DiffRowGenerator.create()
+        .showInlineDiffs(true)
+        .mergeOriginalRevised(true)
+        .inlineDiffByWord(true)
+        .oldTag { start -> if (start) "<strike style=\"color:red\">" else "</strike>" }
+        .newTag { start -> if (start) "<i style=\"color: green\">" else "</i>" }
+        .build()
+    )
+
+    private fun fullContent(data: IssueCommentHistoryEntry) =
+        CommentDiff(
+            data.issueid,
+            data.commentid,
+            data.subject,
+            data.comment.replace("\r", "")
+        )
+
+    private fun diffContent(cur: IssueCommentHistoryEntry, next: IssueCommentHistoryEntry) =
+        CommentDiff(
+            cur.issueid,
+            cur.commentid,
+            cur.subject,
+            diffGenerator.generateDiffRows(
+                next.comment.replace("\r", "").split('\n'),
+                cur.comment.replace("\r", "").split('\n')
+            ).joinToString("\n", transform = DiffRow::getOldLine)
+        )
+
+    private fun fullContent(issue: IssueHistoryEntry) = IssueDiff(
+        issue.issueid,
         issue.subject,
         issue.component,
         issue.status.name,
@@ -60,19 +90,10 @@
         issue.resolved
     )
 
-    private fun diffContent(cur: IssueHistoryData, next: IssueHistoryData): IssueDiff {
-        val generator = DiffRowGenerator.create()
-            .showInlineDiffs(true)
-            .mergeOriginalRevised(true)
-            .inlineDiffByWord(true)
-            .oldTag { start -> if (start) "<strike style=\"color:red\">" else "</strike>" }
-            .newTag { start -> if (start) "<i style=\"color: green\">" else "</i>" }
-            .build()
-
+    private fun diffContent(cur: IssueHistoryEntry, next: IssueHistoryEntry): IssueDiff {
         val prev = fullContent(next)
         val diff = fullContent(cur)
-
-        val result = generator.generateDiffRows(
+        val result = diffGenerator.generateDiffRows(
             listOf(
                 prev.subject, prev.component, prev.status,
                 prev.category, prev.assignee, prev.eta, prev.affected, prev.resolved
@@ -92,7 +113,7 @@
         diff.affected = result[6].oldLine
         diff.resolved = result[7].oldLine
 
-        diff.description = generator.generateDiffRows(
+        diff.description = diffGenerator.generateDiffRows(
             prev.description.split('\n'),
             diff.description.split('\n')
         ).joinToString("\n", transform = DiffRow::getOldLine)
@@ -102,21 +123,42 @@
 
     /**
      * Generates the feed entries.
-     * Assumes that [historyEntry] is already sorted by timestamp (descending).
+     * Assumes that [issueEntries] and [commentEntries] are already sorted by timestamp (descending).
      */
-    private fun generateFeedEntries(historyEntry: List<IssueHistoryEntry>): List<IssueFeedEntry> =
-        if (historyEntry.isEmpty()) {
+    private fun generateFeedEntries(
+        issueEntries: List<IssueHistoryEntry>,
+        commentEntries: List<IssueCommentHistoryEntry>
+    ): List<IssueFeedEntry> =
+        (generateIssueFeedEntries(issueEntries) + generateCommentFeedEntries(commentEntries)).sortedByDescending { it.time }
+
+    private fun generateIssueFeedEntries(entries: List<IssueHistoryEntry>): List<IssueFeedEntry> =
+        if (entries.isEmpty()) {
             emptyList()
         } else {
-            historyEntry.groupBy { it.data.id }.mapValues { (_, history) ->
+            entries.groupBy { it.issueid }.mapValues { (_, history) ->
                 history.zipWithNext().map { (cur, next) ->
                     IssueFeedEntry(
-                        cur.time, cur.type, diffContent(cur.data, next.data)
+                        cur.time, cur.type, issue = diffContent(cur, next)
                     )
                 }.plus(
-                    history.last().let { IssueFeedEntry(it.time, it.type, fullContent(it.data)) }
+                    history.last().let { IssueFeedEntry(it.time, it.type, issue = fullContent(it)) }
                 )
-            }.flatMap { it.value }.sortedByDescending { it.time }
+            }.flatMap { it.value }
+        }
+
+    private fun generateCommentFeedEntries(entries: List<IssueCommentHistoryEntry>): List<IssueFeedEntry> =
+        if (entries.isEmpty()) {
+            emptyList()
+        } else {
+            entries.groupBy { it.commentid }.mapValues { (_, history) ->
+                history.zipWithNext().map { (cur, next) ->
+                    IssueFeedEntry(
+                        cur.time, cur.type, comment = diffContent(cur, next)
+                    )
+                }.plus(
+                    history.last().let { IssueFeedEntry(it.time, it.type, comment = fullContent(it)) }
+                )
+            }.flatMap { it.value }
         }
 
     private fun issues(http: HttpRequest, dao: DataAccessObject) {
@@ -126,6 +168,7 @@
             return
         }
         val assignees = http.param("assignee")?.split(',')
+        val comments = http.param("comments") ?: "all"
 
         val days = http.param("days")?.toIntOrNull() ?: 30
 
@@ -133,9 +176,14 @@
         val issueHistory = if (assignees == null) issuesFromDb else
             issuesFromDb.filter { assignees.contains(it.currentAssignee) }
 
-        // TODO: add comment history depending on parameter
+        val commentsFromDb = dao.listIssueCommentHistory(project.id, days)
+        val commentHistory = when (comments) {
+            "all" -> commentsFromDb
+            "new" -> commentsFromDb.filter { it.type == IssueHistoryType.NewComment }
+            else -> emptyList()
+        }
 
-        http.view = IssueFeed(project, generateFeedEntries(issueHistory))
+        http.view = IssueFeed(project, generateFeedEntries(issueHistory, commentHistory))
         http.renderFeed("issues-feed")
     }
 }
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Sat Nov 27 12:12:20 2021 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Sat Nov 27 13:03:57 2021 +0100
@@ -531,7 +531,7 @@
                     if (!newComment.isNullOrBlank()) {
                         comment.comment = newComment
                         dao.updateComment(comment)
-                        dao.insertHistoryEvent(comment)
+                        dao.insertHistoryEvent(issue, comment)
                     } else {
                         logger().debug("Not updating comment ${comment.id} because nothing changed.")
                     }
@@ -545,7 +545,7 @@
                     comment = http.param("comment") ?: ""
                 }
                 val newId = dao.insertComment(comment)
-                dao.insertHistoryEvent(comment, newId)
+                dao.insertHistoryEvent(issue, comment, newId)
             }
 
             http.renderCommit("${issuesHref}${issue.id}")
@@ -602,7 +602,7 @@
                         comment = newComment
                     }
                     val commentid = dao.insertComment(comment)
-                    dao.insertHistoryEvent(comment, commentid)
+                    dao.insertHistoryEvent(issue, comment, commentid)
                 }
                 issue.id
             }
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt	Sat Nov 27 12:12:20 2021 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt	Sat Nov 27 13:03:57 2021 +0100
@@ -44,10 +44,18 @@
     var resolved: String,
 )
 
+class CommentDiff(
+    val issueid: Int,
+    val id: Int,
+    val currentSubject: String,
+    val comment: String
+)
+
 class IssueFeedEntry(
     val time: Timestamp,
     val type: IssueHistoryType,
-    val issue: IssueDiff
+    val issue: IssueDiff? = null,
+    val comment: CommentDiff? = null
 )
 
 class IssueFeed(
--- a/src/main/webapp/WEB-INF/jsp/issues-feed.jsp	Sat Nov 27 12:12:20 2021 +0100
+++ b/src/main/webapp/WEB-INF/jsp/issues-feed.jsp	Sat Nov 27 13:03:57 2021 +0100
@@ -27,34 +27,50 @@
 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
 <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssueFeed" scope="request"/>
 <channel>
-    <title><c:out value="${viewmodel.project.name}"/> | <fmt:message key="feed.issues.title"/></title>
+    <title>
+        <c:out value="${viewmodel.project.name}"/>
+        |<fmt:message key="feed.issues.title"/></title>
     <description><fmt:message key="feed.issues.description"/></description>
     <link>${baseHref}projects/${viewmodel.project.node}</link>
     <language>${pageContext.response.locale.language}</language>
-    <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>
+    <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.entries}" var="entry">
-        <c:set var="issue" value="${entry.issue}"/>
         <item>
-            <title>[<fmt:message key="feed.issues.type.${entry.type}"/>] #${issue.id} - <c:out value="${issue.currentSubject}"/></title>
-            <description><![CDATA[
-                <h1>#${issue.id} - ${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>
-                <div style="white-space: pre-wrap;">${issue.description}</div>
-            ]]></description>
-            <category><fmt:message key="issue.category.${issue.category}"/></category>
-            <c:set var="link" value="${baseHref}projects/${viewmodel.project.node}/issues/-/-/${issue.id}"/>
+            <c:choose>
+                <c:when test="${not empty entry.issue}">
+                    <c:set var="issue" value="${entry.issue}"/>
+                    <c:set var="link" value="${baseHref}projects/${viewmodel.project.node}/issues/-/-/${issue.id}"/>
+                    <title>[<fmt:message key="feed.issues.type.${entry.type}"/>] #${issue.id} - <c:out value="${issue.currentSubject}"/></title>
+                    <description><![CDATA[
+                        <h1>#${issue.id} - ${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>
+                        <div style="white-space: pre-wrap;">${issue.description}</div>
+                    ]]></description>
+                    <category><fmt:message key="issue.category.${issue.category}"/></category>
+                </c:when>
+                <c:when test="${not empty entry.comment}">
+                    <c:set var="comment" value="${entry.comment}"/>
+                    <c:set var="link" value="${baseHref}projects/${viewmodel.project.node}/issues/-/-/${comment.issueid}"/>
+                    <title>[<fmt:message key="feed.issues.type.${entry.type}"/>] #${comment.issueid} - <c:out value="${comment.currentSubject}"/></title>
+                    <description><![CDATA[
+                        <div style="white-space: pre-wrap;">${comment.comment}</div>
+                    ]]></description>
+                    <category><fmt:message key="feed.issues.type.${entry.type}"/></category>
+                </c:when>
+            </c:choose>
             <link>${link}</link>
             <guid isPermaLink="true">${link}</guid>
-            <pubDate><fmt:formatDate value="${entry.time}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate>
+            <pubDate><fmt:formatDate value="${entry.time}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz"/></pubDate>
         </item>
     </c:forEach>
 </channel>

mercurial