84 styleSheets = listOf("projects") |
81 styleSheets = listOf("projects") |
85 render("projects") |
82 render("projects") |
86 } |
83 } |
87 } |
84 } |
88 |
85 |
89 private fun activeProjectNavMenu( |
|
90 projects: List<Project>, |
|
91 projectInfo: ProjectInfo, |
|
92 selectedVersion: Version? = null, |
|
93 selectedComponent: Component? = null |
|
94 ) = |
|
95 projectNavMenu( |
|
96 projects, |
|
97 projectInfo.versions, |
|
98 projectInfo.components, |
|
99 projectInfo.project, |
|
100 selectedVersion, |
|
101 selectedComponent |
|
102 ) |
|
103 |
|
104 private sealed interface LookupResult<T> |
|
105 private class NotFound<T> : LookupResult<T> |
|
106 private data class Found<T>(val elem: T?) : LookupResult<T> |
|
107 |
|
108 private fun <T : HasNode> HttpRequest.lookupPathParam(paramName: String, list: List<T>): LookupResult<T> { |
|
109 val node = pathParams[paramName] |
|
110 return if (node == null || node == "-") { |
|
111 Found(null) |
|
112 } else { |
|
113 val result = list.find { it.node == node } |
|
114 if (result == null) { |
|
115 NotFound() |
|
116 } else { |
|
117 Found(result) |
|
118 } |
|
119 } |
|
120 } |
|
121 |
|
122 private fun obtainProjectInfo(http: HttpRequest, dao: DataAccessObject): ProjectInfo? { |
|
123 val project = dao.findProjectByNode(http.pathParams["project"] ?: "") ?: return null |
|
124 |
|
125 val versions: List<Version> = dao.listVersions(project) |
|
126 val components: List<Component> = dao.listComponents(project) |
|
127 |
|
128 return ProjectInfo( |
|
129 project, |
|
130 versions, |
|
131 components, |
|
132 dao.collectIssueSummary(project) |
|
133 ) |
|
134 } |
|
135 |
|
136 private fun sanitizeNode(name: String): String { |
86 private fun sanitizeNode(name: String): String { |
137 val san = name.replace(Regex("[/\\\\]"), "-") |
87 val san = name.replace(Regex("[/\\\\]"), "-") |
138 return if (san.startsWith(".")) { |
88 return if (san.startsWith(".")) { |
139 "v$san" |
89 "v$san" |
140 } else { |
90 } else { |
142 } |
92 } |
143 } |
93 } |
144 |
94 |
145 private fun feedPath(project: Project) = "feed/${project.node}/issues.rss" |
95 private fun feedPath(project: Project) = "feed/${project.node}/issues.rss" |
146 |
96 |
147 private data class PathInfos( |
|
148 val projectInfo: ProjectInfo, |
|
149 val version: Version?, |
|
150 val component: Component? |
|
151 ) { |
|
152 val project = projectInfo.project |
|
153 val issuesHref by lazyOf("projects/${project.node}/issues/${version?.node ?: "-"}/${component?.node ?: "-"}/") |
|
154 } |
|
155 |
|
156 private fun withPathInfo(http: HttpRequest, dao: DataAccessObject): PathInfos? { |
|
157 val projectInfo = obtainProjectInfo(http, dao) |
|
158 if (projectInfo == null) { |
|
159 http.response.sendError(404) |
|
160 return null |
|
161 } |
|
162 |
|
163 val version = when (val result = http.lookupPathParam("version", projectInfo.versions)) { |
|
164 is NotFound -> { |
|
165 http.response.sendError(404) |
|
166 return null |
|
167 } |
|
168 is Found -> { |
|
169 result.elem |
|
170 } |
|
171 } |
|
172 val component = when (val result = http.lookupPathParam("component", projectInfo.components)) { |
|
173 is NotFound -> { |
|
174 http.response.sendError(404) |
|
175 return null |
|
176 } |
|
177 is Found -> { |
|
178 result.elem |
|
179 } |
|
180 } |
|
181 |
|
182 return PathInfos(projectInfo, version, component) |
|
183 } |
|
184 |
|
185 private fun project(http: HttpRequest, dao: DataAccessObject) { |
97 private fun project(http: HttpRequest, dao: DataAccessObject) { |
186 withPathInfo(http, dao)?.run { |
98 withPathInfo(http, dao)?.let {path -> |
|
99 val project = path.projectInfo.project |
187 |
100 |
188 val filter = IssueFilter(http) |
101 val filter = IssueFilter(http) |
189 |
102 |
190 val needRelationsMap = filter.onlyBlocker |
103 val needRelationsMap = filter.onlyBlocker |
191 |
104 |
192 val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap() |
105 val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap() |
193 |
106 |
194 val issues = dao.listIssues(project, filter.includeDone, version, component) |
107 val specificVersion = path.versionInfo !is OptionalPathInfo.All |
|
108 val version = if (path.versionInfo is OptionalPathInfo.Specific) path.versionInfo.elem else null |
|
109 val specificComponent = path.componentInfo !is OptionalPathInfo.All |
|
110 val component = if (path.componentInfo is OptionalPathInfo.Specific) path.componentInfo.elem else null |
|
111 |
|
112 val issues = dao.listIssues(project, filter.includeDone, specificVersion, version, specificComponent, component) |
195 .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary)) |
113 .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary)) |
196 .filter { |
114 .filter { |
197 (!filter.onlyMine || (it.assignee?.username ?: "") == (http.remoteUser ?: "<Anonymous>")) && |
115 (!filter.onlyMine || (it.assignee?.username ?: "") == (http.remoteUser ?: "<Anonymous>")) && |
198 (!filter.onlyBlocker || (relationsMap[it.id]?.any { (_,type) -> type.blocking }?:false)) && |
116 (!filter.onlyBlocker || (relationsMap[it.id]?.any { (_,type) -> type.blocking }?:false)) && |
199 (filter.status.isEmpty() || filter.status.contains(it.status)) && |
117 (filter.status.isEmpty() || filter.status.contains(it.status)) && |
200 (filter.category.isEmpty() || filter.category.contains(it.category)) |
118 (filter.category.isEmpty() || filter.category.contains(it.category)) |
201 } |
119 } |
202 |
120 |
203 with(http) { |
121 with(http) { |
204 pageTitle = project.name |
122 pageTitle = project.name |
205 view = ProjectDetails(projectInfo, issues, filter, version, component) |
123 view = ProjectDetails(path, issues, filter) |
206 feedPath = feedPath(project) |
124 feedPath = feedPath(project) |
207 navigationMenu = activeProjectNavMenu( |
125 navigationMenu = projectNavMenu(dao.listProjects(), path) |
208 dao.listProjects(), |
|
209 projectInfo, |
|
210 version, |
|
211 component |
|
212 ) |
|
213 styleSheets = listOf("projects") |
126 styleSheets = listOf("projects") |
214 javascript = "project-details" |
127 javascript = "project-details" |
215 render("project-details") |
128 render("project-details") |
216 } |
129 } |
217 } |
130 } |
218 } |
131 } |
219 |
132 |
220 private fun projectForm(http: HttpRequest, dao: DataAccessObject) { |
133 private fun projectForm(http: HttpRequest, dao: DataAccessObject) { |
|
134 http.styleSheets = listOf("projects") |
221 if (!http.pathParams.containsKey("project")) { |
135 if (!http.pathParams.containsKey("project")) { |
222 http.view = ProjectEditView(Project(-1), dao.listUsers()) |
136 http.view = ProjectEditView(Project(-1), dao.listUsers()) |
223 http.navigationMenu = projectNavMenu(dao.listProjects()) |
137 http.navigationMenu = projectNavMenu(dao.listProjects()) |
|
138 http.render("project-form") |
224 } else { |
139 } else { |
225 val projectInfo = obtainProjectInfo(http, dao) |
140 withPathInfo(http, dao)?.let { path -> |
226 if (projectInfo == null) { |
141 http.view = ProjectEditView(path.projectInfo.project, dao.listUsers()) |
227 http.response.sendError(404) |
142 http.navigationMenu = projectNavMenu(dao.listProjects(), path) |
228 return |
143 http.render("project-form") |
229 } |
144 } |
230 http.view = ProjectEditView(projectInfo.project, dao.listUsers()) |
145 } |
231 http.navigationMenu = activeProjectNavMenu( |
|
232 dao.listProjects(), |
|
233 projectInfo |
|
234 ) |
|
235 } |
|
236 http.styleSheets = listOf("projects") |
|
237 http.render("project-form") |
|
238 } |
146 } |
239 |
147 |
240 private fun projectCommit(http: HttpRequest, dao: DataAccessObject) { |
148 private fun projectCommit(http: HttpRequest, dao: DataAccessObject) { |
241 val project = Project(http.param("id")?.toIntOrNull() ?: -1).apply { |
149 val project = Project(http.param("id")?.toIntOrNull() ?: -1).apply { |
242 name = http.param("name") ?: "" |
150 name = http.param("name") ?: "" |
262 |
170 |
263 http.renderCommit("projects/${project.node}") |
171 http.renderCommit("projects/${project.node}") |
264 } |
172 } |
265 |
173 |
266 private fun vcsAnalyze(http: HttpRequest, dao: DataAccessObject) { |
174 private fun vcsAnalyze(http: HttpRequest, dao: DataAccessObject) { |
267 val projectInfo = obtainProjectInfo(http, dao) |
175 withPathInfo(http, dao)?.let { path -> |
268 if (projectInfo == null) { |
176 // if analysis is not configured, reject the request |
269 http.response.sendError(404) |
177 if (path.projectInfo.project.vcs == VcsType.None) { |
270 return |
178 http.response.sendError(404) |
271 } |
179 return |
272 |
180 } |
273 // if analysis is not configured, reject the request |
181 |
274 if (projectInfo.project.vcs == VcsType.None) { |
182 // obtain the list of issues for this project to filter cross-project references |
275 http.response.sendError(404) |
183 val knownIds = dao.listIssues(path.projectInfo.project, true).map { it.id } |
276 return |
184 |
277 } |
185 // read the provided commit log and merge only the refs that relate issues from the current project |
278 |
186 dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) }) |
279 // obtain the list of issues for this project to filter cross-project references |
187 } |
280 val knownIds = dao.listIssues(projectInfo.project, true).map { it.id } |
|
281 |
|
282 // read the provided commit log and merge only the refs that relate issues from the current project |
|
283 dao.mergeCommitRefs(parseCommitRefs(http.body).filter { knownIds.contains(it.issueId) }) |
|
284 } |
188 } |
285 |
189 |
286 private fun versions(http: HttpRequest, dao: DataAccessObject) { |
190 private fun versions(http: HttpRequest, dao: DataAccessObject) { |
287 val projectInfo = obtainProjectInfo(http, dao) |
191 withPathInfo(http, dao)?.let { path -> |
288 if (projectInfo == null) { |
192 with(http) { |
289 http.response.sendError(404) |
193 pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.versions")}" |
290 return |
194 view = VersionsView( |
291 } |
195 path.projectInfo, |
292 |
196 dao.listVersionSummaries(path.projectInfo.project) |
293 with(http) { |
197 ) |
294 pageTitle = "${projectInfo.project.name} - ${i18n("navmenu.versions")}" |
198 feedPath = feedPath(path.projectInfo.project) |
295 view = VersionsView( |
199 navigationMenu = projectNavMenu(dao.listProjects(), path) |
296 projectInfo, |
200 styleSheets = listOf("projects") |
297 dao.listVersionSummaries(projectInfo.project) |
201 javascript = "project-details" |
298 ) |
202 render("versions") |
299 feedPath = feedPath(projectInfo.project) |
203 } |
300 navigationMenu = activeProjectNavMenu( |
|
301 dao.listProjects(), |
|
302 projectInfo |
|
303 ) |
|
304 styleSheets = listOf("projects") |
|
305 javascript = "project-details" |
|
306 render("versions") |
|
307 } |
204 } |
308 } |
205 } |
309 |
206 |
310 private fun versionForm(http: HttpRequest, dao: DataAccessObject) { |
207 private fun versionForm(http: HttpRequest, dao: DataAccessObject) { |
311 val projectInfo = obtainProjectInfo(http, dao) |
208 withPathInfo(http, dao)?.let { path -> |
312 if (projectInfo == null) { |
209 val version = if (path.versionInfo is OptionalPathInfo.Specific) |
313 http.response.sendError(404) |
210 path.versionInfo.elem else Version(-1, path.projectInfo.project.id) |
314 return |
211 |
315 } |
212 with(http) { |
316 |
213 view = VersionEditView(path.projectInfo, version) |
317 val version: Version |
214 feedPath = feedPath(path.projectInfo.project) |
318 when (val result = http.lookupPathParam("version", projectInfo.versions)) { |
215 navigationMenu = projectNavMenu(dao.listProjects(), path) |
319 is NotFound -> { |
216 styleSheets = listOf("projects") |
320 http.response.sendError(404) |
217 render("version-form") |
321 return |
218 } |
322 } |
|
323 is Found -> { |
|
324 version = result.elem ?: Version(-1, projectInfo.project.id) |
|
325 } |
|
326 } |
|
327 |
|
328 with(http) { |
|
329 view = VersionEditView(projectInfo, version) |
|
330 feedPath = feedPath(projectInfo.project) |
|
331 navigationMenu = activeProjectNavMenu( |
|
332 dao.listProjects(), |
|
333 projectInfo, |
|
334 selectedVersion = version |
|
335 ) |
|
336 styleSheets = listOf("projects") |
|
337 render("version-form") |
|
338 } |
219 } |
339 } |
220 } |
340 |
221 |
341 private fun obtainIdAndProject(http: HttpRequest, dao: DataAccessObject): Pair<Int, Project>? { |
222 private fun obtainIdAndProject(http: HttpRequest, dao: DataAccessObject): Pair<Int, Project>? { |
342 val id = http.param("id")?.toIntOrNull() |
223 val id = http.param("id")?.toIntOrNull() |
383 |
264 |
384 http.renderCommit("projects/${project.node}/versions/") |
265 http.renderCommit("projects/${project.node}/versions/") |
385 } |
266 } |
386 |
267 |
387 private fun components(http: HttpRequest, dao: DataAccessObject) { |
268 private fun components(http: HttpRequest, dao: DataAccessObject) { |
388 val projectInfo = obtainProjectInfo(http, dao) |
269 withPathInfo(http, dao)?.let { path -> |
389 if (projectInfo == null) { |
270 with(http) { |
390 http.response.sendError(404) |
271 pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.components")}" |
391 return |
272 view = ComponentsView( |
392 } |
273 path.projectInfo, |
393 |
274 dao.listComponentSummaries(path.projectInfo.project) |
394 with(http) { |
275 ) |
395 pageTitle = "${projectInfo.project.name} - ${i18n("navmenu.components")}" |
276 feedPath = feedPath(path.projectInfo.project) |
396 view = ComponentsView( |
277 navigationMenu = projectNavMenu(dao.listProjects(), path) |
397 projectInfo, |
278 styleSheets = listOf("projects") |
398 dao.listComponentSummaries(projectInfo.project) |
279 javascript = "project-details" |
399 ) |
280 render("components") |
400 feedPath = feedPath(projectInfo.project) |
281 } |
401 navigationMenu = activeProjectNavMenu( |
|
402 dao.listProjects(), |
|
403 projectInfo |
|
404 ) |
|
405 styleSheets = listOf("projects") |
|
406 javascript = "project-details" |
|
407 render("components") |
|
408 } |
282 } |
409 } |
283 } |
410 |
284 |
411 private fun componentForm(http: HttpRequest, dao: DataAccessObject) { |
285 private fun componentForm(http: HttpRequest, dao: DataAccessObject) { |
412 val projectInfo = obtainProjectInfo(http, dao) |
286 withPathInfo(http, dao)?.let { path -> |
413 if (projectInfo == null) { |
287 val component = if (path.componentInfo is OptionalPathInfo.Specific) |
414 http.response.sendError(404) |
288 path.componentInfo.elem else Component(-1, path.projectInfo.project.id) |
415 return |
289 |
416 } |
290 with(http) { |
417 |
291 view = ComponentEditView(path.projectInfo, component, dao.listUsers()) |
418 val component: Component |
292 feedPath = feedPath(path.projectInfo.project) |
419 when (val result = http.lookupPathParam("component", projectInfo.components)) { |
293 navigationMenu = projectNavMenu(dao.listProjects(), path) |
420 is NotFound -> { |
294 styleSheets = listOf("projects") |
421 http.response.sendError(404) |
295 render("component-form") |
422 return |
296 } |
423 } |
|
424 is Found -> { |
|
425 component = result.elem ?: Component(-1, projectInfo.project.id) |
|
426 } |
|
427 } |
|
428 |
|
429 with(http) { |
|
430 view = ComponentEditView(projectInfo, component, dao.listUsers()) |
|
431 feedPath = feedPath(projectInfo.project) |
|
432 navigationMenu = activeProjectNavMenu( |
|
433 dao.listProjects(), |
|
434 projectInfo, |
|
435 selectedComponent = component |
|
436 ) |
|
437 styleSheets = listOf("projects") |
|
438 render("component-form") |
|
439 } |
297 } |
440 } |
298 } |
441 |
299 |
442 private fun componentCommit(http: HttpRequest, dao: DataAccessObject) { |
300 private fun componentCommit(http: HttpRequest, dao: DataAccessObject) { |
443 val idParams = obtainIdAndProject(http, dao) ?: return |
301 val idParams = obtainIdAndProject(http, dao) ?: return |
482 http: HttpRequest, |
340 http: HttpRequest, |
483 dao: DataAccessObject, |
341 dao: DataAccessObject, |
484 issue: Issue, |
342 issue: Issue, |
485 relationError: String? = null |
343 relationError: String? = null |
486 ) { |
344 ) { |
487 withPathInfo(http, dao)?.run { |
345 withPathInfo(http, dao)?.let {path -> |
488 val comments = dao.listComments(issue) |
346 val comments = dao.listComments(issue) |
489 |
347 |
490 with(http) { |
348 with(http) { |
491 pageTitle = "${projectInfo.project.name}: #${issue.id} ${issue.subject}" |
349 pageTitle = "${path.projectInfo.project.name}: #${issue.id} ${issue.subject}" |
492 view = IssueDetailView( |
350 view = IssueDetailView( |
|
351 path, |
493 issue, |
352 issue, |
494 comments, |
353 comments, |
495 project, |
354 path.projectInfo.project, |
496 version, |
355 dao.listIssues(path.projectInfo.project, true), |
497 component, |
|
498 dao.listIssues(project, true), |
|
499 dao.listIssueRelations(issue), |
356 dao.listIssueRelations(issue), |
500 relationError, |
357 relationError, |
501 dao.listCommitRefs(issue) |
358 dao.listCommitRefs(issue) |
502 ) |
359 ) |
503 feedPath = feedPath(projectInfo.project) |
360 feedPath = feedPath(path.projectInfo.project) |
504 navigationMenu = activeProjectNavMenu( |
361 navigationMenu = projectNavMenu(dao.listProjects(), path) |
505 dao.listProjects(), |
|
506 projectInfo, |
|
507 version, |
|
508 component |
|
509 ) |
|
510 styleSheets = listOf("projects") |
362 styleSheets = listOf("projects") |
511 javascript = "issue-editor" |
363 javascript = "issue-editor" |
512 render("issue-view") |
364 render("issue-view") |
513 } |
365 } |
514 } |
366 } |
515 } |
367 } |
516 |
368 |
517 private fun issueForm(http: HttpRequest, dao: DataAccessObject) { |
369 private fun issueForm(http: HttpRequest, dao: DataAccessObject) { |
518 withPathInfo(http, dao)?.run { |
370 withPathInfo(http, dao)?.let { path -> |
519 val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) ?: Issue( |
371 val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) ?: Issue( |
520 -1, |
372 -1, |
521 project, |
373 path.projectInfo.project, |
522 ) |
374 ) |
523 |
375 |
524 // for new issues set some defaults |
376 // for new issues set some defaults |
525 if (issue.id < 0) { |
377 if (issue.id < 0) { |
526 // pre-select component, if available in the path info |
378 // pre-select component, if available in the path info |
527 issue.component = component |
379 if (path.componentInfo is OptionalPathInfo.Specific) { |
|
380 issue.component = path.componentInfo.elem |
|
381 } |
528 |
382 |
529 // pre-select version, if available in the path info |
383 // pre-select version, if available in the path info |
530 if (version != null) { |
384 if (path.versionInfo is OptionalPathInfo.Specific) { |
531 if (version.status.isReleased) { |
385 if (path.versionInfo.elem.status.isReleased) { |
532 issue.affected = version |
386 issue.affected = path.versionInfo.elem |
533 } else { |
387 } else { |
534 issue.resolved = version |
388 issue.resolved = path.versionInfo.elem |
535 } |
389 } |
536 } |
390 } |
537 } |
391 } |
538 |
392 |
539 with(http) { |
393 with(http) { |
540 view = IssueEditView( |
394 view = IssueEditView( |
541 issue, |
395 issue, |
542 projectInfo.versions, |
396 path.projectInfo.versions, |
543 projectInfo.components, |
397 path.projectInfo.components, |
544 dao.listUsers(), |
398 dao.listUsers(), |
545 project, |
399 path.projectInfo.project, |
546 version, |
400 path |
547 component |
|
548 ) |
401 ) |
549 feedPath = feedPath(projectInfo.project) |
402 feedPath = feedPath(path.projectInfo.project) |
550 navigationMenu = activeProjectNavMenu( |
403 navigationMenu = projectNavMenu(dao.listProjects(), path) |
551 dao.listProjects(), |
|
552 projectInfo, |
|
553 version, |
|
554 component |
|
555 ) |
|
556 styleSheets = listOf("projects") |
404 styleSheets = listOf("projects") |
557 javascript = "issue-editor" |
405 javascript = "issue-editor" |
558 render("issue-form") |
406 render("issue-form") |
559 } |
407 } |
560 } |
408 } |