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 |
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 } |
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 } |