Skip to content

Commit f42dbdb

Browse files
authored
Add Activity page to repository (#2674)
* Add Activity page to repository * Add request data for activity * Add issue data for activity * Add user unit right checks * Add releases to activity * Log repository unit loading error
1 parent 8863e74 commit f42dbdb

File tree

12 files changed

+686
-4
lines changed

12 files changed

+686
-4
lines changed

models/repo.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,9 @@ func (repo *Repository) getUnitsByUserID(e Engine, userID int64, isAdmin bool) (
383383

384384
// UnitEnabled if this repository has the given unit enabled
385385
func (repo *Repository) UnitEnabled(tp UnitType) bool {
386-
repo.getUnits(x)
386+
if err := repo.getUnits(x); err != nil {
387+
log.Warn("Error loading repository (ID: %d) units: %s", repo.ID, err.Error())
388+
}
387389
for _, unit := range repo.Units {
388390
if unit.Type == tp {
389391
return true
@@ -392,6 +394,21 @@ func (repo *Repository) UnitEnabled(tp UnitType) bool {
392394
return false
393395
}
394396

397+
// AnyUnitEnabled if this repository has the any of the given units enabled
398+
func (repo *Repository) AnyUnitEnabled(tps ...UnitType) bool {
399+
if err := repo.getUnits(x); err != nil {
400+
log.Warn("Error loading repository (ID: %d) units: %s", repo.ID, err.Error())
401+
}
402+
for _, unit := range repo.Units {
403+
for _, tp := range tps {
404+
if unit.Type == tp {
405+
return true
406+
}
407+
}
408+
}
409+
return false
410+
}
411+
395412
var (
396413
// ErrUnitNotExist organization does not exist
397414
ErrUnitNotExist = errors.New("Unit does not exist")

models/repo_activity.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"time"
9+
10+
"github.com/go-xorm/xorm"
11+
)
12+
13+
// ActivityStats represets issue and pull request information.
14+
type ActivityStats struct {
15+
OpenedPRs PullRequestList
16+
OpenedPRAuthorCount int64
17+
MergedPRs PullRequestList
18+
MergedPRAuthorCount int64
19+
OpenedIssues IssueList
20+
OpenedIssueAuthorCount int64
21+
ClosedIssues IssueList
22+
ClosedIssueAuthorCount int64
23+
UnresolvedIssues IssueList
24+
PublishedReleases []*Release
25+
PublishedReleaseAuthorCount int64
26+
}
27+
28+
// ActivePRCount returns total active pull request count
29+
func (stats *ActivityStats) ActivePRCount() int {
30+
return stats.OpenedPRCount() + stats.MergedPRCount()
31+
}
32+
33+
// OpenedPRCount returns opened pull request count
34+
func (stats *ActivityStats) OpenedPRCount() int {
35+
return len(stats.OpenedPRs)
36+
}
37+
38+
// OpenedPRPerc returns opened pull request percents from total active
39+
func (stats *ActivityStats) OpenedPRPerc() int {
40+
return int(float32(stats.OpenedPRCount()) / float32(stats.ActivePRCount()) * 100.0)
41+
}
42+
43+
// MergedPRCount returns merged pull request count
44+
func (stats *ActivityStats) MergedPRCount() int {
45+
return len(stats.MergedPRs)
46+
}
47+
48+
// MergedPRPerc returns merged pull request percent from total active
49+
func (stats *ActivityStats) MergedPRPerc() int {
50+
return int(float32(stats.MergedPRCount()) / float32(stats.ActivePRCount()) * 100.0)
51+
}
52+
53+
// ActiveIssueCount returns total active issue count
54+
func (stats *ActivityStats) ActiveIssueCount() int {
55+
return stats.OpenedIssueCount() + stats.ClosedIssueCount()
56+
}
57+
58+
// OpenedIssueCount returns open issue count
59+
func (stats *ActivityStats) OpenedIssueCount() int {
60+
return len(stats.OpenedIssues)
61+
}
62+
63+
// OpenedIssuePerc returns open issue count percent from total active
64+
func (stats *ActivityStats) OpenedIssuePerc() int {
65+
return int(float32(stats.OpenedIssueCount()) / float32(stats.ActiveIssueCount()) * 100.0)
66+
}
67+
68+
// ClosedIssueCount returns closed issue count
69+
func (stats *ActivityStats) ClosedIssueCount() int {
70+
return len(stats.ClosedIssues)
71+
}
72+
73+
// ClosedIssuePerc returns closed issue count percent from total active
74+
func (stats *ActivityStats) ClosedIssuePerc() int {
75+
return int(float32(stats.ClosedIssueCount()) / float32(stats.ActiveIssueCount()) * 100.0)
76+
}
77+
78+
// UnresolvedIssueCount returns unresolved issue and pull request count
79+
func (stats *ActivityStats) UnresolvedIssueCount() int {
80+
return len(stats.UnresolvedIssues)
81+
}
82+
83+
// PublishedReleaseCount returns published release count
84+
func (stats *ActivityStats) PublishedReleaseCount() int {
85+
return len(stats.PublishedReleases)
86+
}
87+
88+
// FillPullRequestsForActivity returns pull request information for activity page
89+
func FillPullRequestsForActivity(stats *ActivityStats, baseRepoID int64, fromTime time.Time) error {
90+
var err error
91+
var count int64
92+
93+
// Merged pull requests
94+
sess := pullRequestsForActivityStatement(baseRepoID, fromTime, true)
95+
sess.OrderBy("pull_request.merged_unix DESC")
96+
stats.MergedPRs = make(PullRequestList, 0)
97+
if err = sess.Find(&stats.MergedPRs); err != nil {
98+
return err
99+
}
100+
if err = stats.MergedPRs.LoadAttributes(); err != nil {
101+
return err
102+
}
103+
104+
// Merged pull request authors
105+
sess = pullRequestsForActivityStatement(baseRepoID, fromTime, true)
106+
if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("pull_request").Get(&count); err != nil {
107+
return err
108+
}
109+
stats.MergedPRAuthorCount = count
110+
111+
// Opened pull requests
112+
sess = pullRequestsForActivityStatement(baseRepoID, fromTime, false)
113+
sess.OrderBy("issue.created_unix ASC")
114+
stats.OpenedPRs = make(PullRequestList, 0)
115+
if err = sess.Find(&stats.OpenedPRs); err != nil {
116+
return err
117+
}
118+
if err = stats.OpenedPRs.LoadAttributes(); err != nil {
119+
return err
120+
}
121+
122+
// Opened pull request authors
123+
sess = pullRequestsForActivityStatement(baseRepoID, fromTime, false)
124+
if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("pull_request").Get(&count); err != nil {
125+
return err
126+
}
127+
stats.OpenedPRAuthorCount = count
128+
129+
return nil
130+
}
131+
132+
func pullRequestsForActivityStatement(baseRepoID int64, fromTime time.Time, merged bool) *xorm.Session {
133+
sess := x.Where("pull_request.base_repo_id=?", baseRepoID).
134+
Join("INNER", "issue", "pull_request.issue_id = issue.id")
135+
136+
if merged {
137+
sess.And("pull_request.has_merged = ?", true)
138+
sess.And("pull_request.merged_unix >= ?", fromTime.Unix())
139+
} else {
140+
sess.And("issue.is_closed = ?", false)
141+
sess.And("issue.created_unix >= ?", fromTime.Unix())
142+
}
143+
144+
return sess
145+
}
146+
147+
// FillIssuesForActivity returns issue information for activity page
148+
func FillIssuesForActivity(stats *ActivityStats, baseRepoID int64, fromTime time.Time) error {
149+
var err error
150+
var count int64
151+
152+
// Closed issues
153+
sess := issuesForActivityStatement(baseRepoID, fromTime, true, false)
154+
sess.OrderBy("issue.updated_unix DESC")
155+
stats.ClosedIssues = make(IssueList, 0)
156+
if err = sess.Find(&stats.ClosedIssues); err != nil {
157+
return err
158+
}
159+
160+
// Closed issue authors
161+
sess = issuesForActivityStatement(baseRepoID, fromTime, true, false)
162+
if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("issue").Get(&count); err != nil {
163+
return err
164+
}
165+
stats.ClosedIssueAuthorCount = count
166+
167+
// New issues
168+
sess = issuesForActivityStatement(baseRepoID, fromTime, false, false)
169+
sess.OrderBy("issue.created_unix ASC")
170+
stats.OpenedIssues = make(IssueList, 0)
171+
if err = sess.Find(&stats.OpenedIssues); err != nil {
172+
return err
173+
}
174+
175+
// Opened issue authors
176+
sess = issuesForActivityStatement(baseRepoID, fromTime, false, false)
177+
if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("issue").Get(&count); err != nil {
178+
return err
179+
}
180+
stats.OpenedIssueAuthorCount = count
181+
182+
return nil
183+
}
184+
185+
// FillUnresolvedIssuesForActivity returns unresolved issue and pull request information for activity page
186+
func FillUnresolvedIssuesForActivity(stats *ActivityStats, baseRepoID int64, fromTime time.Time, issues, prs bool) error {
187+
// Check if we need to select anything
188+
if !issues && !prs {
189+
return nil
190+
}
191+
sess := issuesForActivityStatement(baseRepoID, fromTime, false, true)
192+
if !issues || !prs {
193+
sess.And("issue.is_pull = ?", prs)
194+
}
195+
sess.OrderBy("issue.updated_unix DESC")
196+
stats.UnresolvedIssues = make(IssueList, 0)
197+
return sess.Find(&stats.UnresolvedIssues)
198+
}
199+
200+
func issuesForActivityStatement(baseRepoID int64, fromTime time.Time, closed, unresolved bool) *xorm.Session {
201+
sess := x.Where("issue.repo_id = ?", baseRepoID).
202+
And("issue.is_closed = ?", closed)
203+
204+
if !unresolved {
205+
sess.And("issue.is_pull = ?", false)
206+
sess.And("issue.created_unix >= ?", fromTime.Unix())
207+
} else {
208+
sess.And("issue.created_unix < ?", fromTime.Unix())
209+
sess.And("issue.updated_unix >= ?", fromTime.Unix())
210+
}
211+
212+
return sess
213+
}
214+
215+
// FillReleasesForActivity returns release information for activity page
216+
func FillReleasesForActivity(stats *ActivityStats, baseRepoID int64, fromTime time.Time) error {
217+
var err error
218+
var count int64
219+
220+
// Published releases list
221+
sess := releasesForActivityStatement(baseRepoID, fromTime)
222+
sess.OrderBy("release.created_unix DESC")
223+
stats.PublishedReleases = make([]*Release, 0)
224+
if err = sess.Find(&stats.PublishedReleases); err != nil {
225+
return err
226+
}
227+
228+
// Published releases authors
229+
sess = releasesForActivityStatement(baseRepoID, fromTime)
230+
if _, err = sess.Select("count(distinct release.publisher_id) as `count`").Table("release").Get(&count); err != nil {
231+
return err
232+
}
233+
stats.PublishedReleaseAuthorCount = count
234+
235+
return nil
236+
}
237+
238+
func releasesForActivityStatement(baseRepoID int64, fromTime time.Time) *xorm.Session {
239+
return x.Where("release.repo_id = ?", baseRepoID).
240+
And("release.is_draft = ?", false).
241+
And("release.created_unix >= ?", fromTime.Unix())
242+
}

modules/context/repo.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ func LoadRepoUnits() macaron.Handler {
573573
}
574574
}
575575

576-
// CheckUnit will check whether
576+
// CheckUnit will check whether unit type is enabled
577577
func CheckUnit(unitType models.UnitType) macaron.Handler {
578578
return func(ctx *Context) {
579579
if !ctx.Repo.Repository.UnitEnabled(unitType) {
@@ -582,6 +582,15 @@ func CheckUnit(unitType models.UnitType) macaron.Handler {
582582
}
583583
}
584584

585+
// CheckAnyUnit will check whether any of the unit types are enabled
586+
func CheckAnyUnit(unitTypes ...models.UnitType) macaron.Handler {
587+
return func(ctx *Context) {
588+
if !ctx.Repo.Repository.AnyUnitEnabled(unitTypes...) {
589+
ctx.Handle(404, "CheckAnyUnit", fmt.Errorf("%s: %v", ctx.Tr("units.error.unit_not_allowed"), unitTypes))
590+
}
591+
}
592+
}
593+
585594
// GitHookService checks if repository Git hooks service has been enabled.
586595
func GitHookService() macaron.Handler {
587596
return func(ctx *Context) {

modules/templates/helper.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ func NewFuncMap() []template.FuncMap {
158158
"DisableGitHooks": func() bool {
159159
return setting.DisableGitHooks
160160
},
161+
"TrN": TrN,
161162
}}
162163
}
163164

@@ -342,3 +343,60 @@ func DiffLineTypeToStr(diffType int) string {
342343
}
343344
return "same"
344345
}
346+
347+
// Language specific rules for translating plural texts
348+
var trNLangRules = map[string]func(int64) int{
349+
"en-US": func(cnt int64) int {
350+
if cnt == 1 {
351+
return 0
352+
}
353+
return 1
354+
},
355+
"lv-LV": func(cnt int64) int {
356+
if cnt%10 == 1 && cnt%100 != 11 {
357+
return 0
358+
}
359+
return 1
360+
},
361+
"ru-RU": func(cnt int64) int {
362+
if cnt%10 == 1 && cnt%100 != 11 {
363+
return 0
364+
}
365+
return 1
366+
},
367+
"zh-CN": func(cnt int64) int {
368+
return 0
369+
},
370+
"zh-HK": func(cnt int64) int {
371+
return 0
372+
},
373+
"zh-TW": func(cnt int64) int {
374+
return 0
375+
},
376+
}
377+
378+
// TrN returns key to be used for plural text translation
379+
func TrN(lang string, cnt interface{}, key1, keyN string) string {
380+
var c int64
381+
if t, ok := cnt.(int); ok {
382+
c = int64(t)
383+
} else if t, ok := cnt.(int16); ok {
384+
c = int64(t)
385+
} else if t, ok := cnt.(int32); ok {
386+
c = int64(t)
387+
} else if t, ok := cnt.(int64); ok {
388+
c = t
389+
} else {
390+
return keyN
391+
}
392+
393+
ruleFunc, ok := trNLangRules[lang]
394+
if !ok {
395+
ruleFunc = trNLangRules["en-US"]
396+
}
397+
398+
if ruleFunc(c) == 0 {
399+
return key1
400+
}
401+
return keyN
402+
}

0 commit comments

Comments
 (0)