forked from forgejo/forgejo
Move reaction to models/issues/ (#19264)
* Move reaction to models/issues/ * Fix test * move the function * improve code * Update models/issues/reaction.go Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
43332a483f
commit
d4f84f1c93
18 changed files with 280 additions and 274 deletions
377
models/issues/reaction.go
Normal file
377
models/issues/reaction.go
Normal file
|
@ -0,0 +1,377 @@
|
|||
// Copyright 2017 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 issues
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ErrForbiddenIssueReaction is used when a forbidden reaction was try to created
|
||||
type ErrForbiddenIssueReaction struct {
|
||||
Reaction string
|
||||
}
|
||||
|
||||
// IsErrForbiddenIssueReaction checks if an error is a ErrForbiddenIssueReaction.
|
||||
func IsErrForbiddenIssueReaction(err error) bool {
|
||||
_, ok := err.(ErrForbiddenIssueReaction)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrForbiddenIssueReaction) Error() string {
|
||||
return fmt.Sprintf("'%s' is not an allowed reaction", err.Reaction)
|
||||
}
|
||||
|
||||
// ErrReactionAlreadyExist is used when a existing reaction was try to created
|
||||
type ErrReactionAlreadyExist struct {
|
||||
Reaction string
|
||||
}
|
||||
|
||||
// IsErrReactionAlreadyExist checks if an error is a ErrReactionAlreadyExist.
|
||||
func IsErrReactionAlreadyExist(err error) bool {
|
||||
_, ok := err.(ErrReactionAlreadyExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrReactionAlreadyExist) Error() string {
|
||||
return fmt.Sprintf("reaction '%s' already exists", err.Reaction)
|
||||
}
|
||||
|
||||
// Reaction represents a reactions on issues and comments.
|
||||
type Reaction struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
CommentID int64 `xorm:"INDEX UNIQUE(s)"`
|
||||
UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
OriginalAuthorID int64 `xorm:"INDEX UNIQUE(s) NOT NULL DEFAULT(0)"`
|
||||
OriginalAuthor string `xorm:"INDEX UNIQUE(s)"`
|
||||
User *user_model.User `xorm:"-"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
// LoadUser load user of reaction
|
||||
func (r *Reaction) LoadUser() (*user_model.User, error) {
|
||||
if r.User != nil {
|
||||
return r.User, nil
|
||||
}
|
||||
user, err := user_model.GetUserByIDCtx(db.DefaultContext, r.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.User = user
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// RemapExternalUser ExternalUserRemappable interface
|
||||
func (r *Reaction) RemapExternalUser(externalName string, externalID, userID int64) error {
|
||||
r.OriginalAuthor = externalName
|
||||
r.OriginalAuthorID = externalID
|
||||
r.UserID = userID
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserID ExternalUserRemappable interface
|
||||
func (r *Reaction) GetUserID() int64 { return r.UserID }
|
||||
|
||||
// GetExternalName ExternalUserRemappable interface
|
||||
func (r *Reaction) GetExternalName() string { return r.OriginalAuthor }
|
||||
|
||||
// GetExternalID ExternalUserRemappable interface
|
||||
func (r *Reaction) GetExternalID() int64 { return r.OriginalAuthorID }
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Reaction))
|
||||
}
|
||||
|
||||
// FindReactionsOptions describes the conditions to Find reactions
|
||||
type FindReactionsOptions struct {
|
||||
db.ListOptions
|
||||
IssueID int64
|
||||
CommentID int64
|
||||
UserID int64
|
||||
Reaction string
|
||||
}
|
||||
|
||||
func (opts *FindReactionsOptions) toConds() builder.Cond {
|
||||
// If Issue ID is set add to Query
|
||||
cond := builder.NewCond()
|
||||
if opts.IssueID > 0 {
|
||||
cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID})
|
||||
}
|
||||
// If CommentID is > 0 add to Query
|
||||
// If it is 0 Query ignore CommentID to select
|
||||
// If it is -1 it explicit search of Issue Reactions where CommentID = 0
|
||||
if opts.CommentID > 0 {
|
||||
cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID})
|
||||
} else if opts.CommentID == -1 {
|
||||
cond = cond.And(builder.Eq{"reaction.comment_id": 0})
|
||||
}
|
||||
if opts.UserID > 0 {
|
||||
cond = cond.And(builder.Eq{
|
||||
"reaction.user_id": opts.UserID,
|
||||
"reaction.original_author_id": 0,
|
||||
})
|
||||
}
|
||||
if opts.Reaction != "" {
|
||||
cond = cond.And(builder.Eq{"reaction.type": opts.Reaction})
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
// FindCommentReactions returns a ReactionList of all reactions from an comment
|
||||
func FindCommentReactions(issueID, commentID int64) (ReactionList, int64, error) {
|
||||
return FindReactions(db.DefaultContext, FindReactionsOptions{
|
||||
IssueID: issueID,
|
||||
CommentID: commentID,
|
||||
})
|
||||
}
|
||||
|
||||
// FindIssueReactions returns a ReactionList of all reactions from an issue
|
||||
func FindIssueReactions(issueID int64, listOptions db.ListOptions) (ReactionList, int64, error) {
|
||||
return FindReactions(db.DefaultContext, FindReactionsOptions{
|
||||
ListOptions: listOptions,
|
||||
IssueID: issueID,
|
||||
CommentID: -1,
|
||||
})
|
||||
}
|
||||
|
||||
// FindReactions returns a ReactionList of all reactions from an issue or a comment
|
||||
func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList, int64, error) {
|
||||
sess := db.GetEngine(ctx).
|
||||
Where(opts.toConds()).
|
||||
In("reaction.`type`", setting.UI.Reactions).
|
||||
Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id")
|
||||
if opts.Page != 0 {
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
|
||||
reactions := make([]*Reaction, 0, opts.PageSize)
|
||||
count, err := sess.FindAndCount(&reactions)
|
||||
return reactions, count, err
|
||||
}
|
||||
|
||||
reactions := make([]*Reaction, 0, 10)
|
||||
count, err := sess.FindAndCount(&reactions)
|
||||
return reactions, count, err
|
||||
}
|
||||
|
||||
func createReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) {
|
||||
reaction := &Reaction{
|
||||
Type: opts.Type,
|
||||
UserID: opts.DoerID,
|
||||
IssueID: opts.IssueID,
|
||||
CommentID: opts.CommentID,
|
||||
}
|
||||
findOpts := FindReactionsOptions{
|
||||
IssueID: opts.IssueID,
|
||||
CommentID: opts.CommentID,
|
||||
Reaction: opts.Type,
|
||||
UserID: opts.DoerID,
|
||||
}
|
||||
|
||||
existingR, _, err := FindReactions(ctx, findOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(existingR) > 0 {
|
||||
return existingR[0], ErrReactionAlreadyExist{Reaction: opts.Type}
|
||||
}
|
||||
|
||||
if err := db.Insert(ctx, reaction); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reaction, nil
|
||||
}
|
||||
|
||||
// ReactionOptions defines options for creating or deleting reactions
|
||||
type ReactionOptions struct {
|
||||
Type string
|
||||
DoerID int64
|
||||
IssueID int64
|
||||
CommentID int64
|
||||
}
|
||||
|
||||
// CreateReaction creates reaction for issue or comment.
|
||||
func CreateReaction(opts *ReactionOptions) (*Reaction, error) {
|
||||
if !setting.UI.ReactionsMap[opts.Type] {
|
||||
return nil, ErrForbiddenIssueReaction{opts.Type}
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
reaction, err := createReaction(ctx, opts)
|
||||
if err != nil {
|
||||
return reaction, err
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reaction, nil
|
||||
}
|
||||
|
||||
// CreateIssueReaction creates a reaction on issue.
|
||||
func CreateIssueReaction(doerID, issueID int64, content string) (*Reaction, error) {
|
||||
return CreateReaction(&ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doerID,
|
||||
IssueID: issueID,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateCommentReaction creates a reaction on comment.
|
||||
func CreateCommentReaction(doerID, issueID, commentID int64, content string) (*Reaction, error) {
|
||||
return CreateReaction(&ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doerID,
|
||||
IssueID: issueID,
|
||||
CommentID: commentID,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteReaction deletes reaction for issue or comment.
|
||||
func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
|
||||
reaction := &Reaction{
|
||||
Type: opts.Type,
|
||||
UserID: opts.DoerID,
|
||||
IssueID: opts.IssueID,
|
||||
CommentID: opts.CommentID,
|
||||
}
|
||||
|
||||
_, err := db.GetEngine(ctx).Where("original_author_id = 0").Delete(reaction)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteIssueReaction deletes a reaction on issue.
|
||||
func DeleteIssueReaction(doerID, issueID int64, content string) error {
|
||||
return DeleteReaction(db.DefaultContext, &ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doerID,
|
||||
IssueID: issueID,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCommentReaction deletes a reaction on comment.
|
||||
func DeleteCommentReaction(doerID, issueID, commentID int64, content string) error {
|
||||
return DeleteReaction(db.DefaultContext, &ReactionOptions{
|
||||
Type: content,
|
||||
DoerID: doerID,
|
||||
IssueID: issueID,
|
||||
CommentID: commentID,
|
||||
})
|
||||
}
|
||||
|
||||
// ReactionList represents list of reactions
|
||||
type ReactionList []*Reaction
|
||||
|
||||
// HasUser check if user has reacted
|
||||
func (list ReactionList) HasUser(userID int64) bool {
|
||||
if userID == 0 {
|
||||
return false
|
||||
}
|
||||
for _, reaction := range list {
|
||||
if reaction.OriginalAuthor == "" && reaction.UserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GroupByType returns reactions grouped by type
|
||||
func (list ReactionList) GroupByType() map[string]ReactionList {
|
||||
reactions := make(map[string]ReactionList)
|
||||
for _, reaction := range list {
|
||||
reactions[reaction.Type] = append(reactions[reaction.Type], reaction)
|
||||
}
|
||||
return reactions
|
||||
}
|
||||
|
||||
func (list ReactionList) getUserIDs() []int64 {
|
||||
userIDs := make(map[int64]struct{}, len(list))
|
||||
for _, reaction := range list {
|
||||
if reaction.OriginalAuthor != "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := userIDs[reaction.UserID]; !ok {
|
||||
userIDs[reaction.UserID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(userIDs)
|
||||
}
|
||||
|
||||
func valuesUser(m map[int64]*user_model.User) []*user_model.User {
|
||||
values := make([]*user_model.User, 0, len(m))
|
||||
for _, v := range m {
|
||||
values = append(values, v)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// LoadUsers loads reactions' all users
|
||||
func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Repository) ([]*user_model.User, error) {
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userIDs := list.getUserIDs()
|
||||
userMaps := make(map[int64]*user_model.User, len(userIDs))
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", userIDs).
|
||||
Find(&userMaps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find user: %v", err)
|
||||
}
|
||||
|
||||
for _, reaction := range list {
|
||||
if reaction.OriginalAuthor != "" {
|
||||
reaction.User = user_model.NewReplaceUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name()))
|
||||
} else if user, ok := userMaps[reaction.UserID]; ok {
|
||||
reaction.User = user
|
||||
} else {
|
||||
reaction.User = user_model.NewGhostUser()
|
||||
}
|
||||
}
|
||||
return valuesUser(userMaps), nil
|
||||
}
|
||||
|
||||
// GetFirstUsers returns first reacted user display names separated by comma
|
||||
func (list ReactionList) GetFirstUsers() string {
|
||||
var buffer bytes.Buffer
|
||||
rem := setting.UI.ReactionMaxUserNum
|
||||
for _, reaction := range list {
|
||||
if buffer.Len() > 0 {
|
||||
buffer.WriteString(", ")
|
||||
}
|
||||
buffer.WriteString(reaction.User.DisplayName())
|
||||
if rem--; rem == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// GetMoreUserCount returns count of not shown users in reaction tooltip
|
||||
func (list ReactionList) GetMoreUserCount() int {
|
||||
if len(list) <= setting.UI.ReactionMaxUserNum {
|
||||
return 0
|
||||
}
|
||||
return len(list) - setting.UI.ReactionMaxUserNum
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue