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