# HG changeset patch # User Mike Becker # Date 1629386443 -7200 # Node ID 296e12ff8d1cb1931d56fe369ed2d1ec8919f940 # Parent dcb1d5a7ea3a4c9d198c82d0306ae8f9f6f5d58b #109 adds Stasi that collects intel for the feed diff -r dcb1d5a7ea3a -r 296e12ff8d1c setup/postgres/psql_create_tables.sql --- a/setup/postgres/psql_create_tables.sql Thu Aug 19 14:51:04 2021 +0200 +++ b/setup/postgres/psql_create_tables.sql Thu Aug 19 17:20:43 2021 +0200 @@ -101,6 +101,35 @@ resolved integer references lpit_version (versionid) ); +create type issue_history_event as enum ( + 'New', + 'Update', + 'NewComment', + 'UpdateComment' + ); + +create table lpit_issue_history_event +( + eventid serial primary key, + issueid integer not null references lpit_issue (issueid) on delete cascade, + time timestamp with time zone not null default now(), + type issue_history_event not null +); + +create table lpit_issue_history_data +( + eventid integer not null references lpit_issue_history_event (eventid) on delete cascade, + component text, + status issue_status not null, + category issue_category not null, + subject text not null, + description text, + assignee text, + eta date, + affected text, + resolved text +); + create table lpit_issue_comment ( commentid serial primary key, @@ -111,3 +140,11 @@ updatecount integer not null default 0, comment text not null ); + +create table lpit_issue_comment_history +( + commentid integer not null references lpit_issue_comment (commentid) on delete cascade, + eventid integer not null references lpit_issue_history_event (eventid) on delete cascade, + comment text not null +); + diff -r dcb1d5a7ea3a -r 296e12ff8d1c src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Thu Aug 19 14:51:04 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Thu Aug 19 17:20:43 2021 +0200 @@ -76,6 +76,9 @@ fun listComments(issue: Issue): List fun findComment(id: Int): IssueComment? - fun insertComment(issueComment: IssueComment) + fun insertComment(issueComment: IssueComment): Int fun updateComment(issueComment: IssueComment) -} \ No newline at end of file + + fun insertHistoryEvent(issue: Issue, newId: Int = 0) + fun insertHistoryEvent(issueComment: IssueComment, newId: Int = 0) +} diff -r dcb1d5a7ea3a -r 296e12ff8d1c src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Thu Aug 19 14:51:04 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Thu Aug 19 17:20:43 2021 +0200 @@ -26,6 +26,7 @@ package de.uapcore.lightpit.dao import de.uapcore.lightpit.entities.* +import de.uapcore.lightpit.types.IssueHistoryType import de.uapcore.lightpit.types.WebColor import de.uapcore.lightpit.util.* import de.uapcore.lightpit.viewmodel.ComponentSummary @@ -565,7 +566,7 @@ val id = withStatement( """ insert into lpit_issue (component, status, category, subject, description, assignee, eta, affected, resolved, project) - values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?) + values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?, ?) returning issueid """.trimIndent() ) { @@ -591,6 +592,36 @@ } } + override fun insertHistoryEvent(issue: Issue, newId: Int) { + val type = if (newId > 0) IssueHistoryType.New else IssueHistoryType.Update + 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") { + setInt(1, issueid) + setEnum(2, type) + querySingle { it.getInt(1) }!! + } + withStatement( + """ + insert into lpit_issue_history_data (component, status, category, subject, 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) + setDateOrNull(7, issue.eta) + setStringOrNull(8, issue.affected?.name) + setStringOrNull(9, issue.resolved?.name) + setInt(10, eventid) + executeUpdate() + } + } + // // @@ -616,20 +647,20 @@ querySingle { it.extractIssueComment() } } - override fun insertComment(issueComment: IssueComment) { + override fun insertComment(issueComment: IssueComment): Int = useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate -> - withStatement("insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?)") { + withStatement("insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?) returning commentid") { with(issueComment) { updateIssueDate.setInt(1, issueid) setInt(1, issueid) setStringSafe(2, comment) setIntOrNull(3, author?.id) } - executeUpdate() + val commentid = querySingle { it.getInt(1) }!! updateIssueDate.executeUpdate() + commentid } } - } override fun updateComment(issueComment: IssueComment) { useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate -> @@ -644,5 +675,25 @@ } } } + + + override fun insertHistoryEvent(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") { + setInt(1, issueComment.issueid) + setEnum(2, type) + querySingle { it.getInt(1) }!! + } + withStatement("insert into lpit_issue_comment_history (commentid, eventid, comment) values (?,?,?)") { + setInt(1, commentid) + setInt(2, eventid) + setString(3, issueComment.comment) + executeUpdate() + } + } + // } \ No newline at end of file diff -r dcb1d5a7ea3a -r 296e12ff8d1c src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt --- a/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt Thu Aug 19 14:51:04 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt Thu Aug 19 17:20:43 2021 +0200 @@ -52,5 +52,14 @@ * An issue is overdue, if it is not done and the ETA is before the current time. */ val overdue get() = status.phase != IssueStatusPhase.Done && eta?.before(Date(System.currentTimeMillis())) ?: false + + fun hasChanged(reference: Issue) = !(component == reference.component && + status == reference.status && + category == reference.category && + subject == reference.subject && + description == reference.description && + eta == reference.eta && + affected == reference.affected && + resolved == reference.resolved) } diff -r dcb1d5a7ea3a -r 296e12ff8d1c src/main/kotlin/de/uapcore/lightpit/entities/User.kt --- a/src/main/kotlin/de/uapcore/lightpit/entities/User.kt Thu Aug 19 14:51:04 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/User.kt Thu Aug 19 17:20:43 2021 +0200 @@ -31,11 +31,18 @@ var givenname: String? = null var lastname: String? = null + /** + * The display name without mail address. + */ val shortDisplayname: String get() { val str = "${givenname ?: ""} ${lastname ?: ""}" return if (str.isBlank()) username else str.trim() } + /** + * Shows the full name plus mail address. + * If neither given name nor lastname are provided, the username is used instead. + */ val displayname: String get() = if (mail.isNullOrBlank()) shortDisplayname else "$shortDisplayname <$mail>" } \ No newline at end of file diff -r dcb1d5a7ea3a -r 296e12ff8d1c src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt --- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Thu Aug 19 14:51:04 2021 +0200 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Thu Aug 19 17:20:43 2021 +0200 @@ -25,11 +25,8 @@ package de.uapcore.lightpit.servlet -import de.uapcore.lightpit.AbstractServlet -import de.uapcore.lightpit.HttpRequest -import de.uapcore.lightpit.boolValidator +import de.uapcore.lightpit.* import de.uapcore.lightpit.dao.DataAccessObject -import de.uapcore.lightpit.dateOptValidator import de.uapcore.lightpit.entities.* import de.uapcore.lightpit.types.IssueCategory import de.uapcore.lightpit.types.IssueStatus @@ -524,10 +521,20 @@ val commentId = http.param("commentid")?.toIntOrNull() ?: -1 if (commentId > 0) { val comment = dao.findComment(commentId) - val originalAuthor = comment?.author?.username + if (comment == null) { + http.response.sendError(404) + return + } + val originalAuthor = comment.author?.username if (originalAuthor != null && originalAuthor == http.remoteUser) { - comment.comment = http.param("comment") ?: "" - dao.updateComment(comment) + val newComment = http.param("comment") + if (!newComment.isNullOrBlank()) { + comment.comment = newComment + dao.updateComment(comment) + dao.insertHistoryEvent(comment) + } else { + logger().debug("Not updating comment ${comment.id} because nothing changed.") + } } else { http.response.sendError(403) return @@ -537,7 +544,8 @@ author = http.remoteUser?.let { dao.findUserByName(it) } comment = http.param("comment") ?: "" } - dao.insertComment(comment) + val newId = dao.insertComment(comment) + dao.insertHistoryEvent(comment, newId) } http.renderCommit("${issuesHref}${issue.id}") @@ -570,15 +578,31 @@ } val openId = if (issue.id < 0) { - dao.insertIssue(issue) + val id = dao.insertIssue(issue) + dao.insertHistoryEvent(issue, id) + id } else { - dao.updateIssue(issue) + val reference = dao.findIssue(issue.id) + if (reference == null) { + http.response.sendError(404) + return + } + + if (issue.hasChanged(reference)) { + dao.updateIssue(issue) + dao.insertHistoryEvent(issue) + } else { + logger().debug("Not updating issue ${issue.id} because nothing changed.") + } + val newComment = http.param("comment") if (!newComment.isNullOrBlank()) { - dao.insertComment(IssueComment(-1, issue.id).apply { + val comment = IssueComment(-1, issue.id).apply { author = http.remoteUser?.let { dao.findUserByName(it) } comment = newComment - }) + } + val commentid = dao.insertComment(comment) + dao.insertHistoryEvent(comment, commentid) } issue.id } diff -r dcb1d5a7ea3a -r 296e12ff8d1c src/main/kotlin/de/uapcore/lightpit/types/IssueHistoryType.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/kotlin/de/uapcore/lightpit/types/IssueHistoryType.kt Thu Aug 19 17:20:43 2021 +0200 @@ -0,0 +1,33 @@ +/* + * 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.types + +enum class IssueHistoryType { + New, + Update, + NewComment, + UpdateComment +}