feat(reactions): add reactions struct and routes
This commit is contained in:
parent
85fb8e3443
commit
389ea6258d
|
@ -97,6 +97,7 @@ This document describes the different errors Vikunja can return.
|
|||
| 4022 | 400 | The task has a relative reminder which does not specify relative to what. |
|
||||
| 4023 | 409 | Tried to create a task relation which would create a cycle. |
|
||||
| 4024 | 400 | The provided filter expression is invalid. |
|
||||
| 4025 | 400 | The reaction kind is invalid. |
|
||||
|
||||
## Team
|
||||
|
||||
|
|
10
pkg/db/db.go
10
pkg/db/db.go
|
@ -23,6 +23,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"xorm.io/builder"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
@ -227,3 +228,12 @@ func NewSession() *xorm.Session {
|
|||
func Type() schemas.DBType {
|
||||
return x.Dialect().URI().DBType
|
||||
}
|
||||
|
||||
func GetDialect() string {
|
||||
dialect := config.DatabaseType.GetString()
|
||||
if dialect == "sqlite" {
|
||||
dialect = builder.SQLITE
|
||||
}
|
||||
|
||||
return dialect
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"time"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type reactions20240311173251 struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"reaction"`
|
||||
UserID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
EntityID int64 `xorm:"bigint not null INDEX" json:"entity_id"`
|
||||
EntityKind int `xorm:"bigint not null INDEX" json:"entity_kind"`
|
||||
Value string `xorm:"varchar(20) not null INDEX" json:"value"`
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
}
|
||||
|
||||
func (reactions20240311173251) TableName() string {
|
||||
return "reactions"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20240311173251",
|
||||
Description: "Create reactions table",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(reactions20240311173251{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1060,6 +1060,33 @@ func (err ErrInvalidFilterExpression) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// ErrInvalidReactionEntityKind represents an error where the reaction kind is invalid
|
||||
type ErrInvalidReactionEntityKind struct {
|
||||
Kind string
|
||||
}
|
||||
|
||||
// IsErrInvalidReactionEntityKind checks if an error is ErrInvalidReactionEntityKind.
|
||||
func IsErrInvalidReactionEntityKind(err error) bool {
|
||||
_, ok := err.(ErrInvalidReactionEntityKind)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidReactionEntityKind) Error() string {
|
||||
return fmt.Sprintf("Reaction kind %s is invalid", err.Kind)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidReactionEntityKind holds the unique world-error code of this error
|
||||
const ErrCodeInvalidReactionEntityKind = 4025
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidReactionEntityKind) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeInvalidReactionEntityKind,
|
||||
Message: fmt.Sprintf("The reaction kind '%s' is invalid.", err.Kind),
|
||||
}
|
||||
}
|
||||
|
||||
// ============
|
||||
// Team errors
|
||||
// ============
|
||||
|
|
|
@ -23,7 +23,6 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
|
@ -370,10 +369,7 @@ type projectOptions struct {
|
|||
}
|
||||
|
||||
func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search string, getArchived bool) *builder.Builder {
|
||||
dialect := config.DatabaseType.GetString()
|
||||
if dialect == "sqlite" {
|
||||
dialect = builder.SQLITE
|
||||
}
|
||||
dialect := db.GetDialect()
|
||||
|
||||
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
|
||||
var getArchivedCond builder.Cond = builder.Eq{"1": 1}
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"time"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type ReactionKind int
|
||||
|
||||
const (
|
||||
ReactionKindTask = iota
|
||||
ReactionKindComment
|
||||
)
|
||||
|
||||
type Reaction struct {
|
||||
// The unique numeric id of this reaction
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"reaction"`
|
||||
|
||||
// The user who reacted
|
||||
User *user.User `xorm:"-" json:"user" valid:"-"`
|
||||
UserID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
|
||||
// The id of the entity you're reacting to
|
||||
EntityID int64 `xorm:"bigint not null INDEX" json:"entity_id" param:"entityid"`
|
||||
// The entity kind which you're reacting to. Can be 0 for task, 1 for comment.
|
||||
EntityKind ReactionKind `xorm:"bigint not null INDEX" json:"entity_kind"`
|
||||
EntityKindString string `xorm:"-" json:"-" param:"entitykind"`
|
||||
|
||||
// The actual reaction. This can be any valid utf character or text, up to a length of 20.
|
||||
Value string `xorm:"varchar(20) not null INDEX" json:"value"`
|
||||
|
||||
// A timestamp when this reaction was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (*Reaction) TableName() string {
|
||||
return "reactions"
|
||||
}
|
||||
|
||||
func (r *Reaction) ReadAll(s *xorm.Session, _ web.Auth, _ string, _ int, _ int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
reactions := []*Reaction{}
|
||||
err = s.Where("entity_id = ? AND entity_kind = ?", r.EntityID, r.EntityKind).Find(&reactions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cond := builder.
|
||||
Select("user_id").
|
||||
From("reactions").
|
||||
And(builder.Eq{"entity_id": r.EntityID}).
|
||||
And(builder.Eq{"entity_kind": r.EntityKind})
|
||||
|
||||
users, err := user.GetUsersByCond(s, cond)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
reactionMap := make(map[string][]*user.User)
|
||||
for _, reaction := range reactions {
|
||||
if _, has := reactionMap[reaction.Value]; !has {
|
||||
reactionMap[reaction.Value] = []*user.User{}
|
||||
}
|
||||
|
||||
reactionMap[reaction.Value] = append(reactionMap[reaction.Value], users[reaction.UserID])
|
||||
}
|
||||
|
||||
return reactionMap, len(reactionMap), int64(len(reactions)), nil
|
||||
}
|
||||
|
||||
func (r *Reaction) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
r.UserID = a.GetID()
|
||||
|
||||
_, err = s.Where("user_id = ? AND entity_id = ? AND entity_kind = ?", r.UserID, r.EntityID, r.EntityKind).
|
||||
Delete(&Reaction{})
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Reaction) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
r.UserID = a.GetID()
|
||||
|
||||
exists, err := s.Where("user_id = ? AND entity_id = ? AND entity_kind = ?", r.UserID, r.EntityID, r.EntityKind).
|
||||
Exist(&Reaction{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
_, err = s.Where("user_id = ? AND entity_id = ? AND entity_kind = ?", r.UserID, r.EntityID, r.EntityKind).
|
||||
Delete(&Reaction{})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.Insert(r)
|
||||
return
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func (r *Reaction) setEntityKindFromString() (err error) {
|
||||
switch r.EntityKindString {
|
||||
case "tasks":
|
||||
r.EntityKind = ReactionKindTask
|
||||
return
|
||||
case "comments":
|
||||
r.EntityKind = ReactionKindComment
|
||||
return
|
||||
}
|
||||
|
||||
return ErrInvalidReactionEntityKind{
|
||||
Kind: r.EntityKindString,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reaction) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
t, err := r.getTask(s)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
return t.CanRead(s, a)
|
||||
}
|
||||
|
||||
func (r *Reaction) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
t, err := r.getTask(s)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return t.CanUpdate(s, a)
|
||||
}
|
||||
|
||||
func (r *Reaction) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
t, err := r.getTask(s)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return t.CanUpdate(s, a)
|
||||
}
|
||||
|
||||
func (r *Reaction) getTask(s *xorm.Session) (t *Task, err error) {
|
||||
err = r.setEntityKindFromString()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
t = &Task{ID: r.EntityID}
|
||||
|
||||
if r.EntityKind == ReactionKindComment {
|
||||
tc := &TaskComment{TaskID: r.EntityID}
|
||||
err = getTaskCommentSimple(s, tc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.ID = tc.TaskID
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -589,6 +589,15 @@ func registerAPIRoutes(a *echo.Group) {
|
|||
a.POST("/projects/:project/webhooks/:webhook", webhookProvider.UpdateWeb)
|
||||
a.GET("/webhooks/events", apiv1.GetAvailableWebhookEvents)
|
||||
}
|
||||
|
||||
reactionProvider := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.Reaction{}
|
||||
},
|
||||
}
|
||||
a.GET("/:entitykind/:entityid/reactions", reactionProvider.ReadAllWeb)
|
||||
a.DELETE("/:entitykind/:entityid/reactions", reactionProvider.DeleteWeb)
|
||||
a.PUT("/:entitykind/:entityid/reactions", reactionProvider.CreateWeb)
|
||||
}
|
||||
|
||||
func registerMigrations(m *echo.Group) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
"xorm.io/builder"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
@ -259,13 +260,17 @@ func GetUserWithEmail(s *xorm.Session, user *User) (userOut *User, err error) {
|
|||
|
||||
// GetUsersByIDs returns a map of users from a slice of user ids
|
||||
func GetUsersByIDs(s *xorm.Session, userIDs []int64) (users map[int64]*User, err error) {
|
||||
users = make(map[int64]*User)
|
||||
|
||||
if len(userIDs) == 0 {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
err = s.In("id", userIDs).Find(&users)
|
||||
return GetUsersByCond(s, builder.Dialect(db.GetDialect()).Where(builder.In("id", userIDs)))
|
||||
}
|
||||
|
||||
func GetUsersByCond(s *xorm.Session, cond *builder.Builder) (users map[int64]*User, err error) {
|
||||
users = make(map[int64]*User)
|
||||
|
||||
err = s.Where(cond).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue