]> source.dussan.org Git - gitea.git/commitdiff
Move reaction to models/issues/ (#19264)
authorLunny Xiao <xiaolunwen@gmail.com>
Thu, 31 Mar 2022 09:20:39 +0000 (17:20 +0800)
committerGitHub <noreply@github.com>
Thu, 31 Mar 2022 09:20:39 +0000 (17:20 +0800)
* 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>
20 files changed:
models/action_list.go
models/error.go
models/helper.go
models/issue.go
models/issue_comment.go
models/issue_comment_list.go
models/issue_list.go
models/issue_reaction.go [deleted file]
models/issue_reaction_test.go [deleted file]
models/issues/main_test.go
models/issues/reaction.go [new file with mode: 0644]
models/issues/reaction_test.go [new file with mode: 0644]
models/migrate_test.go
models/notification.go
models/repo_list.go
models/user.go
modules/container/map.go [new file with mode: 0644]
routers/api/v1/repo/issue_reaction.go
routers/web/repo/issue.go
services/migrations/gitea_uploader.go

index ce621753a46f70351ba1372cc82c7da8946fda68..c180a82552c519ac2809bbaac5731e35a0c06c35 100644 (file)
@@ -10,6 +10,7 @@ import (
        "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"
 )
 
 // ActionList defines a list of actions
@@ -22,7 +23,7 @@ func (actions ActionList) getUserIDs() []int64 {
                        userIDs[action.ActUserID] = struct{}{}
                }
        }
-       return keysInt64(userIDs)
+       return container.KeysInt64(userIDs)
 }
 
 func (actions ActionList) loadUsers(e db.Engine) (map[int64]*user_model.User, error) {
@@ -52,7 +53,7 @@ func (actions ActionList) getRepoIDs() []int64 {
                        repoIDs[action.RepoID] = struct{}{}
                }
        }
-       return keysInt64(repoIDs)
+       return container.KeysInt64(repoIDs)
 }
 
 func (actions ActionList) loadRepositories(e db.Engine) error {
index cbfb60790f33abbf9c65d0c0f82c00d46ae05778..fbd2f971857a46217fd6784b0a9d82f6db95ac54 100644 (file)
@@ -765,36 +765,6 @@ func (err ErrPullWasClosed) Error() string {
        return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index)
 }
 
-// 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)
-}
-
 // __________      .__  .__ __________                                     __
 // \______   \__ __|  | |  |\______   \ ____  ________ __   ____   _______/  |_
 //  |     ___/  |  \  | |  | |       _// __ \/ ____/  |  \_/ __ \ /  ___/\   __\
index 15df4245390108a3b0fefe9fa4e6474a43e8a275..c5f2d7a5b83fdb9b00591789aa3057c796afa3e3 100644 (file)
@@ -6,17 +6,8 @@ package models
 
 import (
        repo_model "code.gitea.io/gitea/models/repo"
-       user_model "code.gitea.io/gitea/models/user"
 )
 
-func keysInt64(m map[int64]struct{}) []int64 {
-       keys := make([]int64, 0, len(m))
-       for k := range m {
-               keys = append(keys, k)
-       }
-       return keys
-}
-
 func valuesRepository(m map[int64]*repo_model.Repository) []*repo_model.Repository {
        values := make([]*repo_model.Repository, 0, len(m))
        for _, v := range m {
@@ -24,11 +15,3 @@ func valuesRepository(m map[int64]*repo_model.Repository) []*repo_model.Reposito
        }
        return values
 }
-
-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
-}
index 99688f2ec9c8db9b132e97d56e1e6043d0d5fb69..cf5e4fd8b6fe995b898942678ec38923c40798e2 100644 (file)
@@ -72,7 +72,7 @@ type Issue struct {
 
        Attachments      []*repo_model.Attachment           `xorm:"-"`
        Comments         []*Comment                         `xorm:"-"`
-       Reactions        ReactionList                       `xorm:"-"`
+       Reactions        issues.ReactionList                `xorm:"-"`
        TotalTrackedTime int64                              `xorm:"-"`
        Assignees        []*user_model.User                 `xorm:"-"`
        ForeignReference *foreignreference.ForeignReference `xorm:"-"`
@@ -244,8 +244,7 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) {
        if issue.Reactions != nil {
                return nil
        }
-       e := db.GetEngine(ctx)
-       reactions, _, err := findReactions(e, FindReactionsOptions{
+       reactions, _, err := issues.FindReactions(ctx, issues.FindReactionsOptions{
                IssueID: issue.ID,
        })
        if err != nil {
@@ -255,7 +254,7 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) {
                return err
        }
        // Load reaction user data
-       if _, err := ReactionList(reactions).loadUsers(e, issue.Repo); err != nil {
+       if _, err := issues.ReactionList(reactions).LoadUsers(ctx, issue.Repo); err != nil {
                return err
        }
 
@@ -2111,7 +2110,7 @@ func deleteIssue(ctx context.Context, issue *Issue) error {
                &IssueAssignees{},
                &IssueUser{},
                &Notification{},
-               &Reaction{},
+               &issues.Reaction{},
                &IssueWatch{},
                &Stopwatch{},
                &TrackedTime{},
@@ -2429,7 +2428,7 @@ func deleteIssuesByRepoID(sess db.Engine, repoID int64) (attachmentPaths []strin
        }
 
        if _, err = sess.In("issue_id", deleteCond).
-               Delete(&Reaction{}); err != nil {
+               Delete(&issues.Reaction{}); err != nil {
                return
        }
 
index 7fb013ae924c35d669bda78d365622fc4bdf9ce3..332ae7bdc5e34136e5a2f48f68ab8709af8e82e1 100644 (file)
@@ -244,7 +244,7 @@ type Comment struct {
        CommitSHA string `xorm:"VARCHAR(40)"`
 
        Attachments []*repo_model.Attachment `xorm:"-"`
-       Reactions   ReactionList             `xorm:"-"`
+       Reactions   issues.ReactionList      `xorm:"-"`
 
        // For view issue page.
        ShowRole RoleDescriptor `xorm:"-"`
@@ -631,11 +631,11 @@ func (c *Comment) LoadTime() error {
        return err
 }
 
-func (c *Comment) loadReactions(e db.Engine, repo *repo_model.Repository) (err error) {
+func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository) (err error) {
        if c.Reactions != nil {
                return nil
        }
-       c.Reactions, _, err = findReactions(e, FindReactionsOptions{
+       c.Reactions, _, err = issues.FindReactions(ctx, issues.FindReactionsOptions{
                IssueID:   c.IssueID,
                CommentID: c.ID,
        })
@@ -643,7 +643,7 @@ func (c *Comment) loadReactions(e db.Engine, repo *repo_model.Repository) (err e
                return err
        }
        // Load reaction user data
-       if _, err := c.Reactions.loadUsers(e, repo); err != nil {
+       if _, err := c.Reactions.LoadUsers(ctx, repo); err != nil {
                return err
        }
        return nil
@@ -651,7 +651,7 @@ func (c *Comment) loadReactions(e db.Engine, repo *repo_model.Repository) (err e
 
 // LoadReactions loads comment reactions
 func (c *Comment) LoadReactions(repo *repo_model.Repository) error {
-       return c.loadReactions(db.GetEngine(db.DefaultContext), repo)
+       return c.loadReactions(db.DefaultContext, repo)
 }
 
 func (c *Comment) loadReview(e db.Engine) (err error) {
@@ -1146,14 +1146,15 @@ func DeleteComment(comment *Comment) error {
        }
        defer committer.Close()
 
-       if err := deleteComment(db.GetEngine(ctx), comment); err != nil {
+       if err := deleteComment(ctx, comment); err != nil {
                return err
        }
 
        return committer.Commit()
 }
 
-func deleteComment(e db.Engine, comment *Comment) error {
+func deleteComment(ctx context.Context, comment *Comment) error {
+       e := db.GetEngine(ctx)
        if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil {
                return err
        }
@@ -1177,7 +1178,7 @@ func deleteComment(e db.Engine, comment *Comment) error {
                return err
        }
 
-       return deleteReaction(e, &ReactionOptions{Comment: comment})
+       return issues.DeleteReaction(ctx, &issues.ReactionOptions{CommentID: comment.ID})
 }
 
 // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
index 23a2756dcf2cbc8825f908c4b33026c543693239..2107f4790f8cd8e4fdca4802616d923043c09046 100644 (file)
@@ -10,6 +10,7 @@ import (
        "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"
 )
 
 // CommentList defines a list of comments
@@ -22,7 +23,7 @@ func (comments CommentList) getPosterIDs() []int64 {
                        posterIDs[comment.PosterID] = struct{}{}
                }
        }
-       return keysInt64(posterIDs)
+       return container.KeysInt64(posterIDs)
 }
 
 func (comments CommentList) loadPosters(e db.Engine) error {
@@ -75,7 +76,7 @@ func (comments CommentList) getLabelIDs() []int64 {
                        ids[comment.LabelID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 func (comments CommentList) loadLabels(e db.Engine) error {
@@ -125,7 +126,7 @@ func (comments CommentList) getMilestoneIDs() []int64 {
                        ids[comment.MilestoneID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 func (comments CommentList) loadMilestones(e db.Engine) error {
@@ -168,7 +169,7 @@ func (comments CommentList) getOldMilestoneIDs() []int64 {
                        ids[comment.OldMilestoneID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 func (comments CommentList) loadOldMilestones(e db.Engine) error {
@@ -211,7 +212,7 @@ func (comments CommentList) getAssigneeIDs() []int64 {
                        ids[comment.AssigneeID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 func (comments CommentList) loadAssignees(e db.Engine) error {
@@ -267,7 +268,7 @@ func (comments CommentList) getIssueIDs() []int64 {
                        ids[comment.IssueID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 // Issues returns all the issues of comments
@@ -342,7 +343,7 @@ func (comments CommentList) getDependentIssueIDs() []int64 {
                        ids[comment.DependentIssueID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 func (comments CommentList) loadDependentIssues(ctx context.Context) error {
@@ -444,7 +445,7 @@ func (comments CommentList) getReviewIDs() []int64 {
                        ids[comment.ReviewID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 func (comments CommentList) loadReviews(e db.Engine) error {
index b516e7336e026f24f8948223cf66af9d28a218e1..8d7a3121b091b902a1dc5ec81481ec203ef98ec3 100644 (file)
@@ -10,6 +10,7 @@ import (
        "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"
 
        "xorm.io/builder"
 )
@@ -32,7 +33,7 @@ func (issues IssueList) getRepoIDs() []int64 {
                        repoIDs[issue.RepoID] = struct{}{}
                }
        }
-       return keysInt64(repoIDs)
+       return container.KeysInt64(repoIDs)
 }
 
 func (issues IssueList) loadRepositories(e db.Engine) ([]*repo_model.Repository, error) {
@@ -83,7 +84,7 @@ func (issues IssueList) getPosterIDs() []int64 {
                        posterIDs[issue.PosterID] = struct{}{}
                }
        }
-       return keysInt64(posterIDs)
+       return container.KeysInt64(posterIDs)
 }
 
 func (issues IssueList) loadPosters(e db.Engine) error {
@@ -189,7 +190,7 @@ func (issues IssueList) getMilestoneIDs() []int64 {
                        ids[issue.MilestoneID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 func (issues IssueList) loadMilestones(e db.Engine) error {
diff --git a/models/issue_reaction.go b/models/issue_reaction.go
deleted file mode 100644 (file)
index 45b1d64..0000000
+++ /dev/null
@@ -1,362 +0,0 @@
-// 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 models
-
-import (
-       "bytes"
-       "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/setting"
-       "code.gitea.io/gitea/modules/timeutil"
-
-       "xorm.io/builder"
-)
-
-// 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"`
-}
-
-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(comment *Comment) (ReactionList, int64, error) {
-       return findReactions(db.GetEngine(db.DefaultContext), FindReactionsOptions{
-               IssueID:   comment.IssueID,
-               CommentID: comment.ID,
-       })
-}
-
-// FindIssueReactions returns a ReactionList of all reactions from an issue
-func FindIssueReactions(issue *Issue, listOptions db.ListOptions) (ReactionList, int64, error) {
-       return findReactions(db.GetEngine(db.DefaultContext), FindReactionsOptions{
-               ListOptions: listOptions,
-               IssueID:     issue.ID,
-               CommentID:   -1,
-       })
-}
-
-func findReactions(e db.Engine, opts FindReactionsOptions) ([]*Reaction, int64, error) {
-       sess := e.
-               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(e db.Engine, opts *ReactionOptions) (*Reaction, error) {
-       reaction := &Reaction{
-               Type:    opts.Type,
-               UserID:  opts.Doer.ID,
-               IssueID: opts.Issue.ID,
-       }
-       findOpts := FindReactionsOptions{
-               IssueID:   opts.Issue.ID,
-               CommentID: -1, // reaction to issue only
-               Reaction:  opts.Type,
-               UserID:    opts.Doer.ID,
-       }
-       if opts.Comment != nil {
-               reaction.CommentID = opts.Comment.ID
-               findOpts.CommentID = opts.Comment.ID
-       }
-
-       existingR, _, err := findReactions(e, findOpts)
-       if err != nil {
-               return nil, err
-       }
-       if len(existingR) > 0 {
-               return existingR[0], ErrReactionAlreadyExist{Reaction: opts.Type}
-       }
-
-       if _, err := e.Insert(reaction); err != nil {
-               return nil, err
-       }
-
-       return reaction, nil
-}
-
-// ReactionOptions defines options for creating or deleting reactions
-type ReactionOptions struct {
-       Type    string
-       Doer    *user_model.User
-       Issue   *Issue
-       Comment *Comment
-}
-
-// 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(db.GetEngine(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(doer *user_model.User, issue *Issue, content string) (*Reaction, error) {
-       return CreateReaction(&ReactionOptions{
-               Type:  content,
-               Doer:  doer,
-               Issue: issue,
-       })
-}
-
-// CreateCommentReaction creates a reaction on comment.
-func CreateCommentReaction(doer *user_model.User, issue *Issue, comment *Comment, content string) (*Reaction, error) {
-       return CreateReaction(&ReactionOptions{
-               Type:    content,
-               Doer:    doer,
-               Issue:   issue,
-               Comment: comment,
-       })
-}
-
-func deleteReaction(e db.Engine, opts *ReactionOptions) error {
-       reaction := &Reaction{
-               Type: opts.Type,
-       }
-       if opts.Doer != nil {
-               reaction.UserID = opts.Doer.ID
-       }
-       if opts.Issue != nil {
-               reaction.IssueID = opts.Issue.ID
-       }
-       if opts.Comment != nil {
-               reaction.CommentID = opts.Comment.ID
-       }
-       _, err := e.Where("original_author_id = 0").Delete(reaction)
-       return err
-}
-
-// DeleteReaction deletes reaction for issue or comment.
-func DeleteReaction(opts *ReactionOptions) error {
-       ctx, committer, err := db.TxContext()
-       if err != nil {
-               return err
-       }
-       defer committer.Close()
-
-       if err := deleteReaction(db.GetEngine(ctx), opts); err != nil {
-               return err
-       }
-
-       return committer.Commit()
-}
-
-// DeleteIssueReaction deletes a reaction on issue.
-func DeleteIssueReaction(doer *user_model.User, issue *Issue, content string) error {
-       return DeleteReaction(&ReactionOptions{
-               Type:  content,
-               Doer:  doer,
-               Issue: issue,
-       })
-}
-
-// DeleteCommentReaction deletes a reaction on comment.
-func DeleteCommentReaction(doer *user_model.User, issue *Issue, comment *Comment, content string) error {
-       return DeleteReaction(&ReactionOptions{
-               Type:    content,
-               Doer:    doer,
-               Issue:   issue,
-               Comment: comment,
-       })
-}
-
-// 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
-}
-
-// 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 keysInt64(userIDs)
-}
-
-func (list ReactionList) loadUsers(e db.Engine, 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 := e.
-               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
-}
-
-// LoadUsers loads reactions' all users
-func (list ReactionList) LoadUsers(repo *repo_model.Repository) ([]*user_model.User, error) {
-       return list.loadUsers(db.GetEngine(db.DefaultContext), repo)
-}
-
-// 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
-}
-
-// 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 }
diff --git a/models/issue_reaction_test.go b/models/issue_reaction_test.go
deleted file mode 100644 (file)
index 886d19e..0000000
+++ /dev/null
@@ -1,170 +0,0 @@
-// 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 models
-
-import (
-       "testing"
-
-       "code.gitea.io/gitea/models/db"
-       repo_model "code.gitea.io/gitea/models/repo"
-       "code.gitea.io/gitea/models/unittest"
-       user_model "code.gitea.io/gitea/models/user"
-       "code.gitea.io/gitea/modules/setting"
-
-       "github.com/stretchr/testify/assert"
-)
-
-func addReaction(t *testing.T, doer *user_model.User, issue *Issue, comment *Comment, content string) {
-       var reaction *Reaction
-       var err error
-       if comment == nil {
-               reaction, err = CreateIssueReaction(doer, issue, content)
-       } else {
-               reaction, err = CreateCommentReaction(doer, issue, comment, content)
-       }
-       assert.NoError(t, err)
-       assert.NotNil(t, reaction)
-}
-
-func TestIssueAddReaction(t *testing.T) {
-       assert.NoError(t, unittest.PrepareTestDatabase())
-
-       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
-
-       issue1 := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-
-       addReaction(t, user1, issue1, nil, "heart")
-
-       unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1.ID})
-}
-
-func TestIssueAddDuplicateReaction(t *testing.T) {
-       assert.NoError(t, unittest.PrepareTestDatabase())
-
-       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
-
-       issue1 := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-
-       addReaction(t, user1, issue1, nil, "heart")
-
-       reaction, err := CreateReaction(&ReactionOptions{
-               Doer:  user1,
-               Issue: issue1,
-               Type:  "heart",
-       })
-       assert.Error(t, err)
-       assert.Equal(t, ErrReactionAlreadyExist{Reaction: "heart"}, err)
-
-       existingR := unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1.ID}).(*Reaction)
-       assert.Equal(t, existingR.ID, reaction.ID)
-}
-
-func TestIssueDeleteReaction(t *testing.T) {
-       assert.NoError(t, unittest.PrepareTestDatabase())
-
-       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
-
-       issue1 := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-
-       addReaction(t, user1, issue1, nil, "heart")
-
-       err := DeleteIssueReaction(user1, issue1, "heart")
-       assert.NoError(t, err)
-
-       unittest.AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1.ID})
-}
-
-func TestIssueReactionCount(t *testing.T) {
-       assert.NoError(t, unittest.PrepareTestDatabase())
-
-       setting.UI.ReactionMaxUserNum = 2
-
-       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
-       user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
-       user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
-       user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User)
-       ghost := user_model.NewGhostUser()
-
-       issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
-
-       addReaction(t, user1, issue, nil, "heart")
-       addReaction(t, user2, issue, nil, "heart")
-       addReaction(t, user3, issue, nil, "heart")
-       addReaction(t, user3, issue, nil, "+1")
-       addReaction(t, user4, issue, nil, "+1")
-       addReaction(t, user4, issue, nil, "heart")
-       addReaction(t, ghost, issue, nil, "-1")
-
-       err := issue.loadReactions(db.DefaultContext)
-       assert.NoError(t, err)
-
-       assert.Len(t, issue.Reactions, 7)
-
-       reactions := issue.Reactions.GroupByType()
-       assert.Len(t, reactions["heart"], 4)
-       assert.Equal(t, 2, reactions["heart"].GetMoreUserCount())
-       assert.Equal(t, user1.DisplayName()+", "+user2.DisplayName(), reactions["heart"].GetFirstUsers())
-       assert.True(t, reactions["heart"].HasUser(1))
-       assert.False(t, reactions["heart"].HasUser(5))
-       assert.False(t, reactions["heart"].HasUser(0))
-       assert.Len(t, reactions["+1"], 2)
-       assert.Equal(t, 0, reactions["+1"].GetMoreUserCount())
-       assert.Len(t, reactions["-1"], 1)
-}
-
-func TestIssueCommentAddReaction(t *testing.T) {
-       assert.NoError(t, unittest.PrepareTestDatabase())
-
-       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
-
-       issue1 := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-
-       comment1 := unittest.AssertExistsAndLoadBean(t, &Comment{ID: 1}).(*Comment)
-
-       addReaction(t, user1, issue1, comment1, "heart")
-
-       unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1.ID, CommentID: comment1.ID})
-}
-
-func TestIssueCommentDeleteReaction(t *testing.T) {
-       assert.NoError(t, unittest.PrepareTestDatabase())
-
-       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
-       user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
-       user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
-       user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User)
-
-       issue1 := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-       repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue1.RepoID}).(*repo_model.Repository)
-
-       comment1 := unittest.AssertExistsAndLoadBean(t, &Comment{ID: 1}).(*Comment)
-
-       addReaction(t, user1, issue1, comment1, "heart")
-       addReaction(t, user2, issue1, comment1, "heart")
-       addReaction(t, user3, issue1, comment1, "heart")
-       addReaction(t, user4, issue1, comment1, "+1")
-
-       err := comment1.LoadReactions(repo1)
-       assert.NoError(t, err)
-       assert.Len(t, comment1.Reactions, 4)
-
-       reactions := comment1.Reactions.GroupByType()
-       assert.Len(t, reactions["heart"], 3)
-       assert.Len(t, reactions["+1"], 1)
-}
-
-func TestIssueCommentReactionCount(t *testing.T) {
-       assert.NoError(t, unittest.PrepareTestDatabase())
-
-       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
-
-       issue1 := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
-
-       comment1 := unittest.AssertExistsAndLoadBean(t, &Comment{ID: 1}).(*Comment)
-
-       addReaction(t, user1, issue1, comment1, "heart")
-       assert.NoError(t, DeleteCommentReaction(user1, issue1, comment1, "heart"))
-
-       unittest.AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1.ID, CommentID: comment1.ID})
-}
index af71f038d63e6428784b93ec1e128cb8e063e7ce..1c786d005f1e31cf6fd73bd074413bcc140c9ada 100644 (file)
@@ -9,8 +9,18 @@ import (
        "testing"
 
        "code.gitea.io/gitea/models/unittest"
+       "code.gitea.io/gitea/modules/setting"
 )
 
+func init() {
+       setting.SetCustomPathAndConf("", "", "")
+       setting.LoadForTest()
+}
+
 func TestMain(m *testing.M) {
-       unittest.MainTest(m, filepath.Join("..", ".."), "")
+       unittest.MainTest(m, filepath.Join("..", ".."),
+               "reaction.yml",
+               "user.yml",
+               "repository.yml",
+       )
 }
diff --git a/models/issues/reaction.go b/models/issues/reaction.go
new file mode 100644 (file)
index 0000000..87d6ff4
--- /dev/null
@@ -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
+}
diff --git a/models/issues/reaction_test.go b/models/issues/reaction_test.go
new file mode 100644 (file)
index 0000000..b1216a3
--- /dev/null
@@ -0,0 +1,174 @@
+// 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 (
+       "testing"
+
+       "code.gitea.io/gitea/models/db"
+       repo_model "code.gitea.io/gitea/models/repo"
+       "code.gitea.io/gitea/models/unittest"
+       user_model "code.gitea.io/gitea/models/user"
+       "code.gitea.io/gitea/modules/setting"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
+       var reaction *Reaction
+       var err error
+       if commentID == 0 {
+               reaction, err = CreateIssueReaction(doerID, issueID, content)
+       } else {
+               reaction, err = CreateCommentReaction(doerID, issueID, commentID, content)
+       }
+       assert.NoError(t, err)
+       assert.NotNil(t, reaction)
+}
+
+func TestIssueAddReaction(t *testing.T) {
+       assert.NoError(t, unittest.PrepareTestDatabase())
+
+       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+
+       var issue1ID int64 = 1
+
+       addReaction(t, user1.ID, issue1ID, 0, "heart")
+
+       unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+}
+
+func TestIssueAddDuplicateReaction(t *testing.T) {
+       assert.NoError(t, unittest.PrepareTestDatabase())
+
+       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+
+       var issue1ID int64 = 1
+
+       addReaction(t, user1.ID, issue1ID, 0, "heart")
+
+       reaction, err := CreateReaction(&ReactionOptions{
+               DoerID:  user1.ID,
+               IssueID: issue1ID,
+               Type:    "heart",
+       })
+       assert.Error(t, err)
+       assert.Equal(t, ErrReactionAlreadyExist{Reaction: "heart"}, err)
+
+       existingR := unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}).(*Reaction)
+       assert.Equal(t, existingR.ID, reaction.ID)
+}
+
+func TestIssueDeleteReaction(t *testing.T) {
+       assert.NoError(t, unittest.PrepareTestDatabase())
+
+       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+
+       var issue1ID int64 = 1
+
+       addReaction(t, user1.ID, issue1ID, 0, "heart")
+
+       err := DeleteIssueReaction(user1.ID, issue1ID, "heart")
+       assert.NoError(t, err)
+
+       unittest.AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+}
+
+func TestIssueReactionCount(t *testing.T) {
+       assert.NoError(t, unittest.PrepareTestDatabase())
+
+       setting.UI.ReactionMaxUserNum = 2
+
+       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+       user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+       user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
+       user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User)
+       ghost := user_model.NewGhostUser()
+
+       var issueID int64 = 2
+       repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+
+       addReaction(t, user1.ID, issueID, 0, "heart")
+       addReaction(t, user2.ID, issueID, 0, "heart")
+       addReaction(t, user3.ID, issueID, 0, "heart")
+       addReaction(t, user3.ID, issueID, 0, "+1")
+       addReaction(t, user4.ID, issueID, 0, "+1")
+       addReaction(t, user4.ID, issueID, 0, "heart")
+       addReaction(t, ghost.ID, issueID, 0, "-1")
+
+       reactionsList, _, err := FindReactions(db.DefaultContext, FindReactionsOptions{
+               IssueID: issueID,
+       })
+       assert.NoError(t, err)
+       assert.Len(t, reactionsList, 7)
+       _, err = reactionsList.LoadUsers(db.DefaultContext, repo)
+       assert.NoError(t, err)
+
+       reactions := reactionsList.GroupByType()
+       assert.Len(t, reactions["heart"], 4)
+       assert.Equal(t, 2, reactions["heart"].GetMoreUserCount())
+       assert.Equal(t, user1.DisplayName()+", "+user2.DisplayName(), reactions["heart"].GetFirstUsers())
+       assert.True(t, reactions["heart"].HasUser(1))
+       assert.False(t, reactions["heart"].HasUser(5))
+       assert.False(t, reactions["heart"].HasUser(0))
+       assert.Len(t, reactions["+1"], 2)
+       assert.Equal(t, 0, reactions["+1"].GetMoreUserCount())
+       assert.Len(t, reactions["-1"], 1)
+}
+
+func TestIssueCommentAddReaction(t *testing.T) {
+       assert.NoError(t, unittest.PrepareTestDatabase())
+
+       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+
+       var issue1ID int64 = 1
+       var comment1ID int64 = 1
+
+       addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
+
+       unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+}
+
+func TestIssueCommentDeleteReaction(t *testing.T) {
+       assert.NoError(t, unittest.PrepareTestDatabase())
+
+       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+       user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+       user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
+       user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User)
+
+       var issue1ID int64 = 1
+       var comment1ID int64 = 1
+
+       addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
+       addReaction(t, user2.ID, issue1ID, comment1ID, "heart")
+       addReaction(t, user3.ID, issue1ID, comment1ID, "heart")
+       addReaction(t, user4.ID, issue1ID, comment1ID, "+1")
+
+       reactionsList, _, err := FindReactions(db.DefaultContext, FindReactionsOptions{
+               IssueID:   issue1ID,
+               CommentID: comment1ID,
+       })
+       assert.NoError(t, err)
+       assert.Len(t, reactionsList, 4)
+
+       reactions := reactionsList.GroupByType()
+       assert.Len(t, reactions["heart"], 3)
+       assert.Len(t, reactions["+1"], 1)
+}
+
+func TestIssueCommentReactionCount(t *testing.T) {
+       assert.NoError(t, unittest.PrepareTestDatabase())
+
+       user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+
+       var issue1ID int64 = 1
+       var comment1ID int64 = 1
+
+       addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
+       assert.NoError(t, DeleteCommentReaction(user1.ID, issue1ID, comment1ID, "heart"))
+
+       unittest.AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+}
index f4af7ffe373f8847f371458fae6f91af37cca2f2..6da434d76a27e147f8380e8ad8240941005e2bda 100644 (file)
@@ -9,6 +9,7 @@ import (
        "testing"
 
        "code.gitea.io/gitea/models/foreignreference"
+       issues_model "code.gitea.io/gitea/models/issues"
        repo_model "code.gitea.io/gitea/models/repo"
        "code.gitea.io/gitea/models/unittest"
        user_model "code.gitea.io/gitea/models/user"
@@ -42,7 +43,7 @@ func assertCreateIssues(t *testing.T, isPull bool) {
        label := unittest.AssertExistsAndLoadBean(t, &Label{ID: 1}).(*Label)
        milestone := unittest.AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
        assert.EqualValues(t, milestone.ID, 1)
-       reaction := &Reaction{
+       reaction := &issues_model.Reaction{
                Type:   "heart",
                UserID: owner.ID,
        }
@@ -60,7 +61,7 @@ func assertCreateIssues(t *testing.T, isPull bool) {
                Poster:      owner,
                IsClosed:    true,
                Labels:      []*Label{label},
-               Reactions:   []*Reaction{reaction},
+               Reactions:   []*issues_model.Reaction{reaction},
                ForeignReference: &foreignreference.ForeignReference{
                        ForeignIndex: strconv.FormatInt(foreignIndex, 10),
                        RepoID:       repo.ID,
@@ -75,7 +76,7 @@ func assertCreateIssues(t *testing.T, isPull bool) {
        err = i.LoadAttributes()
        assert.NoError(t, err)
        assert.EqualValues(t, strconv.FormatInt(foreignIndex, 10), i.ForeignReference.ForeignIndex)
-       unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID})
+       unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID})
 }
 
 func TestMigrate_CreateIssuesIsPullFalse(t *testing.T) {
@@ -91,7 +92,7 @@ func TestMigrate_InsertIssueComments(t *testing.T) {
        issue := unittest.AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
        _ = issue.LoadRepo()
        owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}).(*user_model.User)
-       reaction := &Reaction{
+       reaction := &issues_model.Reaction{
                Type:   "heart",
                UserID: owner.ID,
        }
@@ -101,7 +102,7 @@ func TestMigrate_InsertIssueComments(t *testing.T) {
                Poster:    owner,
                IssueID:   issue.ID,
                Issue:     issue,
-               Reactions: []*Reaction{reaction},
+               Reactions: []*issues_model.Reaction{reaction},
        }
 
        err := InsertIssueComments([]*Comment{comment})
index b5217777bfd46a72cf099487cf11a054ff61a034..9d0dc38aa4e28afcbf08b14e448366c705409a24 100644 (file)
@@ -15,6 +15,7 @@ import (
        repo_model "code.gitea.io/gitea/models/repo"
        "code.gitea.io/gitea/models/unit"
        user_model "code.gitea.io/gitea/models/user"
+       "code.gitea.io/gitea/modules/container"
        "code.gitea.io/gitea/modules/log"
        "code.gitea.io/gitea/modules/setting"
        "code.gitea.io/gitea/modules/timeutil"
@@ -520,7 +521,7 @@ func (nl NotificationList) getPendingRepoIDs() []int64 {
                        ids[notification.RepoID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 // LoadRepos loads repositories from database
@@ -596,7 +597,7 @@ func (nl NotificationList) getPendingIssueIDs() []int64 {
                        ids[notification.IssueID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 // LoadIssues loads issues from database
@@ -682,7 +683,7 @@ func (nl NotificationList) getPendingCommentIDs() []int64 {
                        ids[notification.CommentID] = struct{}{}
                }
        }
-       return keysInt64(ids)
+       return container.KeysInt64(ids)
 }
 
 // LoadComments loads comments from database
index 36f57abcc5b95238b39df34e58797379a9834eff..2c6be0a5768aca4058658dcd3895568ab2eda9fe 100644 (file)
@@ -13,6 +13,7 @@ import (
        repo_model "code.gitea.io/gitea/models/repo"
        "code.gitea.io/gitea/models/unit"
        user_model "code.gitea.io/gitea/models/user"
+       "code.gitea.io/gitea/modules/container"
        "code.gitea.io/gitea/modules/structs"
        "code.gitea.io/gitea/modules/util"
 
@@ -62,7 +63,7 @@ func (repos RepositoryList) loadAttributes(e db.Engine) error {
        users := make(map[int64]*user_model.User, len(set))
        if err := e.
                Where("id > 0").
-               In("id", keysInt64(set)).
+               In("id", container.KeysInt64(set)).
                Find(&users); err != nil {
                return fmt.Errorf("find users: %v", err)
        }
index 77442228093d2030f9fb39303429a2b982123fda..e8307796293f87f8906b6ca4ed1bb1dbfc4f0ae9 100644 (file)
@@ -14,6 +14,7 @@ import (
 
        asymkey_model "code.gitea.io/gitea/models/asymkey"
        "code.gitea.io/gitea/models/db"
+       "code.gitea.io/gitea/models/issues"
        "code.gitea.io/gitea/models/organization"
        repo_model "code.gitea.io/gitea/models/repo"
        user_model "code.gitea.io/gitea/models/user"
@@ -76,7 +77,7 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
                &IssueUser{UID: u.ID},
                &user_model.EmailAddress{UID: u.ID},
                &user_model.UserOpenID{UID: u.ID},
-               &Reaction{UserID: u.ID},
+               &issues.Reaction{UserID: u.ID},
                &organization.TeamUser{UID: u.ID},
                &Collaboration{UserID: u.ID},
                &Stopwatch{UserID: u.ID},
@@ -100,14 +101,14 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
                        }
 
                        for _, comment := range comments {
-                               if err = deleteComment(e, comment); err != nil {
+                               if err = deleteComment(ctx, comment); err != nil {
                                        return err
                                }
                        }
                }
 
                // Delete Reactions
-               if err = deleteReaction(e, &ReactionOptions{Doer: u}); err != nil {
+               if err = issues.DeleteReaction(ctx, &issues.ReactionOptions{DoerID: u.ID}); err != nil {
                        return err
                }
        }
diff --git a/modules/container/map.go b/modules/container/map.go
new file mode 100644 (file)
index 0000000..3519de0
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright 2022 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 container
+
+// KeysInt64 returns keys slice for a map with int64 key
+func KeysInt64(m map[int64]struct{}) []int64 {
+       keys := make([]int64, 0, len(m))
+       for k := range m {
+               keys = append(keys, k)
+       }
+       return keys
+}
index 38f4bc47523c8c2f8dbc02d1ac648f7a4fd6f695..5aa73667968ef40841334f3dfc4907751c36fb00 100644 (file)
@@ -9,6 +9,7 @@ import (
        "net/http"
 
        "code.gitea.io/gitea/models"
+       issues_model "code.gitea.io/gitea/models/issues"
        "code.gitea.io/gitea/modules/context"
        "code.gitea.io/gitea/modules/convert"
        api "code.gitea.io/gitea/modules/structs"
@@ -67,12 +68,12 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
                return
        }
 
-       reactions, _, err := models.FindCommentReactions(comment)
+       reactions, _, err := issues_model.FindCommentReactions(comment.IssueID, comment.ID)
        if err != nil {
                ctx.Error(http.StatusInternalServerError, "FindCommentReactions", err)
                return
        }
-       _, err = reactions.LoadUsers(ctx.Repo.Repository)
+       _, err = reactions.LoadUsers(ctx, ctx.Repo.Repository)
        if err != nil {
                ctx.Error(http.StatusInternalServerError, "ReactionList.LoadUsers()", err)
                return
@@ -197,11 +198,11 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
 
        if isCreateType {
                // PostIssueCommentReaction part
-               reaction, err := models.CreateCommentReaction(ctx.Doer, comment.Issue, comment, form.Reaction)
+               reaction, err := issues_model.CreateCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
                if err != nil {
-                       if models.IsErrForbiddenIssueReaction(err) {
+                       if issues_model.IsErrForbiddenIssueReaction(err) {
                                ctx.Error(http.StatusForbidden, err.Error(), err)
-                       } else if models.IsErrReactionAlreadyExist(err) {
+                       } else if issues_model.IsErrReactionAlreadyExist(err) {
                                ctx.JSON(http.StatusOK, api.Reaction{
                                        User:     convert.ToUser(ctx.Doer, ctx.Doer),
                                        Reaction: reaction.Type,
@@ -220,7 +221,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
                })
        } else {
                // DeleteIssueCommentReaction part
-               err = models.DeleteCommentReaction(ctx.Doer, comment.Issue, comment, form.Reaction)
+               err = issues_model.DeleteCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
                if err != nil {
                        ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err)
                        return
@@ -285,12 +286,12 @@ func GetIssueReactions(ctx *context.APIContext) {
                return
        }
 
-       reactions, count, err := models.FindIssueReactions(issue, utils.GetListOptions(ctx))
+       reactions, count, err := issues_model.FindIssueReactions(issue.ID, utils.GetListOptions(ctx))
        if err != nil {
                ctx.Error(http.StatusInternalServerError, "FindIssueReactions", err)
                return
        }
-       _, err = reactions.LoadUsers(ctx.Repo.Repository)
+       _, err = reactions.LoadUsers(ctx, ctx.Repo.Repository)
        if err != nil {
                ctx.Error(http.StatusInternalServerError, "ReactionList.LoadUsers()", err)
                return
@@ -407,11 +408,11 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
 
        if isCreateType {
                // PostIssueReaction part
-               reaction, err := models.CreateIssueReaction(ctx.Doer, issue, form.Reaction)
+               reaction, err := issues_model.CreateIssueReaction(ctx.Doer.ID, issue.ID, form.Reaction)
                if err != nil {
-                       if models.IsErrForbiddenIssueReaction(err) {
+                       if issues_model.IsErrForbiddenIssueReaction(err) {
                                ctx.Error(http.StatusForbidden, err.Error(), err)
-                       } else if models.IsErrReactionAlreadyExist(err) {
+                       } else if issues_model.IsErrReactionAlreadyExist(err) {
                                ctx.JSON(http.StatusOK, api.Reaction{
                                        User:     convert.ToUser(ctx.Doer, ctx.Doer),
                                        Reaction: reaction.Type,
@@ -430,7 +431,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
                })
        } else {
                // DeleteIssueReaction part
-               err = models.DeleteIssueReaction(ctx.Doer, issue, form.Reaction)
+               err = issues_model.DeleteIssueReaction(ctx.Doer.ID, issue.ID, form.Reaction)
                if err != nil {
                        ctx.Error(http.StatusInternalServerError, "DeleteIssueReaction", err)
                        return
index c50e773e99b3dea3460e3ee27c323327e8254d7e..486a63a9e105f626f8656097e59de346ea503e2d 100644 (file)
@@ -19,6 +19,7 @@ import (
 
        "code.gitea.io/gitea/models"
        "code.gitea.io/gitea/models/db"
+       issues_model "code.gitea.io/gitea/models/issues"
        "code.gitea.io/gitea/models/organization"
        project_model "code.gitea.io/gitea/models/project"
        repo_model "code.gitea.io/gitea/models/repo"
@@ -2349,9 +2350,9 @@ func ChangeIssueReaction(ctx *context.Context) {
 
        switch ctx.Params(":action") {
        case "react":
-               reaction, err := models.CreateIssueReaction(ctx.Doer, issue, form.Content)
+               reaction, err := issues_model.CreateIssueReaction(ctx.Doer.ID, issue.ID, form.Content)
                if err != nil {
-                       if models.IsErrForbiddenIssueReaction(err) {
+                       if issues_model.IsErrForbiddenIssueReaction(err) {
                                ctx.ServerError("ChangeIssueReaction", err)
                                return
                        }
@@ -2367,7 +2368,7 @@ func ChangeIssueReaction(ctx *context.Context) {
 
                log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
        case "unreact":
-               if err := models.DeleteIssueReaction(ctx.Doer, issue, form.Content); err != nil {
+               if err := issues_model.DeleteIssueReaction(ctx.Doer.ID, issue.ID, form.Content); err != nil {
                        ctx.ServerError("DeleteIssueReaction", err)
                        return
                }
@@ -2451,9 +2452,9 @@ func ChangeCommentReaction(ctx *context.Context) {
 
        switch ctx.Params(":action") {
        case "react":
-               reaction, err := models.CreateCommentReaction(ctx.Doer, comment.Issue, comment, form.Content)
+               reaction, err := issues_model.CreateCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
                if err != nil {
-                       if models.IsErrForbiddenIssueReaction(err) {
+                       if issues_model.IsErrForbiddenIssueReaction(err) {
                                ctx.ServerError("ChangeIssueReaction", err)
                                return
                        }
@@ -2469,7 +2470,7 @@ func ChangeCommentReaction(ctx *context.Context) {
 
                log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID)
        case "unreact":
-               if err := models.DeleteCommentReaction(ctx.Doer, comment.Issue, comment, form.Content); err != nil {
+               if err := issues_model.DeleteCommentReaction(ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content); err != nil {
                        ctx.ServerError("DeleteCommentReaction", err)
                        return
                }
index 82e57e296d28e53441a519b9e33e804d90bab549..73324981515e56ce719a43bb167784d1ceaa1987 100644 (file)
@@ -18,6 +18,7 @@ import (
        "code.gitea.io/gitea/models"
        "code.gitea.io/gitea/models/db"
        "code.gitea.io/gitea/models/foreignreference"
+       issues_model "code.gitea.io/gitea/models/issues"
        repo_model "code.gitea.io/gitea/models/repo"
        user_model "code.gitea.io/gitea/models/user"
        "code.gitea.io/gitea/modules/git"
@@ -392,7 +393,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
                }
                // add reactions
                for _, reaction := range issue.Reactions {
-                       res := models.Reaction{
+                       res := issues_model.Reaction{
                                Type:        reaction.Content,
                                CreatedUnix: timeutil.TimeStampNow(),
                        }
@@ -448,7 +449,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
 
                // add reactions
                for _, reaction := range comment.Reactions {
-                       res := models.Reaction{
+                       res := issues_model.Reaction{
                                Type:        reaction.Content,
                                CreatedUnix: timeutil.TimeStampNow(),
                        }
@@ -646,7 +647,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
 
        // add reactions
        for _, reaction := range pr.Reactions {
-               res := models.Reaction{
+               res := issues_model.Reaction{
                        Type:        reaction.Content,
                        CreatedUnix: timeutil.TimeStampNow(),
                }