Sat, 27 Nov 2021 13:03:57 +0100
#109 add comment history
--- 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>