Skip to content

Commit bec69f7

Browse files
authored
Add topic support (#3711)
* add topic models and unit tests * fix comments * fix comment * add the UI to show or add topics for a repo * show topics on repositories list * fix test * don't show manage topics link when no permission * use green basic as topic label * fix topic label color * remove trace content * remove debug function
1 parent 1946ce2 commit bec69f7

File tree

17 files changed

+487
-2
lines changed

17 files changed

+487
-2
lines changed

models/fixtures/repo_topic.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-
2+
repo_id: 1
3+
topic_id: 1
4+
5+
-
6+
repo_id: 1
7+
topic_id: 2
8+
9+
-
10+
repo_id: 1
11+
topic_id: 3

models/fixtures/topic.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-
2+
id: 1
3+
name: golang
4+
repo_count: 1
5+
6+
-
7+
id: 2
8+
name: database
9+
repo_count: 1
10+
11+
- id: 3
12+
name: SQL
13+
repo_count: 1

models/repo.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ type Repository struct {
199199
Size int64 `xorm:"NOT NULL DEFAULT 0"`
200200
IndexerStatus *RepoIndexerStatus `xorm:"-"`
201201
IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"`
202+
Topics []string `xorm:"TEXT JSON"`
202203

203204
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
204205
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`

models/topic.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright 2018 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+
"fmt"
9+
"strings"
10+
11+
"code.gitea.io/gitea/modules/util"
12+
13+
"github.com/go-xorm/builder"
14+
)
15+
16+
func init() {
17+
tables = append(tables,
18+
new(Topic),
19+
new(RepoTopic),
20+
)
21+
}
22+
23+
// Topic represents a topic of repositories
24+
type Topic struct {
25+
ID int64
26+
Name string `xorm:"unique"`
27+
RepoCount int
28+
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
29+
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
30+
}
31+
32+
// RepoTopic represents associated repositories and topics
33+
type RepoTopic struct {
34+
RepoID int64 `xorm:"unique(s)"`
35+
TopicID int64 `xorm:"unique(s)"`
36+
}
37+
38+
// ErrTopicNotExist represents an error that a topic is not exist
39+
type ErrTopicNotExist struct {
40+
Name string
41+
}
42+
43+
// IsErrTopicNotExist checks if an error is an ErrTopicNotExist.
44+
func IsErrTopicNotExist(err error) bool {
45+
_, ok := err.(ErrTopicNotExist)
46+
return ok
47+
}
48+
49+
// Error implements error interface
50+
func (err ErrTopicNotExist) Error() string {
51+
return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
52+
}
53+
54+
// GetTopicByName retrieves topic by name
55+
func GetTopicByName(name string) (*Topic, error) {
56+
var topic Topic
57+
if has, err := x.Where("name = ?", name).Get(&topic); err != nil {
58+
return nil, err
59+
} else if !has {
60+
return nil, ErrTopicNotExist{name}
61+
}
62+
return &topic, nil
63+
}
64+
65+
// FindTopicOptions represents the options when fdin topics
66+
type FindTopicOptions struct {
67+
RepoID int64
68+
Keyword string
69+
Limit int
70+
Page int
71+
}
72+
73+
func (opts *FindTopicOptions) toConds() builder.Cond {
74+
var cond = builder.NewCond()
75+
if opts.RepoID > 0 {
76+
cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID})
77+
}
78+
79+
if opts.Keyword != "" {
80+
cond = cond.And(builder.Like{"topic.name", opts.Keyword})
81+
}
82+
83+
return cond
84+
}
85+
86+
// FindTopics retrieves the topics via FindTopicOptions
87+
func FindTopics(opts *FindTopicOptions) (topics []*Topic, err error) {
88+
sess := x.Select("topic.*").Where(opts.toConds())
89+
if opts.RepoID > 0 {
90+
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
91+
}
92+
if opts.Limit > 0 {
93+
sess.Limit(opts.Limit, opts.Page*opts.Limit)
94+
}
95+
return topics, sess.Desc("topic.repo_count").Find(&topics)
96+
}
97+
98+
// SaveTopics save topics to a repository
99+
func SaveTopics(repoID int64, topicNames ...string) error {
100+
topics, err := FindTopics(&FindTopicOptions{
101+
RepoID: repoID,
102+
})
103+
if err != nil {
104+
return err
105+
}
106+
107+
sess := x.NewSession()
108+
defer sess.Close()
109+
110+
if err := sess.Begin(); err != nil {
111+
return err
112+
}
113+
114+
var addedTopicNames []string
115+
for _, topicName := range topicNames {
116+
if strings.TrimSpace(topicName) == "" {
117+
continue
118+
}
119+
120+
var found bool
121+
for _, t := range topics {
122+
if strings.EqualFold(topicName, t.Name) {
123+
found = true
124+
break
125+
}
126+
}
127+
if !found {
128+
addedTopicNames = append(addedTopicNames, topicName)
129+
}
130+
}
131+
132+
var removeTopics []*Topic
133+
for _, t := range topics {
134+
var found bool
135+
for _, topicName := range topicNames {
136+
if strings.EqualFold(topicName, t.Name) {
137+
found = true
138+
break
139+
}
140+
}
141+
if !found {
142+
removeTopics = append(removeTopics, t)
143+
}
144+
}
145+
146+
for _, topicName := range addedTopicNames {
147+
var topic Topic
148+
if has, err := sess.Where("name = ?", topicName).Get(&topic); err != nil {
149+
return err
150+
} else if !has {
151+
topic.Name = topicName
152+
topic.RepoCount = 1
153+
if _, err := sess.Insert(&topic); err != nil {
154+
return err
155+
}
156+
} else {
157+
topic.RepoCount++
158+
if _, err := sess.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil {
159+
return err
160+
}
161+
}
162+
163+
if _, err := sess.Insert(&RepoTopic{
164+
RepoID: repoID,
165+
TopicID: topic.ID,
166+
}); err != nil {
167+
return err
168+
}
169+
}
170+
171+
for _, topic := range removeTopics {
172+
topic.RepoCount--
173+
if _, err := sess.ID(topic.ID).Cols("repo_count").Update(topic); err != nil {
174+
return err
175+
}
176+
177+
if _, err := sess.Delete(&RepoTopic{
178+
RepoID: repoID,
179+
TopicID: topic.ID,
180+
}); err != nil {
181+
return err
182+
}
183+
}
184+
185+
if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{
186+
Topics: topicNames,
187+
}); err != nil {
188+
return err
189+
}
190+
191+
return sess.Commit()
192+
}

models/topic_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2018 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+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestAddTopic(t *testing.T) {
14+
assert.NoError(t, PrepareTestDatabase())
15+
16+
topics, err := FindTopics(&FindTopicOptions{})
17+
assert.NoError(t, err)
18+
assert.EqualValues(t, 3, len(topics))
19+
20+
topics, err = FindTopics(&FindTopicOptions{
21+
Limit: 2,
22+
})
23+
assert.NoError(t, err)
24+
assert.EqualValues(t, 2, len(topics))
25+
26+
topics, err = FindTopics(&FindTopicOptions{
27+
RepoID: 1,
28+
})
29+
assert.NoError(t, err)
30+
assert.EqualValues(t, 3, len(topics))
31+
32+
assert.NoError(t, SaveTopics(2, "golang"))
33+
topics, err = FindTopics(&FindTopicOptions{})
34+
assert.NoError(t, err)
35+
assert.EqualValues(t, 3, len(topics))
36+
37+
topics, err = FindTopics(&FindTopicOptions{
38+
RepoID: 2,
39+
})
40+
assert.NoError(t, err)
41+
assert.EqualValues(t, 1, len(topics))
42+
43+
assert.NoError(t, SaveTopics(2, "golang", "gitea"))
44+
topic, err := GetTopicByName("gitea")
45+
assert.NoError(t, err)
46+
assert.EqualValues(t, 1, topic.RepoCount)
47+
48+
topics, err = FindTopics(&FindTopicOptions{})
49+
assert.NoError(t, err)
50+
assert.EqualValues(t, 4, len(topics))
51+
52+
topics, err = FindTopics(&FindTopicOptions{
53+
RepoID: 2,
54+
})
55+
assert.NoError(t, err)
56+
assert.EqualValues(t, 2, len(topics))
57+
}

modules/auth/repo_form.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,3 +516,8 @@ type AddTimeManuallyForm struct {
516516
func (f *AddTimeManuallyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
517517
return validate(errs, ctx.Data, f, ctx.Locale)
518518
}
519+
520+
// SaveTopicForm form for save topics for repository
521+
type SaveTopicForm struct {
522+
Topics []string `binding:"topics;Required;"`
523+
}

options/locale/locale_en-US.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,9 @@ branch.restore_success = %s successfully restored
11141114
branch.restore_failed = Failed to restore branch %s.
11151115
branch.protected_deletion_failed = It's not possible to delete protected branch %s.
11161116

1117+
topic.manage_topics = Manage Topics
1118+
topic.done = Done
1119+
11171120
[org]
11181121
org_name_holder = Organization Name
11191122
org_full_name_holder = Organization Full Name

public/css/index.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/js/index.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,7 @@ $(document).ready(function () {
15911591
initTeamSettings();
15921592
initCtrlEnterSubmit();
15931593
initNavbarContentToggle();
1594+
initTopicbar();
15941595

15951596
// Repo clone url.
15961597
if ($('#repo-clone-url').length > 0) {
@@ -2122,3 +2123,74 @@ function initNavbarContentToggle() {
21222123
}
21232124
});
21242125
}
2126+
2127+
function initTopicbar() {
2128+
var mgrBtn = $("#manage_topic")
2129+
var editDiv = $("#topic_edit")
2130+
var viewDiv = $("#repo-topic")
2131+
var saveBtn = $("#save_topic")
2132+
2133+
mgrBtn.click(function() {
2134+
viewDiv.hide();
2135+
editDiv.show();
2136+
})
2137+
2138+
saveBtn.click(function() {
2139+
var topics = $("input[name=topics]").val();
2140+
2141+
$.post($(this).data('link'), {
2142+
"_csrf": csrf,
2143+
"topics": topics
2144+
}).success(function(res){
2145+
if (res["status"] != "ok") {
2146+
alert(res.message);
2147+
} else {
2148+
viewDiv.children(".topic").remove();
2149+
var topicArray = topics.split(",");
2150+
var last = viewDiv.children("a").last();
2151+
for (var i=0;i < topicArray.length; i++) {
2152+
$('<div class="ui green basic label topic" style="cursor:pointer;">'+topicArray[i]+'</div>').insertBefore(last)
2153+
}
2154+
}
2155+
}).done(function() {
2156+
editDiv.hide();
2157+
viewDiv.show();
2158+
})
2159+
})
2160+
2161+
$('#topic_edit .dropdown').dropdown({
2162+
allowAdditions: true,
2163+
fields: { name: "description", value: "data-value" },
2164+
saveRemoteData: false,
2165+
label: {
2166+
transition : 'horizontal flip',
2167+
duration : 200,
2168+
variation : false,
2169+
blue : true,
2170+
basic: true,
2171+
},
2172+
className: {
2173+
label: 'ui green basic label'
2174+
},
2175+
apiSettings: {
2176+
url: suburl + '/api/v1/topics/search?q={query}',
2177+
throttle: 500,
2178+
cache: false,
2179+
onResponse: function(res) {
2180+
var formattedResponse = {
2181+
success: false,
2182+
results: new Array(),
2183+
};
2184+
2185+
if (res.topics) {
2186+
formattedResponse.success = true;
2187+
for (var i=0;i < res.topics.length;i++) {
2188+
formattedResponse.results.push({"description": res.topics[i].Name, "data-value":res.topics[i].Name})
2189+
}
2190+
}
2191+
2192+
return formattedResponse;
2193+
},
2194+
},
2195+
});
2196+
}

0 commit comments

Comments
 (0)