diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml
index ca5b1c6cd1df5..9764efe6c8278 100644
--- a/models/fixtures/issue.yml
+++ b/models/fixtures/issue.yml
@@ -14,6 +14,7 @@
created_unix: 946684800
updated_unix: 978307200
is_locked: false
+ weight: 10
-
id: 2
@@ -31,6 +32,7 @@
created_unix: 946684810
updated_unix: 978307190
is_locked: false
+ weight: 12
-
id: 3
@@ -48,6 +50,7 @@
created_unix: 946684820
updated_unix: 978307180
is_locked: false
+ weight: 5
-
id: 4
@@ -65,6 +68,7 @@
created_unix: 946684830
updated_unix: 978307200
is_locked: false
+ weight: 10
-
id: 5
@@ -82,6 +86,7 @@
created_unix: 946684840
updated_unix: 978307200
is_locked: false
+ weight: 20
-
id: 6
@@ -99,6 +104,7 @@
created_unix: 946684850
updated_unix: 978307200
is_locked: false
+ weight: 0
-
id: 7
@@ -116,6 +122,7 @@
created_unix: 946684830
updated_unix: 978307200
is_locked: false
+ weight: 10
-
id: 8
@@ -133,6 +140,7 @@
created_unix: 946684820
updated_unix: 978307180
is_locked: false
+ weight: 0
-
id: 9
@@ -150,6 +158,7 @@
created_unix: 946684820
updated_unix: 978307180
is_locked: false
+ weight: 10
-
id: 10
@@ -168,6 +177,7 @@
created_unix: 946684830
updated_unix: 999307200
is_locked: false
+ weight: 4
-
id: 11
@@ -185,6 +195,7 @@
created_unix: 1579194806
updated_unix: 1579194806
is_locked: false
+ weight: 0
-
id: 12
@@ -202,6 +213,7 @@
created_unix: 1602935696
updated_unix: 1602935696
is_locked: false
+ weight: 22
-
id: 13
@@ -219,6 +231,7 @@
created_unix: 1602935696
updated_unix: 1602935696
is_locked: false
+ weight: 13
-
id: 14
@@ -236,6 +249,7 @@
created_unix: 1602935696
updated_unix: 1602935696
is_locked: false
+ weight: 8
-
id: 15
@@ -253,6 +267,7 @@
created_unix: 1602935696
updated_unix: 1602935696
is_locked: false
+ weight: 52
-
id: 16
@@ -270,6 +285,7 @@
created_unix: 1602935696
updated_unix: 1602935696
is_locked: false
+ weight: 10
-
id: 17
@@ -287,6 +303,7 @@
created_unix: 1602935696
updated_unix: 1602935696
is_locked: false
+ weight: 0
-
id: 18
@@ -304,6 +321,7 @@
created_unix: 946684830
updated_unix: 978307200
is_locked: false
+ weight: 20
-
id: 19
@@ -321,6 +339,7 @@
created_unix: 946684830
updated_unix: 978307200
is_locked: false
+ weight: 10
-
id: 20
@@ -338,6 +357,7 @@
created_unix: 978307210
updated_unix: 978307210
is_locked: false
+ weight: 24
-
id: 21
@@ -355,6 +375,7 @@
created_unix: 1707270422
updated_unix: 1707270422
is_locked: false
+ weight: 40
-
id: 22
@@ -372,3 +393,4 @@
created_unix: 1707270422
updated_unix: 1707270422
is_locked: false
+ weight: 2
diff --git a/models/issues/comment.go b/models/issues/comment.go
index 48b8e335d48ef..6fb4cd3f9c185 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -114,6 +114,10 @@ const (
CommentTypePin // 36 pin Issue
CommentTypeUnpin // 37 unpin Issue
+
+ CommentTypeAddedWeight // 38 Added a weight
+ CommentTypeModifiedWeight // 39 Modified the weight
+ CommentTypeRemovedWeight // 40 Removed a weight
)
var commentStrings = []string{
@@ -960,6 +964,40 @@ func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Is
return comment, nil
}
+func CreateWeightComment(ctx context.Context, doer *user_model.User, issue *Issue, newWeight int) (*Comment, error) {
+ var content string
+ var commentType CommentType
+
+ // weight = 0 means deleting
+ if newWeight == 0 {
+ commentType = CommentTypeRemovedWeight
+ content = fmt.Sprintf("%d", issue.Weight)
+ } else if issue.Weight == 0 || issue.Weight == newWeight {
+ commentType = CommentTypeAddedWeight
+ content = fmt.Sprintf("%d", newWeight)
+ } else { // Otherwise modified
+ commentType = CommentTypeModifiedWeight
+ content = fmt.Sprintf("%d|%d", newWeight, issue.Weight)
+ }
+
+ if err := issue.LoadRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ opts := &CreateCommentOptions{
+ Type: commentType,
+ Doer: doer,
+ Repo: issue.Repo,
+ Issue: issue,
+ Content: content,
+ }
+ comment, err := CreateComment(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ return comment, nil
+}
+
// Creates issue dependency comment
func createIssueDependencyComment(ctx context.Context, doer *user_model.User, issue, dependentIssue *Issue, add bool) (err error) {
cType := CommentTypeAddDependency
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 40462ed09dfd6..f5d2105a74221 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -127,6 +127,7 @@ type Issue struct {
NumComments int
Ref string
PinOrder int `xorm:"DEFAULT 0"`
+ Weight int
DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go
index 1bbc0eee564fd..111c93bcea225 100644
--- a/models/issues/issue_test.go
+++ b/models/issues/issue_test.go
@@ -375,6 +375,26 @@ func TestLoadTotalTrackedTime(t *testing.T) {
assert.Equal(t, int64(3682), milestone.TotalTrackedTime)
}
+func TestMilestoneList_LoadTotalWeight(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ miles := issues_model.MilestoneList{
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}),
+ }
+
+ assert.NoError(t, miles.LoadTotalWeight(db.DefaultContext))
+
+ assert.Equal(t, 12, miles[0].TotalWeight)
+}
+
+func TestLoadTotalWeight(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1})
+
+ assert.NoError(t, milestone.LoadTotalWeight(db.DefaultContext))
+
+ assert.Equal(t, 12, milestone.TotalWeight)
+}
+
func TestCountIssues(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
count, err := issues_model.CountIssues(db.DefaultContext, &issues_model.IssuesOptions{})
diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go
index 31d76be5e0aea..3ed6ea0cac36a 100644
--- a/models/issues/issue_update.go
+++ b/models/issues/issue_update.go
@@ -459,6 +459,31 @@ func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeuti
return committer.Commit()
}
+// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
+func UpdateIssueWeight(ctx context.Context, issue *Issue, weight int, doer *user_model.User) (err error) {
+ // if the weight hasn't changed do nothing
+ if issue.Weight == weight {
+ return nil
+ }
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ // Update the weight
+ if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, Weight: weight}, "weight"); err != nil {
+ return err
+ }
+
+ // Make the comment
+ if _, err = CreateWeightComment(ctx, doer, issue, weight); err != nil {
+ return fmt.Errorf("createWeightComment: %w", err)
+ }
+
+ return committer.Commit()
+}
+
// FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database.
func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) {
rawMentions := references.FindAllMentionsMarkdown(content)
diff --git a/models/issues/milestone.go b/models/issues/milestone.go
index db0312adf0057..3ec1a0b04d37a 100644
--- a/models/issues/milestone.go
+++ b/models/issues/milestone.go
@@ -64,7 +64,9 @@ type Milestone struct {
ClosedDateUnix timeutil.TimeStamp
DeadlineString string `xorm:"-"`
- TotalTrackedTime int64 `xorm:"-"`
+ TotalTrackedTime int64 `xorm:"-"`
+ TotalWeight int `xorm:"-"`
+ WeightedCompleteness int `xorm:"-"`
}
func init() {
@@ -355,6 +357,33 @@ func (m *Milestone) LoadTotalTrackedTime(ctx context.Context) error {
return nil
}
+// LoadTotalWeight loads the total weight for the milestone
+func (m *Milestone) LoadTotalWeight(ctx context.Context) error {
+ type totalWeightByMilestone struct {
+ MilestoneID int64
+ Weight int
+ WeightClosed int
+ }
+
+ totalWeight := &totalWeightByMilestone{MilestoneID: m.ID}
+ has, err := db.GetEngine(ctx).Table("issue").
+ Join("INNER", "milestone", "issue.milestone_id = milestone.id").
+ Select("milestone_id, sum(weight) as weight, sum(CASE WHEN is_closed THEN 0 ELSE weight END) as weight_closed").
+ Where("milestone_id = ?", m.ID).
+ GroupBy("milestone_id").
+ Get(totalWeight)
+ if err != nil {
+ return err
+ } else if !has {
+ return nil
+ }
+
+ m.TotalWeight = totalWeight.Weight
+ m.WeightedCompleteness = totalWeight.WeightClosed * 100 / totalWeight.Weight
+
+ return nil
+}
+
// InsertMilestones creates milestones of repository.
func InsertMilestones(ctx context.Context, ms ...*Milestone) (err error) {
if len(ms) == 0 {
diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go
index 955ab2356df5e..bfc3d53cfc46b 100644
--- a/models/issues/milestone_list.go
+++ b/models/issues/milestone_list.go
@@ -130,6 +130,49 @@ func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error
return nil
}
+// LoadTotalWeight loads for every milestone in the list the TotalWeight by a batch request
+func (milestones MilestoneList) LoadTotalWeight(ctx context.Context) error {
+ type totalWeightByMilestone struct {
+ MilestoneID int64
+ Weight int
+ WeightClosed int
+ }
+ if len(milestones) == 0 {
+ return nil
+ }
+
+ weight := make(map[int64]int, len(milestones))
+ closedWeight := make(map[int64]int, len(milestones))
+
+ rows, err := db.GetEngine(ctx).Table("issue").
+ Join("INNER", "milestone", "issue.milestone_id = milestone.id").
+ Select("milestone_id, sum(weight) as weight, sum(CASE WHEN is_closed THEN 0 ELSE weight END) as weight_closed").
+ In("milestone_id", milestones.getMilestoneIDs()).
+ GroupBy("milestone_id").
+ Rows(new(totalWeightByMilestone))
+ if err != nil {
+ return err
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var totalWeight totalWeightByMilestone
+ err = rows.Scan(&totalWeight)
+ if err != nil {
+ return err
+ }
+ weight[totalWeight.MilestoneID] = totalWeight.Weight
+ closedWeight[totalWeight.MilestoneID] = totalWeight.WeightClosed
+ }
+
+ for _, milestone := range milestones {
+ milestone.TotalWeight = weight[milestone.ID]
+ milestone.WeightedCompleteness = closedWeight[milestone.ID] * 100 / milestone.TotalWeight
+ }
+
+ return nil
+}
+
// CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
func CountMilestonesMap(ctx context.Context, opts FindMilestoneOptions) (map[int64]int64, error) {
sess := db.GetEngine(ctx).Where(opts.ToConds())
diff --git a/models/issues/weight.go b/models/issues/weight.go
new file mode 100644
index 0000000000000..ca5d7dfd55bbd
--- /dev/null
+++ b/models/issues/weight.go
@@ -0,0 +1,60 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issues
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models/db"
+)
+
+// GetIssueTotalWeight returns the total weight for issues by given conditions.
+func GetIssueTotalWeight(ctx context.Context, opts *IssuesOptions) (int, int, error) {
+ if len(opts.IssueIDs) <= MaxQueryParameters {
+ return getIssueTotalWeightChunk(ctx, opts, opts.IssueIDs)
+ }
+
+ // If too long a list of IDs is provided,
+ // we get the statistics in smaller chunks and get accumulates
+ var weightSum int
+ var closedWeightSum int
+ for i := 0; i < len(opts.IssueIDs); {
+ chunk := i + MaxQueryParameters
+ if chunk > len(opts.IssueIDs) {
+ chunk = len(opts.IssueIDs)
+ }
+ weight, closedWeight, err := getIssueTotalWeightChunk(ctx, opts, opts.IssueIDs[i:chunk])
+ if err != nil {
+ return 0, 0, err
+ }
+ weightSum += weight
+ closedWeightSum += closedWeight
+ i = chunk
+ }
+
+ return weightSum, closedWeightSum, nil
+}
+
+func getIssueTotalWeightChunk(ctx context.Context, opts *IssuesOptions, issueIDs []int64) (int, int, error) {
+ type totalWeight struct {
+ Weight int
+ WeightClosed int
+ }
+
+ tw := &totalWeight{}
+
+ session := db.GetEngine(ctx).Table("issue").
+ Select("sum(weight) as weight, sum(CASE WHEN is_closed THEN 0 ELSE weight END) as weight_closed")
+
+ has, err := applyIssuesOptions(session, opts, issueIDs).
+ Get(tw)
+
+ if err != nil {
+ return 0, 0, err
+ } else if !has {
+ return 0, 0, nil
+ }
+
+ return tw.Weight, tw.WeightClosed, nil
+}
diff --git a/models/issues/weight_test.go b/models/issues/weight_test.go
new file mode 100644
index 0000000000000..c1e1d02c83c5a
--- /dev/null
+++ b/models/issues/weight_test.go
@@ -0,0 +1,33 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issues_test
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/unittest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetIssueTotalWeight(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ tw, twc, err := issues_model.GetIssueTotalWeight(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}})
+ assert.NoError(t, err)
+ assert.EqualValues(t, 12, tw)
+ assert.EqualValues(t, 12, twc)
+
+ tw, twc, err = issues_model.GetIssueTotalWeight(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{3}})
+ assert.NoError(t, err)
+ assert.EqualValues(t, 5, tw)
+ assert.EqualValues(t, 5, twc)
+
+ tw, twc, err = issues_model.GetIssueTotalWeight(db.DefaultContext, &issues_model.IssuesOptions{RepoIDs: []int64{2}})
+ assert.NoError(t, err)
+ assert.EqualValues(t, 20, tw)
+ assert.EqualValues(t, 10, twc)
+}
diff --git a/models/migrations/v1_23/v305.go b/models/migrations/v1_23/v305.go
new file mode 100644
index 0000000000000..76ba32d23bc1d
--- /dev/null
+++ b/models/migrations/v1_23/v305.go
@@ -0,0 +1,13 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import "xorm.io/xorm"
+
+func AddIssueWeight(x *xorm.Engine) error {
+ type Issue struct {
+ Weight int
+ }
+ return x.Sync(new(Issue))
+}
diff --git a/models/project/column.go b/models/project/column.go
index 222f44859928e..f6d6614004796 100644
--- a/models/project/column.go
+++ b/models/project/column.go
@@ -57,20 +57,6 @@ func (Column) TableName() string {
return "project_board" // TODO: the legacy table name should be project_column
}
-// NumIssues return counter of all issues assigned to the column
-func (c *Column) NumIssues(ctx context.Context) int {
- total, err := db.GetEngine(ctx).Table("project_issue").
- Where("project_id=?", c.ProjectID).
- And("project_board_id=?", c.ID).
- GroupBy("issue_id").
- Cols("issue_id").
- Count()
- if err != nil {
- return 0
- }
- return int(total)
-}
-
func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
issues := make([]*ProjectIssue, 0, 5)
if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
diff --git a/models/repo/issue.go b/models/repo/issue.go
index 0dd4fd5ed480e..b131e1d209005 100644
--- a/models/repo/issue.go
+++ b/models/repo/issue.go
@@ -38,6 +38,24 @@ func (repo *Repository) IsTimetrackerEnabled(ctx context.Context) bool {
return u.IssuesConfig().EnableTimetracker
}
+func (repo *Repository) CanEnableWeight() bool {
+ return setting.Service.EnableIssueWeight
+}
+
+// IsTimetrackerEnabled returns whether or not the timetracker is enabled. It returns the default value from config if an error occurs.
+func (repo *Repository) IsWeightEnabled(ctx context.Context) bool {
+ if !setting.Service.EnableIssueWeight {
+ return false
+ }
+
+ var u *RepoUnit
+ var err error
+ if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil {
+ return setting.Service.DefaultEnableIssueWeight
+ }
+ return u.IssuesConfig().EnableWeight
+}
+
// AllowOnlyContributorsToTrackTime returns value of IssuesConfig or the default value
func (repo *Repository) AllowOnlyContributorsToTrackTime(ctx context.Context) bool {
var u *RepoUnit
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index cb52c2c9e2058..022cedbdc4cce 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -105,6 +105,7 @@ type IssuesConfig struct {
EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool
EnableDependencies bool
+ EnableWeight bool
}
// FromDB fills up a IssuesConfig from serialized format.
diff --git a/modules/setting/service.go b/modules/setting/service.go
index 3ea1501236dfd..681e7d39d9b62 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -70,6 +70,8 @@ var Service = struct {
DefaultUserIsRestricted bool
EnableTimetracking bool
DefaultEnableTimetracking bool
+ EnableIssueWeight bool
+ DefaultEnableIssueWeight bool
DefaultEnableDependencies bool
AllowCrossRepositoryDependencies bool
DefaultAllowOnlyContributorsToTrackTime bool
@@ -184,6 +186,11 @@ func loadServiceFrom(rootCfg ConfigProvider) {
if Service.EnableTimetracking {
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
}
+ Service.EnableIssueWeight = sec.Key("ENABLE_ISSUE_WEIGHT").MustBool(false)
+ if Service.EnableIssueWeight {
+ Service.DefaultEnableIssueWeight = sec.Key("DEFAULT_ENABLE_ISSUE_WEIGHT").MustBool(true)
+ }
+
Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true)
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index 3682191be5751..c6a201f40f61f 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -80,6 +80,7 @@ type Issue struct {
Repo *RepositoryMeta `json:"repository"`
PinOrder int `json:"pin_order"`
+ Weight int `json:"weight"`
}
// CreateIssueOption options to create one issue
@@ -98,6 +99,7 @@ type CreateIssueOption struct {
// list of label ids
Labels []int64 `json:"labels"`
Closed bool `json:"closed"`
+ Weight int `json:"weight"`
}
// EditIssueOption options for editing an issue
@@ -113,6 +115,7 @@ type EditIssueOption struct {
// swagger:strfmt date-time
Deadline *time.Time `json:"due_date"`
RemoveDeadline *bool `json:"unset_due_date"`
+ Weight *int `json:"weight"`
}
// EditDeadlineOption options for creating a deadline
@@ -122,6 +125,11 @@ type EditDeadlineOption struct {
Deadline *time.Time `json:"due_date"`
}
+type EditWeightOption struct {
+ // required:true
+ Weight int `json:"weight"`
+}
+
// IssueDeadline represents an issue deadline
// swagger:model
type IssueDeadline struct {
@@ -129,6 +137,11 @@ type IssueDeadline struct {
Deadline *time.Time `json:"due_date"`
}
+// IssueWeight represents an issue weight
+type IssueWeight struct {
+ Weight int `json:"weight"`
+}
+
// IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes"
type IssueFormFieldType string
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index fd27df384da2f..12495bf084a9b 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -24,6 +24,8 @@ type InternalTracker struct {
AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"`
// Enable dependencies for issues and pull requests (Built-in issue tracker)
EnableIssueDependencies bool `json:"enable_issue_dependencies"`
+ // Enable issue weight (Built-in issue tracker)
+ EnableWeight bool `json:"enable_issue_weight"`
}
// ExternalTracker represents settings for external tracker
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 957e73b171d32..d5ac90925c5ae 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -18,6 +18,7 @@ language = Language
notifications = Notifications
active_stopwatch = Active Time Tracker
tracked_time_summary = Summary of tracked time based on filters of issue list
+weight_summary = Total weight based on filters of issue list
create_new = Create…
user_profile_and_more = Profile and Settings…
signed_in_as = Signed in as
@@ -1698,6 +1699,14 @@ issues.due_date_modified = "modified the due date from %[2]s to %[1]s %[3]s"
issues.due_date_remove = "removed the due date %s %s"
issues.due_date_overdue = "Overdue"
issues.due_date_invalid = "The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'."
+issues.weight_added = "added weight of %s %s"
+issues.weight_modified = " changed weight to %s from %s %s"
+issues.weight_removed = "removed weight of %s %s"
+
+issues.weight.title = Weight
+issues.weight.no_weight = No Weight
+issues.weight.remove_weight = remove weight
+
issues.dependency.title = Dependencies
issues.dependency.issue_no_dependencies = No dependencies set.
issues.dependency.pr_no_dependencies = No dependencies set.
@@ -2152,6 +2161,7 @@ settings.tracker_issue_style.regexp_pattern_desc = The first captured group will
settings.tracker_url_format_desc = Use the placeholders {user}
, {repo}
and {index}
for the username, repository name and issue index.
settings.enable_timetracker = Enable Time Tracking
settings.allow_only_contributors_to_track_time = Let Only Contributors Track Time
+settings.enable_weight = Enable Weight
settings.pulls_desc = Enable Repository Pull Requests
settings.pulls.ignore_whitespace = Ignore Whitespace for Conflicts
settings.pulls.enable_autodetect_manual_merge = Enable autodetect manual merge (Note: In some special cases, misjudgments can occur)
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index c1218440e5958..8616bd20bbb74 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -707,7 +707,7 @@ func CreateIssue(ctx *context.APIContext) {
form.Labels = make([]int64, 0)
}
- if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
+ if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0, form.Weight); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
} else if errors.Is(err, user_model.ErrBlockedUser) {
@@ -829,6 +829,13 @@ func EditIssue(ctx *context.APIContext) {
}
}
+ if form.Weight != nil {
+ if err := issue_service.ChangeIssueWeight(ctx, issue, ctx.Doer, *form.Weight); err != nil {
+ ctx.Error(http.StatusInternalServerError, "ChangeWeight", err)
+ return
+ }
+ }
+
// Update or remove the deadline, only if set and allowed
if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
var deadlineUnix timeutil.TimeStamp
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 1bcec8fcf7e72..f04e2fb211c1e 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -799,6 +799,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
EnableTimetracker: opts.InternalTracker.EnableTimeTracker,
AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
EnableDependencies: opts.InternalTracker.EnableIssueDependencies,
+ EnableWeight: opts.InternalTracker.EnableWeight,
}
} else if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err != nil {
// Unit type doesn't exist so we make a new config file with default values
@@ -806,6 +807,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true,
+ EnableWeight: false,
}
} else {
config = unit.IssuesConfig()
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 596abb4b9ca5b..6144e31b2c2cb 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -265,6 +265,17 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["TotalTrackedTime"] = totalTrackedTime
}
+ if repo.IsWeightEnabled(ctx) {
+ totalWeight, totalWeightClosed, err := issues_model.GetIssueTotalWeight(ctx, statsOpts)
+ if err != nil {
+ ctx.ServerError("GetIssueTotalWeight", err)
+ return
+ }
+
+ ctx.Data["TotalWeight"] = totalWeight
+ ctx.Data["WeightedCompleteness"] = totalWeightClosed * 100 / totalWeight
+ }
+
archived := ctx.FormBool("archived")
page := ctx.FormInt("page")
@@ -1277,9 +1288,10 @@ func NewIssuePost(ctx *context.Context) {
MilestoneID: milestoneID,
Content: content,
Ref: form.Ref,
+ Weight: form.Weight,
}
- if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID); err != nil {
+ if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID, form.Weight); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
} else if errors.Is(err, user_model.ErrBlockedUser) {
@@ -2360,6 +2372,33 @@ func UpdateIssueDeadline(ctx *context.Context) {
ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
}
+// UpdateIssueWeight updates an issue weight
+func UpdateIssueWeight(ctx *context.Context) {
+ form := web.GetForm(ctx).(*api.EditWeightOption)
+
+ issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index"))
+ if err != nil {
+ if issues_model.IsErrIssueNotExist(err) {
+ ctx.NotFound("GetIssueByIndex", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
+ }
+ return
+ }
+
+ if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
+ ctx.Error(http.StatusForbidden, "", "Not repo writer")
+ return
+ }
+
+ if err := issues_model.UpdateIssueWeight(ctx, issue, form.Weight, ctx.Doer); err != nil {
+ ctx.Error(http.StatusInternalServerError, "UpdateIssueWeight", err.Error())
+ return
+ }
+
+ ctx.Redirect(issue.Link())
+}
+
// UpdateIssueMilestone change issue's milestone
func UpdateIssueMilestone(ctx *context.Context) {
issues := getActionIssues(ctx)
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index e4ee025875ef6..68f1aad8de297 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -79,6 +79,14 @@ func Milestones(ctx *context.Context) {
return
}
}
+
+ if ctx.Repo.Repository.IsWeightEnabled(ctx) {
+ if err := issues_model.MilestoneList(miles).LoadTotalWeight(ctx); err != nil {
+ ctx.ServerError("LoadTotalWeight", err)
+ return
+ }
+ }
+
for _, m := range miles {
m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 664ea7eb76d79..af299bd7e05cc 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -441,6 +441,7 @@ func ViewProject(ctx *context.Context) {
ctx.Data["IsProjectsPage"] = true
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
+ ctx.Data["IsWeightEnabled"] = ctx.Repo.Repository.IsWeightEnabled(ctx)
ctx.Data["Project"] = project
ctx.Data["IssuesMap"] = issuesMap
ctx.Data["Columns"] = columns
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 485bd927fa932..01cdc9afa4bcd 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -523,6 +523,7 @@ func SettingsPost(ctx *context.Context) {
EnableTimetracker: form.EnableTimetracker,
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
EnableDependencies: form.EnableIssueDependencies,
+ EnableWeight: form.EnableIssueWeight,
},
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
diff --git a/routers/web/web.go b/routers/web/web.go
index f1e941a84efcb..797e307bd42a8 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1204,6 +1204,7 @@ func registerRoutes(m *web.Router) {
m.Post("/title", repo.UpdateIssueTitle)
m.Post("/content", repo.UpdateIssueContent)
m.Post("/deadline", web.Bind(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline)
+ m.Post("/weight", web.Bind(structs.EditWeightOption{}), repo.UpdateIssueWeight)
m.Post("/watch", repo.IssueWatch)
m.Post("/ref", repo.UpdateIssueRef)
m.Post("/pin", reqRepoAdmin, repo.IssuePinOrUnpin)
diff --git a/services/convert/issue.go b/services/convert/issue.go
index f514dc431351e..5bd90b186ea14 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -55,6 +55,7 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
Created: issue.CreatedUnix.AsTime(),
Updated: issue.UpdatedUnix.AsTime(),
PinOrder: issue.PinOrder,
+ Weight: issue.Weight,
}
if issue.Repo != nil {
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 988e479a48138..debb15e113c5b 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -167,6 +167,7 @@ type RepoSettingForm struct {
EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool
EnableIssueDependencies bool
+ EnableIssueWeight bool
IsArchived bool
// Signing Settings
@@ -450,6 +451,7 @@ type CreateIssueForm struct {
MilestoneID int64
ProjectID int64
AssigneeID int64
+ Weight int
Content string
Files []string
AllowMaintainerEdit bool
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 72ea66c8d98c5..b3d4e9c14ee0c 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -23,7 +23,7 @@ import (
)
// NewIssue creates new issue with labels for repository.
-func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64) error {
+func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64, weight int) error {
if err := issue.LoadPoster(ctx); err != nil {
return err
}
@@ -46,6 +46,12 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo
return err
}
}
+
+ if weight != 0 {
+ if _, err := issues_model.CreateWeightComment(ctx, issue.Poster, issue, weight); err != nil {
+ return err
+ }
+ }
return nil
}); err != nil {
return err
@@ -119,6 +125,15 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m
return nil
}
+// ChangeIssueWeight changes the weight of this issue, as the given user.
+func ChangeIssueWeight(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, weight int) error {
+ if issue.Weight == weight {
+ return nil
+ }
+
+ return issues_model.UpdateIssueWeight(ctx, issue, weight, doer)
+}
+
// UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s)
// Deleting is done the GitHub way (quote from their api documentation):
// https://developer.github.com/v3/issues/#edit-an-issue
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index f5c1bb76703c4..c429dee88645a 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -145,9 +145,15 @@