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  }