Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API endpoint for accessing repo topics #7963

Merged
merged 35 commits into from
Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ded028c
Create API endpoints for repo topics.
davidsvantesson Aug 24, 2019
6965aa8
Generate swagger
davidsvantesson Aug 24, 2019
5a87bab
Add documentation to functions
davidsvantesson Aug 24, 2019
0839b96
Grammar fix
davidsvantesson Aug 24, 2019
244cbd3
Fix function comment
davidsvantesson Aug 24, 2019
7c721f9
Merge branch 'master' into api-repo-topics
davidsvantesson Aug 24, 2019
f0f49bd
Can't use FindTopics when looking for a single repo topic, as it does…
davidsvantesson Aug 24, 2019
6ec5a0c
Add PUT ​/repos​/{owner}​/{repo}​/topics and remove GET ​/repos​/{own…
davidsvantesson Aug 25, 2019
8daf641
Ignore if topic is sent twice in same request, refactoring.
davidsvantesson Aug 25, 2019
9fdab25
Fix topic dropdown with api changes.
davidsvantesson Aug 25, 2019
1cb206c
Style fix
davidsvantesson Aug 25, 2019
c13a297
Update API documentation
davidsvantesson Aug 25, 2019
4a53681
Better way to handle duplicate topics in slice
davidsvantesson Aug 26, 2019
3705219
Make response element TopicName an array of strings, instead of using…
davidsvantesson Aug 26, 2019
ac93677
Add test cases for API Repo Topics.
davidsvantesson Aug 26, 2019
8bc836c
Fix format of tests
davidsvantesson Aug 26, 2019
3af43e2
Fix comments
davidsvantesson Aug 26, 2019
0e671f0
Merge branch 'master' into api-repo-topics
davidsvantesson Aug 26, 2019
5cdbbbc
Fix unit tests after adding some more topics to the test fixture.
davidsvantesson Aug 26, 2019
db3f6f3
Update models/topic.go
davidsvantesson Aug 27, 2019
2ef6904
Engine as first parameter in function
davidsvantesson Aug 27, 2019
99eb479
Replace magic numbers with http status code constants.
davidsvantesson Aug 27, 2019
ed7604a
Fix variable scope
davidsvantesson Aug 27, 2019
1088eac
Test one read with login and one with token
davidsvantesson Aug 27, 2019
098af67
Add some more tests
davidsvantesson Aug 27, 2019
41b6276
Apply suggestions from code review
davidsvantesson Aug 27, 2019
b654ebf
Add test case to check access for user with write access
davidsvantesson Aug 28, 2019
48989c3
Fix access, repo admin required to change topics
davidsvantesson Aug 28, 2019
db48d14
Correct first test to be without token
davidsvantesson Aug 28, 2019
fa8aa5d
Any repo reader should be able to access topics.
davidsvantesson Aug 29, 2019
1a6a8fc
Merge branch 'master' into api-repo-topics
davidsvantesson Aug 29, 2019
9d9f8dc
No need for string pointer
davidsvantesson Aug 29, 2019
6d9152f
Merge branch 'master' into api-repo-topics
davidsvantesson Aug 29, 2019
189d900
Merge branch 'master' into api-repo-topics
lafriks Aug 30, 2019
0b8409a
Merge branch 'master' into api-repo-topics
sapk Sep 3, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 123 additions & 30 deletions models/topic.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,39 @@ func (err ErrTopicNotExist) Error() string {
return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
}

// ValidateTopic checks topics by length and match pattern rules
// ValidateTopic checks a topic by length and match pattern rules
func ValidateTopic(topic string) bool {
return len(topic) <= 35 && topicPattern.MatchString(topic)
}

// SanitizeAndValidateTopics sanitizes and checks an array or topics
func SanitizeAndValidateTopics(topics []string) (validTopics []string, invalidTopics []string) {
validTopics = make([]string, 0)
invalidTopics = make([]string, 0)

LOOP_TOPICS:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To not have use go-to labels first create map with cleaned up topics and then loop thorough keys to validate them

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment still stands ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt it was bad code but hit my limits of go knowledge 😄. Did you mean something like this?

for _, topic := range topics {
topic = strings.TrimSpace(strings.ToLower(topic))
// ignore empty string
if len(topic) == 0 {
continue LOOP_TOPICS
}
// ignore same topic twice
for _, vTopic := range validTopics {
if topic == vTopic {
continue LOOP_TOPICS
}
}
if ValidateTopic(topic) {
validTopics = append(validTopics, topic)
} else {
invalidTopics = append(invalidTopics, topic)
}
}

return validTopics, invalidTopics
}

// GetTopicByName retrieves topic by name
func GetTopicByName(name string) (*Topic, error) {
var topic Topic
Expand All @@ -70,6 +98,52 @@ func GetTopicByName(name string) (*Topic, error) {
return &topic, nil
}

// addTopicByNameToRepo adds a topic name to a repo and increments the topic count.
// Returns topic after the addition
func addTopicByNameToRepo(repoID int64, topicName string, e Engine) (*Topic, error) {
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
var topic Topic
if has, err := e.Where("name = ?", topicName).Get(&topic); err != nil {
return nil, err
} else if !has {
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
topic.Name = topicName
topic.RepoCount = 1
if _, err := e.Insert(&topic); err != nil {
return nil, err
}
} else {
topic.RepoCount++
if _, err := e.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil {
return nil, err
}
}

if _, err := e.Insert(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
return nil, err
}

return &topic, nil
}

// removeTopicFromRepo remove a topic from a repo and decrements the topic repo count
func removeTopicFromRepo(repoID int64, topic *Topic, e Engine) error {
topic.RepoCount--
if _, err := e.ID(topic.ID).Cols("repo_count").Update(topic); err != nil {
return err
}

if _, err := e.Delete(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
return err
}

return nil
}

// FindTopicOptions represents the options when fdin topics
type FindTopicOptions struct {
RepoID int64
Expand Down Expand Up @@ -103,6 +177,50 @@ func FindTopics(opts *FindTopicOptions) (topics []*Topic, err error) {
return topics, sess.Desc("topic.repo_count").Find(&topics)
}

// GetRepoTopic retrives topic from name for a repo if it exist
func GetRepoTopicByName(repoID int64, topicName string) (*Topic, error) {
var cond = builder.NewCond()
var topic Topic
cond = cond.And(builder.Eq{"repo_topic.repo_id": repoID}).And(builder.Eq{"topic.name": topicName})
sess := x.Table("topic").Where(cond)
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
has, err := sess.Get(&topic)
if has {
return &topic, err
}
return nil, err
}

// AddTopic adds a topic name to a repository (if it does not already have it)
func AddTopic(repoID int64, topicName string) (*Topic, error) {
topic, err := GetRepoTopicByName(repoID, topicName)
if err != nil {
return nil, err
}
if topic != nil {
// Repo already have topic
return topic, nil
}

return addTopicByNameToRepo(repoID, topicName, x)
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
}

// DeleteTopic removes a topic name from a repository (if it has it)
func DeleteTopic(repoID int64, topicName string) (*Topic, error) {
topic, err := GetRepoTopicByName(repoID, topicName)
if err != nil {
return nil, err
}
if topic == nil {
// Repo doesn't have topic, can't be removed
return nil, nil
}

err = removeTopicFromRepo(repoID, topic, x)

return topic, err
}

// SaveTopics save topics to a repository
func SaveTopics(repoID int64, topicNames ...string) error {
topics, err := FindTopics(&FindTopicOptions{
Expand Down Expand Up @@ -152,40 +270,15 @@ func SaveTopics(repoID int64, topicNames ...string) error {
}

for _, topicName := range addedTopicNames {
var topic Topic
if has, err := sess.Where("name = ?", topicName).Get(&topic); err != nil {
return err
} else if !has {
topic.Name = topicName
topic.RepoCount = 1
if _, err := sess.Insert(&topic); err != nil {
return err
}
} else {
topic.RepoCount++
if _, err := sess.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil {
return err
}
}

if _, err := sess.Insert(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
_, err := addTopicByNameToRepo(repoID, topicName, sess)
davidsvantesson marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
}

for _, topic := range removeTopics {
topic.RepoCount--
if _, err := sess.ID(topic.ID).Cols("repo_count").Update(topic); err != nil {
return err
}

if _, err := sess.Delete(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
err := removeTopicFromRepo(repoID, topic, sess)
if err != nil {
return err
}
}
Expand Down
24 changes: 24 additions & 0 deletions modules/structs/repo_topic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2019 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 structs

import (
"time"
)

// TopicResponse for returning topics
type TopicResponse struct {
ID int64 `json:"id"`
Name string `json:"topic_name"`
RepoCount int `json:"repo_count"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}

// RepoTopicOptions a collection of repo topic names
type RepoTopicOptions struct {
// list of topic names
Topics []string `json:"topics"`
}
6 changes: 3 additions & 3 deletions public/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2936,14 +2936,14 @@ function initTopicbar() {
let found = false;
for (let i=0;i < res.topics.length;i++) {
// skip currently added tags
if (current_topics.indexOf(res.topics[i].Name) != -1){
if (current_topics.indexOf(res.topics[i].topic_name) != -1){
continue;
}

if (res.topics[i].Name.toLowerCase() === query.toLowerCase()){
if (res.topics[i].topic_name.toLowerCase() === query.toLowerCase()){
found_query = true;
}
formattedResponse.results.push({"description": res.topics[i].Name, "data-value": res.topics[i].Name});
formattedResponse.results.push({"description": res.topics[i].topic_name, "data-value": res.topics[i].topic_name});
found = true;
}
formattedResponse.success = found;
Expand Down
8 changes: 8 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,14 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile)
}, reqRepoWriter(models.UnitTypeCode), reqToken())
}, reqRepoReader(models.UnitTypeCode))
m.Group("/topics", func() {
m.Combo("").Get(repo.ListTopics).
Put(reqToken(), reqRepoWriter(models.UnitTypeCode), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
m.Group("/:topic", func() {
m.Combo("").Put(reqToken(), reqRepoWriter(models.UnitTypeCode), repo.AddTopic).
lafriks marked this conversation as resolved.
Show resolved Hide resolved
Delete(reqToken(), reqRepoWriter(models.UnitTypeCode), repo.DeleteTopic)
})
}, reqRepoReader(models.UnitTypeCode))
lafriks marked this conversation as resolved.
Show resolved Hide resolved
}, repoAssignment())
})

Expand Down
11 changes: 11 additions & 0 deletions routers/api/v1/convert/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,14 @@ func ToCommitMeta(repo *models.Repository, tag *git.Tag) *api.CommitMeta {
URL: util.URLJoin(repo.APIURL(), "git/commits", tag.ID.String()),
}
}

// ToTopicResponse convert from models.Topic to api.TopicResponse
func ToTopicResponse(topic *models.Topic) *api.TopicResponse {
return &api.TopicResponse{
ID: topic.ID,
Name: topic.Name,
RepoCount: topic.RepoCount,
Created: topic.CreatedUnix.AsTime(),
Updated: topic.UpdatedUnix.AsTime(),
}
}
42 changes: 0 additions & 42 deletions routers/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,45 +866,3 @@ func MirrorSync(ctx *context.APIContext) {
go models.MirrorQueue.Add(repo.ID)
ctx.Status(200)
}

// TopicSearch search for creating topic
func TopicSearch(ctx *context.Context) {
// swagger:operation GET /topics/search repository topicSearch
// ---
// summary: search topics via keyword
// produces:
// - application/json
// parameters:
// - name: q
// in: query
// description: keywords to search
// required: true
// type: string
// responses:
// "200":
// "$ref": "#/responses/Repository"
if ctx.User == nil {
ctx.JSON(403, map[string]interface{}{
"message": "Only owners could change the topics.",
})
return
}

kw := ctx.Query("q")

topics, err := models.FindTopics(&models.FindTopicOptions{
Keyword: kw,
Limit: 10,
})
if err != nil {
log.Error("SearchTopics failed: %v", err)
ctx.JSON(500, map[string]interface{}{
"message": "Search topics failed.",
})
return
}

ctx.JSON(200, map[string]interface{}{
"topics": topics,
})
}
Loading