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