Thu, 19 Aug 2021 17:20:43 +0200
#109 adds Stasi that collects intel for the feed
1.1 --- a/setup/postgres/psql_create_tables.sql Thu Aug 19 14:51:04 2021 +0200 1.2 +++ b/setup/postgres/psql_create_tables.sql Thu Aug 19 17:20:43 2021 +0200 1.3 @@ -101,6 +101,35 @@ 1.4 resolved integer references lpit_version (versionid) 1.5 ); 1.6 1.7 +create type issue_history_event as enum ( 1.8 + 'New', 1.9 + 'Update', 1.10 + 'NewComment', 1.11 + 'UpdateComment' 1.12 + ); 1.13 + 1.14 +create table lpit_issue_history_event 1.15 +( 1.16 + eventid serial primary key, 1.17 + issueid integer not null references lpit_issue (issueid) on delete cascade, 1.18 + time timestamp with time zone not null default now(), 1.19 + type issue_history_event not null 1.20 +); 1.21 + 1.22 +create table lpit_issue_history_data 1.23 +( 1.24 + eventid integer not null references lpit_issue_history_event (eventid) on delete cascade, 1.25 + component text, 1.26 + status issue_status not null, 1.27 + category issue_category not null, 1.28 + subject text not null, 1.29 + description text, 1.30 + assignee text, 1.31 + eta date, 1.32 + affected text, 1.33 + resolved text 1.34 +); 1.35 + 1.36 create table lpit_issue_comment 1.37 ( 1.38 commentid serial primary key, 1.39 @@ -111,3 +140,11 @@ 1.40 updatecount integer not null default 0, 1.41 comment text not null 1.42 ); 1.43 + 1.44 +create table lpit_issue_comment_history 1.45 +( 1.46 + commentid integer not null references lpit_issue_comment (commentid) on delete cascade, 1.47 + eventid integer not null references lpit_issue_history_event (eventid) on delete cascade, 1.48 + comment text not null 1.49 +); 1.50 +
2.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Thu Aug 19 14:51:04 2021 +0200 2.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt Thu Aug 19 17:20:43 2021 +0200 2.3 @@ -76,6 +76,9 @@ 2.4 2.5 fun listComments(issue: Issue): List<IssueComment> 2.6 fun findComment(id: Int): IssueComment? 2.7 - fun insertComment(issueComment: IssueComment) 2.8 + fun insertComment(issueComment: IssueComment): Int 2.9 fun updateComment(issueComment: IssueComment) 2.10 -} 2.11 \ No newline at end of file 2.12 + 2.13 + fun insertHistoryEvent(issue: Issue, newId: Int = 0) 2.14 + fun insertHistoryEvent(issueComment: IssueComment, newId: Int = 0) 2.15 +}
3.1 --- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Thu Aug 19 14:51:04 2021 +0200 3.2 +++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt Thu Aug 19 17:20:43 2021 +0200 3.3 @@ -26,6 +26,7 @@ 3.4 package de.uapcore.lightpit.dao 3.5 3.6 import de.uapcore.lightpit.entities.* 3.7 +import de.uapcore.lightpit.types.IssueHistoryType 3.8 import de.uapcore.lightpit.types.WebColor 3.9 import de.uapcore.lightpit.util.* 3.10 import de.uapcore.lightpit.viewmodel.ComponentSummary 3.11 @@ -565,7 +566,7 @@ 3.12 val id = withStatement( 3.13 """ 3.14 insert into lpit_issue (component, status, category, subject, description, assignee, eta, affected, resolved, project) 3.15 - values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?) 3.16 + values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?, ?) 3.17 returning issueid 3.18 """.trimIndent() 3.19 ) { 3.20 @@ -591,6 +592,36 @@ 3.21 } 3.22 } 3.23 3.24 + override fun insertHistoryEvent(issue: Issue, newId: Int) { 3.25 + val type = if (newId > 0) IssueHistoryType.New else IssueHistoryType.Update 3.26 + val issueid = if (newId > 0) newId else issue.id 3.27 + 3.28 + val eventid = 3.29 + withStatement("insert into lpit_issue_history_event(issueid, type) values (?,?::issue_history_event) returning eventid") { 3.30 + setInt(1, issueid) 3.31 + setEnum(2, type) 3.32 + querySingle { it.getInt(1) }!! 3.33 + } 3.34 + withStatement( 3.35 + """ 3.36 + insert into lpit_issue_history_data (component, status, category, subject, description, assignee, eta, affected, resolved, eventid) 3.37 + values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?, ?, ?) 3.38 + """.trimIndent() 3.39 + ) { 3.40 + setStringOrNull(1, issue.component?.name) 3.41 + setEnum(2, issue.status) 3.42 + setEnum(3, issue.category) 3.43 + setString(4, issue.subject) 3.44 + setStringOrNull(5, issue.description) 3.45 + setStringOrNull(6, issue.assignee?.shortDisplayname) 3.46 + setDateOrNull(7, issue.eta) 3.47 + setStringOrNull(8, issue.affected?.name) 3.48 + setStringOrNull(9, issue.resolved?.name) 3.49 + setInt(10, eventid) 3.50 + executeUpdate() 3.51 + } 3.52 + } 3.53 + 3.54 //</editor-fold> 3.55 3.56 //<editor-fold desc="IssueComment"> 3.57 @@ -616,20 +647,20 @@ 3.58 querySingle { it.extractIssueComment() } 3.59 } 3.60 3.61 - override fun insertComment(issueComment: IssueComment) { 3.62 + override fun insertComment(issueComment: IssueComment): Int = 3.63 useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate -> 3.64 - withStatement("insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?)") { 3.65 + withStatement("insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?) returning commentid") { 3.66 with(issueComment) { 3.67 updateIssueDate.setInt(1, issueid) 3.68 setInt(1, issueid) 3.69 setStringSafe(2, comment) 3.70 setIntOrNull(3, author?.id) 3.71 } 3.72 - executeUpdate() 3.73 + val commentid = querySingle { it.getInt(1) }!! 3.74 updateIssueDate.executeUpdate() 3.75 + commentid 3.76 } 3.77 } 3.78 - } 3.79 3.80 override fun updateComment(issueComment: IssueComment) { 3.81 useStatement("update lpit_issue set updated = now() where issueid = ?") { updateIssueDate -> 3.82 @@ -644,5 +675,25 @@ 3.83 } 3.84 } 3.85 } 3.86 + 3.87 + 3.88 + override fun insertHistoryEvent(issueComment: IssueComment, newId: Int) { 3.89 + val type = if (newId > 0) IssueHistoryType.NewComment else IssueHistoryType.UpdateComment 3.90 + val commentid = if (newId > 0) newId else issueComment.id 3.91 + 3.92 + val eventid = 3.93 + withStatement("insert into lpit_issue_history_event(issueid, type) values (?,?::issue_history_event) returning eventid") { 3.94 + setInt(1, issueComment.issueid) 3.95 + setEnum(2, type) 3.96 + querySingle { it.getInt(1) }!! 3.97 + } 3.98 + withStatement("insert into lpit_issue_comment_history (commentid, eventid, comment) values (?,?,?)") { 3.99 + setInt(1, commentid) 3.100 + setInt(2, eventid) 3.101 + setString(3, issueComment.comment) 3.102 + executeUpdate() 3.103 + } 3.104 + } 3.105 + 3.106 //</editor-fold> 3.107 } 3.108 \ No newline at end of file
4.1 --- a/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt Thu Aug 19 14:51:04 2021 +0200 4.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/Issue.kt Thu Aug 19 17:20:43 2021 +0200 4.3 @@ -52,5 +52,14 @@ 4.4 * An issue is overdue, if it is not done and the ETA is before the current time. 4.5 */ 4.6 val overdue get() = status.phase != IssueStatusPhase.Done && eta?.before(Date(System.currentTimeMillis())) ?: false 4.7 + 4.8 + fun hasChanged(reference: Issue) = !(component == reference.component && 4.9 + status == reference.status && 4.10 + category == reference.category && 4.11 + subject == reference.subject && 4.12 + description == reference.description && 4.13 + eta == reference.eta && 4.14 + affected == reference.affected && 4.15 + resolved == reference.resolved) 4.16 } 4.17
5.1 --- a/src/main/kotlin/de/uapcore/lightpit/entities/User.kt Thu Aug 19 14:51:04 2021 +0200 5.2 +++ b/src/main/kotlin/de/uapcore/lightpit/entities/User.kt Thu Aug 19 17:20:43 2021 +0200 5.3 @@ -31,11 +31,18 @@ 5.4 var givenname: String? = null 5.5 var lastname: String? = null 5.6 5.7 + /** 5.8 + * The display name without mail address. 5.9 + */ 5.10 val shortDisplayname: String 5.11 get() { 5.12 val str = "${givenname ?: ""} ${lastname ?: ""}" 5.13 return if (str.isBlank()) username else str.trim() 5.14 } 5.15 5.16 + /** 5.17 + * Shows the full name plus mail address. 5.18 + * If neither given name nor lastname are provided, the username is used instead. 5.19 + */ 5.20 val displayname: String get() = if (mail.isNullOrBlank()) shortDisplayname else "$shortDisplayname <$mail>" 5.21 } 5.22 \ No newline at end of file
6.1 --- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Thu Aug 19 14:51:04 2021 +0200 6.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt Thu Aug 19 17:20:43 2021 +0200 6.3 @@ -25,11 +25,8 @@ 6.4 6.5 package de.uapcore.lightpit.servlet 6.6 6.7 -import de.uapcore.lightpit.AbstractServlet 6.8 -import de.uapcore.lightpit.HttpRequest 6.9 -import de.uapcore.lightpit.boolValidator 6.10 +import de.uapcore.lightpit.* 6.11 import de.uapcore.lightpit.dao.DataAccessObject 6.12 -import de.uapcore.lightpit.dateOptValidator 6.13 import de.uapcore.lightpit.entities.* 6.14 import de.uapcore.lightpit.types.IssueCategory 6.15 import de.uapcore.lightpit.types.IssueStatus 6.16 @@ -524,10 +521,20 @@ 6.17 val commentId = http.param("commentid")?.toIntOrNull() ?: -1 6.18 if (commentId > 0) { 6.19 val comment = dao.findComment(commentId) 6.20 - val originalAuthor = comment?.author?.username 6.21 + if (comment == null) { 6.22 + http.response.sendError(404) 6.23 + return 6.24 + } 6.25 + val originalAuthor = comment.author?.username 6.26 if (originalAuthor != null && originalAuthor == http.remoteUser) { 6.27 - comment.comment = http.param("comment") ?: "" 6.28 - dao.updateComment(comment) 6.29 + val newComment = http.param("comment") 6.30 + if (!newComment.isNullOrBlank()) { 6.31 + comment.comment = newComment 6.32 + dao.updateComment(comment) 6.33 + dao.insertHistoryEvent(comment) 6.34 + } else { 6.35 + logger().debug("Not updating comment ${comment.id} because nothing changed.") 6.36 + } 6.37 } else { 6.38 http.response.sendError(403) 6.39 return 6.40 @@ -537,7 +544,8 @@ 6.41 author = http.remoteUser?.let { dao.findUserByName(it) } 6.42 comment = http.param("comment") ?: "" 6.43 } 6.44 - dao.insertComment(comment) 6.45 + val newId = dao.insertComment(comment) 6.46 + dao.insertHistoryEvent(comment, newId) 6.47 } 6.48 6.49 http.renderCommit("${issuesHref}${issue.id}") 6.50 @@ -570,15 +578,31 @@ 6.51 } 6.52 6.53 val openId = if (issue.id < 0) { 6.54 - dao.insertIssue(issue) 6.55 + val id = dao.insertIssue(issue) 6.56 + dao.insertHistoryEvent(issue, id) 6.57 + id 6.58 } else { 6.59 - dao.updateIssue(issue) 6.60 + val reference = dao.findIssue(issue.id) 6.61 + if (reference == null) { 6.62 + http.response.sendError(404) 6.63 + return 6.64 + } 6.65 + 6.66 + if (issue.hasChanged(reference)) { 6.67 + dao.updateIssue(issue) 6.68 + dao.insertHistoryEvent(issue) 6.69 + } else { 6.70 + logger().debug("Not updating issue ${issue.id} because nothing changed.") 6.71 + } 6.72 + 6.73 val newComment = http.param("comment") 6.74 if (!newComment.isNullOrBlank()) { 6.75 - dao.insertComment(IssueComment(-1, issue.id).apply { 6.76 + val comment = IssueComment(-1, issue.id).apply { 6.77 author = http.remoteUser?.let { dao.findUserByName(it) } 6.78 comment = newComment 6.79 - }) 6.80 + } 6.81 + val commentid = dao.insertComment(comment) 6.82 + dao.insertHistoryEvent(comment, commentid) 6.83 } 6.84 issue.id 6.85 }
7.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 7.2 +++ b/src/main/kotlin/de/uapcore/lightpit/types/IssueHistoryType.kt Thu Aug 19 17:20:43 2021 +0200 7.3 @@ -0,0 +1,33 @@ 7.4 +/* 7.5 + * Copyright 2021 Mike Becker. All rights reserved. 7.6 + * 7.7 + * Redistribution and use in source and binary forms, with or without 7.8 + * modification, are permitted provided that the following conditions are met: 7.9 + * 7.10 + * 1. Redistributions of source code must retain the above copyright 7.11 + * notice, this list of conditions and the following disclaimer. 7.12 + * 7.13 + * 2. Redistributions in binary form must reproduce the above copyright 7.14 + * notice, this list of conditions and the following disclaimer in the 7.15 + * documentation and/or other materials provided with the distribution. 7.16 + * 7.17 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 7.18 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 7.19 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 7.20 + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 7.21 + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 7.22 + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 7.23 + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 7.24 + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 7.25 + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 7.26 + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 7.27 + */ 7.28 + 7.29 +package de.uapcore.lightpit.types 7.30 + 7.31 +enum class IssueHistoryType { 7.32 + New, 7.33 + Update, 7.34 + NewComment, 7.35 + UpdateComment 7.36 +}