Sat, 09 Oct 2021 20:05:39 +0200
change rss feed to display the issue history
TODO: diffs and comments
--- 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>