From 70a28881724332ae94455102cec179623dd0ca78 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 25 Aug 2022 15:28:26 -0400 Subject: [PATCH 01/10] add priority to issues --- models/issues/comment.go | 15 +- models/issues/issue.go | 15 +- models/issues/priority.go | 771 ++++++++++++++++++++++++++++++++ models/migrations/migrations.go | 3 + models/migrations/v999.go | 46 ++ 5 files changed, 844 insertions(+), 6 deletions(-) create mode 100644 models/issues/priority.go create mode 100644 models/migrations/v999.go diff --git a/models/issues/comment.go b/models/issues/comment.go index a71afda9e0e8f..6b80688e7d73a 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -130,6 +130,8 @@ const ( CommentTypePRScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed CommentTypePRUnScheduledToAutoMerge + // 35 Priority changed + CommentTypePriority ) var commentStrings = []string{ @@ -225,6 +227,8 @@ type Comment struct { Label *Label `xorm:"-"` AddedLabels []*Label `xorm:"-"` RemovedLabels []*Label `xorm:"-"` + PriorityID int64 + Priority *Priority `xorm:"-"` OldProjectID int64 ProjectID int64 OldProject *project_model.Project `xorm:"-"` @@ -960,11 +964,12 @@ func createIssueDependencyComment(ctx context.Context, doer *user_model.User, is // CreateCommentOptions defines options for creating comment type CreateCommentOptions struct { - Type CommentType - Doer *user_model.User - Repo *repo_model.Repository - Issue *Issue - Label *Label + Type CommentType + Doer *user_model.User + Repo *repo_model.Repository + Issue *Issue + Label *Label + Priority *Priority DependentIssueID int64 OldMilestoneID int64 diff --git a/models/issues/issue.go b/models/issues/issue.go index 5bdb60f7c08c5..bbab2290c138d 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -117,7 +117,8 @@ type Issue struct { MilestoneID int64 `xorm:"INDEX"` Milestone *Milestone `xorm:"-"` Project *project_model.Project `xorm:"-"` - Priority int + PriorityID int64 + Priority *Priority `xorm:"-"` AssigneeID int64 `xorm:"-"` Assignee *user_model.User `xorm:"-"` IsClosed bool `xorm:"INDEX"` @@ -236,6 +237,17 @@ func (issue *Issue) LoadLabels(ctx context.Context) (err error) { return nil } +// LoadPriorities loads labels +func (issue *Issue) LoadPriorities(ctx context.Context) (err error) { + if issue.Priority == nil { + issue.Priority, err = GetPriorityByIssueID(ctx, issue.ID) + if err != nil { + return fmt.Errorf("getPriorityByIssueID [%d]: %v", issue.ID, err) + } + } + return nil +} + // LoadPoster loads poster func (issue *Issue) LoadPoster() error { return issue.loadPoster(db.DefaultContext) @@ -1192,6 +1204,7 @@ type IssuesOptions struct { //nolint IsClosed util.OptionalBool IsPull util.OptionalBool LabelIDs []int64 + PriorityIDs []int64 IncludedLabelNames []string ExcludedLabelNames []string IncludeMilestones []string diff --git a/models/issues/priority.go b/models/issues/priority.go new file mode 100644 index 0000000000000..b1ded4934d745 --- /dev/null +++ b/models/issues/priority.go @@ -0,0 +1,771 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues + +import ( + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" + "context" + "fmt" + "html/template" + "regexp" + "strconv" + "strings" + "xorm.io/builder" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" +) + +// ErrRepoPriorityNotExist represents a "RepoPriorityNotExist" kind of error. +type ErrRepoPriorityNotExist struct { + PriorityID int64 + RepoID int64 +} + +// IsErrRepoPriorityNotExist checks if an error is a RepoErrPriorityNotExist. +func IsErrRepoPriorityNotExist(err error) bool { + _, ok := err.(ErrRepoPriorityNotExist) + return ok +} + +func (err ErrRepoPriorityNotExist) Error() string { + return fmt.Sprintf("priority does not exist [priority_id: %d, repo_id: %d]", err.PriorityID, err.RepoID) +} + +// ErrOrgPriorityNotExist represents a "OrgPriorityNotExist" kind of error. +type ErrOrgPriorityNotExist struct { + PriorityID int64 + OrgID int64 +} + +// IsErrOrgPriorityNotExist checks if an error is a OrgErrPriorityNotExist. +func IsErrOrgPriorityNotExist(err error) bool { + _, ok := err.(ErrOrgPriorityNotExist) + return ok +} + +func (err ErrOrgPriorityNotExist) Error() string { + return fmt.Sprintf("priority does not exist [priority_id: %d, org_id: %d]", err.PriorityID, err.OrgID) +} + +// ErrPriorityNotExist represents a "PriorityNotExist" kind of error. +type ErrPriorityNotExist struct { + PriorityID int64 +} + +// IsErrPriorityNotExist checks if an error is a ErrPriorityNotExist. +func IsErrPriorityNotExist(err error) bool { + _, ok := err.(ErrPriorityNotExist) + return ok +} + +func (err ErrPriorityNotExist) Error() string { + return fmt.Sprintf("priority does not exist [priority_id: %d]", err.PriorityID) +} + +// PriorityColorPattern is a regexp witch can validate PriorityColor +var PriorityColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") + +// Priority represents a priority of repository for issues. +type Priority struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + OrgID int64 `xorm:"INDEX"` + Name string + Description string + Color string `xorm:"VARCHAR(7)"` + Weight int + NumIssues int + NumClosedIssues int + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + + NumOpenIssues int `xorm:"-"` + NumOpenRepoIssues int64 `xorm:"-"` + IsChecked bool `xorm:"-"` + QueryString string `xorm:"-"` + IsSelected bool `xorm:"-"` + IsExcluded bool `xorm:"-"` +} + +func init() { + db.RegisterModel(new(Priority)) +} + +// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues. +func (priority *Priority) CalOpenIssues() { + priority.NumOpenIssues = priority.NumIssues - priority.NumClosedIssues +} + +// CalOpenOrgIssues calculates the open issues of a label for a specific repo +func (priority *Priority) CalOpenOrgIssues(repoID, priorityID int64) { + counts, _ := CountIssuesByRepo(&IssuesOptions{ + RepoID: repoID, + PriorityIDs: []int64{priorityID}, + IsClosed: util.OptionalBoolFalse, + }) + + for _, count := range counts { + priority.NumOpenRepoIssues += count + } +} + +// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked +func (priority *Priority) LoadSelectedPrioritiesAfterClick(currentSelectedPriorities []int64) { + var priorityQuerySlice []string + labelSelected := false + labelID := strconv.FormatInt(priority.ID, 10) + for _, s := range currentSelectedPriorities { + if s == priority.ID { + labelSelected = true + } else if -s == priority.ID { + labelSelected = true + priority.IsExcluded = true + } else if s != 0 { + priorityQuerySlice = append(priorityQuerySlice, strconv.FormatInt(s, 10)) + } + } + if !labelSelected { + priorityQuerySlice = append(priorityQuerySlice, labelID) + } + priority.IsSelected = labelSelected + priority.QueryString = strings.Join(priorityQuerySlice, ",") +} + +// BelongsToOrg returns true if label is an organization label +func (priority *Priority) BelongsToOrg() bool { + return priority.OrgID > 0 +} + +// BelongsToRepo returns true if label is a repository label +func (priority *Priority) BelongsToRepo() bool { + return priority.RepoID > 0 +} + +// ForegroundColor calculates the text color for labels based +// on their background color. +func (priority *Priority) ForegroundColor() template.CSS { + if strings.HasPrefix(priority.Color, "#") { + if color, err := strconv.ParseUint(priority.Color[1:], 16, 64); err == nil { + // NOTE: see web_src/js/components/ContextPopup.vue for similar implementation + luminance := Luminance(uint32(color)) + + // prefer white or black based upon contrast + if luminance < LuminanceThreshold { + return template.CSS("#fff") + } + return template.CSS("#000") + } + } + + // default to black + return template.CSS("#000") +} + +// NewPriority creates a new Priority +func NewPriority(ctx context.Context, priority *Priority) error { + if !LabelColorPattern.MatchString(priority.Color) { + return fmt.Errorf("bad color code: %s", priority.Color) + } + + // normalize case + priority.Color = strings.ToLower(priority.Color) + + // add leading hash + if priority.Color[0] != '#' { + priority.Color = "#" + priority.Color + } + + // convert 3-character shorthand into 6-character version + if len(priority.Color) == 4 { + r := priority.Color[1] + g := priority.Color[2] + b := priority.Color[3] + priority.Color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) + } + + return db.Insert(ctx, priority) +} + +// NewPriorities creates new labels +func NewPriorities(priorities ...*Priority) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + for _, priority := range priorities { + if !LabelColorPattern.MatchString(priority.Color) { + return fmt.Errorf("bad color code: %s", priority.Color) + } + if err := db.Insert(ctx, priority); err != nil { + return err + } + } + return committer.Commit() +} + +// UpdateLabel updates label information. +func UpdatePriority(priority *Priority) error { + if !PriorityColorPattern.MatchString(priority.Color) { + return fmt.Errorf("bad color code: %s", priority.Color) + } + return updatePriorityCols(db.DefaultContext, priority, "name", "description", "color", "weight") +} + +// DeleteLabel delete a label +func DeletePriority(id, priorityId int64) error { + priority, err := GetPriorityByID(db.DefaultContext, priorityId) + if err != nil { + if IsErrPriorityNotExist(err) { + return nil + } + return err + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + sess := db.GetEngine(ctx) + + if priority.BelongsToOrg() && priority.OrgID != id { + return nil + } + if priority.BelongsToRepo() && priority.RepoID != id { + return nil + } + + if _, err = sess.ID(priorityId).Delete(new(Priority)); err != nil { + return err + } else if _, err = sess. + Where("label_id = ?", priorityId). + Delete(new(IssueLabel)); err != nil { + return err + } + + // delete comments about now deleted label_id + if _, err = sess.Where("priority_id = ?", priorityId).Cols("priority_id").Delete(&Comment{}); err != nil { + return err + } + + return committer.Commit() +} + +// GetLabelByID returns a label by given ID. +func GetPriorityByID(ctx context.Context, priorityID int64) (*Priority, error) { + if priorityID <= 0 { + return nil, ErrLabelNotExist{priorityID} + } + + priority := &Priority{} + has, err := db.GetEngine(ctx).ID(priorityID).Get(priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPriorityNotExist{priority.ID} + } + return priority, nil +} + +// GetLabelsByIDs returns a list of labels by IDs +func GetPrioritiesByIDs(prioritiesIDs []int64) ([]*Priority, error) { + priorities := make([]*Priority, 0, len(prioritiesIDs)) + return priorities, db.GetEngine(db.DefaultContext).Table("priorities"). + In("id", prioritiesIDs). + Asc("name"). + Cols("id", "repo_id", "org_id"). + Find(&priorities) +} + +// __________ .__ __ +// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. +// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | +// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | +// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| +// \/ \/|__| \/ \/ + +// GetLabelInRepoByName returns a label by name in given repository. +func GetPriorityInRepoByName(ctx context.Context, repoID int64, labelName string) (*Priority, error) { + if len(labelName) == 0 || repoID <= 0 { + return nil, ErrRepoPriorityNotExist{0, repoID} + } + + priority := &Priority{ + Name: labelName, + RepoID: repoID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRepoPriorityNotExist{0, priority.RepoID} + } + return priority, nil +} + +// GetLabelInRepoByID returns a label by ID in given repository. +func GetPriorityInRepoByID(ctx context.Context, repoID, priorityID int64) (*Priority, error) { + if priorityID <= 0 || repoID <= 0 { + return nil, ErrRepoPriorityNotExist{priorityID, repoID} + } + + priority := &Priority{ + ID: priorityID, + RepoID: repoID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRepoPriorityNotExist{priority.ID, priority.RepoID} + } + return priority, nil +} + +// GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given +// repository. +// it silently ignores label names that do not belong to the repository. +func GetPriorityIDsInRepoByNames(repoID int64, priorityNames []string) ([]int64, error) { + priorityIDs := make([]int64, 0, len(priorityNames)) + return priorityIDs, db.GetEngine(db.DefaultContext).Table("priority"). + Where("repo_id = ?", repoID). + In("name", priorityNames). + Asc("name"). + Cols("id"). + Find(&priorityIDs) +} + +// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names +func BuildPriorityNamesIssueIDsCondition(priorityNames []string) *builder.Builder { + return builder.Select("issue_label.issue_id"). + From("issue"). + InnerJoin("priority", "priority.id = issue.priority_id"). + Where( + builder.In("priority.name", priorityNames), + ). + GroupBy("issue.id") +} + +// GetLabelsInRepoByIDs returns a list of labels by IDs in given repository, +// it silently ignores label IDs that do not belong to the repository. +func GetPriorityInRepoByIDs(repoID int64, priorityIDs []int64) ([]*Priority, error) { + priorities := make([]*Priority, 0, len(priorityIDs)) + return priorities, db.GetEngine(db.DefaultContext). + Where("repo_id = ?", repoID). + In("id", priorities). + Asc("name"). + Find(&priorities) +} + +// GetLabelsByRepoID returns all labels that belong to given repository by ID. +func GetPrioritiesByRepoID(ctx context.Context, repoID int64, sortType string, listOptions db.ListOptions) ([]*Priority, error) { + if repoID <= 0 { + return nil, ErrRepoPriorityNotExist{0, repoID} + } + priorities := make([]*Priority, 0, 10) + sess := db.GetEngine(ctx).Where("repo_id = ?", repoID) + + switch sortType { + case "reverseweight": + sess.Desc("weight") + case "weight": + sess.Asc("weight") + case "reversealphabetically": + sess.Desc("name") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + default: + sess.Asc("name") + } + + if listOptions.Page != 0 { + sess = db.SetSessionPagination(sess, &listOptions) + } + + return priorities, sess.Find(&priorities) +} + +// CountLabelsByRepoID count number of all labels that belong to given repository by ID. +func CountPrioritiesByRepoID(repoID int64) (int64, error) { + return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Priority{}) +} + +// ________ +// \_____ \_______ ____ +// / | \_ __ \/ ___\ +// / | \ | \/ /_/ > +// \_______ /__| \___ / +// \/ /_____/ + +// GetLabelInOrgByName returns a label by name in given organization. +func GetPriorityInOrgByName(ctx context.Context, orgID int64, priorityName string) (*Priority, error) { + if len(priorityName) == 0 || orgID <= 0 { + return nil, ErrOrgPriorityNotExist{0, orgID} + } + + priority := &Priority{ + Name: priorityName, + OrgID: orgID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgPriorityNotExist{0, priority.OrgID} + } + return priority, nil +} + +// GetLabelInOrgByID returns a label by ID in given organization. +func GetPriorityInOrgByID(ctx context.Context, orgID, priorityID int64) (*Priority, error) { + if priorityID <= 0 || orgID <= 0 { + return nil, ErrOrgPriorityNotExist{priorityID, orgID} + } + + priority := &Priority{ + ID: priorityID, + OrgID: orgID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgPriorityNotExist{priority.ID, priority.OrgID} + } + return priority, nil +} + +// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given +// organization. +func GetPriorityIDsInOrgByNames(orgID int64, priorityNames []string) ([]int64, error) { + if orgID <= 0 { + return nil, ErrOrgPriorityNotExist{0, orgID} + } + priorityIDs := make([]int64, 0, len(priorityNames)) + + return priorityIDs, db.GetEngine(db.DefaultContext).Table("priority"). + Where("org_id = ?", orgID). + In("name", priorityNames). + Asc("name"). + Cols("id"). + Find(&priorityIDs) +} + +// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization, +// it silently ignores label IDs that do not belong to the organization. +func GetPriorityInOrgByIDs(orgID int64, priorityIDs []int64) ([]*Priority, error) { + priorities := make([]*Priority, 0, len(priorityIDs)) + return priorities, db.GetEngine(db.DefaultContext). + Where("org_id = ?", orgID). + In("id", priorityIDs). + Asc("name"). + Find(&priorities) +} + +// GetLabelsByOrgID returns all labels that belong to given organization by ID. +func GetPrioritiesByOrgID(ctx context.Context, orgID int64, sortType string, listOptions db.ListOptions) ([]*Priority, error) { + if orgID <= 0 { + return nil, ErrOrgPriorityNotExist{0, orgID} + } + priorities := make([]*Priority, 0, 10) + sess := db.GetEngine(ctx).Where("org_id = ?", orgID) + + switch sortType { + case "reverseweight": + sess.Desc("weight") + case "weight": + sess.Asc("weight") + case "reversealphabetically": + sess.Desc("name") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + default: + sess.Asc("name") + } + + if listOptions.Page != 0 { + sess = db.SetSessionPagination(sess, &listOptions) + } + + return priorities, sess.Find(&priorities) +} + +// CountLabelsByOrgID count all labels that belong to given organization by ID. +func CountPrioritiesByOrgID(orgID int64) (int64, error) { + return db.GetEngine(db.DefaultContext).Where("org_id = ?", orgID).Count(&Priority{}) +} + +// .___ +// | | ______ ________ __ ____ +// | |/ ___// ___/ | \_/ __ \ +// | |\___ \ \___ \| | /\ ___/ +// |___/____ >____ >____/ \___ | +// \/ \/ \/ + +// GetLabelsByIssueID returns all labels that belong to given issue by ID. +func GetPrioritiesByIssueID(ctx context.Context, issueID int64) ([]*Priority, error) { + var priorities []*Priority + return priorities, db.GetEngine(ctx).Where("issue.id = ?", issueID). + Join("LEFT", "issue", "issue.priority_id = label.id"). + Asc("priority.name"). + Find(&priorities) +} + +// GetLabelsByIssueID returns all labels that belong to given issue by ID. +func GetPriorityByIssueID(ctx context.Context, issueID int64) (*Priority, error) { + priorities, err := GetPrioritiesByIssueID(ctx, issueID) + if err != nil { + return nil, err + } + if len(priorities) == 0 { + return nil, nil + } + return priorities[0], nil +} + +func updatePriorityCols(ctx context.Context, priority *Priority, cols ...string) error { + _, err := db.GetEngine(ctx).ID(priority.ID). + SetExpr("num_issues", + builder.Select("count(*)").From("priority"). + Where(builder.Eq{"id": priority.ID}), + ). + SetExpr("num_closed_issues", + builder.Select("count(*)").From("priority"). + InnerJoin("issue", "priority.id = issue.priority_id"). + Where(builder.Eq{ + "priority.id": priority.ID, + "issue.is_closed": true, + }), + ). + Cols(cols...).Update(priority) + return err +} + +// HasIssueLabel returns true if issue has been labeled. +func HasIssuePriority(ctx context.Context, issueID, priority int64) bool { + has, _ := db.GetEngine(ctx).Where("id = ? AND priority = ?", issueID, priority).Get(new(Issue)) + return has +} + +// newIssueLabel this function creates a new label it does not check if the label is valid for the issue +// YOU MUST CHECK THIS BEFORE THIS FUNCTION +func newIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) (err error) { + issue.PriorityID = priority.ID + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = UpdateIssueCols(ctx, issue, "priority_id"); err != nil { + return fmt.Errorf("updateIssueCols: %v", err) + } + + if err = issue.LoadRepo(ctx); err != nil { + return + } + + opts := &CreateCommentOptions{ + Type: CommentTypePriority, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Priority: priority, + Content: "1", + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return err + } + + return updatePriorityCols(ctx, priority, "num_issues", "num_closed_issue") +} + +// NewIssueLabel creates a new issue-label relation. +func NewIssuePriority(issue *Issue, priority *Priority, doer *user_model.User) (err error) { + if HasIssueLabel(db.DefaultContext, issue.ID, priority.ID) { + return nil + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = issue.LoadRepo(ctx); err != nil { + return err + } + + // Do NOT add invalid labels + if issue.RepoID != priority.RepoID && issue.Repo.OwnerID != priority.OrgID { + return nil + } + + if err = newIssuePriority(ctx, issue, priority, doer); err != nil { + return err + } + + issue.Priority = nil + if err = issue.LoadPriorities(ctx); err != nil { + return err + } + + return committer.Commit() +} + +// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue +func newIssuePriorities(ctx context.Context, issue *Issue, priorities []*Priority, doer *user_model.User) (err error) { + if err = issue.LoadRepo(ctx); err != nil { + return err + } + for _, priority := range priorities { + // Don't add already present labels and invalid labels + if HasIssueLabel(ctx, issue.ID, priority.ID) || + (priority.RepoID != issue.RepoID && priority.OrgID != issue.Repo.OwnerID) { + continue + } + + if err = newIssuePriority(ctx, issue, priority, doer); err != nil { + return fmt.Errorf("newIssuePriority: %v", err) + } + } + + return nil +} + +// NewIssueLabels creates a list of issue-label relations. +func NewIssuePriorities(issue *Issue, priorities []*Priority, doer *user_model.User) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = newIssuePriorities(ctx, issue, priorities, doer); err != nil { + return err + } + + if err = issue.LoadPriorities(ctx); err != nil { + issue.Priority = nil + return err + } + + return committer.Commit() +} + +func deleteIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) (err error) { + issue.PriorityID = nil + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = UpdateIssueCols(ctx, issue, "priority_id"); err != nil { + return fmt.Errorf("updateIssueCols: %v", err) + } + if err = issue.LoadRepo(ctx); err != nil { + return + } + + opts := &CreateCommentOptions{ + Type: CommentTypePriority, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Priority: priority, + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return err + } + + return updatePriorityCols(ctx, priority, "num_issues", "num_closed_issue") +} + +// DeleteIssueLabel deletes issue-label relation. +func DeleteIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) error { + if err := deleteIssuePriority(ctx, issue, priority, doer); err != nil { + return err + } + + issue.Priority = nil + return issue.LoadPriorities(ctx) +} + +// DeleteLabelsByRepoID deletes labels of some repository +func DeletePrioritiesByRepoID(ctx context.Context, repoID int64) error { + + _, err := db.DeleteByBean(ctx, &Priority{RepoID: repoID}) + return err +} + +// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore +func CountOrphanedPriorities() (int64, error) { + noref, err := db.GetEngine(db.DefaultContext).Table("priority").Where("repo_id=? AND org_id=?", 0, 0).Count() + if err != nil { + return 0, err + } + + norepo, err := db.GetEngine(db.DefaultContext).Table("priority"). + Where(builder.And( + builder.Gt{"repo_id": 0}, + builder.NotIn("repo_id", builder.Select("id").From("repository")), + )). + Count() + if err != nil { + return 0, err + } + + noorg, err := db.GetEngine(db.DefaultContext).Table("priority"). + Where(builder.And( + builder.Gt{"org_id": 0}, + builder.NotIn("org_id", builder.Select("id").From("user")), + )). + Count() + if err != nil { + return 0, err + } + + return noref + norepo + noorg, nil +} + +// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore +func DeleteOrphanedPriorities() error { + // delete labels with no reference + if _, err := db.GetEngine(db.DefaultContext).Table("priority").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Priority)); err != nil { + return err + } + + // delete priorities with none existing repos + if _, err := db.GetEngine(db.DefaultContext). + Where(builder.And( + builder.Gt{"repo_id": 0}, + builder.NotIn("repo_id", builder.Select("id").From("repository")), + )). + Delete(Priority{}); err != nil { + return err + } + + // delete priorities with none existing orgs + if _, err := db.GetEngine(db.DefaultContext). + Where(builder.And( + builder.Gt{"org_id": 0}, + builder.NotIn("org_id", builder.Select("id").From("user")), + )). + Delete(Priority{}); err != nil { + return err + } + + return nil +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 30c4ad250c084..83d21acf0bab9 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -408,6 +408,9 @@ var migrations = []Migration{ NewMigration("Rename CredentialIDBytes column to CredentialID", renameCredentialIDBytes), // v224 -> v225 NewMigration("Add badges to users", creatUserBadgesTable), + + // v999 + NewMigration("Create issue priority tables", createIssuePriorityTables), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v999.go b/models/migrations/v999.go new file mode 100644 index 0000000000000..b693a7c48e89c --- /dev/null +++ b/models/migrations/v999.go @@ -0,0 +1,46 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func createIssuePriorityTables(x *xorm.Engine) error { + // Priority represents a priority of repository for issues. + type Priority struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + OrgID int64 `xorm:"INDEX"` + Name string + Description string + Color string `xorm:"VARCHAR(7)"` + Weight int + NumIssues int + NumClosedIssues int + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + if err := x.Sync2(new(Priority)); err != nil { + return err + } + + type Comment struct { + ID int64 `xorm:"pk autoincr"` + PriorityID int64 + } + if err := x.Sync2(new(Comment)); err != nil { + return err + } + + type Issue struct { + ID int64 `xorm:"pk autoincr"` + PriorityID int64 + } + + return x.Sync2(new(Issue)) +} From c023a1162ff1c22af14372af5a1de1dae11df589 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 25 Aug 2022 15:28:26 -0400 Subject: [PATCH 02/10] add priority to issues --- models/issues/comment.go | 15 +- models/issues/issue.go | 15 +- models/issues/priority.go | 771 ++++++++++++++++++++++++++++++++ models/migrations/migrations.go | 3 + models/migrations/v999.go | 46 ++ 5 files changed, 844 insertions(+), 6 deletions(-) create mode 100644 models/issues/priority.go create mode 100644 models/migrations/v999.go diff --git a/models/issues/comment.go b/models/issues/comment.go index a71afda9e0e8f..6b80688e7d73a 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -130,6 +130,8 @@ const ( CommentTypePRScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed CommentTypePRUnScheduledToAutoMerge + // 35 Priority changed + CommentTypePriority ) var commentStrings = []string{ @@ -225,6 +227,8 @@ type Comment struct { Label *Label `xorm:"-"` AddedLabels []*Label `xorm:"-"` RemovedLabels []*Label `xorm:"-"` + PriorityID int64 + Priority *Priority `xorm:"-"` OldProjectID int64 ProjectID int64 OldProject *project_model.Project `xorm:"-"` @@ -960,11 +964,12 @@ func createIssueDependencyComment(ctx context.Context, doer *user_model.User, is // CreateCommentOptions defines options for creating comment type CreateCommentOptions struct { - Type CommentType - Doer *user_model.User - Repo *repo_model.Repository - Issue *Issue - Label *Label + Type CommentType + Doer *user_model.User + Repo *repo_model.Repository + Issue *Issue + Label *Label + Priority *Priority DependentIssueID int64 OldMilestoneID int64 diff --git a/models/issues/issue.go b/models/issues/issue.go index 5bdb60f7c08c5..bbab2290c138d 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -117,7 +117,8 @@ type Issue struct { MilestoneID int64 `xorm:"INDEX"` Milestone *Milestone `xorm:"-"` Project *project_model.Project `xorm:"-"` - Priority int + PriorityID int64 + Priority *Priority `xorm:"-"` AssigneeID int64 `xorm:"-"` Assignee *user_model.User `xorm:"-"` IsClosed bool `xorm:"INDEX"` @@ -236,6 +237,17 @@ func (issue *Issue) LoadLabels(ctx context.Context) (err error) { return nil } +// LoadPriorities loads labels +func (issue *Issue) LoadPriorities(ctx context.Context) (err error) { + if issue.Priority == nil { + issue.Priority, err = GetPriorityByIssueID(ctx, issue.ID) + if err != nil { + return fmt.Errorf("getPriorityByIssueID [%d]: %v", issue.ID, err) + } + } + return nil +} + // LoadPoster loads poster func (issue *Issue) LoadPoster() error { return issue.loadPoster(db.DefaultContext) @@ -1192,6 +1204,7 @@ type IssuesOptions struct { //nolint IsClosed util.OptionalBool IsPull util.OptionalBool LabelIDs []int64 + PriorityIDs []int64 IncludedLabelNames []string ExcludedLabelNames []string IncludeMilestones []string diff --git a/models/issues/priority.go b/models/issues/priority.go new file mode 100644 index 0000000000000..6d0306d53ac95 --- /dev/null +++ b/models/issues/priority.go @@ -0,0 +1,771 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues + +import ( + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" + "context" + "fmt" + "html/template" + "regexp" + "strconv" + "strings" + "xorm.io/builder" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" +) + +// ErrRepoPriorityNotExist represents a "RepoPriorityNotExist" kind of error. +type ErrRepoPriorityNotExist struct { + PriorityID int64 + RepoID int64 +} + +// IsErrRepoPriorityNotExist checks if an error is a RepoErrPriorityNotExist. +func IsErrRepoPriorityNotExist(err error) bool { + _, ok := err.(ErrRepoPriorityNotExist) + return ok +} + +func (err ErrRepoPriorityNotExist) Error() string { + return fmt.Sprintf("priority does not exist [priority_id: %d, repo_id: %d]", err.PriorityID, err.RepoID) +} + +// ErrOrgPriorityNotExist represents a "OrgPriorityNotExist" kind of error. +type ErrOrgPriorityNotExist struct { + PriorityID int64 + OrgID int64 +} + +// IsErrOrgPriorityNotExist checks if an error is a OrgErrPriorityNotExist. +func IsErrOrgPriorityNotExist(err error) bool { + _, ok := err.(ErrOrgPriorityNotExist) + return ok +} + +func (err ErrOrgPriorityNotExist) Error() string { + return fmt.Sprintf("priority does not exist [priority_id: %d, org_id: %d]", err.PriorityID, err.OrgID) +} + +// ErrPriorityNotExist represents a "PriorityNotExist" kind of error. +type ErrPriorityNotExist struct { + PriorityID int64 +} + +// IsErrPriorityNotExist checks if an error is a ErrPriorityNotExist. +func IsErrPriorityNotExist(err error) bool { + _, ok := err.(ErrPriorityNotExist) + return ok +} + +func (err ErrPriorityNotExist) Error() string { + return fmt.Sprintf("priority does not exist [priority_id: %d]", err.PriorityID) +} + +// PriorityColorPattern is a regexp witch can validate PriorityColor +var PriorityColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") + +// Priority represents a priority of repository for issues. +type Priority struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + OrgID int64 `xorm:"INDEX"` + Name string + Description string + Color string `xorm:"VARCHAR(7)"` + Weight int + NumIssues int + NumClosedIssues int + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + + NumOpenIssues int `xorm:"-"` + NumOpenRepoIssues int64 `xorm:"-"` + IsChecked bool `xorm:"-"` + QueryString string `xorm:"-"` + IsSelected bool `xorm:"-"` + IsExcluded bool `xorm:"-"` +} + +func init() { + db.RegisterModel(new(Priority)) +} + +// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues. +func (priority *Priority) CalOpenIssues() { + priority.NumOpenIssues = priority.NumIssues - priority.NumClosedIssues +} + +// CalOpenOrgIssues calculates the open issues of a label for a specific repo +func (priority *Priority) CalOpenOrgIssues(repoID, priorityID int64) { + counts, _ := CountIssuesByRepo(&IssuesOptions{ + RepoID: repoID, + PriorityIDs: []int64{priorityID}, + IsClosed: util.OptionalBoolFalse, + }) + + for _, count := range counts { + priority.NumOpenRepoIssues += count + } +} + +// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked +func (priority *Priority) LoadSelectedPrioritiesAfterClick(currentSelectedPriorities []int64) { + var priorityQuerySlice []string + labelSelected := false + labelID := strconv.FormatInt(priority.ID, 10) + for _, s := range currentSelectedPriorities { + if s == priority.ID { + labelSelected = true + } else if -s == priority.ID { + labelSelected = true + priority.IsExcluded = true + } else if s != 0 { + priorityQuerySlice = append(priorityQuerySlice, strconv.FormatInt(s, 10)) + } + } + if !labelSelected { + priorityQuerySlice = append(priorityQuerySlice, labelID) + } + priority.IsSelected = labelSelected + priority.QueryString = strings.Join(priorityQuerySlice, ",") +} + +// BelongsToOrg returns true if label is an organization label +func (priority *Priority) BelongsToOrg() bool { + return priority.OrgID > 0 +} + +// BelongsToRepo returns true if label is a repository label +func (priority *Priority) BelongsToRepo() bool { + return priority.RepoID > 0 +} + +// ForegroundColor calculates the text color for labels based +// on their background color. +func (priority *Priority) ForegroundColor() template.CSS { + if strings.HasPrefix(priority.Color, "#") { + if color, err := strconv.ParseUint(priority.Color[1:], 16, 64); err == nil { + // NOTE: see web_src/js/components/ContextPopup.vue for similar implementation + luminance := Luminance(uint32(color)) + + // prefer white or black based upon contrast + if luminance < LuminanceThreshold { + return template.CSS("#fff") + } + return template.CSS("#000") + } + } + + // default to black + return template.CSS("#000") +} + +// NewPriority creates a new Priority +func NewPriority(ctx context.Context, priority *Priority) error { + if !LabelColorPattern.MatchString(priority.Color) { + return fmt.Errorf("bad color code: %s", priority.Color) + } + + // normalize case + priority.Color = strings.ToLower(priority.Color) + + // add leading hash + if priority.Color[0] != '#' { + priority.Color = "#" + priority.Color + } + + // convert 3-character shorthand into 6-character version + if len(priority.Color) == 4 { + r := priority.Color[1] + g := priority.Color[2] + b := priority.Color[3] + priority.Color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b) + } + + return db.Insert(ctx, priority) +} + +// NewPriorities creates new labels +func NewPriorities(priorities ...*Priority) error { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + for _, priority := range priorities { + if !LabelColorPattern.MatchString(priority.Color) { + return fmt.Errorf("bad color code: %s", priority.Color) + } + if err := db.Insert(ctx, priority); err != nil { + return err + } + } + return committer.Commit() +} + +// UpdateLabel updates label information. +func UpdatePriority(priority *Priority) error { + if !PriorityColorPattern.MatchString(priority.Color) { + return fmt.Errorf("bad color code: %s", priority.Color) + } + return updatePriorityCols(db.DefaultContext, priority, "name", "description", "color", "weight") +} + +// DeleteLabel delete a label +func DeletePriority(id, priorityId int64) error { + priority, err := GetPriorityByID(db.DefaultContext, priorityId) + if err != nil { + if IsErrPriorityNotExist(err) { + return nil + } + return err + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + sess := db.GetEngine(ctx) + + if priority.BelongsToOrg() && priority.OrgID != id { + return nil + } + if priority.BelongsToRepo() && priority.RepoID != id { + return nil + } + + if _, err = sess.ID(priorityId).Delete(new(Priority)); err != nil { + return err + } else if _, err = sess. + Where("label_id = ?", priorityId). + Delete(new(IssueLabel)); err != nil { + return err + } + + // delete comments about now deleted label_id + if _, err = sess.Where("priority_id = ?", priorityId).Cols("priority_id").Delete(&Comment{}); err != nil { + return err + } + + return committer.Commit() +} + +// GetLabelByID returns a label by given ID. +func GetPriorityByID(ctx context.Context, priorityID int64) (*Priority, error) { + if priorityID <= 0 { + return nil, ErrLabelNotExist{priorityID} + } + + priority := &Priority{} + has, err := db.GetEngine(ctx).ID(priorityID).Get(priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPriorityNotExist{priority.ID} + } + return priority, nil +} + +// GetLabelsByIDs returns a list of labels by IDs +func GetPrioritiesByIDs(prioritiesIDs []int64) ([]*Priority, error) { + priorities := make([]*Priority, 0, len(prioritiesIDs)) + return priorities, db.GetEngine(db.DefaultContext).Table("priorities"). + In("id", prioritiesIDs). + Asc("name"). + Cols("id", "repo_id", "org_id"). + Find(&priorities) +} + +// __________ .__ __ +// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. +// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | +// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | +// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| +// \/ \/|__| \/ \/ + +// GetLabelInRepoByName returns a label by name in given repository. +func GetPriorityInRepoByName(ctx context.Context, repoID int64, labelName string) (*Priority, error) { + if len(labelName) == 0 || repoID <= 0 { + return nil, ErrRepoPriorityNotExist{0, repoID} + } + + priority := &Priority{ + Name: labelName, + RepoID: repoID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRepoPriorityNotExist{0, priority.RepoID} + } + return priority, nil +} + +// GetLabelInRepoByID returns a label by ID in given repository. +func GetPriorityInRepoByID(ctx context.Context, repoID, priorityID int64) (*Priority, error) { + if priorityID <= 0 || repoID <= 0 { + return nil, ErrRepoPriorityNotExist{priorityID, repoID} + } + + priority := &Priority{ + ID: priorityID, + RepoID: repoID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRepoPriorityNotExist{priority.ID, priority.RepoID} + } + return priority, nil +} + +// GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given +// repository. +// it silently ignores label names that do not belong to the repository. +func GetPriorityIDsInRepoByNames(repoID int64, priorityNames []string) ([]int64, error) { + priorityIDs := make([]int64, 0, len(priorityNames)) + return priorityIDs, db.GetEngine(db.DefaultContext).Table("priority"). + Where("repo_id = ?", repoID). + In("name", priorityNames). + Asc("name"). + Cols("id"). + Find(&priorityIDs) +} + +// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names +func BuildPriorityNamesIssueIDsCondition(priorityNames []string) *builder.Builder { + return builder.Select("issue_label.issue_id"). + From("issue"). + InnerJoin("priority", "priority.id = issue.priority_id"). + Where( + builder.In("priority.name", priorityNames), + ). + GroupBy("issue.id") +} + +// GetLabelsInRepoByIDs returns a list of labels by IDs in given repository, +// it silently ignores label IDs that do not belong to the repository. +func GetPriorityInRepoByIDs(repoID int64, priorityIDs []int64) ([]*Priority, error) { + priorities := make([]*Priority, 0, len(priorityIDs)) + return priorities, db.GetEngine(db.DefaultContext). + Where("repo_id = ?", repoID). + In("id", priorities). + Asc("name"). + Find(&priorities) +} + +// GetLabelsByRepoID returns all labels that belong to given repository by ID. +func GetPrioritiesByRepoID(ctx context.Context, repoID int64, sortType string, listOptions db.ListOptions) ([]*Priority, error) { + if repoID <= 0 { + return nil, ErrRepoPriorityNotExist{0, repoID} + } + priorities := make([]*Priority, 0, 10) + sess := db.GetEngine(ctx).Where("repo_id = ?", repoID) + + switch sortType { + case "reverseweight": + sess.Desc("weight") + case "weight": + sess.Asc("weight") + case "reversealphabetically": + sess.Desc("name") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + default: + sess.Asc("name") + } + + if listOptions.Page != 0 { + sess = db.SetSessionPagination(sess, &listOptions) + } + + return priorities, sess.Find(&priorities) +} + +// CountLabelsByRepoID count number of all labels that belong to given repository by ID. +func CountPrioritiesByRepoID(repoID int64) (int64, error) { + return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Priority{}) +} + +// ________ +// \_____ \_______ ____ +// / | \_ __ \/ ___\ +// / | \ | \/ /_/ > +// \_______ /__| \___ / +// \/ /_____/ + +// GetLabelInOrgByName returns a label by name in given organization. +func GetPriorityInOrgByName(ctx context.Context, orgID int64, priorityName string) (*Priority, error) { + if len(priorityName) == 0 || orgID <= 0 { + return nil, ErrOrgPriorityNotExist{0, orgID} + } + + priority := &Priority{ + Name: priorityName, + OrgID: orgID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgPriorityNotExist{0, priority.OrgID} + } + return priority, nil +} + +// GetLabelInOrgByID returns a label by ID in given organization. +func GetPriorityInOrgByID(ctx context.Context, orgID, priorityID int64) (*Priority, error) { + if priorityID <= 0 || orgID <= 0 { + return nil, ErrOrgPriorityNotExist{priorityID, orgID} + } + + priority := &Priority{ + ID: priorityID, + OrgID: orgID, + } + has, err := db.GetByBean(ctx, priority) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgPriorityNotExist{priority.ID, priority.OrgID} + } + return priority, nil +} + +// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given +// organization. +func GetPriorityIDsInOrgByNames(orgID int64, priorityNames []string) ([]int64, error) { + if orgID <= 0 { + return nil, ErrOrgPriorityNotExist{0, orgID} + } + priorityIDs := make([]int64, 0, len(priorityNames)) + + return priorityIDs, db.GetEngine(db.DefaultContext).Table("priority"). + Where("org_id = ?", orgID). + In("name", priorityNames). + Asc("name"). + Cols("id"). + Find(&priorityIDs) +} + +// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization, +// it silently ignores label IDs that do not belong to the organization. +func GetPriorityInOrgByIDs(orgID int64, priorityIDs []int64) ([]*Priority, error) { + priorities := make([]*Priority, 0, len(priorityIDs)) + return priorities, db.GetEngine(db.DefaultContext). + Where("org_id = ?", orgID). + In("id", priorityIDs). + Asc("name"). + Find(&priorities) +} + +// GetLabelsByOrgID returns all labels that belong to given organization by ID. +func GetPrioritiesByOrgID(ctx context.Context, orgID int64, sortType string, listOptions db.ListOptions) ([]*Priority, error) { + if orgID <= 0 { + return nil, ErrOrgPriorityNotExist{0, orgID} + } + priorities := make([]*Priority, 0, 10) + sess := db.GetEngine(ctx).Where("org_id = ?", orgID) + + switch sortType { + case "reverseweight": + sess.Desc("weight") + case "weight": + sess.Asc("weight") + case "reversealphabetically": + sess.Desc("name") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + default: + sess.Asc("name") + } + + if listOptions.Page != 0 { + sess = db.SetSessionPagination(sess, &listOptions) + } + + return priorities, sess.Find(&priorities) +} + +// CountLabelsByOrgID count all labels that belong to given organization by ID. +func CountPrioritiesByOrgID(orgID int64) (int64, error) { + return db.GetEngine(db.DefaultContext).Where("org_id = ?", orgID).Count(&Priority{}) +} + +// .___ +// | | ______ ________ __ ____ +// | |/ ___// ___/ | \_/ __ \ +// | |\___ \ \___ \| | /\ ___/ +// |___/____ >____ >____/ \___ | +// \/ \/ \/ + +// GetLabelsByIssueID returns all labels that belong to given issue by ID. +func GetPrioritiesByIssueID(ctx context.Context, issueID int64) ([]*Priority, error) { + var priorities []*Priority + return priorities, db.GetEngine(ctx).Where("issue.id = ?", issueID). + Join("LEFT", "issue", "issue.priority_id = label.id"). + Asc("priority.name"). + Find(&priorities) +} + +// GetLabelsByIssueID returns all labels that belong to given issue by ID. +func GetPriorityByIssueID(ctx context.Context, issueID int64) (*Priority, error) { + priorities, err := GetPrioritiesByIssueID(ctx, issueID) + if err != nil { + return nil, err + } + if len(priorities) == 0 { + return nil, nil + } + return priorities[0], nil +} + +func updatePriorityCols(ctx context.Context, priority *Priority, cols ...string) error { + _, err := db.GetEngine(ctx).ID(priority.ID). + SetExpr("num_issues", + builder.Select("count(*)").From("priority"). + Where(builder.Eq{"id": priority.ID}), + ). + SetExpr("num_closed_issues", + builder.Select("count(*)").From("priority"). + InnerJoin("issue", "priority.id = issue.priority_id"). + Where(builder.Eq{ + "priority.id": priority.ID, + "issue.is_closed": true, + }), + ). + Cols(cols...).Update(priority) + return err +} + +// HasIssueLabel returns true if issue has been labeled. +func HasIssuePriority(ctx context.Context, issueID, priority int64) bool { + has, _ := db.GetEngine(ctx).Where("id = ? AND priority = ?", issueID, priority).Get(new(Issue)) + return has +} + +// newIssueLabel this function creates a new label it does not check if the label is valid for the issue +// YOU MUST CHECK THIS BEFORE THIS FUNCTION +func newIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) (err error) { + issue.PriorityID = priority.ID + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = UpdateIssueCols(ctx, issue, "priority_id"); err != nil { + return fmt.Errorf("updateIssueCols: %v", err) + } + + if err = issue.LoadRepo(ctx); err != nil { + return + } + + opts := &CreateCommentOptions{ + Type: CommentTypePriority, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Priority: priority, + Content: "1", + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return err + } + + return updatePriorityCols(ctx, priority, "num_issues", "num_closed_issue") +} + +// NewIssueLabel creates a new issue-label relation. +func NewIssuePriority(issue *Issue, priority *Priority, doer *user_model.User) (err error) { + if HasIssueLabel(db.DefaultContext, issue.ID, priority.ID) { + return nil + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = issue.LoadRepo(ctx); err != nil { + return err + } + + // Do NOT add invalid labels + if issue.RepoID != priority.RepoID && issue.Repo.OwnerID != priority.OrgID { + return nil + } + + if err = newIssuePriority(ctx, issue, priority, doer); err != nil { + return err + } + + issue.Priority = nil + if err = issue.LoadPriorities(ctx); err != nil { + return err + } + + return committer.Commit() +} + +// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue +func newIssuePriorities(ctx context.Context, issue *Issue, priorities []*Priority, doer *user_model.User) (err error) { + if err = issue.LoadRepo(ctx); err != nil { + return err + } + for _, priority := range priorities { + // Don't add already present labels and invalid labels + if HasIssueLabel(ctx, issue.ID, priority.ID) || + (priority.RepoID != issue.RepoID && priority.OrgID != issue.Repo.OwnerID) { + continue + } + + if err = newIssuePriority(ctx, issue, priority, doer); err != nil { + return fmt.Errorf("newIssuePriority: %v", err) + } + } + + return nil +} + +// NewIssueLabels creates a list of issue-label relations. +func NewIssuePriorities(issue *Issue, priorities []*Priority, doer *user_model.User) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = newIssuePriorities(ctx, issue, priorities, doer); err != nil { + return err + } + + if err = issue.LoadPriorities(ctx); err != nil { + issue.Priority = nil + return err + } + + return committer.Commit() +} + +func deleteIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) (err error) { + issue.PriorityID = 0 + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = UpdateIssueCols(ctx, issue, "priority_id"); err != nil { + return fmt.Errorf("updateIssueCols: %v", err) + } + if err = issue.LoadRepo(ctx); err != nil { + return + } + + opts := &CreateCommentOptions{ + Type: CommentTypePriority, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Priority: priority, + } + if _, err = CreateCommentCtx(ctx, opts); err != nil { + return err + } + + return updatePriorityCols(ctx, priority, "num_issues", "num_closed_issue") +} + +// DeleteIssueLabel deletes issue-label relation. +func DeleteIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) error { + if err := deleteIssuePriority(ctx, issue, priority, doer); err != nil { + return err + } + + issue.Priority = nil + return issue.LoadPriorities(ctx) +} + +// DeleteLabelsByRepoID deletes labels of some repository +func DeletePrioritiesByRepoID(ctx context.Context, repoID int64) error { + + _, err := db.DeleteByBean(ctx, &Priority{RepoID: repoID}) + return err +} + +// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore +func CountOrphanedPriorities() (int64, error) { + noref, err := db.GetEngine(db.DefaultContext).Table("priority").Where("repo_id=? AND org_id=?", 0, 0).Count() + if err != nil { + return 0, err + } + + norepo, err := db.GetEngine(db.DefaultContext).Table("priority"). + Where(builder.And( + builder.Gt{"repo_id": 0}, + builder.NotIn("repo_id", builder.Select("id").From("repository")), + )). + Count() + if err != nil { + return 0, err + } + + noorg, err := db.GetEngine(db.DefaultContext).Table("priority"). + Where(builder.And( + builder.Gt{"org_id": 0}, + builder.NotIn("org_id", builder.Select("id").From("user")), + )). + Count() + if err != nil { + return 0, err + } + + return noref + norepo + noorg, nil +} + +// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore +func DeleteOrphanedPriorities() error { + // delete labels with no reference + if _, err := db.GetEngine(db.DefaultContext).Table("priority").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Priority)); err != nil { + return err + } + + // delete priorities with none existing repos + if _, err := db.GetEngine(db.DefaultContext). + Where(builder.And( + builder.Gt{"repo_id": 0}, + builder.NotIn("repo_id", builder.Select("id").From("repository")), + )). + Delete(Priority{}); err != nil { + return err + } + + // delete priorities with none existing orgs + if _, err := db.GetEngine(db.DefaultContext). + Where(builder.And( + builder.Gt{"org_id": 0}, + builder.NotIn("org_id", builder.Select("id").From("user")), + )). + Delete(Priority{}); err != nil { + return err + } + + return nil +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 30c4ad250c084..83d21acf0bab9 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -408,6 +408,9 @@ var migrations = []Migration{ NewMigration("Rename CredentialIDBytes column to CredentialID", renameCredentialIDBytes), // v224 -> v225 NewMigration("Add badges to users", creatUserBadgesTable), + + // v999 + NewMigration("Create issue priority tables", createIssuePriorityTables), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v999.go b/models/migrations/v999.go new file mode 100644 index 0000000000000..b693a7c48e89c --- /dev/null +++ b/models/migrations/v999.go @@ -0,0 +1,46 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func createIssuePriorityTables(x *xorm.Engine) error { + // Priority represents a priority of repository for issues. + type Priority struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + OrgID int64 `xorm:"INDEX"` + Name string + Description string + Color string `xorm:"VARCHAR(7)"` + Weight int + NumIssues int + NumClosedIssues int + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + if err := x.Sync2(new(Priority)); err != nil { + return err + } + + type Comment struct { + ID int64 `xorm:"pk autoincr"` + PriorityID int64 + } + if err := x.Sync2(new(Comment)); err != nil { + return err + } + + type Issue struct { + ID int64 `xorm:"pk autoincr"` + PriorityID int64 + } + + return x.Sync2(new(Issue)) +} From f4f5492a6c18404274d31c3eb961ee9e9e01fcf5 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 25 Aug 2022 15:34:17 -0400 Subject: [PATCH 03/10] miss 0 --- models/issues/priority.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/issues/priority.go b/models/issues/priority.go index b1ded4934d745..6d0306d53ac95 100644 --- a/models/issues/priority.go +++ b/models/issues/priority.go @@ -665,7 +665,7 @@ func NewIssuePriorities(issue *Issue, priorities []*Priority, doer *user_model.U } func deleteIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) (err error) { - issue.PriorityID = nil + issue.PriorityID = 0 ctx, committer, err := db.TxContext() if err != nil { return err From f5044045a8b64397cc37026ff35a4dbcc0f18886 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Thu, 25 Aug 2022 15:47:19 -0400 Subject: [PATCH 04/10] make fmt --- models/issues/priority.go | 8 ++++---- models/migrations/migrations.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/models/issues/priority.go b/models/issues/priority.go index 6d0306d53ac95..8f893a47bb944 100644 --- a/models/issues/priority.go +++ b/models/issues/priority.go @@ -5,18 +5,19 @@ package issues import ( - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/util" "context" "fmt" "html/template" "regexp" "strconv" "strings" - "xorm.io/builder" "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" ) // ErrRepoPriorityNotExist represents a "RepoPriorityNotExist" kind of error. @@ -705,7 +706,6 @@ func DeleteIssuePriority(ctx context.Context, issue *Issue, priority *Priority, // DeleteLabelsByRepoID deletes labels of some repository func DeletePrioritiesByRepoID(ctx context.Context, repoID int64) error { - _, err := db.DeleteByBean(ctx, &Priority{RepoID: repoID}) return err } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 93b88a5e640b3..7542fb2d81c8c 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -414,7 +414,7 @@ var migrations = []Migration{ // v225 -> v226 NewMigration("Alter gpg_key/public_key content TEXT fields to MEDIUMTEXT", alterPublicGPGKeyContentFieldsToMediumText), - // v999 + // v999 NewMigration("Create issue priority tables", createIssuePriorityTables), } From f6ca1e397b8c52a3e823451df5c0bb6743df503f Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Tue, 20 Sep 2022 10:41:23 -0400 Subject: [PATCH 05/10] add templates for priorities --- templates/repo/issue/new_form.tmpl | 37 +++++++ templates/repo/issue/priorities.tmpl | 26 +++++ .../priorities/edit_delete_priority.tmpl | 57 +++++++++++ templates/repo/issue/priorities/priority.tmpl | 9 ++ .../repo/issue/priorities/priority_list.tmpl | 96 +++++++++++++++++++ .../priorities/priority_load_template.tmpl | 24 +++++ .../repo/issue/priorities/priority_new.tmpl | 27 ++++++ .../issue/priorities/priority_sidebar.tmpl | 11 +++ .../repo/issue/view_content/sidebar.tmpl | 35 +++++++ 9 files changed, 322 insertions(+) create mode 100644 templates/repo/issue/priorities.tmpl create mode 100644 templates/repo/issue/priorities/edit_delete_priority.tmpl create mode 100644 templates/repo/issue/priorities/priority.tmpl create mode 100644 templates/repo/issue/priorities/priority_list.tmpl create mode 100644 templates/repo/issue/priorities/priority_load_template.tmpl create mode 100644 templates/repo/issue/priorities/priority_new.tmpl create mode 100644 templates/repo/issue/priorities/priority_sidebar.tmpl diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 1f03014b542bb..bbc09602ea5bb 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -74,6 +74,43 @@
+ + + {{template "repo/issue/priorities/priority_sidebar" dict "root" $ "ctx" .}} + +
+ +{{template "base/footer" .}} diff --git a/templates/repo/issue/priorities/edit_delete_priority.tmpl b/templates/repo/issue/priorities/edit_delete_priority.tmpl new file mode 100644 index 0000000000000..34872ef8d7f0e --- /dev/null +++ b/templates/repo/issue/priorities/edit_delete_priority.tmpl @@ -0,0 +1,57 @@ + + + diff --git a/templates/repo/issue/priorities/priority.tmpl b/templates/repo/issue/priorities/priority.tmpl new file mode 100644 index 0000000000000..7b4500570de64 --- /dev/null +++ b/templates/repo/issue/priorities/priority.tmpl @@ -0,0 +1,9 @@ + + {{.priority.Name | RenderEmoji}} + diff --git a/templates/repo/issue/priorities/priority_list.tmpl b/templates/repo/issue/priorities/priority_list.tmpl new file mode 100644 index 0000000000000..d764af9675fa6 --- /dev/null +++ b/templates/repo/issue/priorities/priority_list.tmpl @@ -0,0 +1,96 @@ +

+ {{.locale.Tr "repo.issues.prority_count" .NumPriorities}} + +

+ +
+
+ {{if and (not $.PageIsOrgSettingsPriorities) (or $.CanWriteIssues $.CanWritePulls) (eq .NumPriorities 0) (not $.Repository.IsArchived) }} + {{template "repo/issue/priorities/priority_load_template" .}} +
+ {{else if and ($.PageIsOrgSettingsPriorities) (eq .NumPriorities 0)}} + {{template "repo/issue/priorities/priority_load_template" .}} + {{end}} + {{range .Priorities}} +
  • +
    +
    +
    {{svg "octicon-tag"}} {{.Name | RenderEmoji}}
    +
    +
    +
    + {{.Description | RenderEmoji}} +
    +
    + + +
    +
  • + {{end}} + {{if and (not .PageIsOrgSettingsPriorities) (.OrgPriorities) }} +
  • +
    +
    + {{$.locale.Tr "repo.org_priorities_desc" | Str2html}} + {{if .IsOrganizationOwner}} + ({{$.locale.Tr "repo.org_priorities_desc_manage"}}): + {{end}} +
    +
    +
  • + {{if (not $.PageIsOrgSettingsPriorities)}} +
    + {{range .OrgPriorities}} +
  • +
    +
    +
    {{svg "octicon-tag"}} {{.Name | RenderEmoji}}
    +
    +
    +
    + {{.Description | RenderEmoji}} +
    +
    + +
    +
    +
    +
  • + {{end}} +
    + {{end}} + {{end}} +
    +
    diff --git a/templates/repo/issue/priorities/priority_load_template.tmpl b/templates/repo/issue/priorities/priority_load_template.tmpl new file mode 100644 index 0000000000000..defa9563c44ff --- /dev/null +++ b/templates/repo/issue/priorities/priority_load_template.tmpl @@ -0,0 +1,24 @@ +
    +
    +
    +

    {{.locale.Tr "repo.issues.priority_templates.info"}}

    +
    +
    + {{.CsrfTokenHtml}} +
    + +
    + +
    +
    +
    +
    diff --git a/templates/repo/issue/priorities/priority_new.tmpl b/templates/repo/issue/priorities/priority_new.tmpl new file mode 100644 index 0000000000000..c6d0838d14484 --- /dev/null +++ b/templates/repo/issue/priorities/priority_new.tmpl @@ -0,0 +1,27 @@ +
    +
    + {{.CsrfTokenHtml}} +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + {{template "repo/issue/label_precolors"}} +
    +
    +
    {{.locale.Tr "repo.milestones.cancel"}}
    + +
    +
    +
    +
    diff --git a/templates/repo/issue/priorities/priority_sidebar.tmpl b/templates/repo/issue/priorities/priority_sidebar.tmpl new file mode 100644 index 0000000000000..13b607bf42ab7 --- /dev/null +++ b/templates/repo/issue/priorities/priority_sidebar.tmpl @@ -0,0 +1,11 @@ +
    + {{.ctx.locale.Tr "repo.issues.new.no_priority"}} + + {{range .ctx.Priorities}} + {{template "repo/issue/priorities/priority" dict "root" $.root "priority" .}} + {{end}} + {{range .ctx.OrgPriorities}} + {{template "repo/issue/priorities/priority" dict "root" $.root "priority" .}} + {{end}} + +
    diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 46fd88c7538c2..b71dbd04dd05f 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -141,6 +141,41 @@
    + + {{template "repo/issue/priorities/priority_sidebar" dict "root" $ "ctx" .}} + +
    +
    {{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} {{template "repo/issue/labels/label_new" .}} + {{template "repo/issue/priorities/priority_new" .}} {{end}} {{template "base/alert" .}} {{template "repo/issue/labels/label_list" .}} +
    +
    + {{template "repo/issue/priorities/priority_list" .}} +
    {{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} diff --git a/templates/repo/issue/priorities/edit_delete_priority.tmpl b/templates/repo/issue/priorities/edit_delete_priority.tmpl index 34872ef8d7f0e..e7c18f7ae9dd1 100644 --- a/templates/repo/issue/priorities/edit_delete_priority.tmpl +++ b/templates/repo/issue/priorities/edit_delete_priority.tmpl @@ -25,6 +25,7 @@
    {{.CsrfTokenHtml}} +
    diff --git a/templates/repo/issue/priorities/priority_new.tmpl b/templates/repo/issue/priorities/priority_new.tmpl index c6d0838d14484..6ad210f410b55 100644 --- a/templates/repo/issue/priorities/priority_new.tmpl +++ b/templates/repo/issue/priorities/priority_new.tmpl @@ -1,6 +1,7 @@ -
    +
    {{.CsrfTokenHtml}} +
    @@ -12,6 +13,11 @@
    +
    +
    + +
    +
    diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js index df294078fa67b..63cec36033f84 100644 --- a/web_src/js/features/comp/LabelEdit.js +++ b/web_src/js/features/comp/LabelEdit.js @@ -12,6 +12,14 @@ export function initCompLabelEdit(selector) { $newLabelPanel.hide(); }); + const $newPriorityPanel = $('.new-priority.segment'); + $('.new-priority.button').on('click', () => { + $newPriorityPanel.show(); + }); + $('.new-priority.segment .cancel').on('click', () => { + $newPriorityPanel.hide(); + }); + initCompColorPicker(); $('.edit-label-button').on('click', function () { diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 57d54a08f6327..cddbdd8cb47be 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -2688,7 +2688,8 @@ } .edit-label.modal, -.new-label.segment { +.new-label.segment, +.new-priority.segment { .form { .column { padding-right: 0; From 3623f8358c527b4f7d2baa3d09be46f52f177854 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Mon, 26 Sep 2022 20:50:38 -0400 Subject: [PATCH 07/10] make fmt --- modules/repository/init.go | 29 ++++++++++++++++++ routers/web/repo/issue_label.go | 30 +++++++++++++++++++ templates/repo/issue/new_form.tmpl | 4 +-- templates/repo/issue/priorities.tmpl | 2 +- .../repo/issue/priorities/priority_list.tmpl | 4 +-- .../repo/issue/view_content/sidebar.tmpl | 4 +-- 6 files changed, 66 insertions(+), 7 deletions(-) diff --git a/modules/repository/init.go b/modules/repository/init.go index 37ed0748b4d34..aea8d6df833f4 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -471,3 +471,32 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg } return nil } + +// InitializeLabels adds a label set to a repository using a template +func InitializePriorities(ctx context.Context, id int64, labelTemplate string, isOrg bool) error { + list, err := GetLabelTemplateFile(labelTemplate) + if err != nil { + return err + } + + priorities := make([]*issues_model.Priority, len(list)) + for i := 0; i < len(list); i++ { + priorities[i] = &issues_model.Priority{ + Name: list[i][0], + Description: list[i][2], + Color: list[i][1], + Weight: 10, // FIXME: this should be configurable + } + if isOrg { + priorities[i].OrgID = id + } else { + priorities[i].RepoID = id + } + } + for _, priority := range priorities { + if err = issues_model.NewPriority(ctx, priority); err != nil { + return err + } + } + return nil +} diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index 7af415a8faed1..08659f98bcd61 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -66,7 +66,18 @@ func RetrieveLabels(ctx *context.Context) { l.CalOpenIssues() } + priorities, err := issues_model.GetPrioritiesByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{}) + if err != nil { + ctx.ServerError("RetrieveLabels.GetPriorities", err) + return + } + + for _, p := range priorities { + p.CalOpenIssues() + } + ctx.Data["Labels"] = labels + ctx.Data["Priorities"] = priorities if ctx.Repo.Owner.IsOrganization() { orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) @@ -79,6 +90,16 @@ func RetrieveLabels(ctx *context.Context) { } ctx.Data["OrgLabels"] = orgLabels + orgPriorities, err := issues_model.GetPrioritiesByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) + if err != nil { + ctx.ServerError("GetPrioritiesByOrgID", err) + return + } + for _, p := range orgPriorities { + p.CalOpenOrgIssues(ctx.Repo.Repository.ID, p.ID) + } + ctx.Data["OrgPriorities"] = orgPriorities + org, err := organization.GetOrgByName(ctx.Repo.Owner.LowerName) if err != nil { ctx.ServerError("GetOrgByName", err) @@ -96,6 +117,7 @@ func RetrieveLabels(ctx *context.Context) { } } ctx.Data["NumLabels"] = len(labels) + ctx.Data["NumPriorities"] = len(priorities) ctx.Data["SortType"] = ctx.FormString("sort") } @@ -105,6 +127,10 @@ func NewLabel(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.labels") ctx.Data["PageIsLabels"] = true + if form.Priority { + NewPriority(ctx) + return + } if ctx.HasError() { ctx.Flash.Error(ctx.Data["ErrorMsg"].(string)) ctx.Redirect(ctx.Repo.RepoLink + "/labels") @@ -127,6 +153,10 @@ func NewLabel(ctx *context.Context) { // UpdateLabel update a label's name and color func UpdateLabel(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateLabelForm) + if form.Priority { + UpdatePriority(ctx) + return + } l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID) if err != nil { switch { diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 1dc7fe07836b3..622e5f79266a5 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -92,13 +92,13 @@ {{if or .Priorities .OrgPriorities}} {{range .Priorities}}
    {{svg "octicon-check"}} {{.Name | RenderEmoji}} - {{if .Description }}
    {{.Description | RenderEmoji}}{{end}}
    + {{if .Description}}
    {{.Description | RenderEmoji}}{{end}} {{end}}
    {{range .OrgPriorities}} {{svg "octicon-check"}} {{.Name | RenderEmoji}} - {{if .Description }}
    {{.Description | RenderEmoji}}{{end}}
    + {{if .Description}}
    {{.Description | RenderEmoji}}{{end}} {{end}} {{else}}
    {{.locale.Tr "repo.issues.new.no_items"}}
    diff --git a/templates/repo/issue/priorities.tmpl b/templates/repo/issue/priorities.tmpl index bb41ffa5eb494..94fb65bb4109e 100644 --- a/templates/repo/issue/priorities.tmpl +++ b/templates/repo/issue/priorities.tmpl @@ -19,7 +19,7 @@
    -{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived) }} +{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} {{template "repo/issue/priorities/edit_delete_priority" .}} {{end}}
    diff --git a/templates/repo/issue/priorities/priority_list.tmpl b/templates/repo/issue/priorities/priority_list.tmpl index d764af9675fa6..f8d3a73588e7e 100644 --- a/templates/repo/issue/priorities/priority_list.tmpl +++ b/templates/repo/issue/priorities/priority_list.tmpl @@ -21,7 +21,7 @@
    - {{if and (not $.PageIsOrgSettingsPriorities) (or $.CanWriteIssues $.CanWritePulls) (eq .NumPriorities 0) (not $.Repository.IsArchived) }} + {{if and (not $.PageIsOrgSettingsPriorities) (or $.CanWriteIssues $.CanWritePulls) (eq .NumPriorities 0) (not $.Repository.IsArchived)}} {{template "repo/issue/priorities/priority_load_template" .}}
    {{else if and ($.PageIsOrgSettingsPriorities) (eq .NumPriorities 0)}} @@ -57,7 +57,7 @@
    {{end}} - {{if and (not .PageIsOrgSettingsPriorities) (.OrgPriorities) }} + {{if and (not .PageIsOrgSettingsPriorities) (.OrgPriorities)}}
  • diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 502197d6e1950..99925eb7ab2a3 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -160,12 +160,12 @@ {{if or .Priorities .OrgPriorities}} {{range .Priorities}} {{svg "octicon-check"}} {{.Name | RenderEmoji}} - {{if .Description }}
    {{.Description | RenderEmoji}}{{end}}
    + {{if .Description}}
    {{.Description | RenderEmoji}}{{end}} {{end}}
    {{range .OrgPriorities}} {{svg "octicon-check"}} {{.Name | RenderEmoji}} - {{if .Description }}
    {{.Description | RenderEmoji}}{{end}}
    + {{if .Description}}
    {{.Description | RenderEmoji}}{{end}} {{end}} {{else}}
    {{.locale.Tr "repo.issues.new.no_items"}}
    From bfefa1866ab12f2a5eb44e51587af1ca43e0fed2 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Mon, 26 Sep 2022 21:01:48 -0400 Subject: [PATCH 08/10] make lint --- .golangci.yml | 12 ++++++++++++ models/issues/priority.go | 24 +++++++----------------- web_src/less/_repository.less | 2 +- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 0e796a2016b0a..599f7f1bbdbc7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -177,3 +177,15 @@ issues: linters: - revive text: "exported: type name will be used as user.UserBadge by other packages, and that stutters; consider calling this Badge" + - path: routers/web/repo/issue_priority.go + linters: + - dupl + - path: routers/web/repo/issue_label.go + linters: + - dupl + - path: models/issues/priority.go + linters: + - dupl + - path: models/issues/label.go + linters: + - dupl diff --git a/models/issues/priority.go b/models/issues/priority.go index 8f893a47bb944..8ce132a07fa1c 100644 --- a/models/issues/priority.go +++ b/models/issues/priority.go @@ -218,9 +218,9 @@ func UpdatePriority(priority *Priority) error { return updatePriorityCols(db.DefaultContext, priority, "name", "description", "color", "weight") } -// DeleteLabel delete a label -func DeletePriority(id, priorityId int64) error { - priority, err := GetPriorityByID(db.DefaultContext, priorityId) +// DeletePriority delete a label +func DeletePriority(id, priorityID int64) error { + priority, err := GetPriorityByID(db.DefaultContext, priorityID) if err != nil { if IsErrPriorityNotExist(err) { return nil @@ -243,16 +243,16 @@ func DeletePriority(id, priorityId int64) error { return nil } - if _, err = sess.ID(priorityId).Delete(new(Priority)); err != nil { + if _, err = sess.ID(priorityID).Delete(new(Priority)); err != nil { return err } else if _, err = sess. - Where("label_id = ?", priorityId). + Where("label_id = ?", priorityID). Delete(new(IssueLabel)); err != nil { return err } // delete comments about now deleted label_id - if _, err = sess.Where("priority_id = ?", priorityId).Cols("priority_id").Delete(&Comment{}); err != nil { + if _, err = sess.Where("priority_id = ?", priorityID).Cols("priority_id").Delete(&Comment{}); err != nil { return err } @@ -559,15 +559,10 @@ func HasIssuePriority(ctx context.Context, issueID, priority int64) bool { return has } -// newIssueLabel this function creates a new label it does not check if the label is valid for the issue +// newIssuePriority this function creates a new label it does not check if the label is valid for the issue // YOU MUST CHECK THIS BEFORE THIS FUNCTION func newIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) (err error) { issue.PriorityID = priority.ID - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() if err = UpdateIssueCols(ctx, issue, "priority_id"); err != nil { return fmt.Errorf("updateIssueCols: %v", err) @@ -667,11 +662,6 @@ func NewIssuePriorities(issue *Issue, priorities []*Priority, doer *user_model.U func deleteIssuePriority(ctx context.Context, issue *Issue, priority *Priority, doer *user_model.User) (err error) { issue.PriorityID = 0 - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() if err = UpdateIssueCols(ctx, issue, "priority_id"); err != nil { return fmt.Errorf("updateIssueCols: %v", err) diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index cddbdd8cb47be..de5aabb68f8c3 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -2689,7 +2689,7 @@ .edit-label.modal, .new-label.segment, -.new-priority.segment { +.new-priority.segment { .form { .column { padding-right: 0; From 86514e0d6dbed7d9101b910ae2734c567aa46645 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Mon, 26 Sep 2022 21:18:37 -0400 Subject: [PATCH 09/10] truthy smash --- routers/web/repo/issue_priority.go | 19 ++++++++++--------- .../priorities/edit_delete_priority.tmpl | 2 +- .../repo/issue/priorities/priority_new.tmpl | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/routers/web/repo/issue_priority.go b/routers/web/repo/issue_priority.go index b442d408cae20..dd33a686bbb2d 100644 --- a/routers/web/repo/issue_priority.go +++ b/routers/web/repo/issue_priority.go @@ -49,14 +49,15 @@ func NewPriority(ctx *context.Context) { return } - l := &issues_model.Label{ + p := &issues_model.Priority{ RepoID: ctx.Repo.Repository.ID, Name: form.Title, Description: form.Description, Color: form.Color, + Weight: form.Weight, } - if err := issues_model.NewLabel(ctx, l); err != nil { - ctx.ServerError("NewLabel", err) + if err := issues_model.NewPriority(ctx, p); err != nil { + ctx.ServerError("NewPriority", err) return } ctx.Redirect(ctx.Repo.RepoLink + "/labels") @@ -65,7 +66,7 @@ func NewPriority(ctx *context.Context) { // UpdateLabel update a label's name and color func UpdatePriority(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateLabelForm) - l, err := issues_model.GetPriorityInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID) + p, err := issues_model.GetPriorityInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID) if err != nil { switch { case issues_model.IsErrRepoPriorityNotExist(err): @@ -76,11 +77,11 @@ func UpdatePriority(ctx *context.Context) { return } - l.Name = form.Title - l.Description = form.Description - l.Weight = form.Weight - l.Color = form.Color - if err := issues_model.UpdatePriority(l); err != nil { + p.Name = form.Title + p.Description = form.Description + p.Weight = form.Weight + p.Color = form.Color + if err := issues_model.UpdatePriority(p); err != nil { ctx.ServerError("UpdatePriority", err) return } diff --git a/templates/repo/issue/priorities/edit_delete_priority.tmpl b/templates/repo/issue/priorities/edit_delete_priority.tmpl index e7c18f7ae9dd1..fa84d26ffc9cd 100644 --- a/templates/repo/issue/priorities/edit_delete_priority.tmpl +++ b/templates/repo/issue/priorities/edit_delete_priority.tmpl @@ -25,7 +25,7 @@
    {{.CsrfTokenHtml}} - +
    diff --git a/templates/repo/issue/priorities/priority_new.tmpl b/templates/repo/issue/priorities/priority_new.tmpl index 6ad210f410b55..b32d0edd7efa0 100644 --- a/templates/repo/issue/priorities/priority_new.tmpl +++ b/templates/repo/issue/priorities/priority_new.tmpl @@ -1,7 +1,7 @@
    {{.CsrfTokenHtml}} - +
    From 26e9a7457412e55eac60afba7e7b3763e2f8fe2b Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Mon, 26 Sep 2022 21:44:24 -0400 Subject: [PATCH 10/10] add labels on create --- routers/web/repo/issue.go | 95 ++++++++++++++++++++++++++++--------- routers/web/repo/pull.go | 2 +- services/forms/repo_form.go | 1 + 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 5dab770d55308..f51cd9a2154d4 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -338,6 +338,39 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti ctx.Data["Labels"] = labels ctx.Data["NumLabels"] = len(labels) + var prioritiesIDs []int64 + selectPriorities := ctx.FormString("priorities") + if len(selectPriorities) > 0 && selectPriorities != "0" { + labelIDs, err = base.StringsToInt64s(strings.Split(selectPriorities, ",")) + if err != nil { + ctx.ServerError("StringsToInt64s", err) + return + } + } + + priorities, err := issues_model.GetPrioritiesByRepoID(ctx, repo.ID, "", db.ListOptions{}) + if err != nil { + ctx.ServerError("GetPrioritiesByRepoID", err) + return + } + + if repo.Owner.IsOrganization() { + orgPriorities, err := issues_model.GetPrioritiesByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) + if err != nil { + ctx.ServerError("GetPrioritiesByOrgID", err) + return + } + + ctx.Data["OrgLabels"] = orgPriorities + priorities = append(priorities, orgPriorities...) + } + + for _, p := range priorities { + p.LoadSelectedPrioritiesAfterClick(prioritiesIDs) + } + ctx.Data["Priorities"] = priorities + ctx.Data["NumPriorities"] = len(priorities) + if ctx.FormInt64("assignee") == 0 { assigneeID = 0 // Reset ID to prevent unexpected selection of assignee. } @@ -691,48 +724,64 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is } // RetrieveRepoMetas find all the meta information of a repository -func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label { +func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) ([]*issues_model.Label, []*issues_model.Priority) { if !ctx.Repo.CanWriteIssuesOrPulls(isPull) { - return nil + return nil, nil } labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) if err != nil { ctx.ServerError("GetLabelsByRepoID", err) - return nil + return nil, nil } ctx.Data["Labels"] = labels if repo.Owner.IsOrganization() { orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) if err != nil { - return nil + return nil, nil } ctx.Data["OrgLabels"] = orgLabels labels = append(labels, orgLabels...) } + priorities, err := issues_model.GetPrioritiesByRepoID(ctx, repo.ID, "", db.ListOptions{}) + if err != nil { + ctx.ServerError("GetPrioritiesByRepoID", err) + return nil, nil + } + ctx.Data["Priorities"] = priorities + if repo.Owner.IsOrganization() { + orgPriorities, err := issues_model.GetPrioritiesByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) + if err != nil { + return nil, nil + } + + ctx.Data["OrgPriorities"] = orgPriorities + priorities = append(priorities, orgPriorities...) + } + RetrieveRepoMilestonesAndAssignees(ctx, repo) if ctx.Written() { - return nil + return nil, nil } retrieveProjects(ctx, repo) if ctx.Written() { - return nil + return nil, nil } brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) if err != nil { ctx.ServerError("GetBranches", err) - return nil + return nil, nil } ctx.Data["Branches"] = brs // Contains true if the user can create issue dependencies ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.Doer, isPull) - return labels + return labels, priorities } func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) map[string]error { @@ -928,15 +977,15 @@ func DeleteIssue(ctx *context.Context) { } // ValidateRepoMetas check and returns repository's meta information -func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) { +func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64, int64) { var ( repo = ctx.Repo.Repository err error ) - labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull) + labels, priorities := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull) if ctx.Written() { - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } var labelIDs []int64 @@ -945,7 +994,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull if len(form.LabelIDs) > 0 { labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ",")) if err != nil { - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } labelIDMark := base.Int64sToMap(labelIDs) @@ -958,6 +1007,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull } ctx.Data["Labels"] = labels + ctx.Data["Priorities"] = priorities ctx.Data["HasSelectedLabel"] = hasSelected ctx.Data["label_ids"] = form.LabelIDs @@ -967,11 +1017,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) if err != nil { ctx.ServerError("GetMilestoneByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } if milestone.RepoID != repo.ID { ctx.ServerError("GetMilestoneByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } ctx.Data["Milestone"] = milestone ctx.Data["milestone_id"] = milestoneID @@ -981,11 +1031,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull p, err := project_model.GetProjectByID(ctx, form.ProjectID) if err != nil { ctx.ServerError("GetProjectByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } if p.RepoID != ctx.Repo.Repository.ID { ctx.NotFound("", nil) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } ctx.Data["Project"] = p @@ -997,7 +1047,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull if len(form.AssigneeIDs) > 0 { assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ",")) if err != nil { - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } // Check if the passed assignees actually exists and is assignable @@ -1005,18 +1055,18 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull assignee, err := user_model.GetUserByID(aID) if err != nil { ctx.ServerError("GetUserByID", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull) if err != nil { ctx.ServerError("CanBeAssigned", err) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } if !valid { ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) - return nil, nil, 0, 0 + return nil, nil, 0, 0, 0 } } } @@ -1026,7 +1076,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull assigneeIDs = append(assigneeIDs, form.AssigneeID) } - return labelIDs, assigneeIDs, milestoneID, form.ProjectID + return labelIDs, assigneeIDs, milestoneID, form.ProjectID, form.Priority } // NewIssuePost response for creating new issue @@ -1044,7 +1094,7 @@ func NewIssuePost(ctx *context.Context) { attachments []string ) - labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false) + labelIDs, assigneeIDs, milestoneID, projectID, priority := ValidateRepoMetas(ctx, *form, false) if ctx.Written() { return } @@ -1079,6 +1129,7 @@ func NewIssuePost(ctx *context.Context) { MilestoneID: milestoneID, Content: content, Ref: form.Ref, + PriorityID: priority, } if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index aa2c4cdb53b2d..5bf66faaa7687 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1164,7 +1164,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } - labelIDs, assigneeIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true) + labelIDs, assigneeIDs, milestoneID, _, _ := ValidateRepoMetas(ctx, *form, true) if ctx.Written() { return } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 033dd48f59efe..72f3afc02e5a9 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -436,6 +436,7 @@ type CreateIssueForm struct { MilestoneID int64 ProjectID int64 AssigneeID int64 + Priority int64 Content string Files []string AllowMaintainerEdit bool