Sat, 09 Oct 2021 20:05:39 +0200
change rss feed to display the issue history
TODO: diffs and comments
1.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Sat Oct 09 17:46:12 2021 +0200 1.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Sat Oct 09 20:05:39 2021 +0200 1.3 @@ -81,4 +81,6 @@ 1.4 1.5 fun insertHistoryEvent(issue: Issue, newId: Int = 0) 1.6 fun insertHistoryEvent(issueComment: IssueComment, newId: Int = 0) 1.7 + 1.8 + fun listIssueHistory(projectId: Int, days: Int): List<IssueHistoryEntry> 1.9 }
2.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Sat Oct 09 17:46:12 2021 +0200 2.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Sat Oct 09 20:05:39 2021 +0200 2.3 @@ -696,4 +696,43 @@ 2.4 } 2.5 2.6 //</editor-fold> 2.7 + 2.8 + //<editor-fold desc="Issue History"> 2.9 + 2.10 + override fun listIssueHistory(projectId: Int, days: Int) = 2.11 + withStatement( 2.12 + """ 2.13 + select evt.*, evtdata.* 2.14 + from lpit_issue_history_event evt 2.15 + join lpit_issue using (issueid) 2.16 + join lpit_issue_history_data evtdata using (eventid) 2.17 + where project = ? 2.18 + and time > now() - (? * interval '1' day) 2.19 + order by time desc 2.20 + """.trimIndent() 2.21 + ) { 2.22 + setInt(1, projectId) 2.23 + setInt(2, days) 2.24 + queryAll { rs-> 2.25 + with(rs) { 2.26 + IssueHistoryEntry( 2.27 + getTimestamp("time"), 2.28 + getEnum("type"), 2.29 + IssueHistoryData(getInt("issueid"), 2.30 + component = getString("component") ?: "", 2.31 + status = getEnum("status"), 2.32 + category = getEnum("category"), 2.33 + subject = getString("subject"), 2.34 + description = getString("description") ?: "", 2.35 + assignee = getString("assignee") ?: "", 2.36 + eta = getDate("eta"), 2.37 + affected = getString("affected") ?: "", 2.38 + resolved = getString("resolved") ?: "" 2.39 + ) 2.40 + ) 2.41 + } 2.42 + } 2.43 + } 2.44 + 2.45 + //</editor-fold> 2.46 } 2.47 \ No newline at end of file
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/IssueHistoryEntry.kt Sat Oct 09 20:05:39 2021 +0200 3.3 @@ -0,0 +1,47 @@ 3.4 +/* 3.5 + * Copyright 2021 Mike Becker. All rights reserved. 3.6 + * 3.7 + * Redistribution and use in source and binary forms, with or without 3.8 + * modification, are permitted provided that the following conditions are met: 3.9 + * 3.10 + * 1. Redistributions of source code must retain the above copyright 3.11 + * notice, this list of conditions and the following disclaimer. 3.12 + * 3.13 + * 2. Redistributions in binary form must reproduce the above copyright 3.14 + * notice, this list of conditions and the following disclaimer in the 3.15 + * documentation and/or other materials provided with the distribution. 3.16 + * 3.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 3.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 3.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 3.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 3.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 3.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 3.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 3.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 3.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 3.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 3.27 + */ 3.28 + 3.29 +package de.uapcore.lightpit.entities 3.30 + 3.31 +import de.uapcore.lightpit.types.IssueCategory 3.32 +import de.uapcore.lightpit.types.IssueHistoryType 3.33 +import de.uapcore.lightpit.types.IssueStatus 3.34 +import java.sql.Date 3.35 +import java.sql.Timestamp 3.36 + 3.37 +class IssueHistoryData( 3.38 + val id: Int, 3.39 + val component: String, 3.40 + val status: IssueStatus, 3.41 + val category: IssueCategory, 3.42 + val subject: String, 3.43 + val description: String, 3.44 + val assignee: String, 3.45 + val eta: Date?, 3.46 + val affected: String, 3.47 + val resolved: String, 3.48 +) 3.49 + 3.50 +class IssueHistoryEntry(val time: Timestamp, val type: IssueHistoryType, val data: IssueHistoryData) 3.51 \ No newline at end of file
4.1 --- a/src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt Sat Oct 09 17:46:12 2021 +0200 4.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/FeedServlet.kt Sat Oct 09 20:05:39 2021 +0200 4.3 @@ -28,10 +28,12 @@ 4.4 import de.uapcore.lightpit.AbstractServlet 4.5 import de.uapcore.lightpit.HttpRequest 4.6 import de.uapcore.lightpit.dao.DataAccessObject 4.7 -import de.uapcore.lightpit.util.IssueFilter 4.8 -import de.uapcore.lightpit.util.IssueSorter 4.9 -import de.uapcore.lightpit.util.SpecificFilter 4.10 +import de.uapcore.lightpit.entities.IssueHistoryData 4.11 +import de.uapcore.lightpit.entities.IssueHistoryEntry 4.12 +import de.uapcore.lightpit.viewmodel.IssueDiff 4.13 import de.uapcore.lightpit.viewmodel.IssueFeed 4.14 +import de.uapcore.lightpit.viewmodel.IssueFeedEntry 4.15 +import java.text.SimpleDateFormat 4.16 import javax.servlet.annotation.WebServlet 4.17 4.18 @WebServlet(urlPatterns = ["/feed/*"]) 4.19 @@ -41,6 +43,43 @@ 4.20 get("/%project/issues.rss", this::issues) 4.21 } 4.22 4.23 + private fun String.convertLF() = replace("\r", "").replace("\n", "<br>") 4.24 + 4.25 + private fun fullContent(issue: IssueHistoryData) = IssueDiff( 4.26 + issue.id, 4.27 + issue.subject, 4.28 + issue.component, 4.29 + issue.status.name, 4.30 + issue.category.name, 4.31 + issue.subject, 4.32 + issue.description.convertLF(), 4.33 + issue.assignee, 4.34 + issue.eta?.let { SimpleDateFormat("dd.MM.yyyy").format(it) } ?: "", 4.35 + issue.affected, 4.36 + issue.resolved 4.37 + ) 4.38 + 4.39 + private fun diffContent(cur: IssueHistoryData, next: IssueHistoryData): IssueDiff { 4.40 + val prev = fullContent(next) 4.41 + val diff = fullContent(cur) 4.42 + // TODO: compute and apply diff 4.43 + return diff 4.44 + } 4.45 + 4.46 + /** 4.47 + * Generates the feed entries. 4.48 + * Assumes that [historyEntry] is already sorted by timestamp (descending). 4.49 + */ 4.50 + private fun generateFeedEntries(historyEntry: List<IssueHistoryEntry>) = 4.51 + if (historyEntry.isEmpty()) emptyList() 4.52 + else historyEntry.zipWithNext().map { (cur, next) -> 4.53 + IssueFeedEntry( 4.54 + cur.time, cur.type, diffContent(cur.data, next.data) 4.55 + ) 4.56 + }.plus( 4.57 + historyEntry.last().let { IssueFeedEntry(it.time, it.type, fullContent(it.data)) } 4.58 + ) 4.59 + 4.60 private fun issues(http: HttpRequest, dao: DataAccessObject) { 4.61 val project = http.pathParams["project"]?.let { dao.findProjectByNode(it) } 4.62 if (project == null) { 4.63 @@ -48,10 +87,12 @@ 4.64 return 4.65 } 4.66 4.67 - // TODO: add a timestamp filter (e.g. last 30 days) 4.68 - val issues = dao.listIssues(IssueFilter(SpecificFilter(project))).sortedWith(IssueSorter.DEFAULT_ISSUE_SORTER) 4.69 + val days = http.param("days")?.toIntOrNull() ?: 30 4.70 4.71 - http.view = IssueFeed(project, issues) 4.72 + val issueHistory = dao.listIssueHistory(project.id, days) 4.73 + // TODO: add comment history depending on parameter 4.74 + 4.75 + http.view = IssueFeed(project, generateFeedEntries(issueHistory)) 4.76 http.renderFeed("issues-feed") 4.77 } 4.78 } 4.79 \ No newline at end of file
5.1 --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt Sat Oct 09 17:46:12 2021 +0200 5.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/Feeds.kt Sat Oct 09 20:05:39 2021 +0200 5.3 @@ -25,14 +25,35 @@ 5.4 5.5 package de.uapcore.lightpit.viewmodel 5.6 5.7 -import de.uapcore.lightpit.entities.Issue 5.8 import de.uapcore.lightpit.entities.Project 5.9 +import de.uapcore.lightpit.types.IssueHistoryType 5.10 import java.sql.Timestamp 5.11 import java.time.Instant 5.12 5.13 +class IssueDiff( 5.14 + val id: Int, 5.15 + val currentSubject: String, 5.16 + var component: String, 5.17 + var status: String, 5.18 + var category: String, 5.19 + var subject: String, 5.20 + var description: String, 5.21 + var assignee: String, 5.22 + var eta: String, 5.23 + var affected: String, 5.24 + var resolved: String, 5.25 +) 5.26 + 5.27 +class IssueFeedEntry( 5.28 + val time: Timestamp, 5.29 + val type: IssueHistoryType, 5.30 + val issue: IssueDiff 5.31 +) 5.32 + 5.33 class IssueFeed( 5.34 val project: Project, 5.35 - val issues: List<Issue> 5.36 + val entries: List<IssueFeedEntry> 5.37 ) : View() { 5.38 - val lastModified = issues.map(Issue::updated).maxOrNull() ?: Timestamp.from(Instant.now()) 5.39 + val lastModified: Timestamp = 5.40 + entries.map(IssueFeedEntry::time).maxOrNull() ?: Timestamp.from(Instant.now()) 5.41 } 5.42 \ No newline at end of file
6.1 --- a/src/main/resources/localization/strings.properties Sat Oct 09 17:46:12 2021 +0200 6.2 +++ b/src/main/resources/localization/strings.properties Sat Oct 09 20:05:39 2021 +0200 6.3 @@ -59,10 +59,12 @@ 6.4 error.message = Server Message 6.5 error.returnLink = Return to 6.6 error.timestamp = Timestamp 6.7 -feed.issues.created=Issue has been created. 6.8 feed.issues.description=Feed about recently updated issues. 6.9 feed.issues.title=LightPIT Issues 6.10 -feed.issues.updated=Issue has been updated. 6.11 +feed.issues.type.New=New 6.12 +feed.issues.type.Update=Update 6.13 +feed.issues.type.NewComment=Comment 6.14 +feed.issues.type.UpdateComment=Comment Update 6.15 feed=Feed 6.16 issue.affected-versions=Affected 6.17 issue.assignee=Assignee
7.1 --- a/src/main/resources/localization/strings_de.properties Sat Oct 09 17:46:12 2021 +0200 7.2 +++ b/src/main/resources/localization/strings_de.properties Sat Oct 09 20:05:39 2021 +0200 7.3 @@ -58,10 +58,12 @@ 7.4 error.message = Server Nachricht 7.5 error.returnLink = Kehre zurück zu 7.6 error.timestamp = Zeitstempel 7.7 -feed.issues.created=Vorgang wurde erstellt. 7.8 feed.issues.description=Feed \u00fcber k\u00fcrzlich aktualisierte Vorg\u00e4nge. 7.9 feed.issues.title=LightPIT Vorg\u00e4nge 7.10 -feed.issues.updated=Vorgang wurde aktualisiert. 7.11 +feed.issues.type.New=Neu 7.12 +feed.issues.type.Update=Update 7.13 +feed.issues.type.NewComment=Kommentar 7.14 +feed.issues.type.UpdateComment=Kommentar Update 7.15 feed=Feed 7.16 issue.affected-versions=Betroffen 7.17 issue.assignee=Zugewiesen
8.1 --- a/src/main/webapp/WEB-INF/jsp/issues-feed.jsp Sat Oct 09 17:46:12 2021 +0200 8.2 +++ b/src/main/webapp/WEB-INF/jsp/issues-feed.jsp Sat Oct 09 20:05:39 2021 +0200 8.3 @@ -34,21 +34,27 @@ 8.4 <pubDate><fmt:formatDate value="${viewmodel.lastModified}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate> 8.5 <lastBuildDate><fmt:formatDate value="${viewmodel.lastModified}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></lastBuildDate> 8.6 8.7 - <c:forEach items="${viewmodel.issues}" var="issue"> 8.8 + <c:forEach items="${viewmodel.entries}" var="entry"> 8.9 + <c:set var="issue" value="${entry.issue}"/> 8.10 <item> 8.11 - <title><c:if test="${not empty issue.component}"><c:out value="${issue.component.name}"/> - </c:if><c:out value="${issue.subject}"/></title> 8.12 - <description><c:choose> 8.13 - <c:when test="${issue.created eq issue.updated}"> 8.14 - <fmt:message key="feed.issues.created"/> 8.15 - </c:when> 8.16 - <c:otherwise> 8.17 - <fmt:message key="feed.issues.updated"/> 8.18 - </c:otherwise> 8.19 - </c:choose></description> 8.20 + <title>[<fmt:message key="feed.issues.type.${entry.type}"/>] #${issue.id} - <c:out value="${issue.currentSubject}"/></title> 8.21 + <description><![CDATA[ 8.22 + <h1>#${issue.id} - <c:out value="${issue.subject}"/></h1> 8.23 + <div><b><fmt:message key="component"/></b>: ${issue.component}</div> 8.24 + <div><b><fmt:message key="issue.category"/></b>: ${issue.category}</div> 8.25 + <div><b><fmt:message key="issue.status"/></b>: ${issue.status}</div> 8.26 + <div><b><fmt:message key="issue.resolved-versions"/></b>: ${issue.resolved}</div> 8.27 + <div><b><fmt:message key="issue.affected-versions"/></b>: ${issue.affected}</div> 8.28 + <div><b><fmt:message key="issue.assignee"/></b>: ${issue.assignee}</div> 8.29 + <div><b><fmt:message key="issue.eta"/></b>: ${issue.eta}</div> 8.30 + <h2><fmt:message key="issue.description"/></h2> 8.31 + ${issue.description} 8.32 + ]]></description> 8.33 <category><fmt:message key="issue.category.${issue.category}"/></category> 8.34 - <link>${baseHref}projects/${issue.project.node}/issues/-/${empty issue.component ? '-' : issue.component.node}/${issue.id}</link> 8.35 - <guid isPermaLink="true">${baseHref}projects/${issue.project.node}/issues/-/-/${issue.id}</guid> 8.36 - <pubDate><fmt:formatDate value="${issue.updated}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate> 8.37 + <c:set var="link" value="${baseHref}projects/${viewmodel.project.node}/issues/-/-/${issue.id}"/> 8.38 + <link>${link}</link> 8.39 + <guid isPermaLink="true">${link}</guid> 8.40 + <pubDate><fmt:formatDate value="${entry.time}" pattern="EEE, dd MMM yyyy HH:mm:ss zzz" /></pubDate> 8.41 </item> 8.42 </c:forEach> 8.43 </channel>