28 import com.github.difflib.text.DiffRow |
28 import com.github.difflib.text.DiffRow |
29 import com.github.difflib.text.DiffRowGenerator |
29 import com.github.difflib.text.DiffRowGenerator |
30 import de.uapcore.lightpit.AbstractServlet |
30 import de.uapcore.lightpit.AbstractServlet |
31 import de.uapcore.lightpit.HttpRequest |
31 import de.uapcore.lightpit.HttpRequest |
32 import de.uapcore.lightpit.dao.DataAccessObject |
32 import de.uapcore.lightpit.dao.DataAccessObject |
33 import de.uapcore.lightpit.entities.IssueHistoryData |
33 import de.uapcore.lightpit.entities.IssueCommentHistoryEntry |
34 import de.uapcore.lightpit.entities.IssueHistoryEntry |
34 import de.uapcore.lightpit.entities.IssueHistoryEntry |
|
35 import de.uapcore.lightpit.types.IssueHistoryType |
|
36 import de.uapcore.lightpit.viewmodel.CommentDiff |
35 import de.uapcore.lightpit.viewmodel.IssueDiff |
37 import de.uapcore.lightpit.viewmodel.IssueDiff |
36 import de.uapcore.lightpit.viewmodel.IssueFeed |
38 import de.uapcore.lightpit.viewmodel.IssueFeed |
37 import de.uapcore.lightpit.viewmodel.IssueFeedEntry |
39 import de.uapcore.lightpit.viewmodel.IssueFeedEntry |
38 import java.text.SimpleDateFormat |
40 import java.text.SimpleDateFormat |
39 import javax.servlet.annotation.WebServlet |
41 import javax.servlet.annotation.WebServlet |
44 |
46 |
45 init { |
47 init { |
46 get("/%project/issues.rss", this::issues) |
48 get("/%project/issues.rss", this::issues) |
47 } |
49 } |
48 |
50 |
49 private fun fullContent(issue: IssueHistoryData) = IssueDiff( |
51 val diffGenerator by lazyOf(DiffRowGenerator.create() |
50 issue.id, |
52 .showInlineDiffs(true) |
|
53 .mergeOriginalRevised(true) |
|
54 .inlineDiffByWord(true) |
|
55 .oldTag { start -> if (start) "<strike style=\"color:red\">" else "</strike>" } |
|
56 .newTag { start -> if (start) "<i style=\"color: green\">" else "</i>" } |
|
57 .build() |
|
58 ) |
|
59 |
|
60 private fun fullContent(data: IssueCommentHistoryEntry) = |
|
61 CommentDiff( |
|
62 data.issueid, |
|
63 data.commentid, |
|
64 data.subject, |
|
65 data.comment.replace("\r", "") |
|
66 ) |
|
67 |
|
68 private fun diffContent(cur: IssueCommentHistoryEntry, next: IssueCommentHistoryEntry) = |
|
69 CommentDiff( |
|
70 cur.issueid, |
|
71 cur.commentid, |
|
72 cur.subject, |
|
73 diffGenerator.generateDiffRows( |
|
74 next.comment.replace("\r", "").split('\n'), |
|
75 cur.comment.replace("\r", "").split('\n') |
|
76 ).joinToString("\n", transform = DiffRow::getOldLine) |
|
77 ) |
|
78 |
|
79 private fun fullContent(issue: IssueHistoryEntry) = IssueDiff( |
|
80 issue.issueid, |
51 issue.subject, |
81 issue.subject, |
52 issue.component, |
82 issue.component, |
53 issue.status.name, |
83 issue.status.name, |
54 issue.category.name, |
84 issue.category.name, |
55 issue.subject, |
85 issue.subject, |
58 issue.eta?.let { SimpleDateFormat("dd.MM.yyyy").format(it) } ?: "", |
88 issue.eta?.let { SimpleDateFormat("dd.MM.yyyy").format(it) } ?: "", |
59 issue.affected, |
89 issue.affected, |
60 issue.resolved |
90 issue.resolved |
61 ) |
91 ) |
62 |
92 |
63 private fun diffContent(cur: IssueHistoryData, next: IssueHistoryData): IssueDiff { |
93 private fun diffContent(cur: IssueHistoryEntry, next: IssueHistoryEntry): IssueDiff { |
64 val generator = DiffRowGenerator.create() |
|
65 .showInlineDiffs(true) |
|
66 .mergeOriginalRevised(true) |
|
67 .inlineDiffByWord(true) |
|
68 .oldTag { start -> if (start) "<strike style=\"color:red\">" else "</strike>" } |
|
69 .newTag { start -> if (start) "<i style=\"color: green\">" else "</i>" } |
|
70 .build() |
|
71 |
|
72 val prev = fullContent(next) |
94 val prev = fullContent(next) |
73 val diff = fullContent(cur) |
95 val diff = fullContent(cur) |
74 |
96 val result = diffGenerator.generateDiffRows( |
75 val result = generator.generateDiffRows( |
|
76 listOf( |
97 listOf( |
77 prev.subject, prev.component, prev.status, |
98 prev.subject, prev.component, prev.status, |
78 prev.category, prev.assignee, prev.eta, prev.affected, prev.resolved |
99 prev.category, prev.assignee, prev.eta, prev.affected, prev.resolved |
79 ), |
100 ), |
80 listOf( |
101 listOf( |
90 diff.assignee = result[4].oldLine |
111 diff.assignee = result[4].oldLine |
91 diff.eta = result[5].oldLine |
112 diff.eta = result[5].oldLine |
92 diff.affected = result[6].oldLine |
113 diff.affected = result[6].oldLine |
93 diff.resolved = result[7].oldLine |
114 diff.resolved = result[7].oldLine |
94 |
115 |
95 diff.description = generator.generateDiffRows( |
116 diff.description = diffGenerator.generateDiffRows( |
96 prev.description.split('\n'), |
117 prev.description.split('\n'), |
97 diff.description.split('\n') |
118 diff.description.split('\n') |
98 ).joinToString("\n", transform = DiffRow::getOldLine) |
119 ).joinToString("\n", transform = DiffRow::getOldLine) |
99 |
120 |
100 return diff |
121 return diff |
101 } |
122 } |
102 |
123 |
103 /** |
124 /** |
104 * Generates the feed entries. |
125 * Generates the feed entries. |
105 * Assumes that [historyEntry] is already sorted by timestamp (descending). |
126 * Assumes that [issueEntries] and [commentEntries] are already sorted by timestamp (descending). |
106 */ |
127 */ |
107 private fun generateFeedEntries(historyEntry: List<IssueHistoryEntry>): List<IssueFeedEntry> = |
128 private fun generateFeedEntries( |
108 if (historyEntry.isEmpty()) { |
129 issueEntries: List<IssueHistoryEntry>, |
|
130 commentEntries: List<IssueCommentHistoryEntry> |
|
131 ): List<IssueFeedEntry> = |
|
132 (generateIssueFeedEntries(issueEntries) + generateCommentFeedEntries(commentEntries)).sortedByDescending { it.time } |
|
133 |
|
134 private fun generateIssueFeedEntries(entries: List<IssueHistoryEntry>): List<IssueFeedEntry> = |
|
135 if (entries.isEmpty()) { |
109 emptyList() |
136 emptyList() |
110 } else { |
137 } else { |
111 historyEntry.groupBy { it.data.id }.mapValues { (_, history) -> |
138 entries.groupBy { it.issueid }.mapValues { (_, history) -> |
112 history.zipWithNext().map { (cur, next) -> |
139 history.zipWithNext().map { (cur, next) -> |
113 IssueFeedEntry( |
140 IssueFeedEntry( |
114 cur.time, cur.type, diffContent(cur.data, next.data) |
141 cur.time, cur.type, issue = diffContent(cur, next) |
115 ) |
142 ) |
116 }.plus( |
143 }.plus( |
117 history.last().let { IssueFeedEntry(it.time, it.type, fullContent(it.data)) } |
144 history.last().let { IssueFeedEntry(it.time, it.type, issue = fullContent(it)) } |
118 ) |
145 ) |
119 }.flatMap { it.value }.sortedByDescending { it.time } |
146 }.flatMap { it.value } |
|
147 } |
|
148 |
|
149 private fun generateCommentFeedEntries(entries: List<IssueCommentHistoryEntry>): List<IssueFeedEntry> = |
|
150 if (entries.isEmpty()) { |
|
151 emptyList() |
|
152 } else { |
|
153 entries.groupBy { it.commentid }.mapValues { (_, history) -> |
|
154 history.zipWithNext().map { (cur, next) -> |
|
155 IssueFeedEntry( |
|
156 cur.time, cur.type, comment = diffContent(cur, next) |
|
157 ) |
|
158 }.plus( |
|
159 history.last().let { IssueFeedEntry(it.time, it.type, comment = fullContent(it)) } |
|
160 ) |
|
161 }.flatMap { it.value } |
120 } |
162 } |
121 |
163 |
122 private fun issues(http: HttpRequest, dao: DataAccessObject) { |
164 private fun issues(http: HttpRequest, dao: DataAccessObject) { |
123 val project = http.pathParams["project"]?.let { dao.findProjectByNode(it) } |
165 val project = http.pathParams["project"]?.let { dao.findProjectByNode(it) } |
124 if (project == null) { |
166 if (project == null) { |
125 http.response.sendError(404) |
167 http.response.sendError(404) |
126 return |
168 return |
127 } |
169 } |
128 val assignees = http.param("assignee")?.split(',') |
170 val assignees = http.param("assignee")?.split(',') |
|
171 val comments = http.param("comments") ?: "all" |
129 |
172 |
130 val days = http.param("days")?.toIntOrNull() ?: 30 |
173 val days = http.param("days")?.toIntOrNull() ?: 30 |
131 |
174 |
132 val issuesFromDb = dao.listIssueHistory(project.id, days) |
175 val issuesFromDb = dao.listIssueHistory(project.id, days) |
133 val issueHistory = if (assignees == null) issuesFromDb else |
176 val issueHistory = if (assignees == null) issuesFromDb else |
134 issuesFromDb.filter { assignees.contains(it.currentAssignee) } |
177 issuesFromDb.filter { assignees.contains(it.currentAssignee) } |
135 |
178 |
136 // TODO: add comment history depending on parameter |
179 val commentsFromDb = dao.listIssueCommentHistory(project.id, days) |
|
180 val commentHistory = when (comments) { |
|
181 "all" -> commentsFromDb |
|
182 "new" -> commentsFromDb.filter { it.type == IssueHistoryType.NewComment } |
|
183 else -> emptyList() |
|
184 } |
137 |
185 |
138 http.view = IssueFeed(project, generateFeedEntries(issueHistory)) |
186 http.view = IssueFeed(project, generateFeedEntries(issueHistory, commentHistory)) |
139 http.renderFeed("issues-feed") |
187 http.renderFeed("issues-feed") |
140 } |
188 } |
141 } |
189 } |