Browse Source

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
Lauris BH 3 years ago
parent
commit
f42dbdbae5

+ 18 - 1
models/repo.go

@@ -383,7 +383,9 @@ func (repo *Repository) getUnitsByUserID(e Engine, userID int64, isAdmin bool) (
383 383
 
384 384
 // UnitEnabled if this repository has the given unit enabled
385 385
 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
+	}
387 389
 	for _, unit := range repo.Units {
388 390
 		if unit.Type == tp {
389 391
 			return true
@@ -392,6 +394,21 @@ func (repo *Repository) UnitEnabled(tp UnitType) bool {
392 394
 	return false
393 395
 }
394 396
 
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
+
395 412
 var (
396 413
 	// ErrUnitNotExist organization does not exist
397 414
 	ErrUnitNotExist = errors.New("Unit does not exist")

+ 242 - 0
models/repo_activity.go

@@ -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
+}

+ 10 - 1
modules/context/repo.go

@@ -573,7 +573,7 @@ func LoadRepoUnits() macaron.Handler {
573 573
 	}
574 574
 }
575 575
 
576
-// CheckUnit will check whether
576
+// CheckUnit will check whether unit type is enabled
577 577
 func CheckUnit(unitType models.UnitType) macaron.Handler {
578 578
 	return func(ctx *Context) {
579 579
 		if !ctx.Repo.Repository.UnitEnabled(unitType) {
@@ -582,6 +582,15 @@ func CheckUnit(unitType models.UnitType) macaron.Handler {
582 582
 	}
583 583
 }
584 584
 
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
+
585 594
 // GitHookService checks if repository Git hooks service has been enabled.
586 595
 func GitHookService() macaron.Handler {
587 596
 	return func(ctx *Context) {

+ 58 - 0
modules/templates/helper.go

@@ -158,6 +158,7 @@ func NewFuncMap() []template.FuncMap {
158 158
 		"DisableGitHooks": func() bool {
159 159
 			return setting.DisableGitHooks
160 160
 		},
161
+		"TrN": TrN,
161 162
 	}}
162 163
 }
163 164
 
@@ -342,3 +343,60 @@ func DiffLineTypeToStr(diffType int) string {
342 343
 	}
343 344
 	return "same"
344 345
 }
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
+}

+ 42 - 0
options/locale/locale_en-US.ini

@@ -806,6 +806,48 @@ wiki.page_already_exists = A wiki page with the same name already exists.
806 806
 wiki.pages = Pages
807 807
 wiki.last_updated = Last updated %s
808 808
 
809
+activity = Activity
810
+activity.period.filter_label = Period:
811
+activity.period.daily = 1 day
812
+activity.period.halfweekly = 3 days
813
+activity.period.weekly = 1 week
814
+activity.period.monthly = 1 month
815
+activity.overview = Overview
816
+activity.active_prs_count_1 = <strong>%d</strong> Active Pull Request
817
+activity.active_prs_count_n = <strong>%d</strong> Active Pull Requests
818
+activity.merged_prs_count_1 = Merged Pull Request
819
+activity.merged_prs_count_n = Merged Pull Requests
820
+activity.opened_prs_count_1 = Proposed Pull Request
821
+activity.opened_prs_count_n = Proposed Pull Requests
822
+activity.title.user_1 = %d user
823
+activity.title.user_n = %d users
824
+activity.title.prs_1 = %d Pull request
825
+activity.title.prs_n = %d Pull requests
826
+activity.title.prs_merged_by = %s merged by %s
827
+activity.title.prs_opened_by = %s proposed by %s
828
+activity.merged_prs_label = Merged
829
+activity.opened_prs_label = Proposed
830
+activity.active_issues_count_1 = <strong>%d</strong> Active Issue
831
+activity.active_issues_count_n = <strong>%d</strong> Active Issues
832
+activity.closed_issues_count_1 = Closed Issue
833
+activity.closed_issues_count_n = Closed Issues
834
+activity.title.issues_1 = %d Issue
835
+activity.title.issues_n = %d Issues
836
+activity.title.issues_closed_by = %s closed by %s
837
+activity.title.issues_created_by = %s created by %s
838
+activity.closed_issue_label = Closed
839
+activity.new_issues_count_1 = New Issue
840
+activity.new_issues_count_n = New Issues
841
+activity.new_issue_label = Opened
842
+activity.title.unresolved_conv_1 = %d Unresolved conversation
843
+activity.title.unresolved_conv_n = %d Unresolved conversations
844
+activity.unresolved_conv_desc = List of all old issues and pull requests that have changed recently but has not been resloved yet.
845
+activity.unresolved_conv_label = Open
846
+activity.title.releases_1 = %d Release
847
+activity.title.releases_n = %d Releases
848
+activity.title.releases_published_by = %s published by %s
849
+activity.published_release_label = Published
850
+
809 851
 settings = Settings
810 852
 settings.desc = Settings is where you can manage the settings for the repository
811 853
 settings.options = Options

File diff suppressed because it is too large
+ 1 - 1
public/css/index.css


+ 33 - 1
public/less/_base.less

@@ -299,8 +299,40 @@ pre, code {
299 299
 		padding: 8px 15px;
300 300
 		font-weight: normal;
301 301
 	}
302
+
303
+	.background {
304
+		&.red {
305
+			background-color: #d95c5c !important;
306
+		}
307
+		&.blue {
308
+			background-color: #428bca !important;
309
+		}
310
+		&.black {
311
+			background-color: #444;
312
+		}
313
+		&.grey {
314
+			background-color: #767676 !important;
315
+		}
316
+		&.light.grey {
317
+			background-color: #888 !important;
318
+		}
319
+		&.green {
320
+			background-color: #6cc644 !important;
321
+		}
322
+		&.purple {
323
+			background-color: #6e5494 !important;
324
+		}
325
+		&.yellow {
326
+			background-color: #FBBD08 !important;
327
+		}
328
+		&.gold {
329
+			background-color: #a1882b !important;
330
+		}
331
+	}
302 332
 }
303 333
 
334
+
335
+
304 336
 .overflow.menu {
305 337
 	.items {
306 338
 		max-height: 300px;
@@ -477,4 +509,4 @@ footer {
477 509
     margin-top: 0 !important;
478 510
     border-bottom-width: 0 !important;
479 511
     margin-bottom: 2px !important;
480
-}
512
+}

+ 11 - 0
public/less/_repository.less

@@ -1554,3 +1554,14 @@
1554 1554
 	}
1555 1555
 	.generate-tab-size(@n, (@i + 1));
1556 1556
 }
1557
+
1558
+.table {
1559
+	display: table;
1560
+	width: 100%;
1561
+	.table-cell {
1562
+		display: table-cell;
1563
+		&.tiny {
1564
+			height: .5em;
1565
+		}
1566
+	}
1567
+}

+ 76 - 0
routers/repo/activity.go

@@ -0,0 +1,76 @@
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 repo
6
+
7
+import (
8
+	"time"
9
+
10
+	"code.gitea.io/gitea/models"
11
+	"code.gitea.io/gitea/modules/base"
12
+	"code.gitea.io/gitea/modules/context"
13
+)
14
+
15
+const (
16
+	tplActivity base.TplName = "repo/activity"
17
+)
18
+
19
+// Activity render the page to show repository latest changes
20
+func Activity(ctx *context.Context) {
21
+	ctx.Data["Title"] = ctx.Tr("repo.activity")
22
+	ctx.Data["PageIsActivity"] = true
23
+
24
+	ctx.Data["Period"] = ctx.Params("period")
25
+
26
+	timeUntil := time.Now()
27
+	var timeFrom time.Time
28
+
29
+	switch ctx.Data["Period"] {
30
+	case "daily":
31
+		timeFrom = timeUntil.Add(-time.Hour * 24)
32
+	case "halfweekly":
33
+		timeFrom = timeUntil.Add(-time.Hour * 72)
34
+	case "weekly":
35
+		timeFrom = timeUntil.Add(-time.Hour * 168)
36
+	case "monthly":
37
+		timeFrom = timeUntil.AddDate(0, -1, 0)
38
+	default:
39
+		ctx.Data["Period"] = "weekly"
40
+		timeFrom = timeUntil.Add(-time.Hour * 168)
41
+	}
42
+	ctx.Data["DateFrom"] = timeFrom.Format("January 2, 2006")
43
+	ctx.Data["DateUntil"] = timeUntil.Format("January 2, 2006")
44
+	ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
45
+
46
+	stats := &models.ActivityStats{}
47
+
48
+	if ctx.Repo.Repository.UnitEnabled(models.UnitTypeReleases) {
49
+		if err := models.FillReleasesForActivity(stats, ctx.Repo.Repository.ID, timeFrom); err != nil {
50
+			ctx.Handle(500, "FillReleasesForActivity", err)
51
+			return
52
+		}
53
+	}
54
+	if ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) {
55
+		if err := models.FillPullRequestsForActivity(stats, ctx.Repo.Repository.ID, timeFrom); err != nil {
56
+			ctx.Handle(500, "FillPullRequestsForActivity", err)
57
+			return
58
+		}
59
+	}
60
+	if ctx.Repo.Repository.UnitEnabled(models.UnitTypeIssues) {
61
+		if err := models.FillIssuesForActivity(stats, ctx.Repo.Repository.ID, timeFrom); err != nil {
62
+			ctx.Handle(500, "FillIssuesForActivity", err)
63
+			return
64
+		}
65
+	}
66
+	if err := models.FillUnresolvedIssuesForActivity(stats, ctx.Repo.Repository.ID, timeFrom,
67
+		ctx.Repo.Repository.UnitEnabled(models.UnitTypeIssues),
68
+		ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests)); err != nil {
69
+		ctx.Handle(500, "FillUnresolvedIssuesForActivity", err)
70
+		return
71
+	}
72
+
73
+	ctx.Data["Activity"] = stats
74
+
75
+	ctx.HTML(200, tplActivity)
76
+}

+ 5 - 0
routers/routes/routes.go

@@ -613,6 +613,11 @@ func RegisterRoutes(m *macaron.Macaron) {
613 613
 			m.Get("/*", repo.WikiRaw)
614 614
 		}, repo.MustEnableWiki)
615 615
 
616
+		m.Group("/activity", func() {
617
+			m.Get("", repo.Activity)
618
+			m.Get("/:period", repo.Activity)
619
+		}, context.RepoRef(), repo.MustBeNotBare, context.CheckAnyUnit(models.UnitTypePullRequests, models.UnitTypeIssues, models.UnitTypeReleases))
620
+
616 621
 		m.Get("/archive/*", repo.MustBeNotBare, context.CheckUnit(models.UnitTypeCode), repo.Download)
617 622
 
618 623
 		m.Group("/pulls/:index", func() {

+ 184 - 0
templates/repo/activity.tmpl

@@ -0,0 +1,184 @@
1
+{{template "base/head" .}}
2
+<div class="repository commits">
3
+	{{template "repo/header" .}}
4
+	<div class="ui container">
5
+		<h2 class="ui header">{{.DateFrom}} - {{.DateUntil}}
6
+			<div class="ui right">
7
+				<!-- Period -->
8
+				<div class="ui floating dropdown jump filter">
9
+					<div class="ui basic compact button">
10
+						<span class="text">
11
+							{{.i18n.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong>
12
+							<i class="dropdown icon"></i>
13
+						</span>
14
+					</div>
15
+					<div class="menu">
16
+						<a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{.i18n.Tr "repo.activity.period.daily"}}</a>
17
+						<a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{.i18n.Tr "repo.activity.period.halfweekly"}}</a>
18
+						<a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{.i18n.Tr "repo.activity.period.weekly"}}</a>
19
+						<a class="{{if eq .Period "monthly"}}active {{end}}item" href="{{$.RepoLink}}/activity/monthly">{{.i18n.Tr "repo.activity.period.monthly"}}</a>
20
+					</div>
21
+				</div>
22
+			</div>
23
+		</h2>
24
+		<div class="ui divider"></div>
25
+
26
+		{{if (or (.Repository.UnitEnabled $.UnitTypeIssues) (.Repository.UnitEnabled $.UnitTypePullRequests))}}
27
+		<h4 class="ui top attached header">{{.i18n.Tr "repo.activity.overview"}}</h4>
28
+		<div class="ui attached segment two column grid">
29
+			{{if .Repository.UnitEnabled $.UnitTypePullRequests}}
30
+				<div class="column">
31
+					{{if gt .Activity.ActivePRCount 0}}
32
+					<div class="table">
33
+						<a href="#merged-pull-requests" class="table-cell tiny background purple" style="width: {{.Activity.MergedPRPerc}}%"></a>
34
+						<a href="#proposed-pull-requests" class="table-cell tiny background green"></a>
35
+					</div>
36
+					{{end}}
37
+					{{.i18n.Tr (TrN .i18n.Lang .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n") .Activity.ActivePRCount | Safe }}
38
+				</div>
39
+			{{end}}
40
+			{{if .Repository.UnitEnabled $.UnitTypeIssues}}
41
+				<div class="column">
42
+					{{if gt .Activity.ActiveIssueCount 0}}
43
+					<div class="table">
44
+						<a href="#closed-issues" class="table-cell tiny background red" style="width: {{.Activity.ClosedIssuePerc}}%"></a>
45
+						<a href="#new-issues" class="table-cell tiny background green"></a>
46
+					</div>
47
+					{{end}}
48
+					{{.i18n.Tr (TrN .i18n.Lang .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n") .Activity.ActiveIssueCount | Safe }}
49
+				</div>
50
+			{{end}}
51
+		</div>
52
+		<div class="ui attached segment horizontal segments">
53
+			{{if .Repository.UnitEnabled $.UnitTypePullRequests}}
54
+				<a href="#merged-pull-requests" class="ui attached segment text center">
55
+					<i class="text purple octicon octicon-git-pull-request"></i> <strong>{{.Activity.MergedPRCount}}</strong><br>
56
+					{{.i18n.Tr (TrN .i18n.Lang .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n") }}
57
+				</a>
58
+				<a href="#proposed-pull-requests" class="ui attached segment text center">
59
+					<i class="text green octicon octicon-git-branch"></i> <strong>{{.Activity.OpenedPRCount}}</strong><br>
60
+					{{.i18n.Tr (TrN .i18n.Lang .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n") }}
61
+				</a>
62
+			{{end}}
63
+			{{if .Repository.UnitEnabled $.UnitTypeIssues}}
64
+				<a href="#closed-issues" class="ui attached segment text center">
65
+					<i class="text red octicon octicon-issue-closed"></i> <strong>{{.Activity.ClosedIssueCount}}</strong><br>
66
+					{{.i18n.Tr (TrN .i18n.Lang .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n") }}
67
+				</a>
68
+				<a href="#new-issues" class="ui attached segment text center">
69
+					<i class="text green octicon octicon-issue-opened"></i> <strong>{{.Activity.OpenedIssueCount}}</strong><br>
70
+					{{.i18n.Tr (TrN .i18n.Lang .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n") }}
71
+				</a>
72
+			{{end}}
73
+		</div>
74
+		{{end}}
75
+
76
+		{{if gt .Activity.PublishedReleaseCount 0}}
77
+			<h4 class="ui horizontal divider header" id="published-releases">
78
+				<i class="text octicon octicon-tag"></i>
79
+				{{.i18n.Tr "repo.activity.title.releases_published_by" (.i18n.Tr (TrN .i18n.Lang .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n") .Activity.PublishedReleaseCount) (.i18n.Tr (TrN .i18n.Lang .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n") .Activity.PublishedReleaseAuthorCount) }}
80
+			</h4>
81
+			<div class="list">
82
+				{{range .Activity.PublishedReleases}}
83
+					<p class="desc">
84
+						<div class="ui green label">{{$.i18n.Tr "repo.activity.published_release_label"}}</div>
85
+						{{.TagName}}
86
+						{{if not .IsTag}}
87
+							<a class="title has-emoji" href="{{$.Repository.HTMLURL}}/src/{{.TagName}}">{{.Title}}</a>
88
+						{{end}}
89
+						{{TimeSince .Created $.Lang}}
90
+					</p>
91
+				{{end}}
92
+			</div>
93
+		{{end}}
94
+
95
+		{{if gt .Activity.MergedPRCount 0}}
96
+			<h4 class="ui horizontal divider header" id="merged-pull-requests">
97
+				<i class="text octicon octicon-git-pull-request"></i>
98
+				{{.i18n.Tr "repo.activity.title.prs_merged_by" (.i18n.Tr (TrN .i18n.Lang .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n") .Activity.MergedPRCount) (.i18n.Tr (TrN .i18n.Lang .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n") .Activity.MergedPRAuthorCount) }}
99
+			</h4>
100
+			<div class="list">
101
+				{{range .Activity.MergedPRs}}
102
+					<p class="desc">
103
+						<div class="ui purple label">{{$.i18n.Tr "repo.activity.merged_prs_label"}}</div>
104
+						#{{.Index}} <a class="title has-emoji" href="{{$.Repository.HTMLURL}}/pulls/{{.Index}}">{{.Issue.Title}}</a>
105
+						{{TimeSince .Merged $.Lang}}
106
+					</p>
107
+				{{end}}
108
+			</div>
109
+		{{end}}
110
+
111
+		{{if gt .Activity.OpenedPRCount 0}}
112
+			<h4 class="ui horizontal divider header" id="proposed-pull-requests">
113
+				<i class="text octicon octicon-git-branch"></i>
114
+				{{.i18n.Tr "repo.activity.title.prs_opened_by" (.i18n.Tr (TrN .i18n.Lang .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n") .Activity.OpenedPRCount) (.i18n.Tr (TrN .i18n.Lang .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n") .Activity.OpenedPRAuthorCount) }}
115
+			</h4>
116
+			<div class="list">
117
+				{{range .Activity.OpenedPRs}}
118
+					<p class="desc">
119
+						<div class="ui green label">{{$.i18n.Tr "repo.activity.opened_prs_label"}}</div>
120
+						#{{.Index}} <a class="title has-emoji" href="{{$.Repository.HTMLURL}}/pulls/{{.Index}}">{{.Issue.Title}}</a>
121
+						{{TimeSince .Issue.Created $.Lang}}
122
+					</p>
123
+				{{end}}
124
+			</div>
125
+		{{end}}
126
+
127
+		{{if gt .Activity.ClosedIssueCount 0}}
128
+			<h4 class="ui horizontal divider header" id="closed-issues">
129
+				<i class="text octicon octicon-issue-closed"></i>
130
+				{{.i18n.Tr "repo.activity.title.issues_closed_by" (.i18n.Tr (TrN .i18n.Lang .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n") .Activity.ClosedIssueCount) (.i18n.Tr (TrN .i18n.Lang .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n") .Activity.ClosedIssueAuthorCount) }}
131
+			</h4>
132
+			<div class="list">
133
+				{{range .Activity.ClosedIssues}}
134
+					<p class="desc">
135
+						<div class="ui red label">{{$.i18n.Tr "repo.activity.closed_issue_label"}}</div>
136
+						#{{.Index}} <a class="title has-emoji" href="{{$.Repository.HTMLURL}}/issues/{{.Index}}">{{.Title}}</a>
137
+						{{TimeSince .Updated $.Lang}}
138
+					</p>
139
+				{{end}}
140
+			</div>
141
+		{{end}}
142
+
143
+		{{if gt .Activity.OpenedIssueCount 0}}
144
+			<h4 class="ui horizontal divider header" id="new-issues">
145
+				<i class="text octicon octicon-issue-opened"></i>
146
+				{{.i18n.Tr "repo.activity.title.issues_created_by" (.i18n.Tr (TrN .i18n.Lang .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n") .Activity.OpenedIssueCount) (.i18n.Tr (TrN .i18n.Lang .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n") .Activity.OpenedIssueAuthorCount) }}
147
+			</h4>
148
+			<div class="list">
149
+				{{range .Activity.OpenedIssues}}
150
+					<p class="desc">
151
+						<div class="ui green label">{{$.i18n.Tr "repo.activity.new_issue_label"}}</div>
152
+						#{{.Index}} <a class="title has-emoji" href="{{$.Repository.HTMLURL}}/issues/{{.Index}}">{{.Title}}</a>
153
+						{{TimeSince .Created $.Lang}}
154
+					</p>
155
+				{{end}}
156
+			</div>
157
+		{{end}}
158
+
159
+		{{if gt .Activity.UnresolvedIssueCount 0}}
160
+			<h4 class="ui horizontal divider header" id="unresolved-conversations">
161
+				<i class="text octicon octicon-comment-discussion"></i>
162
+				{{.i18n.Tr (TrN .i18n.Lang .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n") .Activity.UnresolvedIssueCount }}
163
+			</h4>
164
+			<div class="text center desc">
165
+				{{.i18n.Tr "repo.activity.unresolved_conv_desc"}}
166
+			</div>
167
+			<div class="list">
168
+				{{range .Activity.UnresolvedIssues}}
169
+					<p class="desc">
170
+						<div class="ui green label">{{$.i18n.Tr "repo.activity.unresolved_conv_label"}}</div>
171
+						#{{.Index}}
172
+						{{if .IsPull}}
173
+						<a class="title has-emoji" href="{{$.Repository.HTMLURL}}/pulls/{{.Index}}">{{.Title}}</a>
174
+						{{else}}
175
+						<a class="title has-emoji" href="{{$.Repository.HTMLURL}}/issues/{{.Index}}">{{.Title}}</a>
176
+						{{end}}
177
+						{{TimeSince .Updated $.Lang}}
178
+					</p>
179
+				{{end}}
180
+			</div>
181
+		{{end}}
182
+	</div>
183
+</div>
184
+{{template "base/footer" .}}

+ 6 - 0
templates/repo/header.tmpl

@@ -91,6 +91,12 @@
91 91
 				</a>
92 92
 			{{end}}
93 93
 
94
+			{{if and (.Repository.AnyUnitEnabled $.UnitTypePullRequests $.UnitTypeIssues $.UnitTypeReleases) (not .IsBareRepo)}}
95
+				<a class="{{if .PageIsActivity}}active{{end}} item" href="{{.RepoLink}}/activity">
96
+					<i class="octicon octicon-pulse"></i> {{.i18n.Tr "repo.activity"}}
97
+				</a>
98
+			{{end}}
99
+
94 100
 			{{if .IsRepositoryAdmin}}
95 101
 				<div class="right menu">
96 102
 					<a class="{{if .PageIsSettings}}active{{end}} item" href="{{.RepoLink}}/settings">