src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt

changeset 311
bf67e0ff7131
parent 307
23fe9f174d2d
equal deleted inserted replaced
310:bbf4eb9a71f8 311:bf67e0ff7131
25 25
26 package de.uapcore.lightpit.servlet 26 package de.uapcore.lightpit.servlet
27 27
28 import de.uapcore.lightpit.* 28 import de.uapcore.lightpit.*
29 import de.uapcore.lightpit.dao.DataAccessObject 29 import de.uapcore.lightpit.dao.DataAccessObject
30 import de.uapcore.lightpit.entities.* 30 import de.uapcore.lightpit.entities.Component
31 import de.uapcore.lightpit.types.* 31 import de.uapcore.lightpit.entities.Issue
32 import de.uapcore.lightpit.entities.Project
33 import de.uapcore.lightpit.entities.Version
34 import de.uapcore.lightpit.logic.*
35 import de.uapcore.lightpit.types.VcsType
36 import de.uapcore.lightpit.types.VersionStatus
37 import de.uapcore.lightpit.types.WebColor
38 import de.uapcore.lightpit.types.parseCommitRefs
32 import de.uapcore.lightpit.viewmodel.* 39 import de.uapcore.lightpit.viewmodel.*
33 import jakarta.servlet.annotation.WebServlet 40 import jakarta.servlet.annotation.WebServlet
34 import java.sql.Date 41 import java.sql.Date
35 42
36 @WebServlet(urlPatterns = ["/projects/*"]) 43 @WebServlet(urlPatterns = ["/projects/*"])
90 } else { 97 } else {
91 san 98 san
92 } 99 }
93 } 100 }
94 101
95 private fun feedPath(project: Project) = "feed/${project.node}/issues.rss"
96
97 private fun project(http: HttpRequest, dao: DataAccessObject) { 102 private fun project(http: HttpRequest, dao: DataAccessObject) {
98 withPathInfo(http, dao)?.let {path -> 103 withPathInfo(http, dao)?.let {path ->
99 val project = path.projectInfo.project 104 val project = path.projectInfo.project
100 105
101 val filter = IssueFilter(http) 106 val filter = IssueFilter(http, dao)
102 107
103 val needRelationsMap = filter.onlyBlocker 108 val needRelationsMap = filter.onlyBlocker
104 109
105 val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap() 110 val relationsMap = if (needRelationsMap) dao.getIssueRelationMap(project, filter.includeDone) else emptyMap()
106 111
109 val specificComponent = path.componentInfo !is OptionalPathInfo.All 114 val specificComponent = path.componentInfo !is OptionalPathInfo.All
110 val component = if (path.componentInfo is OptionalPathInfo.Specific) path.componentInfo.elem else null 115 val component = if (path.componentInfo is OptionalPathInfo.Specific) path.componentInfo.elem else null
111 116
112 val issues = dao.listIssues(project, filter.includeDone, specificVersion, version, specificComponent, component) 117 val issues = dao.listIssues(project, filter.includeDone, specificVersion, version, specificComponent, component)
113 .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary)) 118 .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary))
114 .filter { 119 .filter(issueFilterFunction(filter, relationsMap, http.remoteUser ?: "<Anonymous>"))
115 (!filter.onlyMine || (it.assignee?.username ?: "") == (http.remoteUser ?: "<Anonymous>")) &&
116 (!filter.onlyBlocker || (relationsMap[it.id]?.any { (_,type) -> type.blocking }?:false)) &&
117 (filter.status.isEmpty() || filter.status.contains(it.status)) &&
118 (filter.category.isEmpty() || filter.category.contains(it.category)) &&
119 (filter.onlyMine || filter.assignee.isEmpty() || filter.assignee.contains(it.assignee?.id ?: -1))
120 }
121 120
122 with(http) { 121 with(http) {
123 pageTitle = project.name 122 pageTitle = project.name
124 view = ProjectDetails(path, issues, filter, dao.listUsers().sortedBy(User::shortDisplayname)) 123 view = ProjectDetails(path, issues, filter)
125 feedPath = feedPath(project) 124 navigationMenu = projectNavMenu(dao.listProjects(), path)
126 navigationMenu = projectNavMenu(dao.listProjects(), path) 125 styleSheets = listOf("projects")
127 styleSheets = listOf("projects") 126 javascript = "issue-overview"
128 javascript = "project-details"
129 render("project-details") 127 render("project-details")
130 } 128 }
131 } 129 }
132 } 130 }
133 131
194 pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.versions")}" 192 pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.versions")}"
195 view = VersionsView( 193 view = VersionsView(
196 path.projectInfo, 194 path.projectInfo,
197 dao.listVersionSummaries(path.projectInfo.project) 195 dao.listVersionSummaries(path.projectInfo.project)
198 ) 196 )
199 feedPath = feedPath(path.projectInfo.project) 197 navigationMenu = projectNavMenu(dao.listProjects(), path)
200 navigationMenu = projectNavMenu(dao.listProjects(), path) 198 styleSheets = listOf("projects")
201 styleSheets = listOf("projects") 199 javascript = "issue-overview"
202 javascript = "project-details"
203 render("versions") 200 render("versions")
204 } 201 }
205 } 202 }
206 } 203 }
207 204
210 val version = if (path.versionInfo is OptionalPathInfo.Specific) 207 val version = if (path.versionInfo is OptionalPathInfo.Specific)
211 path.versionInfo.elem else Version(-1, path.projectInfo.project.id) 208 path.versionInfo.elem else Version(-1, path.projectInfo.project.id)
212 209
213 with(http) { 210 with(http) {
214 view = VersionEditView(path.projectInfo, version) 211 view = VersionEditView(path.projectInfo, version)
215 feedPath = feedPath(path.projectInfo.project)
216 navigationMenu = projectNavMenu(dao.listProjects(), path) 212 navigationMenu = projectNavMenu(dao.listProjects(), path)
217 styleSheets = listOf("projects") 213 styleSheets = listOf("projects")
218 render("version-form") 214 render("version-form")
219 } 215 }
220 } 216 }
272 pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.components")}" 268 pageTitle = "${path.projectInfo.project.name} - ${i18n("navmenu.components")}"
273 view = ComponentsView( 269 view = ComponentsView(
274 path.projectInfo, 270 path.projectInfo,
275 dao.listComponentSummaries(path.projectInfo.project) 271 dao.listComponentSummaries(path.projectInfo.project)
276 ) 272 )
277 feedPath = feedPath(path.projectInfo.project) 273 navigationMenu = projectNavMenu(dao.listProjects(), path)
278 navigationMenu = projectNavMenu(dao.listProjects(), path) 274 styleSheets = listOf("projects")
279 styleSheets = listOf("projects") 275 javascript = "issue-overview"
280 javascript = "project-details"
281 render("components") 276 render("components")
282 } 277 }
283 } 278 }
284 } 279 }
285 280
288 val component = if (path.componentInfo is OptionalPathInfo.Specific) 283 val component = if (path.componentInfo is OptionalPathInfo.Specific)
289 path.componentInfo.elem else Component(-1, path.projectInfo.project.id) 284 path.componentInfo.elem else Component(-1, path.projectInfo.project.id)
290 285
291 with(http) { 286 with(http) {
292 view = ComponentEditView(path.projectInfo, component, dao.listUsers()) 287 view = ComponentEditView(path.projectInfo, component, dao.listUsers())
293 feedPath = feedPath(path.projectInfo.project)
294 navigationMenu = projectNavMenu(dao.listProjects(), path) 288 navigationMenu = projectNavMenu(dao.listProjects(), path)
295 styleSheets = listOf("projects") 289 styleSheets = listOf("projects")
296 render("component-form") 290 render("component-form")
297 } 291 }
298 } 292 }
332 val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) 326 val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue)
333 if (issue == null) { 327 if (issue == null) {
334 http.response.sendError(404) 328 http.response.sendError(404)
335 return 329 return
336 } 330 }
337 renderIssueView(http, dao, issue) 331 withPathInfo(http, dao)?.let { path ->
338 } 332 renderIssueView(http, dao, issue, path)
339
340 private fun renderIssueView(
341 http: HttpRequest,
342 dao: DataAccessObject,
343 issue: Issue,
344 relationError: String? = null
345 ) {
346 withPathInfo(http, dao)?.let {path ->
347 val comments = dao.listComments(issue)
348
349 with(http) {
350 pageTitle = "#${issue.id} ${issue.subject} (${path.projectInfo.project.name})"
351 view = IssueDetailView(
352 path,
353 issue,
354 comments,
355 path.projectInfo.project,
356 dao.listIssues(path.projectInfo.project, true),
357 dao.listIssueRelations(issue),
358 relationError,
359 dao.listCommitRefs(issue)
360 )
361 feedPath = feedPath(path.projectInfo.project)
362 navigationMenu = projectNavMenu(dao.listProjects(), path)
363 styleSheets = listOf("projects")
364 javascript = "issue-editor"
365 render("issue-view")
366 }
367 } 333 }
368 } 334 }
369 335
370 private fun issueForm(http: HttpRequest, dao: DataAccessObject) { 336 private fun issueForm(http: HttpRequest, dao: DataAccessObject) {
371 withPathInfo(http, dao)?.let { path -> 337 withPathInfo(http, dao)?.let { path ->
398 path.projectInfo.components, 364 path.projectInfo.components,
399 dao.listUsers(), 365 dao.listUsers(),
400 path.projectInfo.project, 366 path.projectInfo.project,
401 path 367 path
402 ) 368 )
403 feedPath = feedPath(path.projectInfo.project)
404 navigationMenu = projectNavMenu(dao.listProjects(), path) 369 navigationMenu = projectNavMenu(dao.listProjects(), path)
405 styleSheets = listOf("projects") 370 styleSheets = listOf("projects")
406 javascript = "issue-editor" 371 javascript = "issue-editor"
407 render("issue-form") 372 render("issue-form")
408 } 373 }
409 } 374 }
410 } 375 }
411 376
412 private fun issueComment(http: HttpRequest, dao: DataAccessObject) { 377 private fun issueComment(http: HttpRequest, dao: DataAccessObject) {
413 withPathInfo(http, dao)?.run { 378 withPathInfo(http, dao)?.let {path ->
414 val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) 379 commitIssueComment(http, dao, path)
415 if (issue == null) {
416 http.response.sendError(404)
417 return
418 }
419
420 val commentId = http.param("commentid")?.toIntOrNull() ?: -1
421 if (commentId > 0) {
422 val comment = dao.findComment(commentId)
423 if (comment == null) {
424 http.response.sendError(404)
425 return
426 }
427 val originalAuthor = comment.author?.username
428 if (originalAuthor != null && originalAuthor == http.remoteUser) {
429 val newComment = http.param("comment")
430 if (!newComment.isNullOrBlank()) {
431 comment.comment = newComment
432 dao.updateComment(comment)
433 dao.insertHistoryEvent(issue, comment)
434 } else {
435 logger.debug("Not updating comment ${comment.id} because nothing changed.")
436 }
437 } else {
438 http.response.sendError(403)
439 return
440 }
441 } else {
442 val comment = IssueComment(-1, issue.id).apply {
443 author = http.remoteUser?.let { dao.findUserByName(it) }
444 comment = http.param("comment") ?: ""
445 }
446 val newId = dao.insertComment(comment)
447 dao.insertHistoryEvent(issue, comment, newId)
448 }
449
450 http.renderCommit("${issuesHref}${issue.id}")
451 } 380 }
452 } 381 }
453 382
454 private fun issueCommit(http: HttpRequest, dao: DataAccessObject) { 383 private fun issueCommit(http: HttpRequest, dao: DataAccessObject) {
455 withPathInfo(http, dao)?.run { 384 withPathInfo(http, dao)?.run {
456 val issue = Issue( 385 val issue = Issue(
457 http.param("id")?.toIntOrNull() ?: -1, 386 http.param("id")?.toIntOrNull() ?: -1,
458 projectInfo.project 387 projectInfo.project
459 ).apply { 388 ).applyFormData(http, dao)
460 component = dao.findComponent(http.param("component")?.toIntOrNull() ?: -1)
461 category = IssueCategory.valueOf(http.param("category") ?: "")
462 status = IssueStatus.valueOf(http.param("status") ?: "")
463 subject = http.param("subject") ?: ""
464 description = http.param("description") ?: ""
465 assignee = http.param("assignee")?.toIntOrNull()?.let {
466 when (it) {
467 -1 -> null
468 -2 -> (component?.lead ?: projectInfo.project.owner)
469 else -> dao.findUser(it)
470 }
471 }
472 // TODO: process error messages
473 eta = http.param("eta", ::dateOptValidator, null, mutableListOf())
474
475 affected = http.param("affected")?.toIntOrNull()?.takeIf { it > 0 }?.let { Version(it, project.id) }
476 resolved = http.param("resolved")?.toIntOrNull()?.takeIf { it > 0 }?.let { Version(it, project.id) }
477 }
478 389
479 val openId = if (issue.id < 0) { 390 val openId = if (issue.id < 0) {
480 val id = dao.insertIssue(issue) 391 val id = dao.insertIssue(issue)
481 dao.insertHistoryEvent(issue, id) 392 dao.insertHistoryEvent(issue, id)
482 id 393 id
484 val reference = dao.findIssue(issue.id) 395 val reference = dao.findIssue(issue.id)
485 if (reference == null) { 396 if (reference == null) {
486 http.response.sendError(404) 397 http.response.sendError(404)
487 return 398 return
488 } 399 }
489 400 processIssueForm(issue, reference, http, dao)
490 if (issue.hasChanged(reference)) {
491 dao.updateIssue(issue)
492 dao.insertHistoryEvent(issue)
493 } else {
494 logger.debug("Not updating issue ${issue.id} because nothing changed.")
495 }
496
497 val newComment = http.param("comment")
498 if (!newComment.isNullOrBlank()) {
499 val comment = IssueComment(-1, issue.id).apply {
500 author = http.remoteUser?.let { dao.findUserByName(it) }
501 comment = newComment
502 }
503 val commentid = dao.insertComment(comment)
504 dao.insertHistoryEvent(issue, comment, commentid)
505 }
506 issue.id 401 issue.id
507 } 402 }
508 403
509 if (http.param("more") != null) { 404 if (http.param("more") != null) {
510 http.renderCommit("${issuesHref}-/create") 405 http.renderCommit("${issuesHref}-/create")
515 } 410 }
516 } 411 }
517 } 412 }
518 413
519 private fun issueRelation(http: HttpRequest, dao: DataAccessObject) { 414 private fun issueRelation(http: HttpRequest, dao: DataAccessObject) {
520 withPathInfo(http, dao)?.run { 415 withPathInfo(http, dao)?.let {path ->
521 val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) 416 addIssueRelation(http, dao, path)
522 if (issue == null) {
523 http.response.sendError(404)
524 return
525 }
526
527 // determine the relation type
528 val type: Pair<RelationType, Boolean>? = http.param("type")?.let {
529 try {
530 if (it.startsWith("!")) {
531 Pair(RelationType.valueOf(it.substring(1)), true)
532 } else {
533 Pair(RelationType.valueOf(it), false)
534 }
535 } catch (_: IllegalArgumentException) {
536 null
537 }
538 }
539
540 // if the relation type was invalid, send HTTP 500
541 if (type == null) {
542 http.response.sendError(500)
543 return
544 }
545
546 // determine the target issue
547 val targetIssue: Issue? = http.param("issue")?.let {
548 if (it.startsWith("#") && it.length > 1) {
549 it.substring(1).split(" ", limit = 2)[0].toIntOrNull()
550 ?.let(dao::findIssue)
551 ?.takeIf { target -> target.project.id == issue.project.id }
552 } else {
553 null
554 }
555 }
556
557 // check if the target issue is valid
558 if (targetIssue == null) {
559 renderIssueView(http, dao, issue, "issue.relations.target.invalid")
560 return
561 }
562
563 // commit the result
564 dao.insertIssueRelation(IssueRelation(issue, targetIssue, type.first, type.second))
565 http.renderCommit("${issuesHref}${issue.id}")
566 } 417 }
567 } 418 }
568 419
569 private fun issueRemoveRelation(http: HttpRequest, dao: DataAccessObject) { 420 private fun issueRemoveRelation(http: HttpRequest, dao: DataAccessObject) {
570 withPathInfo(http, dao)?.run { 421 withPathInfo(http, dao)?.let {path ->
571 val issue = http.pathParams["issue"]?.toIntOrNull()?.let(dao::findIssue) 422 removeIssueRelation(http, dao, path)
572 if (issue == null) {
573 http.response.sendError(404)
574 return
575 }
576
577 // determine relation
578 val type = http.param("type")?.let {
579 try {RelationType.valueOf(it)}
580 catch (_:IllegalArgumentException) {null}
581 }
582 if (type == null) {
583 http.response.sendError(500)
584 return
585 }
586 val rel = http.param("to")?.toIntOrNull()?.let(dao::findIssue)?.let {
587 IssueRelation(
588 issue,
589 it,
590 type,
591 http.param("reverse")?.toBoolean() ?: false
592 )
593 }
594
595 // execute removal, if there is something to remove
596 rel?.run(dao::deleteIssueRelation)
597
598 // always pretend that the operation was successful - if there was nothing to remove, it's okay
599 http.renderCommit("${issuesHref}${issue.id}")
600 } 423 }
601 } 424 }
602 } 425 }

mercurial