Sat, 27 Nov 2021 13:03:57 +0100
#109 add comment history
1.1 --- a/setup/postgres/psql_create_tables.sql Sat Nov 27 12:12:20 2021 +0100 1.2 +++ b/setup/postgres/psql_create_tables.sql Sat Nov 27 13:03:57 2021 +0100 1.3 @@ -112,6 +112,7 @@ 1.4 ( 1.5 eventid serial primary key, 1.6 issueid integer not null references lpit_issue (issueid) on delete cascade, 1.7 + subject text not null, 1.8 time timestamp with time zone not null default now(), 1.9 type issue_history_event not null 1.10 ); 1.11 @@ -122,7 +123,6 @@ 1.12 component text, 1.13 status issue_status not null, 1.14 category issue_category not null, 1.15 - subject text not null, 1.16 description text, 1.17 assignee text, 1.18 eta date,
2.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Sat Nov 27 12:12:20 2021 +0100 2.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Sat Nov 27 13:03:57 2021 +0100 2.3 @@ -80,7 +80,7 @@ 2.4 fun updateComment(issueComment: IssueComment) 2.5 2.6 fun insertHistoryEvent(issue: Issue, newId: Int = 0) 2.7 - fun insertHistoryEvent(issueComment: IssueComment, newId: Int = 0) 2.8 + fun insertHistoryEvent(issue: Issue, issueComment: IssueComment, newId: Int = 0) 2.9 2.10 /** 2.11 * Lists the issue history of the project with [projectId] for the past [days].
3.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Sat Nov 27 12:12:20 2021 +0100 3.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Sat Nov 27 13:03:57 2021 +0100 3.3 @@ -597,28 +597,27 @@ 3.4 val issueid = if (newId > 0) newId else issue.id 3.5 3.6 val eventid = 3.7 - withStatement("insert into lpit_issue_history_event(issueid, type) values (?,?::issue_history_event) returning eventid") { 3.8 + withStatement("insert into lpit_issue_history_event(issueid, subject, type) values (?,?,?::issue_history_event) returning eventid") { 3.9 setInt(1, issueid) 3.10 - setEnum(2, type) 3.11 + setString(2, issue.subject) 3.12 + setEnum(3, type) 3.13 querySingle { it.getInt(1) }!! 3.14 } 3.15 withStatement( 3.16 """ 3.17 - insert into lpit_issue_history_data (component, status, category, subject, description, assignee, assignee_username, eta, affected, resolved, eventid) 3.18 - values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?, ?, ?) 3.19 + insert into lpit_issue_history_data (component, status, category, description, assignee, eta, affected, resolved, eventid) 3.20 + values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?) 3.21 """.trimIndent() 3.22 ) { 3.23 setStringOrNull(1, issue.component?.name) 3.24 setEnum(2, issue.status) 3.25 setEnum(3, issue.category) 3.26 - setString(4, issue.subject) 3.27 - setStringOrNull(5, issue.description) 3.28 - setStringOrNull(6, issue.assignee?.shortDisplayname) 3.29 - setStringOrNull(7, issue.assignee?.username) 3.30 - setDateOrNull(8, issue.eta) 3.31 - setStringOrNull(9, issue.affected?.name) 3.32 - setStringOrNull(10, issue.resolved?.name) 3.33 - setInt(11, eventid) 3.34 + setStringOrNull(4, issue.description) 3.35 + setStringOrNull(5, issue.assignee?.shortDisplayname) 3.36 + setDateOrNull(6, issue.eta) 3.37 + setStringOrNull(7, issue.affected?.name) 3.38 + setStringOrNull(8, issue.resolved?.name) 3.39 + setInt(9, eventid) 3.40 executeUpdate() 3.41 } 3.42 } 3.43 @@ -678,17 +677,18 @@ 3.44 } 3.45 3.46 3.47 - override fun insertHistoryEvent(issueComment: IssueComment, newId: Int) { 3.48 + override fun insertHistoryEvent(issue: Issue, issueComment: IssueComment, newId: Int) { 3.49 val type = if (newId > 0) IssueHistoryType.NewComment else IssueHistoryType.UpdateComment 3.50 val commentid = if (newId > 0) newId else issueComment.id 3.51 3.52 val eventid = 3.53 - withStatement("insert into lpit_issue_history_event(issueid, type) values (?,?::issue_history_event) returning eventid") { 3.54 + withStatement("insert into lpit_issue_history_event(issueid, subject, type) values (?,?,?::issue_history_event) returning eventid") { 3.55 setInt(1, issueComment.issueid) 3.56 - setEnum(2, type) 3.57 + setString(1, issue.subject) 3.58 + setEnum(3, type) 3.59 querySingle { it.getInt(1) }!! 3.60 } 3.61 - withStatement("insert into lpit_issue_comment_history (commentid, eventid, comment) values (?,?,?)") { 3.62 + withStatement("insert into lpit_issue_comment_history (commentid, eventid, comment) values (?,?,?,?)") { 3.63 setInt(1, commentid) 3.64 setInt(2, eventid) 3.65 setString(3, issueComment.comment) 3.66 @@ -718,20 +718,19 @@ 3.67 queryAll { rs-> 3.68 with(rs) { 3.69 IssueHistoryEntry( 3.70 - getTimestamp("time"), 3.71 - getEnum("type"), 3.72 - getString("current_assignee"), 3.73 - IssueHistoryData(getInt("issueid"), 3.74 - component = getString("component") ?: "", 3.75 - status = getEnum("status"), 3.76 - category = getEnum("category"), 3.77 - subject = getString("subject"), 3.78 - description = getString("description") ?: "", 3.79 - assignee = getString("assignee") ?: "", 3.80 - eta = getDate("eta"), 3.81 - affected = getString("affected") ?: "", 3.82 - resolved = getString("resolved") ?: "" 3.83 - ) 3.84 + subject = getString("subject"), 3.85 + time = getTimestamp("time"), 3.86 + type = getEnum("type"), 3.87 + currentAssignee = getString("current_assignee"), 3.88 + issueid = getInt("issueid"), 3.89 + component = getString("component") ?: "", 3.90 + status = getEnum("status"), 3.91 + category = getEnum("category"), 3.92 + description = getString("description") ?: "", 3.93 + assignee = getString("assignee") ?: "", 3.94 + eta = getDate("eta"), 3.95 + affected = getString("affected") ?: "", 3.96 + resolved = getString("resolved") ?: "" 3.97 ) 3.98 } 3.99 } 3.100 @@ -755,13 +754,13 @@ 3.101 queryAll { rs-> 3.102 with(rs) { 3.103 IssueCommentHistoryEntry( 3.104 - getTimestamp("time"), 3.105 - getEnum("type"), 3.106 - getString("current_assignee"), 3.107 - IssueCommentHistoryData( 3.108 - getInt("commentid"), 3.109 - getString("comment") 3.110 - ) 3.111 + subject = getString("subject"), 3.112 + time = getTimestamp("time"), 3.113 + type = getEnum("type"), 3.114 + currentAssignee = getString("current_assignee"), 3.115 + issueid = getInt("issueid"), 3.116 + commentid = getInt("commentid"), 3.117 + comment = getString("comment") 3.118 ) 3.119 } 3.120 }
4.1 --- a/src/main/kotlin/de/uapcore/lightpit/entities/IssueHistoryEntry.kt Sat Nov 27 12:12:20 2021 +0100 4.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/IssueHistoryEntry.kt Sat Nov 27 13:03:57 2021 +0100 4.3 @@ -31,12 +31,15 @@ 4.4 import java.sql.Date 4.5 import java.sql.Timestamp 4.6 4.7 -class IssueHistoryData( 4.8 - val id: Int, 4.9 +class IssueHistoryEntry( 4.10 + val subject: String, 4.11 + val time: Timestamp, 4.12 + val type: IssueHistoryType, 4.13 + val currentAssignee: String?, 4.14 + val issueid: Int, 4.15 val component: String, 4.16 val status: IssueStatus, 4.17 val category: IssueCategory, 4.18 - val subject: String, 4.19 val description: String, 4.20 val assignee: String, 4.21 val eta: Date?, 4.22 @@ -44,21 +47,12 @@ 4.23 val resolved: String, 4.24 ) 4.25 4.26 -class IssueCommentHistoryData( 4.27 - val commentid: Int, 4.28 - val comment: String 4.29 -) 4.30 - 4.31 -class IssueHistoryEntry( 4.32 +class IssueCommentHistoryEntry( 4.33 + val subject: String, 4.34 val time: Timestamp, 4.35 val type: IssueHistoryType, 4.36 val currentAssignee: String?, 4.37 - val data: IssueHistoryData 4.38 -) 4.39 - 4.40 -class IssueCommentHistoryEntry( 4.41 - val time: Timestamp, 4.42 - val type: IssueHistoryType, 4.43 - val currentAssignee: String?, 4.44 - val data: IssueCommentHistoryData 4.45 + val issueid: Int, 4.46 + val commentid: Int, 4.47 + val comment: String, 4.48 ) 4.49 \ No newline at end of file
5.1 --- a/src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt Sat Nov 27 12:12:20 2021 +0100 5.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt Sat Nov 27 13:03:57 2021 +0100 5.3 @@ -30,8 +30,10 @@ 5.4 import de.uapcore.lightpit.AbstractServlet 5.5 import de.uapcore.lightpit.HttpRequest 5.6 import de.uapcore.lightpit.dao.DataAccessObject 5.7 -import de.uapcore.lightpit.entities.IssueHistoryData 5.8 +import de.uapcore.lightpit.entities.IssueCommentHistoryEntry 5.9 import de.uapcore.lightpit.entities.IssueHistoryEntry 5.10 +import de.uapcore.lightpit.types.IssueHistoryType 5.11 +import de.uapcore.lightpit.viewmodel.CommentDiff 5.12 import de.uapcore.lightpit.viewmodel.IssueDiff 5.13 import de.uapcore.lightpit.viewmodel.IssueFeed 5.14 import de.uapcore.lightpit.viewmodel.IssueFeedEntry 5.15 @@ -46,8 +48,36 @@ 5.16 get("/%project/issues.rss", this::issues) 5.17 } 5.18 5.19 - private fun fullContent(issue: IssueHistoryData) = IssueDiff( 5.20 - issue.id, 5.21 + val diffGenerator by lazyOf(DiffRowGenerator.create() 5.22 + .showInlineDiffs(true) 5.23 + .mergeOriginalRevised(true) 5.24 + .inlineDiffByWord(true) 5.25 + .oldTag { start -> if (start) "<strike style=\"color:red\">" else "</strike>" } 5.26 + .newTag { start -> if (start) "<i style=\"color: green\">" else "</i>" } 5.27 + .build() 5.28 + ) 5.29 + 5.30 + private fun fullContent(data: IssueCommentHistoryEntry) = 5.31 + CommentDiff( 5.32 + data.issueid, 5.33 + data.commentid, 5.34 + data.subject, 5.35 + data.comment.replace("\r", "") 5.36 + ) 5.37 + 5.38 + private fun diffContent(cur: IssueCommentHistoryEntry, next: IssueCommentHistoryEntry) = 5.39 + CommentDiff( 5.40 + cur.issueid, 5.41 + cur.commentid, 5.42 + cur.subject, 5.43 + diffGenerator.generateDiffRows( 5.44 + next.comment.replace("\r", "").split('\n'), 5.45 + cur.comment.replace("\r", "").split('\n') 5.46 + ).joinToString("\n", transform = DiffRow::getOldLine) 5.47 + ) 5.48 + 5.49 + private fun fullContent(issue: IssueHistoryEntry) = IssueDiff( 5.50 + issue.issueid, 5.51 issue.subject, 5.52 issue.component, 5.53 issue.status.name, 5.54 @@ -60,19 +90,10 @@ 5.55 issue.resolved 5.56 ) 5.57 5.58 - private fun diffContent(cur: IssueHistoryData, next: IssueHistoryData): IssueDiff { 5.59 - val generator = DiffRowGenerator.create() 5.60 - .showInlineDiffs(true) 5.61 - .mergeOriginalRevised(true) 5.62 - .inlineDiffByWord(true) 5.63 - .oldTag { start -> if (start) "<strike style=\"color:red\">" else "</strike>" } 5.64 - .newTag { start -> if (start) "<i style=\"color: green\">" else "</i>" } 5.65 - .build() 5.66 - 5.67 + private fun diffContent(cur: IssueHistoryEntry, next: IssueHistoryEntry): IssueDiff { 5.68 val prev = fullContent(next) 5.69 val diff = fullContent(cur) 5.70 - 5.71 - val result = generator.generateDiffRows( 5.72 + val result = diffGenerator.generateDiffRows( 5.73 listOf( 5.74 prev.subject, prev.component, prev.status, 5.75 prev.category, prev.assignee, prev.eta, prev.affected, prev.resolved 5.76 @@ -92,7 +113,7 @@ 5.77 diff.affected = result[6].oldLine 5.78 diff.resolved = result[7].oldLine 5.79 5.80 - diff.description = generator.generateDiffRows( 5.81 + diff.description = diffGenerator.generateDiffRows( 5.82 prev.description.split('\n'), 5.83 diff.description.split('\n') 5.84 ).joinToString("\n", transform = DiffRow::getOldLine) 5.85 @@ -102,21 +123,42 @@ 5.86 5.87 /** 5.88 * Generates the feed entries. 5.89 - * Assumes that [historyEntry] is already sorted by timestamp (descending). 5.90 + * Assumes that [issueEntries] and [commentEntries] are already sorted by timestamp (descending). 5.91 */ 5.92 - private fun generateFeedEntries(historyEntry: List<IssueHistoryEntry>): List<IssueFeedEntry> = 5.93 - if (historyEntry.isEmpty()) { 5.94 + private fun generateFeedEntries( 5.95 + issueEntries: List<IssueHistoryEntry>, 5.96 + commentEntries: List<IssueCommentHistoryEntry> 5.97 + ): List<IssueFeedEntry> = 5.98 + (generateIssueFeedEntries(issueEntries) + generateCommentFeedEntries(commentEntries)).sortedByDescending { it.time } 5.99 + 5.100 + private fun generateIssueFeedEntries(entries: List<IssueHistoryEntry>): List<IssueFeedEntry> = 5.101 + if (entries.isEmpty()) { 5.102 emptyList() 5.103 } else { 5.104 - historyEntry.groupBy { it.data.id }.mapValues { (_, history) -> 5.105 + entries.groupBy { it.issueid }.mapValues { (_, history) -> 5.106 history.zipWithNext().map { (cur, next) -> 5.107 IssueFeedEntry( 5.108 - cur.time, cur.type, diffContent(cur.data, next.data) 5.109 + cur.time, cur.type, issue = diffContent(cur, next) 5.110 ) 5.111 }.plus( 5.112 - history.last().let { IssueFeedEntry(it.time, it.type, fullContent(it.data)) } 5.113 + history.last().let { IssueFeedEntry(it.time, it.type, issue = fullContent(it)) } 5.114 ) 5.115 - }.flatMap { it.value }.sortedByDescending { it.time } 5.116 + }.flatMap { it.value } 5.117 + } 5.118 + 5.119 + private fun generateCommentFeedEntries(entries: List<IssueCommentHistoryEntry>): List<IssueFeedEntry> = 5.120 + if (entries.isEmpty()) { 5.121 + emptyList() 5.122 + } else { 5.123 + entries.groupBy { it.commentid }.mapValues { (_, history) -> 5.124 + history.zipWithNext().map { (cur, next) -> 5.125 + IssueFeedEntry( 5.126 + cur.time, cur.type, comment = diffContent(cur, next) 5.127 + ) 5.128 + }.plus( 5.129 + history.last().let { IssueFeedEntry(it.time, it.type, comment = fullContent(it)) } 5.130 + ) 5.131 + }.flatMap { it.value } 5.132 } 5.133 5.134 private fun issues(http: HttpRequest, dao: DataAccessObject) { 5.135 @@ -126,6 +168,7 @@ 5.136 return 5.137 } 5.138 val assignees = http.param("assignee")?.split(',') 5.139 + val comments = http.param("comments") ?: "all" 5.140 5.141 val days = http.param("days")?.toIntOrNull() ?: 30 5.142 5.143 @@ -133,9 +176,14 @@ 5.144 val issueHistory = if (assignees == null) issuesFromDb else 5.145 issuesFromDb.filter { assignees.contains(it.currentAssignee) } 5.146 5.147 - // TODO: add comment history depending on parameter 5.148 + val commentsFromDb = dao.listIssueCommentHistory(project.id, days) 5.149 + val commentHistory = when (comments) { 5.150 + "all" -> commentsFromDb 5.151 + "new" -> commentsFromDb.filter { it.type == IssueHistoryType.NewComment } 5.152 + else -> emptyList() 5.153 + } 5.154 5.155 - http.view = IssueFeed(project, generateFeedEntries(issueHistory)) 5.156 + http.view = IssueFeed(project, generateFeedEntries(issueHistory, commentHistory)) 5.157 http.renderFeed("issues-feed") 5.158 } 5.159 } 5.160 \ No newline at end of file
6.1 --- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Sat Nov 27 12:12:20 2021 +0100 6.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Sat Nov 27 13:03:57 2021 +0100 6.3 @@ -531,7 +531,7 @@ 6.4 if (!newComment.isNullOrBlank()) { 6.5 comment.comment = newComment 6.6 dao.updateComment(comment) 6.7 - dao.insertHistoryEvent(comment) 6.8 + dao.insertHistoryEvent(issue, comment) 6.9 } else { 6.10 logger().debug("Not updating comment ${comment.id} because nothing changed.") 6.11 } 6.12 @@ -545,7 +545,7 @@ 6.13 comment = http.param("comment") ?: "" 6.14 } 6.15 val newId = dao.insertComment(comment) 6.16 - dao.insertHistoryEvent(comment, newId) 6.17 + dao.insertHistoryEvent(issue, comment, newId) 6.18 } 6.19 6.20 http.renderCommit("${issuesHref}${issue.id}") 6.21 @@ -602,7 +602,7 @@ 6.22 comment = newComment 6.23 } 6.24 val commentid = dao.insertComment(comment) 6.25 - dao.insertHistoryEvent(comment, commentid) 6.26 + dao.insertHistoryEvent(issue, comment, commentid) 6.27 } 6.28 issue.id 6.29 }
7.1 --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt Sat Nov 27 12:12:20 2021 +0100 7.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt Sat Nov 27 13:03:57 2021 +0100 7.3 @@ -44,10 +44,18 @@ 7.4 var resolved: String, 7.5 ) 7.6 7.7 +class CommentDiff( 7.8 + val issueid: Int, 7.9 + val id: Int, 7.10 + val currentSubject: String, 7.11 + val comment: String 7.12 +) 7.13 + 7.14 class IssueFeedEntry( 7.15 val time: Timestamp, 7.16 val type: IssueHistoryType, 7.17 - val issue: IssueDiff 7.18 + val issue: IssueDiff? = null, 7.19 + val comment: CommentDiff? = null 7.20 ) 7.21 7.22 class IssueFeed(
8.1 --- a/src/main/webapp/WEB-INF/jsp/issues-feed.jsp Sat Nov 27 12:12:20 2021 +0100 8.2 +++ b/src/main/webapp/WEB-INF/jsp/issues-feed.jsp Sat Nov 27 13:03:57 2021 +0100 8.3 @@ -27,34 +27,50 @@ 8.4 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> 8.5 <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssueFeed" scope="request"/> 8.6 <channel> 8.7 - <title><c:out value="${viewmodel.project.name}"/> | <fmt:message key="feed.issues.title"/></title> 8.8 + <title> 8.9 + <c:out value="${viewmodel.project.name}"/> 8.10 + |<fmt:message key="feed.issues.title"/></title> 8.11 <description><fmt:message key="feed.issues.description"/></description> 8.12 <link>${baseHref}projects/${viewmodel.project.node}</link> 8.13 <language>${pageContext.response.locale.language}</language> 8.14 - <pubDate><fmt:formatDate value="${viewmodel.lastModified}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate> 8.15 - <lastBuildDate><fmt:formatDate value="${viewmodel.lastModified}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></lastBuildDate> 8.16 + <pubDate><fmt:formatDate value="${viewmodel.lastModified}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz"/></pubDate> 8.17 + <lastBuildDate><fmt:formatDate value="${viewmodel.lastModified}" 8.18 + pattern="EEE, dd MMM yyyy HH:mm:ss zzz"/></lastBuildDate> 8.19 8.20 <c:forEach items="${viewmodel.entries}" var="entry"> 8.21 - <c:set var="issue" value="${entry.issue}"/> 8.22 <item> 8.23 - <title>[<fmt:message key="feed.issues.type.${entry.type}"/>] #${issue.id} - <c:out value="${issue.currentSubject}"/></title> 8.24 - <description><![CDATA[ 8.25 - <h1>#${issue.id} - ${issue.subject}</h1> 8.26 - <div><b><fmt:message key="component"/></b>: ${issue.component}</div> 8.27 - <div><b><fmt:message key="issue.category"/></b>: ${issue.category}</div> 8.28 - <div><b><fmt:message key="issue.status"/></b>: ${issue.status}</div> 8.29 - <div><b><fmt:message key="issue.resolved-versions"/></b>: ${issue.resolved}</div> 8.30 - <div><b><fmt:message key="issue.affected-versions"/></b>: ${issue.affected}</div> 8.31 - <div><b><fmt:message key="issue.assignee"/></b>: ${issue.assignee}</div> 8.32 - <div><b><fmt:message key="issue.eta"/></b>: ${issue.eta}</div> 8.33 - <h2><fmt:message key="issue.description"/></h2> 8.34 - <div style="white-space: pre-wrap;">${issue.description}</div> 8.35 - ]]></description> 8.36 - <category><fmt:message key="issue.category.${issue.category}"/></category> 8.37 - <c:set var="link" value="${baseHref}projects/${viewmodel.project.node}/issues/-/-/${issue.id}"/> 8.38 + <c:choose> 8.39 + <c:when test="${not empty entry.issue}"> 8.40 + <c:set var="issue" value="${entry.issue}"/> 8.41 + <c:set var="link" value="${baseHref}projects/${viewmodel.project.node}/issues/-/-/${issue.id}"/> 8.42 + <title>[<fmt:message key="feed.issues.type.${entry.type}"/>] #${issue.id} - <c:out value="${issue.currentSubject}"/></title> 8.43 + <description><![CDATA[ 8.44 + <h1>#${issue.id} - ${issue.subject}</h1> 8.45 + <div><b><fmt:message key="component"/></b>: ${issue.component}</div> 8.46 + <div><b><fmt:message key="issue.category"/></b>: ${issue.category}</div> 8.47 + <div><b><fmt:message key="issue.status"/></b>: ${issue.status}</div> 8.48 + <div><b><fmt:message key="issue.resolved-versions"/></b>: ${issue.resolved}</div> 8.49 + <div><b><fmt:message key="issue.affected-versions"/></b>: ${issue.affected}</div> 8.50 + <div><b><fmt:message key="issue.assignee"/></b>: ${issue.assignee}</div> 8.51 + <div><b><fmt:message key="issue.eta"/></b>: ${issue.eta}</div> 8.52 + <h2><fmt:message key="issue.description"/></h2> 8.53 + <div style="white-space: pre-wrap;">${issue.description}</div> 8.54 + ]]></description> 8.55 + <category><fmt:message key="issue.category.${issue.category}"/></category> 8.56 + </c:when> 8.57 + <c:when test="${not empty entry.comment}"> 8.58 + <c:set var="comment" value="${entry.comment}"/> 8.59 + <c:set var="link" value="${baseHref}projects/${viewmodel.project.node}/issues/-/-/${comment.issueid}"/> 8.60 + <title>[<fmt:message key="feed.issues.type.${entry.type}"/>] #${comment.issueid} - <c:out value="${comment.currentSubject}"/></title> 8.61 + <description><![CDATA[ 8.62 + <div style="white-space: pre-wrap;">${comment.comment}</div> 8.63 + ]]></description> 8.64 + <category><fmt:message key="feed.issues.type.${entry.type}"/></category> 8.65 + </c:when> 8.66 + </c:choose> 8.67 <link>${link}</link> 8.68 <guid isPermaLink="true">${link}</guid> 8.69 - <pubDate><fmt:formatDate value="${entry.time}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate> 8.70 + <pubDate><fmt:formatDate value="${entry.time}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz"/></pubDate> 8.71 </item> 8.72 </c:forEach> 8.73 </channel>