code.gitea.io/gitea@v1.22.3/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  	"strings"
    11  
    12  	"code.gitea.io/gitea/models/db"
    13  	issues_model "code.gitea.io/gitea/models/issues"
    14  	project_model "code.gitea.io/gitea/models/project"
    15  	attachment_model "code.gitea.io/gitea/models/repo"
    16  	"code.gitea.io/gitea/models/unit"
    17  	"code.gitea.io/gitea/modules/base"
    18  	"code.gitea.io/gitea/modules/json"
    19  	"code.gitea.io/gitea/modules/optional"
    20  	"code.gitea.io/gitea/modules/setting"
    21  	"code.gitea.io/gitea/modules/templates"
    22  	"code.gitea.io/gitea/modules/web"
    23  	shared_user "code.gitea.io/gitea/routers/web/shared/user"
    24  	"code.gitea.io/gitea/services/context"
    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 := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
    63  		ListOptions: db.ListOptions{
    64  			Page:     page,
    65  			PageSize: setting.UI.IssuePagingNum,
    66  		},
    67  		OwnerID:  ctx.ContextUser.ID,
    68  		IsClosed: optional.Some(isShowClosed),
    69  		OrderBy:  project_model.GetSearchOrderByBySortType(sortType),
    70  		Type:     projectType,
    71  		Title:    keyword,
    72  	})
    73  	if err != nil {
    74  		ctx.ServerError("FindProjects", err)
    75  		return
    76  	}
    77  
    78  	opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
    79  		OwnerID:  ctx.ContextUser.ID,
    80  		IsClosed: optional.Some(!isShowClosed),
    81  		Type:     projectType,
    82  	})
    83  	if err != nil {
    84  		ctx.ServerError("CountProjects", err)
    85  		return
    86  	}
    87  
    88  	if isShowClosed {
    89  		ctx.Data["OpenCount"] = opTotal
    90  		ctx.Data["ClosedCount"] = total
    91  	} else {
    92  		ctx.Data["OpenCount"] = total
    93  		ctx.Data["ClosedCount"] = opTotal
    94  	}
    95  
    96  	ctx.Data["Projects"] = projects
    97  	shared_user.RenderUserHeader(ctx)
    98  
    99  	if isShowClosed {
   100  		ctx.Data["State"] = "closed"
   101  	} else {
   102  		ctx.Data["State"] = "open"
   103  	}
   104  
   105  	for _, project := range projects {
   106  		project.RenderedContent = templates.RenderMarkdownToHtml(ctx, project.Description)
   107  	}
   108  
   109  	err = shared_user.LoadHeaderCount(ctx)
   110  	if err != nil {
   111  		ctx.ServerError("LoadHeaderCount", err)
   112  		return
   113  	}
   114  
   115  	numPages := 0
   116  	if total > 0 {
   117  		numPages = (int(total) - 1/setting.UI.IssuePagingNum)
   118  	}
   119  
   120  	pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
   121  	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
   122  	ctx.Data["Page"] = pager
   123  
   124  	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
   125  	ctx.Data["IsShowClosed"] = isShowClosed
   126  	ctx.Data["PageIsViewProjects"] = true
   127  	ctx.Data["SortType"] = sortType
   128  
   129  	ctx.HTML(http.StatusOK, tplProjects)
   130  }
   131  
   132  func canWriteProjects(ctx *context.Context) bool {
   133  	if ctx.ContextUser.IsOrganization() {
   134  		return ctx.Org.CanWriteUnit(ctx, unit.TypeProjects)
   135  	}
   136  	return ctx.Doer != nil && ctx.ContextUser.ID == ctx.Doer.ID
   137  }
   138  
   139  // RenderNewProject render creating a project page
   140  func RenderNewProject(ctx *context.Context) {
   141  	ctx.Data["Title"] = ctx.Tr("repo.projects.new")
   142  	ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
   143  	ctx.Data["CardTypes"] = project_model.GetCardConfig()
   144  	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
   145  	ctx.Data["PageIsViewProjects"] = true
   146  	ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
   147  	ctx.Data["CancelLink"] = ctx.ContextUser.HomeLink() + "/-/projects"
   148  	shared_user.RenderUserHeader(ctx)
   149  
   150  	err := shared_user.LoadHeaderCount(ctx)
   151  	if err != nil {
   152  		ctx.ServerError("LoadHeaderCount", err)
   153  		return
   154  	}
   155  
   156  	ctx.HTML(http.StatusOK, tplProjectsNew)
   157  }
   158  
   159  // NewProjectPost creates a new project
   160  func NewProjectPost(ctx *context.Context) {
   161  	form := web.GetForm(ctx).(*forms.CreateProjectForm)
   162  	ctx.Data["Title"] = ctx.Tr("repo.projects.new")
   163  	shared_user.RenderUserHeader(ctx)
   164  
   165  	if ctx.HasError() {
   166  		RenderNewProject(ctx)
   167  		return
   168  	}
   169  
   170  	newProject := project_model.Project{
   171  		OwnerID:     ctx.ContextUser.ID,
   172  		Title:       form.Title,
   173  		Description: form.Content,
   174  		CreatorID:   ctx.Doer.ID,
   175  		BoardType:   form.BoardType,
   176  		CardType:    form.CardType,
   177  	}
   178  
   179  	if ctx.ContextUser.IsOrganization() {
   180  		newProject.Type = project_model.TypeOrganization
   181  	} else {
   182  		newProject.Type = project_model.TypeIndividual
   183  	}
   184  
   185  	if err := project_model.NewProject(ctx, &newProject); err != nil {
   186  		ctx.ServerError("NewProject", err)
   187  		return
   188  	}
   189  
   190  	ctx.Flash.Success(ctx.Tr("repo.projects.create_success", form.Title))
   191  	ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects")
   192  }
   193  
   194  // ChangeProjectStatus updates the status of a project between "open" and "close"
   195  func ChangeProjectStatus(ctx *context.Context) {
   196  	var toClose bool
   197  	switch ctx.Params(":action") {
   198  	case "open":
   199  		toClose = false
   200  	case "close":
   201  		toClose = true
   202  	default:
   203  		ctx.JSONRedirect(ctx.ContextUser.HomeLink() + "/-/projects")
   204  		return
   205  	}
   206  	id := ctx.ParamsInt64(":id")
   207  
   208  	if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
   209  		ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
   210  		return
   211  	}
   212  	ctx.JSONRedirect(fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), id))
   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  		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
   220  		return
   221  	}
   222  	if p.OwnerID != ctx.ContextUser.ID {
   223  		ctx.NotFound("", nil)
   224  		return
   225  	}
   226  
   227  	if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil {
   228  		ctx.Flash.Error("DeleteProjectByID: " + err.Error())
   229  	} else {
   230  		ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
   231  	}
   232  
   233  	ctx.JSONRedirect(ctx.ContextUser.HomeLink() + "/-/projects")
   234  }
   235  
   236  // RenderEditProject allows a project to be edited
   237  func RenderEditProject(ctx *context.Context) {
   238  	ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
   239  	ctx.Data["PageIsEditProjects"] = true
   240  	ctx.Data["PageIsViewProjects"] = true
   241  	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
   242  	ctx.Data["CardTypes"] = project_model.GetCardConfig()
   243  
   244  	shared_user.RenderUserHeader(ctx)
   245  
   246  	p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
   247  	if err != nil {
   248  		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
   249  		return
   250  	}
   251  	if p.OwnerID != ctx.ContextUser.ID {
   252  		ctx.NotFound("", nil)
   253  		return
   254  	}
   255  
   256  	ctx.Data["projectID"] = p.ID
   257  	ctx.Data["title"] = p.Title
   258  	ctx.Data["content"] = p.Description
   259  	ctx.Data["redirect"] = ctx.FormString("redirect")
   260  	ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
   261  	ctx.Data["card_type"] = p.CardType
   262  	ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), p.ID)
   263  
   264  	ctx.HTML(http.StatusOK, tplProjectsNew)
   265  }
   266  
   267  // EditProjectPost response for editing a project
   268  func EditProjectPost(ctx *context.Context) {
   269  	form := web.GetForm(ctx).(*forms.CreateProjectForm)
   270  	projectID := ctx.ParamsInt64(":id")
   271  	ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
   272  	ctx.Data["PageIsEditProjects"] = true
   273  	ctx.Data["PageIsViewProjects"] = true
   274  	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
   275  	ctx.Data["CardTypes"] = project_model.GetCardConfig()
   276  	ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID)
   277  
   278  	shared_user.RenderUserHeader(ctx)
   279  
   280  	err := shared_user.LoadHeaderCount(ctx)
   281  	if err != nil {
   282  		ctx.ServerError("LoadHeaderCount", err)
   283  		return
   284  	}
   285  
   286  	if ctx.HasError() {
   287  		ctx.HTML(http.StatusOK, tplProjectsNew)
   288  		return
   289  	}
   290  
   291  	p, err := project_model.GetProjectByID(ctx, projectID)
   292  	if err != nil {
   293  		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
   294  		return
   295  	}
   296  	if p.OwnerID != ctx.ContextUser.ID {
   297  		ctx.NotFound("", nil)
   298  		return
   299  	}
   300  
   301  	p.Title = form.Title
   302  	p.Description = form.Content
   303  	p.CardType = form.CardType
   304  	if err = project_model.UpdateProject(ctx, p); err != nil {
   305  		ctx.ServerError("UpdateProjects", err)
   306  		return
   307  	}
   308  
   309  	ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title))
   310  	if ctx.FormString("redirect") == "project" {
   311  		ctx.Redirect(p.Link(ctx))
   312  	} else {
   313  		ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects")
   314  	}
   315  }
   316  
   317  // ViewProject renders the project board for a project
   318  func ViewProject(ctx *context.Context) {
   319  	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
   320  	if err != nil {
   321  		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
   322  		return
   323  	}
   324  	if project.OwnerID != ctx.ContextUser.ID {
   325  		ctx.NotFound("", nil)
   326  		return
   327  	}
   328  
   329  	boards, err := project.GetBoards(ctx)
   330  	if err != nil {
   331  		ctx.ServerError("GetProjectBoards", err)
   332  		return
   333  	}
   334  
   335  	issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
   336  	if err != nil {
   337  		ctx.ServerError("LoadIssuesOfBoards", err)
   338  		return
   339  	}
   340  
   341  	if project.CardType != project_model.CardTypeTextOnly {
   342  		issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
   343  		for _, issuesList := range issuesMap {
   344  			for _, issue := range issuesList {
   345  				if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
   346  					issuesAttachmentMap[issue.ID] = issueAttachment
   347  				}
   348  			}
   349  		}
   350  		ctx.Data["issuesAttachmentMap"] = issuesAttachmentMap
   351  	}
   352  
   353  	linkedPrsMap := make(map[int64][]*issues_model.Issue)
   354  	for _, issuesList := range issuesMap {
   355  		for _, issue := range issuesList {
   356  			var referencedIDs []int64
   357  			for _, comment := range issue.Comments {
   358  				if comment.RefIssueID != 0 && comment.RefIsPull {
   359  					referencedIDs = append(referencedIDs, comment.RefIssueID)
   360  				}
   361  			}
   362  
   363  			if len(referencedIDs) > 0 {
   364  				if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
   365  					IssueIDs: referencedIDs,
   366  					IsPull:   optional.Some(true),
   367  				}); err == nil {
   368  					linkedPrsMap[issue.ID] = linkedPrs
   369  				}
   370  			}
   371  		}
   372  	}
   373  
   374  	project.RenderedContent = templates.RenderMarkdownToHtml(ctx, project.Description)
   375  	ctx.Data["LinkedPRs"] = linkedPrsMap
   376  	ctx.Data["PageIsViewProjects"] = true
   377  	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
   378  	ctx.Data["Project"] = project
   379  	ctx.Data["IssuesMap"] = issuesMap
   380  	ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend
   381  	shared_user.RenderUserHeader(ctx)
   382  
   383  	err = shared_user.LoadHeaderCount(ctx)
   384  	if err != nil {
   385  		ctx.ServerError("LoadHeaderCount", err)
   386  		return
   387  	}
   388  
   389  	ctx.HTML(http.StatusOK, tplProjectsView)
   390  }
   391  
   392  // DeleteProjectBoard allows for the deletion of a project board
   393  func DeleteProjectBoard(ctx *context.Context) {
   394  	if ctx.Doer == nil {
   395  		ctx.JSON(http.StatusForbidden, map[string]string{
   396  			"message": "Only signed in users are allowed to perform this action.",
   397  		})
   398  		return
   399  	}
   400  
   401  	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
   402  	if err != nil {
   403  		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
   404  		return
   405  	}
   406  
   407  	pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
   408  	if err != nil {
   409  		ctx.ServerError("GetProjectBoard", err)
   410  		return
   411  	}
   412  	if pb.ProjectID != ctx.ParamsInt64(":id") {
   413  		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
   414  			"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
   415  		})
   416  		return
   417  	}
   418  
   419  	if project.OwnerID != ctx.ContextUser.ID {
   420  		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
   421  			"message": fmt.Sprintf("ProjectBoard[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
   422  		})
   423  		return
   424  	}
   425  
   426  	if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil {
   427  		ctx.ServerError("DeleteProjectBoardByID", err)
   428  		return
   429  	}
   430  
   431  	ctx.JSONOK()
   432  }
   433  
   434  // AddBoardToProjectPost allows a new board to be added to a project.
   435  func AddBoardToProjectPost(ctx *context.Context) {
   436  	form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
   437  
   438  	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
   439  	if err != nil {
   440  		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
   441  		return
   442  	}
   443  
   444  	if err := project_model.NewBoard(ctx, &project_model.Board{
   445  		ProjectID: project.ID,
   446  		Title:     form.Title,
   447  		Color:     form.Color,
   448  		CreatorID: ctx.Doer.ID,
   449  	}); err != nil {
   450  		ctx.ServerError("NewProjectBoard", err)
   451  		return
   452  	}
   453  
   454  	ctx.JSONOK()
   455  }
   456  
   457  // CheckProjectBoardChangePermissions check permission
   458  func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) {
   459  	if ctx.Doer == nil {
   460  		ctx.JSON(http.StatusForbidden, map[string]string{
   461  			"message": "Only signed in users are allowed to perform this action.",
   462  		})
   463  		return nil, nil
   464  	}
   465  
   466  	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
   467  	if err != nil {
   468  		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
   469  		return nil, nil
   470  	}
   471  
   472  	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
   473  	if err != nil {
   474  		ctx.ServerError("GetProjectBoard", err)
   475  		return nil, nil
   476  	}
   477  	if board.ProjectID != ctx.ParamsInt64(":id") {
   478  		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
   479  			"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
   480  		})
   481  		return nil, nil
   482  	}
   483  
   484  	if project.OwnerID != ctx.ContextUser.ID {
   485  		ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
   486  			"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, project.ID),
   487  		})
   488  		return nil, nil
   489  	}
   490  	return project, board
   491  }
   492  
   493  // EditProjectBoard allows a project board's to be updated
   494  func EditProjectBoard(ctx *context.Context) {
   495  	form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
   496  	_, board := CheckProjectBoardChangePermissions(ctx)
   497  	if ctx.Written() {
   498  		return
   499  	}
   500  
   501  	if form.Title != "" {
   502  		board.Title = form.Title
   503  	}
   504  
   505  	board.Color = form.Color
   506  
   507  	if form.Sorting != 0 {
   508  		board.Sorting = form.Sorting
   509  	}
   510  
   511  	if err := project_model.UpdateBoard(ctx, board); err != nil {
   512  		ctx.ServerError("UpdateProjectBoard", err)
   513  		return
   514  	}
   515  
   516  	ctx.JSONOK()
   517  }
   518  
   519  // SetDefaultProjectBoard set default board for uncategorized issues/pulls
   520  func SetDefaultProjectBoard(ctx *context.Context) {
   521  	project, board := CheckProjectBoardChangePermissions(ctx)
   522  	if ctx.Written() {
   523  		return
   524  	}
   525  
   526  	if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil {
   527  		ctx.ServerError("SetDefaultBoard", err)
   528  		return
   529  	}
   530  
   531  	ctx.JSONOK()
   532  }
   533  
   534  // MoveIssues moves or keeps issues in a column and sorts them inside that column
   535  func MoveIssues(ctx *context.Context) {
   536  	if ctx.Doer == nil {
   537  		ctx.JSON(http.StatusForbidden, map[string]string{
   538  			"message": "Only signed in users are allowed to perform this action.",
   539  		})
   540  		return
   541  	}
   542  
   543  	project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
   544  	if err != nil {
   545  		ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
   546  		return
   547  	}
   548  	if project.OwnerID != ctx.ContextUser.ID {
   549  		ctx.NotFound("InvalidRepoID", nil)
   550  		return
   551  	}
   552  
   553  	board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
   554  	if err != nil {
   555  		ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err)
   556  		return
   557  	}
   558  
   559  	if board.ProjectID != project.ID {
   560  		ctx.NotFound("BoardNotInProject", nil)
   561  		return
   562  	}
   563  
   564  	type movedIssuesForm struct {
   565  		Issues []struct {
   566  			IssueID int64 `json:"issueID"`
   567  			Sorting int64 `json:"sorting"`
   568  		} `json:"issues"`
   569  	}
   570  
   571  	form := &movedIssuesForm{}
   572  	if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
   573  		ctx.ServerError("DecodeMovedIssuesForm", err)
   574  		return
   575  	}
   576  
   577  	issueIDs := make([]int64, 0, len(form.Issues))
   578  	sortedIssueIDs := make(map[int64]int64)
   579  	for _, issue := range form.Issues {
   580  		issueIDs = append(issueIDs, issue.IssueID)
   581  		sortedIssueIDs[issue.Sorting] = issue.IssueID
   582  	}
   583  	movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
   584  	if err != nil {
   585  		ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err)
   586  		return
   587  	}
   588  
   589  	if len(movedIssues) != len(form.Issues) {
   590  		ctx.ServerError("some issues do not exist", errors.New("some issues do not exist"))
   591  		return
   592  	}
   593  
   594  	if _, err = movedIssues.LoadRepositories(ctx); err != nil {
   595  		ctx.ServerError("LoadRepositories", err)
   596  		return
   597  	}
   598  
   599  	for _, issue := range movedIssues {
   600  		if issue.RepoID != project.RepoID && issue.Repo.OwnerID != project.OwnerID {
   601  			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"))
   602  			return
   603  		}
   604  	}
   605  
   606  	if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil {
   607  		ctx.ServerError("MoveIssuesOnProjectBoard", err)
   608  		return
   609  	}
   610  
   611  	ctx.JSONOK()
   612  }