code.gitea.io/gitea@v1.21.7/routers/web/org/projects.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package org 5 6 import ( 7 "errors" 8 "fmt" 9 "net/http" 10 "net/url" 11 "strconv" 12 "strings" 13 14 issues_model "code.gitea.io/gitea/models/issues" 15 project_model "code.gitea.io/gitea/models/project" 16 attachment_model "code.gitea.io/gitea/models/repo" 17 "code.gitea.io/gitea/models/unit" 18 "code.gitea.io/gitea/modules/base" 19 "code.gitea.io/gitea/modules/context" 20 "code.gitea.io/gitea/modules/json" 21 "code.gitea.io/gitea/modules/setting" 22 "code.gitea.io/gitea/modules/util" 23 "code.gitea.io/gitea/modules/web" 24 shared_user "code.gitea.io/gitea/routers/web/shared/user" 25 "code.gitea.io/gitea/services/forms" 26 ) 27 28 const ( 29 tplProjects base.TplName = "org/projects/list" 30 tplProjectsNew base.TplName = "org/projects/new" 31 tplProjectsView base.TplName = "org/projects/view" 32 ) 33 34 // MustEnableProjects check if projects are enabled in settings 35 func MustEnableProjects(ctx *context.Context) { 36 if unit.TypeProjects.UnitGlobalDisabled() { 37 ctx.NotFound("EnableKanbanBoard", nil) 38 return 39 } 40 } 41 42 // Projects renders the home page of projects 43 func Projects(ctx *context.Context) { 44 shared_user.PrepareContextForProfileBigAvatar(ctx) 45 ctx.Data["Title"] = ctx.Tr("repo.project_board") 46 47 sortType := ctx.FormTrim("sort") 48 49 isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" 50 keyword := ctx.FormTrim("q") 51 page := ctx.FormInt("page") 52 if page <= 1 { 53 page = 1 54 } 55 56 var projectType project_model.Type 57 if ctx.ContextUser.IsOrganization() { 58 projectType = project_model.TypeOrganization 59 } else { 60 projectType = project_model.TypeIndividual 61 } 62 projects, total, err := project_model.FindProjects(ctx, project_model.SearchOptions{ 63 OwnerID: ctx.ContextUser.ID, 64 Page: page, 65 IsClosed: util.OptionalBoolOf(isShowClosed), 66 OrderBy: project_model.GetSearchOrderByBySortType(sortType), 67 Type: projectType, 68 Title: keyword, 69 }) 70 if err != nil { 71 ctx.ServerError("FindProjects", err) 72 return 73 } 74 75 opTotal, err := project_model.CountProjects(ctx, project_model.SearchOptions{ 76 OwnerID: ctx.ContextUser.ID, 77 IsClosed: util.OptionalBoolOf(!isShowClosed), 78 Type: projectType, 79 }) 80 if err != nil { 81 ctx.ServerError("CountProjects", err) 82 return 83 } 84 85 if isShowClosed { 86 ctx.Data["OpenCount"] = opTotal 87 ctx.Data["ClosedCount"] = total 88 } else { 89 ctx.Data["OpenCount"] = total 90 ctx.Data["ClosedCount"] = opTotal 91 } 92 93 ctx.Data["Projects"] = projects 94 shared_user.RenderUserHeader(ctx) 95 96 if isShowClosed { 97 ctx.Data["State"] = "closed" 98 } else { 99 ctx.Data["State"] = "open" 100 } 101 102 for _, project := range projects { 103 project.RenderedContent = project.Description 104 } 105 106 err = shared_user.LoadHeaderCount(ctx) 107 if err != nil { 108 ctx.ServerError("LoadHeaderCount", err) 109 return 110 } 111 112 numPages := 0 113 if total > 0 { 114 numPages = (int(total) - 1/setting.UI.IssuePagingNum) 115 } 116 117 pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages) 118 pager.AddParam(ctx, "state", "State") 119 ctx.Data["Page"] = pager 120 121 ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) 122 ctx.Data["IsShowClosed"] = isShowClosed 123 ctx.Data["PageIsViewProjects"] = true 124 ctx.Data["SortType"] = sortType 125 126 ctx.HTML(http.StatusOK, tplProjects) 127 } 128 129 func canWriteProjects(ctx *context.Context) bool { 130 if ctx.ContextUser.IsOrganization() { 131 return ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) 132 } 133 return ctx.Doer != nil && ctx.ContextUser.ID == ctx.Doer.ID 134 } 135 136 // RenderNewProject render creating a project page 137 func RenderNewProject(ctx *context.Context) { 138 ctx.Data["Title"] = ctx.Tr("repo.projects.new") 139 ctx.Data["BoardTypes"] = project_model.GetBoardConfig() 140 ctx.Data["CardTypes"] = project_model.GetCardConfig() 141 ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) 142 ctx.Data["PageIsViewProjects"] = true 143 ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() 144 ctx.Data["CancelLink"] = ctx.ContextUser.HomeLink() + "/-/projects" 145 shared_user.RenderUserHeader(ctx) 146 147 err := shared_user.LoadHeaderCount(ctx) 148 if err != nil { 149 ctx.ServerError("LoadHeaderCount", err) 150 return 151 } 152 153 ctx.HTML(http.StatusOK, tplProjectsNew) 154 } 155 156 // NewProjectPost creates a new project 157 func NewProjectPost(ctx *context.Context) { 158 form := web.GetForm(ctx).(*forms.CreateProjectForm) 159 ctx.Data["Title"] = ctx.Tr("repo.projects.new") 160 shared_user.RenderUserHeader(ctx) 161 162 if ctx.HasError() { 163 RenderNewProject(ctx) 164 return 165 } 166 167 newProject := project_model.Project{ 168 OwnerID: ctx.ContextUser.ID, 169 Title: form.Title, 170 Description: form.Content, 171 CreatorID: ctx.Doer.ID, 172 BoardType: form.BoardType, 173 CardType: form.CardType, 174 } 175 176 if ctx.ContextUser.IsOrganization() { 177 newProject.Type = project_model.TypeOrganization 178 } else { 179 newProject.Type = project_model.TypeIndividual 180 } 181 182 if err := project_model.NewProject(ctx, &newProject); err != nil { 183 ctx.ServerError("NewProject", err) 184 return 185 } 186 187 ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title)) 188 ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects") 189 } 190 191 // ChangeProjectStatus updates the status of a project between "open" and "close" 192 func ChangeProjectStatus(ctx *context.Context) { 193 toClose := false 194 switch ctx.Params(":action") { 195 case "open": 196 toClose = false 197 case "close": 198 toClose = true 199 default: 200 ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects") 201 } 202 id := ctx.ParamsInt64(":id") 203 204 if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil { 205 if project_model.IsErrProjectNotExist(err) { 206 ctx.NotFound("", err) 207 } else { 208 ctx.ServerError("ChangeProjectStatusByRepoIDAndID", err) 209 } 210 return 211 } 212 ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects?state=" + url.QueryEscape(ctx.Params(":action"))) 213 } 214 215 // DeleteProject delete a project 216 func DeleteProject(ctx *context.Context) { 217 p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 218 if err != nil { 219 if project_model.IsErrProjectNotExist(err) { 220 ctx.NotFound("", nil) 221 } else { 222 ctx.ServerError("GetProjectByID", err) 223 } 224 return 225 } 226 if p.OwnerID != ctx.ContextUser.ID { 227 ctx.NotFound("", nil) 228 return 229 } 230 231 if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil { 232 ctx.Flash.Error("DeleteProjectByID: " + err.Error()) 233 } else { 234 ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success")) 235 } 236 237 ctx.JSONRedirect(ctx.ContextUser.HomeLink() + "/-/projects") 238 } 239 240 // RenderEditProject allows a project to be edited 241 func RenderEditProject(ctx *context.Context) { 242 ctx.Data["Title"] = ctx.Tr("repo.projects.edit") 243 ctx.Data["PageIsEditProjects"] = true 244 ctx.Data["PageIsViewProjects"] = true 245 ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) 246 ctx.Data["CardTypes"] = project_model.GetCardConfig() 247 248 shared_user.RenderUserHeader(ctx) 249 250 p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 251 if err != nil { 252 if project_model.IsErrProjectNotExist(err) { 253 ctx.NotFound("", nil) 254 } else { 255 ctx.ServerError("GetProjectByID", err) 256 } 257 return 258 } 259 if p.OwnerID != ctx.ContextUser.ID { 260 ctx.NotFound("", nil) 261 return 262 } 263 264 ctx.Data["projectID"] = p.ID 265 ctx.Data["title"] = p.Title 266 ctx.Data["content"] = p.Description 267 ctx.Data["redirect"] = ctx.FormString("redirect") 268 ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() 269 ctx.Data["card_type"] = p.CardType 270 ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), p.ID) 271 272 ctx.HTML(http.StatusOK, tplProjectsNew) 273 } 274 275 // EditProjectPost response for editing a project 276 func EditProjectPost(ctx *context.Context) { 277 form := web.GetForm(ctx).(*forms.CreateProjectForm) 278 projectID := ctx.ParamsInt64(":id") 279 ctx.Data["Title"] = ctx.Tr("repo.projects.edit") 280 ctx.Data["PageIsEditProjects"] = true 281 ctx.Data["PageIsViewProjects"] = true 282 ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) 283 ctx.Data["CardTypes"] = project_model.GetCardConfig() 284 ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID) 285 286 shared_user.RenderUserHeader(ctx) 287 288 err := shared_user.LoadHeaderCount(ctx) 289 if err != nil { 290 ctx.ServerError("LoadHeaderCount", err) 291 return 292 } 293 294 if ctx.HasError() { 295 ctx.HTML(http.StatusOK, tplProjectsNew) 296 return 297 } 298 299 p, err := project_model.GetProjectByID(ctx, projectID) 300 if err != nil { 301 if project_model.IsErrProjectNotExist(err) { 302 ctx.NotFound("", nil) 303 } else { 304 ctx.ServerError("GetProjectByID", err) 305 } 306 return 307 } 308 if p.OwnerID != ctx.ContextUser.ID { 309 ctx.NotFound("", nil) 310 return 311 } 312 313 p.Title = form.Title 314 p.Description = form.Content 315 p.CardType = form.CardType 316 if err = project_model.UpdateProject(ctx, p); err != nil { 317 ctx.ServerError("UpdateProjects", err) 318 return 319 } 320 321 ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title)) 322 if ctx.FormString("redirect") == "project" { 323 ctx.Redirect(p.Link(ctx)) 324 } else { 325 ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects") 326 } 327 } 328 329 // ViewProject renders the project board for a project 330 func ViewProject(ctx *context.Context) { 331 project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 332 if err != nil { 333 if project_model.IsErrProjectNotExist(err) { 334 ctx.NotFound("", nil) 335 } else { 336 ctx.ServerError("GetProjectByID", err) 337 } 338 return 339 } 340 if project.OwnerID != ctx.ContextUser.ID { 341 ctx.NotFound("", nil) 342 return 343 } 344 345 boards, err := project.GetBoards(ctx) 346 if err != nil { 347 ctx.ServerError("GetProjectBoards", err) 348 return 349 } 350 351 if boards[0].ID == 0 { 352 boards[0].Title = ctx.Tr("repo.projects.type.uncategorized") 353 } 354 355 issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) 356 if err != nil { 357 ctx.ServerError("LoadIssuesOfBoards", err) 358 return 359 } 360 361 if project.CardType != project_model.CardTypeTextOnly { 362 issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment) 363 for _, issuesList := range issuesMap { 364 for _, issue := range issuesList { 365 if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil { 366 issuesAttachmentMap[issue.ID] = issueAttachment 367 } 368 } 369 } 370 ctx.Data["issuesAttachmentMap"] = issuesAttachmentMap 371 } 372 373 linkedPrsMap := make(map[int64][]*issues_model.Issue) 374 for _, issuesList := range issuesMap { 375 for _, issue := range issuesList { 376 var referencedIds []int64 377 for _, comment := range issue.Comments { 378 if comment.RefIssueID != 0 && comment.RefIsPull { 379 referencedIds = append(referencedIds, comment.RefIssueID) 380 } 381 } 382 383 if len(referencedIds) > 0 { 384 if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{ 385 IssueIDs: referencedIds, 386 IsPull: util.OptionalBoolTrue, 387 }); err == nil { 388 linkedPrsMap[issue.ID] = linkedPrs 389 } 390 } 391 } 392 } 393 394 project.RenderedContent = project.Description 395 ctx.Data["LinkedPRs"] = linkedPrsMap 396 ctx.Data["PageIsViewProjects"] = true 397 ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) 398 ctx.Data["Project"] = project 399 ctx.Data["IssuesMap"] = issuesMap 400 ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend 401 shared_user.RenderUserHeader(ctx) 402 403 err = shared_user.LoadHeaderCount(ctx) 404 if err != nil { 405 ctx.ServerError("LoadHeaderCount", err) 406 return 407 } 408 409 ctx.HTML(http.StatusOK, tplProjectsView) 410 } 411 412 func getActionIssues(ctx *context.Context) issues_model.IssueList { 413 commaSeparatedIssueIDs := ctx.FormString("issue_ids") 414 if len(commaSeparatedIssueIDs) == 0 { 415 return nil 416 } 417 issueIDs := make([]int64, 0, 10) 418 for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") { 419 issueID, err := strconv.ParseInt(stringIssueID, 10, 64) 420 if err != nil { 421 ctx.ServerError("ParseInt", err) 422 return nil 423 } 424 issueIDs = append(issueIDs, issueID) 425 } 426 issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) 427 if err != nil { 428 ctx.ServerError("GetIssuesByIDs", err) 429 return nil 430 } 431 // Check access rights for all issues 432 issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues) 433 prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests) 434 for _, issue := range issues { 435 if issue.RepoID != ctx.Repo.Repository.ID { 436 ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect")) 437 return nil 438 } 439 if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled { 440 ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil) 441 return nil 442 } 443 if err = issue.LoadAttributes(ctx); err != nil { 444 ctx.ServerError("LoadAttributes", err) 445 return nil 446 } 447 } 448 return issues 449 } 450 451 // UpdateIssueProject change an issue's project 452 func UpdateIssueProject(ctx *context.Context) { 453 issues := getActionIssues(ctx) 454 if ctx.Written() { 455 return 456 } 457 458 if err := issues.LoadProjects(ctx); err != nil { 459 ctx.ServerError("LoadProjects", err) 460 return 461 } 462 463 projectID := ctx.FormInt64("id") 464 for _, issue := range issues { 465 if issue.Project != nil { 466 if issue.Project.ID == projectID { 467 continue 468 } 469 } 470 471 if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { 472 ctx.ServerError("ChangeProjectAssign", err) 473 return 474 } 475 } 476 477 ctx.JSONOK() 478 } 479 480 // DeleteProjectBoard allows for the deletion of a project board 481 func DeleteProjectBoard(ctx *context.Context) { 482 if ctx.Doer == nil { 483 ctx.JSON(http.StatusForbidden, map[string]string{ 484 "message": "Only signed in users are allowed to perform this action.", 485 }) 486 return 487 } 488 489 project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 490 if err != nil { 491 if project_model.IsErrProjectNotExist(err) { 492 ctx.NotFound("", nil) 493 } else { 494 ctx.ServerError("GetProjectByID", err) 495 } 496 return 497 } 498 499 pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) 500 if err != nil { 501 ctx.ServerError("GetProjectBoard", err) 502 return 503 } 504 if pb.ProjectID != ctx.ParamsInt64(":id") { 505 ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ 506 "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID), 507 }) 508 return 509 } 510 511 if project.OwnerID != ctx.ContextUser.ID { 512 ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ 513 "message": fmt.Sprintf("ProjectBoard[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID), 514 }) 515 return 516 } 517 518 if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil { 519 ctx.ServerError("DeleteProjectBoardByID", err) 520 return 521 } 522 523 ctx.JSONOK() 524 } 525 526 // AddBoardToProjectPost allows a new board to be added to a project. 527 func AddBoardToProjectPost(ctx *context.Context) { 528 form := web.GetForm(ctx).(*forms.EditProjectBoardForm) 529 530 project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 531 if err != nil { 532 if project_model.IsErrProjectNotExist(err) { 533 ctx.NotFound("", nil) 534 } else { 535 ctx.ServerError("GetProjectByID", err) 536 } 537 return 538 } 539 540 if err := project_model.NewBoard(ctx, &project_model.Board{ 541 ProjectID: project.ID, 542 Title: form.Title, 543 Color: form.Color, 544 CreatorID: ctx.Doer.ID, 545 }); err != nil { 546 ctx.ServerError("NewProjectBoard", err) 547 return 548 } 549 550 ctx.JSONOK() 551 } 552 553 // CheckProjectBoardChangePermissions check permission 554 func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) { 555 if ctx.Doer == nil { 556 ctx.JSON(http.StatusForbidden, map[string]string{ 557 "message": "Only signed in users are allowed to perform this action.", 558 }) 559 return nil, nil 560 } 561 562 project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 563 if err != nil { 564 if project_model.IsErrProjectNotExist(err) { 565 ctx.NotFound("", nil) 566 } else { 567 ctx.ServerError("GetProjectByID", err) 568 } 569 return nil, nil 570 } 571 572 board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) 573 if err != nil { 574 ctx.ServerError("GetProjectBoard", err) 575 return nil, nil 576 } 577 if board.ProjectID != ctx.ParamsInt64(":id") { 578 ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ 579 "message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID), 580 }) 581 return nil, nil 582 } 583 584 if project.OwnerID != ctx.ContextUser.ID { 585 ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ 586 "message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, project.ID), 587 }) 588 return nil, nil 589 } 590 return project, board 591 } 592 593 // EditProjectBoard allows a project board's to be updated 594 func EditProjectBoard(ctx *context.Context) { 595 form := web.GetForm(ctx).(*forms.EditProjectBoardForm) 596 _, board := CheckProjectBoardChangePermissions(ctx) 597 if ctx.Written() { 598 return 599 } 600 601 if form.Title != "" { 602 board.Title = form.Title 603 } 604 605 board.Color = form.Color 606 607 if form.Sorting != 0 { 608 board.Sorting = form.Sorting 609 } 610 611 if err := project_model.UpdateBoard(ctx, board); err != nil { 612 ctx.ServerError("UpdateProjectBoard", err) 613 return 614 } 615 616 ctx.JSONOK() 617 } 618 619 // SetDefaultProjectBoard set default board for uncategorized issues/pulls 620 func SetDefaultProjectBoard(ctx *context.Context) { 621 project, board := CheckProjectBoardChangePermissions(ctx) 622 if ctx.Written() { 623 return 624 } 625 626 if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil { 627 ctx.ServerError("SetDefaultBoard", err) 628 return 629 } 630 631 ctx.JSONOK() 632 } 633 634 // UnsetDefaultProjectBoard unset default board for uncategorized issues/pulls 635 func UnsetDefaultProjectBoard(ctx *context.Context) { 636 project, _ := CheckProjectBoardChangePermissions(ctx) 637 if ctx.Written() { 638 return 639 } 640 641 if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil { 642 ctx.ServerError("SetDefaultBoard", err) 643 return 644 } 645 646 ctx.JSONOK() 647 } 648 649 // MoveIssues moves or keeps issues in a column and sorts them inside that column 650 func MoveIssues(ctx *context.Context) { 651 if ctx.Doer == nil { 652 ctx.JSON(http.StatusForbidden, map[string]string{ 653 "message": "Only signed in users are allowed to perform this action.", 654 }) 655 return 656 } 657 658 project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) 659 if err != nil { 660 if project_model.IsErrProjectNotExist(err) { 661 ctx.NotFound("ProjectNotExist", nil) 662 } else { 663 ctx.ServerError("GetProjectByID", err) 664 } 665 return 666 } 667 if project.OwnerID != ctx.ContextUser.ID { 668 ctx.NotFound("InvalidRepoID", nil) 669 return 670 } 671 672 var board *project_model.Board 673 674 if ctx.ParamsInt64(":boardID") == 0 { 675 board = &project_model.Board{ 676 ID: 0, 677 ProjectID: project.ID, 678 Title: ctx.Tr("repo.projects.type.uncategorized"), 679 } 680 } else { 681 board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) 682 if err != nil { 683 if project_model.IsErrProjectBoardNotExist(err) { 684 ctx.NotFound("ProjectBoardNotExist", nil) 685 } else { 686 ctx.ServerError("GetProjectBoard", err) 687 } 688 return 689 } 690 if board.ProjectID != project.ID { 691 ctx.NotFound("BoardNotInProject", nil) 692 return 693 } 694 } 695 696 type movedIssuesForm struct { 697 Issues []struct { 698 IssueID int64 `json:"issueID"` 699 Sorting int64 `json:"sorting"` 700 } `json:"issues"` 701 } 702 703 form := &movedIssuesForm{} 704 if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil { 705 ctx.ServerError("DecodeMovedIssuesForm", err) 706 } 707 708 issueIDs := make([]int64, 0, len(form.Issues)) 709 sortedIssueIDs := make(map[int64]int64) 710 for _, issue := range form.Issues { 711 issueIDs = append(issueIDs, issue.IssueID) 712 sortedIssueIDs[issue.Sorting] = issue.IssueID 713 } 714 movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) 715 if err != nil { 716 if issues_model.IsErrIssueNotExist(err) { 717 ctx.NotFound("IssueNotExisting", nil) 718 } else { 719 ctx.ServerError("GetIssueByID", err) 720 } 721 return 722 } 723 724 if len(movedIssues) != len(form.Issues) { 725 ctx.ServerError("some issues do not exist", errors.New("some issues do not exist")) 726 return 727 } 728 729 if _, err = movedIssues.LoadRepositories(ctx); err != nil { 730 ctx.ServerError("LoadRepositories", err) 731 return 732 } 733 734 for _, issue := range movedIssues { 735 if issue.RepoID != project.RepoID && issue.Repo.OwnerID != project.OwnerID { 736 ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("Some issue's repoID is not equal to project's repoID")) 737 return 738 } 739 } 740 741 if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil { 742 ctx.ServerError("MoveIssuesOnProjectBoard", err) 743 return 744 } 745 746 ctx.JSONOK() 747 }