Add events (#777)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/777 Co-authored-by: konrad <konrad@kola-entertainments.de> Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
@ -101,6 +101,8 @@ const (
|
||||
LogHTTP Key = `log.http`
|
||||
LogEcho Key = `log.echo`
|
||||
LogPath Key = `log.path`
|
||||
LogEvents Key = `log.events`
|
||||
LogEventsLevel Key = `log.eventslevel`
|
||||
|
||||
RateLimitEnabled Key = `ratelimit.enabled`
|
||||
RateLimitKind Key = `ratelimit.kind`
|
||||
@ -281,6 +283,8 @@ func InitDefaultConfig() {
|
||||
LogHTTP.setDefault("stdout")
|
||||
LogEcho.setDefault("off")
|
||||
LogPath.setDefault(ServiceRootpath.GetString() + "/logs")
|
||||
LogEvents.setDefault("stdout")
|
||||
LogEventsLevel.setDefault("INFO")
|
||||
// Rate Limit
|
||||
RateLimitEnabled.setDefault(false)
|
||||
RateLimitKind.setDefault("user")
|
||||
|
97
pkg/events/events.go
Normal file
97
pkg/events/events.go
Normal file
@ -0,0 +1,97 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
vmetrics "code.vikunja.io/api/pkg/metrics"
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/ThreeDotsLabs/watermill/components/metrics"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
|
||||
"github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
|
||||
)
|
||||
|
||||
var pubsub *gochannel.GoChannel
|
||||
|
||||
// Event represents the event interface used by all events
|
||||
type Event interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
// InitEvents sets up everything needed to work with events
|
||||
func InitEvents() (err error) {
|
||||
logger := log.NewWatermillLogger()
|
||||
|
||||
router, err := message.NewRouter(
|
||||
message.RouterConfig{},
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
router.AddMiddleware(
|
||||
middleware.Retry{
|
||||
MaxRetries: 5,
|
||||
InitialInterval: time.Millisecond * 100,
|
||||
Logger: logger,
|
||||
Multiplier: 2,
|
||||
}.Middleware,
|
||||
middleware.Recoverer,
|
||||
)
|
||||
|
||||
metricsBuilder := metrics.NewPrometheusMetricsBuilder(vmetrics.GetRegistry(), "", "")
|
||||
metricsBuilder.AddPrometheusRouterMetrics(router)
|
||||
|
||||
pubsub = gochannel.NewGoChannel(
|
||||
gochannel.Config{
|
||||
OutputChannelBuffer: 1024,
|
||||
},
|
||||
logger,
|
||||
)
|
||||
|
||||
for topic, funcs := range listeners {
|
||||
for _, handler := range funcs {
|
||||
router.AddNoPublisherHandler(topic+"."+handler.Name(), topic, pubsub, func(msg *message.Message) error {
|
||||
return handler.Handle(msg.Payload)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return router.Run(context.Background())
|
||||
}
|
||||
|
||||
// Dispatch dispatches an event
|
||||
func Dispatch(event Event) error {
|
||||
if isUnderTest {
|
||||
dispatchedTestEvents = append(dispatchedTestEvents, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := message.NewMessage(watermill.NewUUID(), content)
|
||||
return pubsub.Publish(event.Name(), msg)
|
||||
}
|
36
pkg/events/listeners.go
Normal file
36
pkg/events/listeners.go
Normal file
@ -0,0 +1,36 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 events
|
||||
|
||||
import "github.com/ThreeDotsLabs/watermill/message"
|
||||
|
||||
// Listener represents something that listens to events
|
||||
type Listener interface {
|
||||
Handle(payload message.Payload) error
|
||||
Name() string
|
||||
}
|
||||
|
||||
var listeners map[string][]Listener
|
||||
|
||||
func init() {
|
||||
listeners = make(map[string][]Listener)
|
||||
}
|
||||
|
||||
// RegisterListener is used to register a listener when a specific event happens
|
||||
func RegisterListener(name string, listener Listener) {
|
||||
listeners[name] = append(listeners[name], listener)
|
||||
}
|
49
pkg/events/testing.go
Normal file
49
pkg/events/testing.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 events
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
isUnderTest bool
|
||||
dispatchedTestEvents []Event
|
||||
)
|
||||
|
||||
// Fake sets up the "test mode" of the events package. Typically you'd call this function in the TestMain function
|
||||
// in the package you're testing. It will prevent any events from being fired, instead they will be recorded and be
|
||||
// available for assertions.
|
||||
func Fake() {
|
||||
isUnderTest = true
|
||||
dispatchedTestEvents = nil
|
||||
}
|
||||
|
||||
// AssertDispatched asserts an event has been dispatched.
|
||||
func AssertDispatched(t *testing.T, event Event) {
|
||||
var found bool
|
||||
for _, testEvent := range dispatchedTestEvents {
|
||||
if event.Name() == testEvent.Name() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, found, "Failed to assert "+event.Name()+" has been dispatched.")
|
||||
}
|
29
pkg/initialize/events.go
Normal file
29
pkg/initialize/events.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 initialize
|
||||
|
||||
import "time"
|
||||
|
||||
// BootedEvent represents a BootedEvent event
|
||||
type BootedEvent struct {
|
||||
BootedAt time.Time
|
||||
}
|
||||
|
||||
// TopicName defines the name for BootedEvent
|
||||
func (t *BootedEvent) Name() string {
|
||||
return "booted"
|
||||
}
|
@ -17,8 +17,11 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/cron"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
@ -85,4 +88,21 @@ func FullInit() {
|
||||
// Start the cron
|
||||
cron.Init()
|
||||
models.RegisterReminderCron()
|
||||
|
||||
// Start processing events
|
||||
go func() {
|
||||
models.RegisterListeners()
|
||||
user.RegisterListeners()
|
||||
err := events.InitEvents()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
err = events.Dispatch(&BootedEvent{
|
||||
BootedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -24,6 +24,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
@ -85,6 +87,7 @@ func setupTestEnv() (e *echo.Echo, err error) {
|
||||
files.InitTests()
|
||||
user.InitTests()
|
||||
models.SetupTests()
|
||||
events.Fake()
|
||||
|
||||
err = db.LoadFixtures()
|
||||
if err != nil {
|
||||
|
@ -49,6 +49,7 @@ func InitLogger() {
|
||||
config.LogDatabase.Set("off")
|
||||
config.LogHTTP.Set("off")
|
||||
config.LogEcho.Set("off")
|
||||
config.LogEvents.Set("off")
|
||||
return
|
||||
}
|
||||
|
||||
|
91
pkg/log/watermill_logger.go
Normal file
91
pkg/log/watermill_logger.go
Normal file
@ -0,0 +1,91 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
const watermillFmt = `%{color}%{time:` + time.RFC3339Nano + `}: %{level}` + "\t" + `▶ [EVENTS] %{id:03x}%{color:reset} %{message}`
|
||||
|
||||
const watermillLogModule = `vikunja_events`
|
||||
|
||||
type WatermillLogger struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
func NewWatermillLogger() *WatermillLogger {
|
||||
lvl := strings.ToUpper(config.LogEventsLevel.GetString())
|
||||
level, err := logging.LogLevel(lvl)
|
||||
if err != nil {
|
||||
Criticalf("Error setting events log level %s: %s", lvl, err.Error())
|
||||
}
|
||||
|
||||
watermillLogger := &WatermillLogger{
|
||||
logger: logging.MustGetLogger(watermillLogModule),
|
||||
}
|
||||
|
||||
logBackend := logging.NewLogBackend(GetLogWriter("events"), "", 0)
|
||||
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(watermillFmt+"\n"))
|
||||
|
||||
backendLeveled := logging.AddModuleLevel(backend)
|
||||
backendLeveled.SetLevel(level, watermillLogModule)
|
||||
|
||||
watermillLogger.logger.SetBackend(backendLeveled)
|
||||
|
||||
return watermillLogger
|
||||
}
|
||||
|
||||
func concatFields(fields watermill.LogFields) string {
|
||||
full := ""
|
||||
|
||||
for key, val := range fields {
|
||||
full += fmt.Sprintf("%s=%s, ", key, val)
|
||||
}
|
||||
|
||||
if full != "" {
|
||||
full = full[:len(full)-2]
|
||||
}
|
||||
|
||||
return full
|
||||
}
|
||||
|
||||
func (w *WatermillLogger) Error(msg string, err error, fields watermill.LogFields) {
|
||||
w.logger.Errorf("%s: %s, %s", msg, err, concatFields(fields))
|
||||
}
|
||||
|
||||
func (w *WatermillLogger) Info(msg string, fields watermill.LogFields) {
|
||||
w.logger.Infof("%s, %s", msg, concatFields(fields))
|
||||
}
|
||||
|
||||
func (w *WatermillLogger) Debug(msg string, fields watermill.LogFields) {
|
||||
w.logger.Debugf("%s, %s", msg, concatFields(fields))
|
||||
}
|
||||
|
||||
func (w *WatermillLogger) Trace(msg string, fields watermill.LogFields) {
|
||||
w.logger.Debugf("%s, %s", msg, concatFields(fields))
|
||||
}
|
||||
|
||||
func (w *WatermillLogger) With(fields watermill.LogFields) watermill.LoggerAdapter {
|
||||
return w
|
||||
}
|
@ -44,7 +44,7 @@ func NewXormLogger(lvl string) *XormLogger {
|
||||
}
|
||||
level, err := logging.LogLevel(lvl)
|
||||
if err != nil {
|
||||
Critical("Error setting database log level: %s", err.Error())
|
||||
Criticalf("Error setting database log level: %s", err.Error())
|
||||
}
|
||||
|
||||
xormLogger := &XormLogger{
|
||||
|
@ -17,7 +17,6 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@ -41,6 +40,18 @@ const (
|
||||
TeamCountKey = `teamcount`
|
||||
)
|
||||
|
||||
var registry *prometheus.Registry
|
||||
|
||||
func GetRegistry() *prometheus.Registry {
|
||||
if registry == nil {
|
||||
registry = prometheus.NewRegistry()
|
||||
registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
|
||||
registry.MustRegister(prometheus.NewGoCollector())
|
||||
}
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
// InitMetrics Initializes the metrics
|
||||
func InitMetrics() {
|
||||
// init active users, sometimes we'll have garbage from previous runs in redis instead
|
||||
@ -48,50 +59,67 @@ func InitMetrics() {
|
||||
log.Fatalf("Could not set initial count for active users, error was %s", err)
|
||||
}
|
||||
|
||||
GetRegistry()
|
||||
|
||||
// Register total list count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_list_count",
|
||||
Help: "The number of lists on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(ListCountKey)
|
||||
return float64(count)
|
||||
})
|
||||
}))
|
||||
if err != nil {
|
||||
log.Criticalf("Could not register metrics for %s: %s", ListCountKey, err)
|
||||
}
|
||||
|
||||
// Register total user count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_user_count",
|
||||
Help: "The total number of users on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(UserCountKey)
|
||||
return float64(count)
|
||||
})
|
||||
}))
|
||||
if err != nil {
|
||||
log.Criticalf("Could not register metrics for %s: %s", UserCountKey, err)
|
||||
}
|
||||
|
||||
// Register total Namespaces count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_namespcae_count",
|
||||
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_namespace_count",
|
||||
Help: "The total number of namespaces on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(NamespaceCountKey)
|
||||
return float64(count)
|
||||
})
|
||||
}))
|
||||
if err != nil {
|
||||
log.Criticalf("Could not register metrics for %s: %s", NamespaceCountKey, err)
|
||||
}
|
||||
|
||||
// Register total Tasks count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_task_count",
|
||||
Help: "The total number of tasks on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(TaskCountKey)
|
||||
return float64(count)
|
||||
})
|
||||
}))
|
||||
if err != nil {
|
||||
log.Criticalf("Could not register metrics for %s: %s", TaskCountKey, err)
|
||||
}
|
||||
|
||||
// Register total user count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_team_count",
|
||||
Help: "The total number of teams on this instance",
|
||||
}, func() float64 {
|
||||
count, _ := GetCount(TeamCountKey)
|
||||
return float64(count)
|
||||
})
|
||||
}))
|
||||
if err != nil {
|
||||
log.Criticalf("Could not register metrics for %s: %s", TeamCountKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCount returns the current count from redis
|
||||
@ -113,22 +141,3 @@ func GetCount(key string) (count int64, err error) {
|
||||
func SetCount(count int64, key string) error {
|
||||
return keyvalue.Put(key, count)
|
||||
}
|
||||
|
||||
// UpdateCount updates a count with a given amount
|
||||
func UpdateCount(update int64, key string) {
|
||||
if !config.ServiceEnableMetrics.GetBool() {
|
||||
return
|
||||
}
|
||||
if update > 0 {
|
||||
err := keyvalue.IncrBy(key, update)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
if update < 0 {
|
||||
err := keyvalue.DecrBy(key, update)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,14 +78,14 @@ func (bt *BulkTask) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its list)"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/bulk [post]
|
||||
func (bt *BulkTask) Update(s *xorm.Session) (err error) {
|
||||
func (bt *BulkTask) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
for _, oldtask := range bt.Tasks {
|
||||
|
||||
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
|
||||
updateDone(oldtask, &bt.Task)
|
||||
|
||||
// Update the assignees
|
||||
if err := oldtask.updateTaskAssignees(s, bt.Assignees); err != nil {
|
||||
if err := oldtask.updateTaskAssignees(s, bt.Assignees, a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ func TestBulkTask_Update(t *testing.T) {
|
||||
if !allowed != tt.wantForbidden {
|
||||
t.Errorf("BulkTask.Update() want forbidden, got %v, want %v", allowed, tt.wantForbidden)
|
||||
}
|
||||
if err := bt.Update(s); (err != nil) != tt.wantErr {
|
||||
if err := bt.Update(s, tt.fields.User); (err != nil) != tt.wantErr {
|
||||
t.Errorf("BulkTask.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
|
247
pkg/models/events.go
Normal file
247
pkg/models/events.go
Normal file
@ -0,0 +1,247 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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"
|
||||
)
|
||||
|
||||
/////////////////
|
||||
// Task Events //
|
||||
/////////////////
|
||||
|
||||
// TaskCreatedEvent represents an event where a task has been created
|
||||
type TaskCreatedEvent struct {
|
||||
Task *Task
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for TaskCreatedEvent
|
||||
func (t *TaskCreatedEvent) Name() string {
|
||||
return "task.created"
|
||||
}
|
||||
|
||||
// TaskUpdatedEvent represents an event where a task has been updated
|
||||
type TaskUpdatedEvent struct {
|
||||
Task *Task
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for TaskUpdatedEvent
|
||||
func (t *TaskUpdatedEvent) Name() string {
|
||||
return "task.updated"
|
||||
}
|
||||
|
||||
// TaskDeletedEvent represents a TaskDeletedEvent event
|
||||
type TaskDeletedEvent struct {
|
||||
Task *Task
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for TaskDeletedEvent
|
||||
func (t *TaskDeletedEvent) Name() string {
|
||||
return "task.deleted"
|
||||
}
|
||||
|
||||
// TaskAssigneeCreatedEvent represents an event where a task has been assigned to a user
|
||||
type TaskAssigneeCreatedEvent struct {
|
||||
Task *Task
|
||||
Assignee *user.User
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for TaskAssigneeCreatedEvent
|
||||
func (t *TaskAssigneeCreatedEvent) Name() string {
|
||||
return "task.assignee.created"
|
||||
}
|
||||
|
||||
// TaskCommentCreatedEvent represents an event where a task comment has been created
|
||||
type TaskCommentCreatedEvent struct {
|
||||
Task *Task
|
||||
Comment *TaskComment
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for TaskCommentCreatedEvent
|
||||
func (t *TaskCommentCreatedEvent) Name() string {
|
||||
return "task.comment.created"
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Namespace Events //
|
||||
//////////////////////
|
||||
|
||||
// NamespaceCreatedEvent represents an event where a namespace has been created
|
||||
type NamespaceCreatedEvent struct {
|
||||
Namespace *Namespace
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for NamespaceCreatedEvent
|
||||
func (n *NamespaceCreatedEvent) Name() string {
|
||||
return "namespace.created"
|
||||
}
|
||||
|
||||
// NamespaceUpdatedEvent represents an event where a namespace has been updated
|
||||
type NamespaceUpdatedEvent struct {
|
||||
Namespace *Namespace
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for NamespaceUpdatedEvent
|
||||
func (n *NamespaceUpdatedEvent) Name() string {
|
||||
return "namespace.updated"
|
||||
}
|
||||
|
||||
// NamespaceDeletedEvent represents a NamespaceDeletedEvent event
|
||||
type NamespaceDeletedEvent struct {
|
||||
Namespace *Namespace
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// TopicName defines the name for NamespaceDeletedEvent
|
||||
func (t *NamespaceDeletedEvent) Name() string {
|
||||
return "namespace.deleted"
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// List Events //
|
||||
/////////////////
|
||||
|
||||
// ListCreatedEvent represents an event where a list has been created
|
||||
type ListCreatedEvent struct {
|
||||
List *List
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for ListCreatedEvent
|
||||
func (l *ListCreatedEvent) Name() string {
|
||||
return "list.created"
|
||||
}
|
||||
|
||||
// ListUpdatedEvent represents an event where a list has been updated
|
||||
type ListUpdatedEvent struct {
|
||||
List *List
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for ListUpdatedEvent
|
||||
func (l *ListUpdatedEvent) Name() string {
|
||||
return "list.updated"
|
||||
}
|
||||
|
||||
// ListDeletedEvent represents an event where a list has been deleted
|
||||
type ListDeletedEvent struct {
|
||||
List *List
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for ListDeletedEvent
|
||||
func (t *ListDeletedEvent) Name() string {
|
||||
return "list.deleted"
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// Sharing Events //
|
||||
////////////////////
|
||||
|
||||
// ListSharedWithUserEvent represents an event where a list has been shared with a user
|
||||
type ListSharedWithUserEvent struct {
|
||||
List *List
|
||||
User *user.User
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for ListSharedWithUserEvent
|
||||
func (l *ListSharedWithUserEvent) Name() string {
|
||||
return "list.shared.user"
|
||||
}
|
||||
|
||||
// ListSharedWithTeamEvent represents an event where a list has been shared with a team
|
||||
type ListSharedWithTeamEvent struct {
|
||||
List *List
|
||||
Team *Team
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for ListSharedWithTeamEvent
|
||||
func (l *ListSharedWithTeamEvent) Name() string {
|
||||
return "list.shared.team"
|
||||
}
|
||||
|
||||
// NamespaceSharedWithUserEvent represents an event where a namespace has been shared with a user
|
||||
type NamespaceSharedWithUserEvent struct {
|
||||
Namespace *Namespace
|
||||
User *user.User
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for NamespaceSharedWithUserEvent
|
||||
func (n *NamespaceSharedWithUserEvent) Name() string {
|
||||
return "namespace.shared.user"
|
||||
}
|
||||
|
||||
// NamespaceSharedWithTeamEvent represents an event where a namespace has been shared with a team
|
||||
type NamespaceSharedWithTeamEvent struct {
|
||||
Namespace *Namespace
|
||||
Team *Team
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for NamespaceSharedWithTeamEvent
|
||||
func (n *NamespaceSharedWithTeamEvent) Name() string {
|
||||
return "namespace.shared.team"
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Team Events //
|
||||
/////////////////
|
||||
|
||||
// TeamMemberAddedEvent defines an event where a user is added to a team
|
||||
type TeamMemberAddedEvent struct {
|
||||
Team *Team
|
||||
Member *user.User
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for TeamMemberAddedEvent
|
||||
func (t *TeamMemberAddedEvent) Name() string {
|
||||
return "team.member.added"
|
||||
}
|
||||
|
||||
// TeamCreatedEvent represents a TeamCreatedEvent event
|
||||
type TeamCreatedEvent struct {
|
||||
Team *Team
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for TeamCreatedEvent
|
||||
func (t *TeamCreatedEvent) Name() string {
|
||||
return "team.created"
|
||||
}
|
||||
|
||||
// TeamDeletedEvent represents a TeamDeletedEvent event
|
||||
type TeamDeletedEvent struct {
|
||||
Team *Team
|
||||
Doer web.Auth
|
||||
}
|
||||
|
||||
// Name defines the name for TeamDeletedEvent
|
||||
func (t *TeamDeletedEvent) Name() string {
|
||||
return "team.deleted"
|
||||
}
|
@ -190,7 +190,7 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/buckets/{bucketID} [post]
|
||||
func (b *Bucket) Update(s *xorm.Session) (err error) {
|
||||
func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
_, err = s.
|
||||
Where("id = ?", b.ID).
|
||||
Cols("title", "limit").
|
||||
@ -211,7 +211,7 @@ func (b *Bucket) Update(s *xorm.Session) (err error) {
|
||||
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/buckets/{bucketID} [delete]
|
||||
func (b *Bucket) Delete(s *xorm.Session) (err error) {
|
||||
func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Prevent removing the last bucket
|
||||
total, err := s.Where("list_id = ?", b.ListID).Count(&Bucket{})
|
||||
|
@ -92,6 +92,8 @@ func TestBucket_ReadAll(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBucket_Delete(t *testing.T) {
|
||||
user := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -101,7 +103,7 @@ func TestBucket_Delete(t *testing.T) {
|
||||
ID: 2, // The second bucket only has 3 tasks
|
||||
ListID: 1,
|
||||
}
|
||||
err := b.Delete(s)
|
||||
err := b.Delete(s, user)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -125,7 +127,7 @@ func TestBucket_Delete(t *testing.T) {
|
||||
ID: 34,
|
||||
ListID: 18,
|
||||
}
|
||||
err := b.Delete(s)
|
||||
err := b.Delete(s, user)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrCannotRemoveLastBucket(err))
|
||||
err = s.Commit()
|
||||
@ -141,7 +143,7 @@ func TestBucket_Delete(t *testing.T) {
|
||||
func TestBucket_Update(t *testing.T) {
|
||||
|
||||
testAndAssertBucketUpdate := func(t *testing.T, b *Bucket, s *xorm.Session) {
|
||||
err := b.Update(s)
|
||||
err := b.Update(s, &user.User{ID: 1})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = s.Commit()
|
||||
|
@ -93,7 +93,7 @@ func (l *Label) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 404 {object} web.HTTPError "Label not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /labels/{id} [put]
|
||||
func (l *Label) Update(s *xorm.Session) (err error) {
|
||||
func (l *Label) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
_, err = s.
|
||||
ID(l.ID).
|
||||
Cols(
|
||||
@ -106,7 +106,7 @@ func (l *Label) Update(s *xorm.Session) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
err = l.ReadOne(s)
|
||||
err = l.ReadOne(s, a)
|
||||
return
|
||||
}
|
||||
|
||||
@ -123,7 +123,7 @@ func (l *Label) Update(s *xorm.Session) (err error) {
|
||||
// @Failure 404 {object} web.HTTPError "Label not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /labels/{id} [delete]
|
||||
func (l *Label) Delete(s *xorm.Session) (err error) {
|
||||
func (l *Label) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
_, err = s.ID(l.ID).Delete(&Label{})
|
||||
return err
|
||||
}
|
||||
@ -178,7 +178,7 @@ func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, pe
|
||||
// @Failure 404 {object} web.HTTPError "Label not found"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /labels/{id} [get]
|
||||
func (l *Label) ReadOne(s *xorm.Session) (err error) {
|
||||
func (l *Label) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
label, err := getLabelByIDSimple(s, l.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -61,7 +61,7 @@ func (LabelTask) TableName() string {
|
||||
// @Failure 404 {object} web.HTTPError "Label not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{task}/labels/{label} [delete]
|
||||
func (lt *LabelTask) Delete(s *xorm.Session) (err error) {
|
||||
func (lt *LabelTask) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
_, err = s.Delete(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
|
||||
return err
|
||||
}
|
||||
@ -208,6 +208,10 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if len(labels) == 0 {
|
||||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
// Get all created by users
|
||||
var userids []int64
|
||||
for _, l := range labels {
|
||||
|
@ -318,7 +318,7 @@ func TestLabelTask_Delete(t *testing.T) {
|
||||
if !allowed && !tt.wantForbidden {
|
||||
t.Errorf("LabelTask.CanDelete() forbidden, want %v", tt.wantForbidden)
|
||||
}
|
||||
err := l.Delete(s)
|
||||
err := l.Delete(s, tt.auth)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("LabelTask.Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ func TestLabel_ReadOne(t *testing.T) {
|
||||
if !allowed && !tt.wantForbidden {
|
||||
t.Errorf("Label.CanRead() forbidden, want %v", tt.wantForbidden)
|
||||
}
|
||||
err := l.ReadOne(s)
|
||||
err := l.ReadOne(s, tt.auth)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Label.ReadOne() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
@ -419,7 +419,7 @@ func TestLabel_Update(t *testing.T) {
|
||||
if !allowed && !tt.wantForbidden {
|
||||
t.Errorf("Label.CanUpdate() forbidden, want %v", tt.wantForbidden)
|
||||
}
|
||||
if err := l.Update(s); (err != nil) != tt.wantErr {
|
||||
if err := l.Update(s, tt.auth); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Label.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !tt.wantErr && !tt.wantForbidden {
|
||||
@ -505,7 +505,7 @@ func TestLabel_Delete(t *testing.T) {
|
||||
if !allowed && !tt.wantForbidden {
|
||||
t.Errorf("Label.CanDelete() forbidden, want %v", tt.wantForbidden)
|
||||
}
|
||||
if err := l.Delete(s); (err != nil) != tt.wantErr {
|
||||
if err := l.Delete(s, tt.auth); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Label.Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !tt.wantErr && !tt.wantForbidden {
|
||||
|
@ -127,7 +127,7 @@ func (share *LinkSharing) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 404 {object} web.HTTPError "Share Link not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{list}/shares/{share} [get]
|
||||
func (share *LinkSharing) ReadOne(s *xorm.Session) (err error) {
|
||||
func (share *LinkSharing) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
exists, err := s.Where("id = ?", share.ID).Get(share)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -216,7 +216,7 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
||||
// @Failure 404 {object} web.HTTPError "Share Link not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{list}/shares/{share} [delete]
|
||||
func (share *LinkSharing) Delete(s *xorm.Session) (err error) {
|
||||
func (share *LinkSharing) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
_, err = s.Where("id = ?", share.ID).Delete(share)
|
||||
return
|
||||
}
|
||||
|
@ -21,10 +21,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
@ -186,7 +187,7 @@ func (l *List) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id} [get]
|
||||
func (l *List) ReadOne(s *xorm.Session) (err error) {
|
||||
func (l *List) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
if l.ID == FavoritesPseudoList.ID {
|
||||
// Already "built" the list in CanRead
|
||||
@ -388,6 +389,10 @@ func getRawListsForUser(s *xorm.Session, opts *listOptions) (lists []*List, resu
|
||||
|
||||
// addListDetails adds owner user objects and list tasks to all lists in the slice
|
||||
func addListDetails(s *xorm.Session, lists []*List) (err error) {
|
||||
if len(lists) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var ownerIDs []int64
|
||||
for _, l := range lists {
|
||||
ownerIDs = append(ownerIDs, l.OwnerID)
|
||||
@ -411,6 +416,10 @@ func addListDetails(s *xorm.Session, lists []*List) (err error) {
|
||||
fileIDs = append(fileIDs, l.BackgroundFileID)
|
||||
}
|
||||
|
||||
if len(fileIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Unsplash background file info
|
||||
us := []*UnsplashPhoto{}
|
||||
err = s.In("file_id", fileIDs).Find(&us)
|
||||
@ -466,7 +475,7 @@ func (l *List) CheckIsArchived(s *xorm.Session) (err error) {
|
||||
}
|
||||
|
||||
// CreateOrUpdateList updates a list or creates it if it doesn't exist
|
||||
func CreateOrUpdateList(s *xorm.Session, list *List) (err error) {
|
||||
func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error) {
|
||||
|
||||
// Check if the namespace exists
|
||||
if list.NamespaceID != 0 && list.NamespaceID != FavoritesPseudoNamespace.ID {
|
||||
@ -492,7 +501,6 @@ func CreateOrUpdateList(s *xorm.Session, list *List) (err error) {
|
||||
|
||||
if list.ID == 0 {
|
||||
_, err = s.Insert(list)
|
||||
metrics.UpdateCount(1, metrics.ListCountKey)
|
||||
} else {
|
||||
// We need to specify the cols we want to update here to be able to un-archive lists
|
||||
colsToUpdate := []string{
|
||||
@ -522,7 +530,7 @@ func CreateOrUpdateList(s *xorm.Session, list *List) (err error) {
|
||||
}
|
||||
|
||||
*list = *l
|
||||
err = list.ReadOne(s)
|
||||
err = list.ReadOne(s, auth)
|
||||
return
|
||||
|
||||
}
|
||||
@ -541,8 +549,16 @@ func CreateOrUpdateList(s *xorm.Session, list *List) (err error) {
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id} [post]
|
||||
func (l *List) Update(s *xorm.Session) (err error) {
|
||||
return CreateOrUpdateList(s, l)
|
||||
func (l *List) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
err = CreateOrUpdateList(s, l, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListUpdatedEvent{
|
||||
List: l,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
func updateListLastUpdated(s *xorm.Session, list *List) error {
|
||||
@ -589,7 +605,7 @@ func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
l.Owner = doer
|
||||
l.ID = 0 // Otherwise only the first time a new list would be created
|
||||
|
||||
err = CreateOrUpdateList(s, l)
|
||||
err = CreateOrUpdateList(s, l, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -599,7 +615,15 @@ func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
ListID: l.ID,
|
||||
Title: "New Bucket",
|
||||
}
|
||||
return b.Create(s, a)
|
||||
err = b.Create(s, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListCreatedEvent{
|
||||
List: l,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete implements the delete method of CRUDable
|
||||
@ -614,18 +638,24 @@ func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id} [delete]
|
||||
func (l *List) Delete(s *xorm.Session) (err error) {
|
||||
func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Delete the list
|
||||
_, err = s.ID(l.ID).Delete(&List{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
metrics.UpdateCount(-1, metrics.ListCountKey)
|
||||
|
||||
// Delete all todotasks on that list
|
||||
// Delete all tasks on that list
|
||||
_, err = s.Where("list_id = ?", l.ID).Delete(&Task{})
|
||||
return
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListDeletedEvent{
|
||||
List: l,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// SetListBackground sets a background file as list background in the db
|
||||
|
@ -67,15 +67,15 @@ func (ld *ListDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool,
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/duplicate [put]
|
||||
//nolint:gocyclo
|
||||
func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
||||
|
||||
log.Debugf("Duplicating list %d", ld.ListID)
|
||||
|
||||
ld.List.ID = 0
|
||||
ld.List.Identifier = "" // Reset the identifier to trigger regenerating a new one
|
||||
// Set the owner to the current user
|
||||
ld.List.OwnerID = a.GetID()
|
||||
if err := CreateOrUpdateList(s, ld.List); err != nil {
|
||||
ld.List.OwnerID = doer.GetID()
|
||||
if err := CreateOrUpdateList(s, ld.List, doer); err != nil {
|
||||
// If there is no available unique list identifier, just reset it.
|
||||
if IsErrListIdentifierIsNotUnique(err) {
|
||||
ld.List.Identifier = ""
|
||||
@ -99,7 +99,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
oldID := b.ID
|
||||
b.ID = 0
|
||||
b.ListID = ld.List.ID
|
||||
if err := b.Create(s, a); err != nil {
|
||||
if err := b.Create(s, doer); err != nil {
|
||||
return err
|
||||
}
|
||||
bucketMap[oldID] = b.ID
|
||||
@ -108,7 +108,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
log.Debugf("Duplicated all buckets from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
// Get all tasks + all task details
|
||||
tasks, _, _, err := getTasksForLists(s, []*List{{ID: ld.ListID}}, a, &taskOptions{})
|
||||
tasks, _, _, err := getTasksForLists(s, []*List{{ID: ld.ListID}}, doer, &taskOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -124,7 +124,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
t.ListID = ld.List.ID
|
||||
t.BucketID = bucketMap[t.BucketID]
|
||||
t.UID = ""
|
||||
err := createTask(s, t, a, false)
|
||||
err := createTask(s, t, doer, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -163,7 +163,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err := attachment.NewAttachment(s, attachment.File.File, attachment.File.Name, attachment.File.Size, a)
|
||||
err := attachment.NewAttachment(s, attachment.File.File, attachment.File.Name, attachment.File.Size, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -206,7 +206,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
ID: taskMap[a.TaskID],
|
||||
ListID: ld.List.ID,
|
||||
}
|
||||
if err := t.addNewAssigneeByID(s, a.UserID, ld.List); err != nil {
|
||||
if err := t.addNewAssigneeByID(s, a.UserID, ld.List, doer); err != nil {
|
||||
if IsErrUserDoesNotHaveAccessToList(err) {
|
||||
continue
|
||||
}
|
||||
@ -269,7 +269,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
defer f.File.Close()
|
||||
|
||||
file, err := files.Create(f.File, f.Name, f.Size, a)
|
||||
file, err := files.Create(f.File, f.Name, f.Size, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
@ -77,9 +79,9 @@ func (tl *TeamList) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(s, tl.TeamID)
|
||||
team, err := GetTeamByID(s, tl.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the list exists
|
||||
@ -105,6 +107,15 @@ func (tl *TeamList) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&ListSharedWithTeamEvent{
|
||||
List: l,
|
||||
Team: team,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, l)
|
||||
return
|
||||
}
|
||||
@ -122,7 +133,7 @@ func (tl *TeamList) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 404 {object} web.HTTPError "Team or list does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/teams/{teamID} [delete]
|
||||
func (tl *TeamList) Delete(s *xorm.Session) (err error) {
|
||||
func (tl *TeamList) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(s, tl.TeamID)
|
||||
@ -234,7 +245,7 @@ func (tl *TeamList) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
||||
// @Failure 404 {object} web.HTTPError "Team or list does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/teams/{teamID} [post]
|
||||
func (tl *TeamList) Update(s *xorm.Session) (err error) {
|
||||
func (tl *TeamList) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tl.Right.isValid(); err != nil {
|
||||
|
@ -158,6 +158,8 @@ func TestTeamList_Create(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTeamList_Delete(t *testing.T) {
|
||||
user := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -165,7 +167,7 @@ func TestTeamList_Delete(t *testing.T) {
|
||||
TeamID: 1,
|
||||
ListID: 3,
|
||||
}
|
||||
err := tl.Delete(s)
|
||||
err := tl.Delete(s, user)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -181,7 +183,7 @@ func TestTeamList_Delete(t *testing.T) {
|
||||
TeamID: 9999,
|
||||
ListID: 1,
|
||||
}
|
||||
err := tl.Delete(s)
|
||||
err := tl.Delete(s, user)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
@ -193,7 +195,7 @@ func TestTeamList_Delete(t *testing.T) {
|
||||
TeamID: 1,
|
||||
ListID: 9999,
|
||||
}
|
||||
err := tl.Delete(s)
|
||||
err := tl.Delete(s, user)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotHaveAccessToList(err))
|
||||
_ = s.Close()
|
||||
@ -267,7 +269,7 @@ func TestTeamList_Update(t *testing.T) {
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := tl.Update(s)
|
||||
err := tl.Update(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TeamList.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
||||
NamespaceID: 1,
|
||||
}
|
||||
list.Description = "Lorem Ipsum dolor sit amet."
|
||||
err := list.Update(s)
|
||||
err := list.Update(s, usr)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -143,7 +143,7 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
||||
ID: 99999999,
|
||||
Title: "test",
|
||||
}
|
||||
err := list.Update(s)
|
||||
err := list.Update(s, usr)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
@ -172,7 +172,7 @@ func TestList_Delete(t *testing.T) {
|
||||
list := List{
|
||||
ID: 1,
|
||||
}
|
||||
err := list.Delete(s)
|
||||
err := list.Delete(s, &user.User{ID: 1})
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
@ -19,6 +19,8 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
@ -112,6 +114,15 @@ func (lu *ListUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&ListSharedWithUserEvent{
|
||||
List: l,
|
||||
User: u,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, l)
|
||||
return
|
||||
}
|
||||
@ -129,7 +140,7 @@ func (lu *ListUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 404 {object} web.HTTPError "user or list does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/users/{userID} [delete]
|
||||
func (lu *ListUser) Delete(s *xorm.Session) (err error) {
|
||||
func (lu *ListUser) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
u, err := user.GetUserByUsername(s, lu.Username)
|
||||
@ -231,7 +242,7 @@ func (lu *ListUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
||||
// @Failure 404 {object} web.HTTPError "User or list does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/users/{userID} [post]
|
||||
func (lu *ListUser) Update(s *xorm.Session) (err error) {
|
||||
func (lu *ListUser) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := lu.Right.isValid(); err != nil {
|
||||
|
@ -311,7 +311,7 @@ func TestListUser_Update(t *testing.T) {
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := lu.Update(s)
|
||||
err := lu.Update(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ListUser.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
@ -393,7 +393,7 @@ func TestListUser_Delete(t *testing.T) {
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := lu.Delete(s)
|
||||
err := lu.Delete(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ListUser.Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
154
pkg/models/listeners.go
Normal file
154
pkg/models/listeners.go
Normal file
@ -0,0 +1,154 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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/events"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
)
|
||||
|
||||
// RegisterListeners registers all event listeners
|
||||
func RegisterListeners() {
|
||||
events.RegisterListener((&ListCreatedEvent{}).Name(), &IncreaseListCounter{})
|
||||
events.RegisterListener((&ListDeletedEvent{}).Name(), &DecreaseListCounter{})
|
||||
events.RegisterListener((&NamespaceCreatedEvent{}).Name(), &IncreaseNamespaceCounter{})
|
||||
events.RegisterListener((&NamespaceDeletedEvent{}).Name(), &DecreaseNamespaceCounter{})
|
||||
events.RegisterListener((&TaskCreatedEvent{}).Name(), &IncreaseTaskCounter{})
|
||||
events.RegisterListener((&TaskDeletedEvent{}).Name(), &DecreaseTaskCounter{})
|
||||
events.RegisterListener((&TeamDeletedEvent{}).Name(), &DecreaseTeamCounter{})
|
||||
events.RegisterListener((&TeamCreatedEvent{}).Name(), &IncreaseTeamCounter{})
|
||||
}
|
||||
|
||||
//////
|
||||
// Task Events
|
||||
|
||||
// IncreaseTaskCounter represents a listener
|
||||
type IncreaseTaskCounter struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the IncreaseTaskCounter listener
|
||||
func (s *IncreaseTaskCounter) Name() string {
|
||||
return "task.counter.increase"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event IncreaseTaskCounter listens on is fired
|
||||
func (s *IncreaseTaskCounter) Handle(payload message.Payload) (err error) {
|
||||
return keyvalue.IncrBy(metrics.TaskCountKey, 1)
|
||||
}
|
||||
|
||||
// DecreaseTaskCounter represents a listener
|
||||
type DecreaseTaskCounter struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the DecreaseTaskCounter listener
|
||||
func (s *DecreaseTaskCounter) Name() string {
|
||||
return "task.counter.decrease"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event DecreaseTaskCounter listens on is fired
|
||||
func (s *DecreaseTaskCounter) Handle(payload message.Payload) (err error) {
|
||||
return keyvalue.DecrBy(metrics.TaskCountKey, 1)
|
||||
}
|
||||
|
||||
///////
|
||||
// List Event Listeners
|
||||
|
||||
type IncreaseListCounter struct {
|
||||
}
|
||||
|
||||
func (s *IncreaseListCounter) Name() string {
|
||||
return "list.counter.increase"
|
||||
}
|
||||
|
||||
func (s *IncreaseListCounter) Handle(payload message.Payload) (err error) {
|
||||
return keyvalue.IncrBy(metrics.ListCountKey, 1)
|
||||
}
|
||||
|
||||
type DecreaseListCounter struct {
|
||||
}
|
||||
|
||||
func (s *DecreaseListCounter) Name() string {
|
||||
return "list.counter.decrease"
|
||||
}
|
||||
|
||||
func (s *DecreaseListCounter) Handle(payload message.Payload) (err error) {
|
||||
return keyvalue.DecrBy(metrics.ListCountKey, 1)
|
||||
}
|
||||
|
||||
//////
|
||||
// Namespace events
|
||||
|
||||
// IncreaseNamespaceCounter represents a listener
|
||||
type IncreaseNamespaceCounter struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the IncreaseNamespaceCounter listener
|
||||
func (s *IncreaseNamespaceCounter) Name() string {
|
||||
return "namespace.counter.increase"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event IncreaseNamespaceCounter listens on is fired
|
||||
func (s *IncreaseNamespaceCounter) Handle(payload message.Payload) (err error) {
|
||||
return keyvalue.IncrBy(metrics.NamespaceCountKey, 1)
|
||||
}
|
||||
|
||||
// DecreaseNamespaceCounter represents a listener
|
||||
type DecreaseNamespaceCounter struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the DecreaseNamespaceCounter listener
|
||||
func (s *DecreaseNamespaceCounter) Name() string {
|
||||
return "namespace.counter.decrease"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event DecreaseNamespaceCounter listens on is fired
|
||||
func (s *DecreaseNamespaceCounter) Handle(payload message.Payload) (err error) {
|
||||
return keyvalue.DecrBy(metrics.NamespaceCountKey, 1)
|
||||
}
|
||||
|
||||
///////
|
||||
// Team Events
|
||||
|
||||
// IncreaseTeamCounter represents a listener
|
||||
type IncreaseTeamCounter struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the IncreaseTeamCounter listener
|
||||
func (s *IncreaseTeamCounter) Name() string {
|
||||
return "team.counter.increase"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event IncreaseTeamCounter listens on is fired
|
||||
func (s *IncreaseTeamCounter) Handle(payload message.Payload) (err error) {
|
||||
return keyvalue.IncrBy(metrics.TeamCountKey, 1)
|
||||
}
|
||||
|
||||
// DecreaseTeamCounter represents a listener
|
||||
type DecreaseTeamCounter struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the DecreaseTeamCounter listener
|
||||
func (s *DecreaseTeamCounter) Name() string {
|
||||
return "team.counter.decrease"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event DecreaseTeamCounter listens on is fired
|
||||
func (s *DecreaseTeamCounter) Handle(payload message.Payload) (err error) {
|
||||
return keyvalue.DecrBy(metrics.TeamCountKey, 1)
|
||||
}
|
@ -22,6 +22,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
@ -64,5 +66,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
SetupTests()
|
||||
|
||||
events.Fake()
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
@ -22,8 +22,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
@ -159,7 +160,7 @@ func (n *Namespace) CheckIsArchived(s *xorm.Session) error {
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to that namespace."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id} [get]
|
||||
func (n *Namespace) ReadOne(s *xorm.Session) (err error) {
|
||||
func (n *Namespace) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
nn, err := GetNamespaceByID(s, n.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -478,7 +479,14 @@ func (n *Namespace) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
metrics.UpdateCount(1, metrics.NamespaceCountKey)
|
||||
err = events.Dispatch(&NamespaceCreatedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -504,7 +512,7 @@ func CreateNewNamespaceForUser(s *xorm.Session, user *user.User) (err error) {
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{id} [delete]
|
||||
func (n *Namespace) Delete(s *xorm.Session) (err error) {
|
||||
func (n *Namespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the namespace exists
|
||||
_, err = GetNamespaceByID(s, n.ID)
|
||||
@ -523,6 +531,14 @@ func (n *Namespace) Delete(s *xorm.Session) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(lists) == 0 {
|
||||
return events.Dispatch(&NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
var listIDs []int64
|
||||
// We need to do that for here because we need the list ids to delete two times:
|
||||
// 1) to delete the lists itself
|
||||
@ -543,9 +559,10 @@ func (n *Namespace) Delete(s *xorm.Session) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
metrics.UpdateCount(-1, metrics.NamespaceCountKey)
|
||||
|
||||
return
|
||||
return events.Dispatch(&NamespaceDeletedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Update implements the update method via the interface
|
||||
@ -562,7 +579,7 @@ func (n *Namespace) Delete(s *xorm.Session) (err error) {
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespace/{id} [post]
|
||||
func (n *Namespace) Update(s *xorm.Session) (err error) {
|
||||
func (n *Namespace) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Check if we have at least a name
|
||||
if n.Title == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: n.ID}
|
||||
@ -605,5 +622,12 @@ func (n *Namespace) Update(s *xorm.Session) (err error) {
|
||||
ID(currentNamespace.ID).
|
||||
Cols(colsToUpdate...).
|
||||
Update(n)
|
||||
return
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&NamespaceUpdatedEvent{
|
||||
Namespace: n,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
@ -71,15 +73,15 @@ func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(s, tn.TeamID)
|
||||
team, err := GetTeamByID(s, tn.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
_, err = GetNamespaceByID(s, tn.NamespaceID)
|
||||
namespace, err := GetNamespaceByID(s, tn.NamespaceID)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the team already has access to the namespace
|
||||
@ -96,7 +98,15 @@ func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Insert the new team
|
||||
_, err = s.Insert(tn)
|
||||
return
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&NamespaceSharedWithTeamEvent{
|
||||
Namespace: namespace,
|
||||
Team: team,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes a team <-> namespace relation based on the namespace & team id
|
||||
@ -112,7 +122,7 @@ func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 404 {object} web.HTTPError "team or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/teams/{teamID} [delete]
|
||||
func (tn *TeamNamespace) Delete(s *xorm.Session) (err error) {
|
||||
func (tn *TeamNamespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(s, tn.TeamID)
|
||||
@ -219,7 +229,7 @@ func (tn *TeamNamespace) ReadAll(s *xorm.Session, a web.Auth, search string, pag
|
||||
// @Failure 404 {object} web.HTTPError "Team or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/teams/{teamID} [post]
|
||||
func (tn *TeamNamespace) Update(s *xorm.Session) (err error) {
|
||||
func (tn *TeamNamespace) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tn.Right.isValid(); err != nil {
|
||||
|
@ -157,7 +157,7 @@ func TestTeamNamespace_Delete(t *testing.T) {
|
||||
s := db.NewSession()
|
||||
allowed, _ := tn.CanDelete(s, u)
|
||||
assert.True(t, allowed)
|
||||
err := tn.Delete(s)
|
||||
err := tn.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -174,7 +174,7 @@ func TestTeamNamespace_Delete(t *testing.T) {
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Delete(s)
|
||||
err := tn.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
@ -186,7 +186,7 @@ func TestTeamNamespace_Delete(t *testing.T) {
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := tn.Delete(s)
|
||||
err := tn.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotHaveAccessToNamespace(err))
|
||||
_ = s.Close()
|
||||
@ -260,7 +260,7 @@ func TestTeamNamespace_Update(t *testing.T) {
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := tl.Update(s)
|
||||
err := tl.Update(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("TeamNamespace.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
@ -69,11 +69,13 @@ func TestNamespace_Create(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespace_ReadOne(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
n := &Namespace{ID: 1}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := n.ReadOne(s)
|
||||
err := n.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, n.Title, "testnamespace")
|
||||
_ = s.Close()
|
||||
@ -82,7 +84,7 @@ func TestNamespace_ReadOne(t *testing.T) {
|
||||
n := &Namespace{ID: 99999}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
err := n.ReadOne(s)
|
||||
err := n.ReadOne(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
@ -90,6 +92,8 @@ func TestNamespace_ReadOne(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespace_Update(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -97,7 +101,7 @@ func TestNamespace_Update(t *testing.T) {
|
||||
ID: 1,
|
||||
Title: "Lorem Ipsum",
|
||||
}
|
||||
err := n.Update(s)
|
||||
err := n.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -114,7 +118,7 @@ func TestNamespace_Update(t *testing.T) {
|
||||
ID: 99999,
|
||||
Title: "Lorem Ipsum",
|
||||
}
|
||||
err := n.Update(s)
|
||||
err := n.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
@ -127,7 +131,7 @@ func TestNamespace_Update(t *testing.T) {
|
||||
Title: "Lorem Ipsum",
|
||||
Owner: &user.User{ID: 99999},
|
||||
}
|
||||
err := n.Update(s)
|
||||
err := n.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
@ -138,7 +142,7 @@ func TestNamespace_Update(t *testing.T) {
|
||||
n := &Namespace{
|
||||
ID: 1,
|
||||
}
|
||||
err := n.Update(s)
|
||||
err := n.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
|
||||
_ = s.Close()
|
||||
@ -146,13 +150,15 @@ func TestNamespace_Update(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespace_Delete(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
n := &Namespace{
|
||||
ID: 1,
|
||||
}
|
||||
err := n.Delete(s)
|
||||
err := n.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -167,7 +173,7 @@ func TestNamespace_Delete(t *testing.T) {
|
||||
n := &Namespace{
|
||||
ID: 9999,
|
||||
}
|
||||
err := n.Delete(s)
|
||||
err := n.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
|
@ -19,6 +19,8 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
user2 "code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
@ -75,7 +77,7 @@ func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
l, err := GetNamespaceByID(s, nu.NamespaceID)
|
||||
n, err := GetNamespaceByID(s, nu.NamespaceID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -89,7 +91,7 @@ func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the user already has access or is owner of that namespace
|
||||
// We explicitly DO NOT check for teams here
|
||||
if l.OwnerID == nu.UserID {
|
||||
if n.OwnerID == nu.UserID {
|
||||
return ErrUserAlreadyHasNamespaceAccess{UserID: nu.UserID, NamespaceID: nu.NamespaceID}
|
||||
}
|
||||
|
||||
@ -105,8 +107,15 @@ func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Insert user <-> namespace relation
|
||||
_, err = s.Insert(nu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
return events.Dispatch(&NamespaceSharedWithUserEvent{
|
||||
Namespace: n,
|
||||
User: user,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes a namespace <-> user relation
|
||||
@ -122,7 +131,7 @@ func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 404 {object} web.HTTPError "user or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/users/{userID} [delete]
|
||||
func (nu *NamespaceUser) Delete(s *xorm.Session) (err error) {
|
||||
func (nu *NamespaceUser) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
user, err := user2.GetUserByUsername(s, nu.Username)
|
||||
@ -220,7 +229,7 @@ func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, pag
|
||||
// @Failure 404 {object} web.HTTPError "User or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/users/{userID} [post]
|
||||
func (nu *NamespaceUser) Update(s *xorm.Session) (err error) {
|
||||
func (nu *NamespaceUser) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := nu.Right.isValid(); err != nil {
|
||||
|
@ -315,7 +315,7 @@ func TestNamespaceUser_Update(t *testing.T) {
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := nu.Update(s)
|
||||
err := nu.Update(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NamespaceUser.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
@ -396,7 +396,7 @@ func TestNamespaceUser_Delete(t *testing.T) {
|
||||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
err := nu.Delete(s)
|
||||
err := nu.Delete(s, &user.User{ID: 1})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NamespaceUser.Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ func getSavedFilterSimpleByID(s *xorm.Session, id int64) (sf *SavedFilter, err e
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [get]
|
||||
func (sf *SavedFilter) ReadOne(s *xorm.Session) error {
|
||||
func (sf *SavedFilter) ReadOne(s *xorm.Session, a web.Auth) error {
|
||||
// s already contains almost the full saved filter from the rights check, we only need to add the user
|
||||
u, err := user.GetUserByID(s, sf.OwnerID)
|
||||
sf.Owner = u
|
||||
@ -153,7 +153,7 @@ func (sf *SavedFilter) ReadOne(s *xorm.Session) error {
|
||||
// @Failure 404 {object} web.HTTPError "The saved filter does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [post]
|
||||
func (sf *SavedFilter) Update(s *xorm.Session) error {
|
||||
func (sf *SavedFilter) Update(s *xorm.Session, a web.Auth) error {
|
||||
_, err := s.
|
||||
Where("id = ?", sf.ID).
|
||||
Cols(
|
||||
@ -178,7 +178,7 @@ func (sf *SavedFilter) Update(s *xorm.Session) error {
|
||||
// @Failure 404 {object} web.HTTPError "The saved filter does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [delete]
|
||||
func (sf *SavedFilter) Delete(s *xorm.Session) error {
|
||||
func (sf *SavedFilter) Delete(s *xorm.Session, a web.Auth) error {
|
||||
_, err := s.
|
||||
Where("id = ?", sf.ID).
|
||||
Delete(sf)
|
||||
|
@ -86,7 +86,7 @@ func TestSavedFilter_ReadOne(t *testing.T) {
|
||||
// canRead pre-populates the struct
|
||||
_, _, err := sf.CanRead(s, user1)
|
||||
assert.NoError(t, err)
|
||||
err = sf.ReadOne(s)
|
||||
err = sf.ReadOne(s, user1)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sf.Owner)
|
||||
}
|
||||
@ -102,7 +102,7 @@ func TestSavedFilter_Update(t *testing.T) {
|
||||
Description: "", // Explicitly reset the description
|
||||
Filters: &TaskCollection{},
|
||||
}
|
||||
err := sf.Update(s)
|
||||
err := sf.Update(s, &user.User{ID: 1})
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -121,7 +121,7 @@ func TestSavedFilter_Delete(t *testing.T) {
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
}
|
||||
err := sf.Delete(s)
|
||||
err := sf.Delete(s, &user.User{ID: 1})
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
@ -19,6 +19,8 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
@ -57,7 +59,7 @@ func getRawTaskAssigneesForTasks(s *xorm.Session, taskIDs []int64) (taskAssignee
|
||||
}
|
||||
|
||||
// Create or update a bunch of task assignees
|
||||
func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User) (err error) {
|
||||
func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User, doer web.Auth) (err error) {
|
||||
|
||||
// Load the current assignees
|
||||
currentAssignees, err := getRawTaskAssigneesForTasks(s, []int64{t.ID})
|
||||
@ -132,7 +134,7 @@ func (t *Task) updateTaskAssignees(s *xorm.Session, assignees []*user.User) (err
|
||||
}
|
||||
|
||||
// Add the new assignee
|
||||
err = t.addNewAssigneeByID(s, u.ID, list)
|
||||
err = t.addNewAssigneeByID(s, u.ID, list, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -166,7 +168,7 @@ func (t *Task) setTaskAssignees(assignees []*user.User) {
|
||||
// @Failure 403 {object} web.HTTPError "Not allowed to delete the assignee."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/assignees/{userID} [delete]
|
||||
func (la *TaskAssginee) Delete(s *xorm.Session) (err error) {
|
||||
func (la *TaskAssginee) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
_, err = s.Delete(&TaskAssginee{TaskID: la.TaskID, UserID: la.UserID})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -198,10 +200,10 @@ func (la *TaskAssginee) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
task := &Task{ID: la.TaskID}
|
||||
return task.addNewAssigneeByID(s, la.UserID, list)
|
||||
return task.addNewAssigneeByID(s, la.UserID, list, a)
|
||||
}
|
||||
|
||||
func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *List) (err error) {
|
||||
func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *List, auth web.Auth) (err error) {
|
||||
// Check if the user exists and has access to the list
|
||||
newAssignee, err := user.GetUserByID(s, newAssigneeID)
|
||||
if err != nil {
|
||||
@ -223,6 +225,15 @@ func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *Li
|
||||
return err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&TaskAssigneeCreatedEvent{
|
||||
Task: t,
|
||||
Assignee: newAssignee,
|
||||
Doer: auth,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
@ -313,6 +324,6 @@ func (ba *BulkAssignees) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
task.Assignees = append(task.Assignees, &a.User)
|
||||
}
|
||||
|
||||
err = task.updateTaskAssignees(s, ba.Assignees)
|
||||
err = task.updateTaskAssignees(s, ba.Assignees, a)
|
||||
return
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ func (ta *TaskAttachment) NewAttachment(s *xorm.Session, f io.ReadCloser, realna
|
||||
}
|
||||
|
||||
// ReadOne returns a task attachment
|
||||
func (ta *TaskAttachment) ReadOne(s *xorm.Session) (err error) {
|
||||
func (ta *TaskAttachment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
exists, err := s.Where("id = ?", ta.ID).Get(ta)
|
||||
if err != nil {
|
||||
return
|
||||
@ -176,9 +176,9 @@ func (ta *TaskAttachment) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
||||
// @Failure 404 {object} models.Message "The task does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{id}/attachments/{attachmentID} [delete]
|
||||
func (ta *TaskAttachment) Delete(s *xorm.Session) error {
|
||||
func (ta *TaskAttachment) Delete(s *xorm.Session, a web.Auth) error {
|
||||
// Load the attachment
|
||||
err := ta.ReadOne(s)
|
||||
err := ta.ReadOne(s, a)
|
||||
if err != nil && !files.IsErrFileDoesNotExist(err) {
|
||||
return err
|
||||
}
|
||||
@ -209,6 +209,10 @@ func getTaskAttachmentsByTaskIDs(s *xorm.Session, taskIDs []int64) (attachments
|
||||
return
|
||||
}
|
||||
|
||||
if len(attachments) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fileIDs := []int64{}
|
||||
userIDs := []int64{}
|
||||
for _, a := range attachments {
|
||||
|
@ -30,6 +30,8 @@ import (
|
||||
)
|
||||
|
||||
func TestTaskAttachment_ReadOne(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("Normal File", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -39,7 +41,7 @@ func TestTaskAttachment_ReadOne(t *testing.T) {
|
||||
ta := &TaskAttachment{
|
||||
ID: 1,
|
||||
}
|
||||
err := ta.ReadOne(s)
|
||||
err := ta.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, ta.File)
|
||||
assert.True(t, ta.File.ID == ta.FileID && ta.FileID != 0)
|
||||
@ -63,7 +65,7 @@ func TestTaskAttachment_ReadOne(t *testing.T) {
|
||||
ta := &TaskAttachment{
|
||||
ID: 9999,
|
||||
}
|
||||
err := ta.ReadOne(s)
|
||||
err := ta.ReadOne(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskAttachmentDoesNotExist(err))
|
||||
})
|
||||
@ -76,7 +78,7 @@ func TestTaskAttachment_ReadOne(t *testing.T) {
|
||||
ta := &TaskAttachment{
|
||||
ID: 2,
|
||||
}
|
||||
err := ta.ReadOne(s)
|
||||
err := ta.ReadOne(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "file 9999 does not exist")
|
||||
})
|
||||
@ -153,10 +155,12 @@ func TestTaskAttachment_Delete(t *testing.T) {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
files.InitTestFileFixtures(t)
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
ta := &TaskAttachment{ID: 1}
|
||||
err := ta.Delete(s)
|
||||
err := ta.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
// Check if the file itself was deleted
|
||||
_, err = files.FileStat("/1") // The new file has the id 2 since it's the second attachment
|
||||
@ -165,14 +169,14 @@ func TestTaskAttachment_Delete(t *testing.T) {
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
files.InitTestFileFixtures(t)
|
||||
ta := &TaskAttachment{ID: 9999}
|
||||
err := ta.Delete(s)
|
||||
err := ta.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskAttachmentDoesNotExist(err))
|
||||
})
|
||||
t.Run("Existing attachment, nonexisting file", func(t *testing.T) {
|
||||
files.InitTestFileFixtures(t)
|
||||
ta := &TaskAttachment{ID: 2}
|
||||
err := ta.Delete(s)
|
||||
err := ta.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
@ -60,7 +62,7 @@ func (tc *TaskComment) TableName() string {
|
||||
// @Router /tasks/{taskID}/comments [put]
|
||||
func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Check if the task exists
|
||||
_, err = GetTaskSimple(s, &Task{ID: tc.TaskID})
|
||||
task, err := GetTaskSimple(s, &Task{ID: tc.TaskID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -70,6 +72,16 @@ func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = events.Dispatch(&TaskCommentCreatedEvent{
|
||||
Task: &task,
|
||||
Comment: tc,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tc.Author, err = user.GetUserByID(s, a.GetID())
|
||||
return
|
||||
}
|
||||
@ -88,7 +100,7 @@ func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 404 {object} web.HTTPError "The task comment was not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/comments/{commentID} [delete]
|
||||
func (tc *TaskComment) Delete(s *xorm.Session) error {
|
||||
func (tc *TaskComment) Delete(s *xorm.Session, a web.Auth) error {
|
||||
deleted, err := s.
|
||||
ID(tc.ID).
|
||||
NoAutoCondition().
|
||||
@ -113,7 +125,7 @@ func (tc *TaskComment) Delete(s *xorm.Session) error {
|
||||
// @Failure 404 {object} web.HTTPError "The task comment was not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/comments/{commentID} [post]
|
||||
func (tc *TaskComment) Update(s *xorm.Session) error {
|
||||
func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
|
||||
updated, err := s.
|
||||
ID(tc.ID).
|
||||
Cols("comment").
|
||||
@ -138,7 +150,7 @@ func (tc *TaskComment) Update(s *xorm.Session) error {
|
||||
// @Failure 404 {object} web.HTTPError "The task comment was not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/comments/{commentID} [get]
|
||||
func (tc *TaskComment) ReadOne(s *xorm.Session) (err error) {
|
||||
func (tc *TaskComment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
exists, err := s.Get(tc)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -65,13 +65,15 @@ func TestTaskComment_Create(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskComment_Delete(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tc := &TaskComment{ID: 1}
|
||||
err := tc.Delete(s)
|
||||
err := tc.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -86,13 +88,15 @@ func TestTaskComment_Delete(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
tc := &TaskComment{ID: 9999}
|
||||
err := tc.Delete(s)
|
||||
err := tc.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskCommentDoesNotExist(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTaskComment_Update(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -102,7 +106,7 @@ func TestTaskComment_Update(t *testing.T) {
|
||||
ID: 1,
|
||||
Comment: "testing",
|
||||
}
|
||||
err := tc.Update(s)
|
||||
err := tc.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -120,20 +124,22 @@ func TestTaskComment_Update(t *testing.T) {
|
||||
tc := &TaskComment{
|
||||
ID: 9999,
|
||||
}
|
||||
err := tc.Update(s)
|
||||
err := tc.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskCommentDoesNotExist(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTaskComment_ReadOne(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tc := &TaskComment{ID: 1}
|
||||
err := tc.ReadOne(s)
|
||||
err := tc.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Lorem Ipsum Dolor Sit Amet", tc.Comment)
|
||||
assert.NotEmpty(t, tc.Author.ID)
|
||||
@ -144,7 +150,7 @@ func TestTaskComment_ReadOne(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
tc := &TaskComment{ID: 9999}
|
||||
err := tc.ReadOne(s)
|
||||
err := tc.ReadOne(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskCommentDoesNotExist(err))
|
||||
})
|
||||
|
@ -201,7 +201,7 @@ func (rel *TaskRelation) Create(s *xorm.Session, a web.Auth) error {
|
||||
// @Failure 404 {object} web.HTTPError "The task relation was not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/relations [delete]
|
||||
func (rel *TaskRelation) Delete(s *xorm.Session) error {
|
||||
func (rel *TaskRelation) Delete(s *xorm.Session, a web.Auth) error {
|
||||
// Check if the relation exists
|
||||
exists, err := s.
|
||||
Cols("task_id", "other_task_id", "relation_kind").
|
||||
|
@ -97,6 +97,8 @@ func TestTaskRelation_Create(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTaskRelation_Delete(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -107,7 +109,7 @@ func TestTaskRelation_Delete(t *testing.T) {
|
||||
OtherTaskID: 29,
|
||||
RelationKind: RelationKindSubtask,
|
||||
}
|
||||
err := rel.Delete(s)
|
||||
err := rel.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -127,7 +129,7 @@ func TestTaskRelation_Delete(t *testing.T) {
|
||||
OtherTaskID: 3,
|
||||
RelationKind: RelationKindSubtask,
|
||||
}
|
||||
err := rel.Delete(s)
|
||||
err := rel.Delete(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrRelationDoesNotExist(err))
|
||||
})
|
||||
|
@ -22,10 +22,11 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/web"
|
||||
@ -596,6 +597,11 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]
|
||||
for _, rt := range relatedTasks {
|
||||
relatedTaskIDs = append(relatedTaskIDs, rt.OtherTaskID)
|
||||
}
|
||||
|
||||
if len(relatedTaskIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fullRelatedTasks := make(map[int64]*Task)
|
||||
err = s.In("id", relatedTaskIDs).Find(&fullRelatedTasks)
|
||||
if err != nil {
|
||||
@ -814,7 +820,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
||||
|
||||
// Update the assignees
|
||||
if updateAssignees {
|
||||
if err := t.updateTaskAssignees(s, t.Assignees); err != nil {
|
||||
if err := t.updateTaskAssignees(s, t.Assignees, a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -824,10 +830,16 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
||||
return err
|
||||
}
|
||||
|
||||
metrics.UpdateCount(1, metrics.TaskCountKey)
|
||||
|
||||
t.setIdentifier(l)
|
||||
|
||||
err = events.Dispatch(&TaskCreatedEvent{
|
||||
Task: t,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
@ -847,7 +859,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{id} [post]
|
||||
//nolint:gocyclo
|
||||
func (t *Task) Update(s *xorm.Session) (err error) {
|
||||
func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the task exists and get the old values
|
||||
ot, err := GetTaskByIDSimple(s, t.ID)
|
||||
@ -870,7 +882,7 @@ func (t *Task) Update(s *xorm.Session) (err error) {
|
||||
updateDone(&ot, t)
|
||||
|
||||
// Update the assignees
|
||||
if err := ot.updateTaskAssignees(s, t.Assignees); err != nil {
|
||||
if err := ot.updateTaskAssignees(s, t.Assignees, a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -1028,6 +1040,14 @@ func (t *Task) Update(s *xorm.Session) (err error) {
|
||||
}
|
||||
t.Updated = nt.Updated
|
||||
|
||||
err = events.Dispatch(&TaskUpdatedEvent{
|
||||
Task: t,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
}
|
||||
|
||||
@ -1166,7 +1186,7 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err erro
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{id} [delete]
|
||||
func (t *Task) Delete(s *xorm.Session) (err error) {
|
||||
func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
if _, err = s.ID(t.ID).Delete(Task{}); err != nil {
|
||||
return err
|
||||
@ -1177,7 +1197,13 @@ func (t *Task) Delete(s *xorm.Session) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
metrics.UpdateCount(-1, metrics.TaskCountKey)
|
||||
err = events.Dispatch(&TaskDeletedEvent{
|
||||
Task: t,
|
||||
Doer: a,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
return
|
||||
@ -1195,7 +1221,7 @@ func (t *Task) Delete(s *xorm.Session) (err error) {
|
||||
// @Failure 404 {object} models.Message "Task not found"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{ID} [get]
|
||||
func (t *Task) ReadOne(s *xorm.Session) (err error) {
|
||||
func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
taskMap := make(map[int64]*Task, 1)
|
||||
taskMap[t.ID] = &Task{}
|
||||
|
@ -20,6 +20,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -65,6 +67,7 @@ func TestTask_Create(t *testing.T) {
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
|
||||
events.AssertDispatched(t, &TaskCreatedEvent{})
|
||||
})
|
||||
t.Run("empty title", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
@ -127,6 +130,8 @@ func TestTask_Create(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTask_Update(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -138,7 +143,7 @@ func TestTask_Update(t *testing.T) {
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
}
|
||||
err := task.Update(s)
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -161,7 +166,7 @@ func TestTask_Update(t *testing.T) {
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
}
|
||||
err := task.Update(s)
|
||||
err := task.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskDoesNotExist(err))
|
||||
})
|
||||
@ -177,7 +182,7 @@ func TestTask_Update(t *testing.T) {
|
||||
ListID: 1,
|
||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||
}
|
||||
err := task.Update(s)
|
||||
err := task.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrBucketLimitExceeded(err))
|
||||
})
|
||||
@ -194,7 +199,7 @@ func TestTask_Update(t *testing.T) {
|
||||
ListID: 1,
|
||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||
}
|
||||
err := task.Update(s)
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@ -208,7 +213,7 @@ func TestTask_Delete(t *testing.T) {
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
}
|
||||
err := task.Delete(s)
|
||||
err := task.Delete(s, &user.User{ID: 1})
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -440,13 +445,15 @@ func TestUpdateDone(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTask_ReadOne(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{ID: 1}
|
||||
err := task.ReadOne(s)
|
||||
err := task.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "task #1", task.Title)
|
||||
})
|
||||
@ -456,7 +463,7 @@ func TestTask_ReadOne(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{ID: 99999}
|
||||
err := task.ReadOne(s)
|
||||
err := task.ReadOne(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskDoesNotExist(err))
|
||||
})
|
||||
|
@ -17,6 +17,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
user2 "code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
@ -39,9 +40,9 @@ import (
|
||||
func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Check if the team extst
|
||||
_, err = GetTeamByID(s, tm.TeamID)
|
||||
team, err := GetTeamByID(s, tm.TeamID)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
@ -64,7 +65,15 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Insert the user
|
||||
_, err = s.Insert(tm)
|
||||
return
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&TeamMemberAddedEvent{
|
||||
Team: team,
|
||||
Member: user,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes a user from a team
|
||||
@ -78,7 +87,7 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Success 200 {object} models.Message "The user was successfully removed from the team."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /teams/{id}/members/{userID} [delete]
|
||||
func (tm *TeamMember) Delete(s *xorm.Session) (err error) {
|
||||
func (tm *TeamMember) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
total, err := s.Where("team_id = ?", tm.TeamID).Count(&TeamMember{})
|
||||
if err != nil {
|
||||
@ -110,7 +119,7 @@ func (tm *TeamMember) Delete(s *xorm.Session) (err error) {
|
||||
// @Success 200 {object} models.Message "The member right was successfully changed."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /teams/{id}/members/{userID}/admin [post]
|
||||
func (tm *TeamMember) Update(s *xorm.Session) (err error) {
|
||||
func (tm *TeamMember) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Find the numeric user id
|
||||
user, err := user2.GetUserByUsername(s, tm.Username)
|
||||
if err != nil {
|
||||
|
@ -101,7 +101,7 @@ func TestTeamMember_Delete(t *testing.T) {
|
||||
TeamID: 1,
|
||||
Username: "user1",
|
||||
}
|
||||
err := tm.Delete(s)
|
||||
err := tm.Delete(s, &user.User{ID: 1})
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -114,6 +114,8 @@ func TestTeamMember_Delete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTeamMember_Update(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -124,7 +126,7 @@ func TestTeamMember_Update(t *testing.T) {
|
||||
Username: "user1",
|
||||
Admin: true,
|
||||
}
|
||||
err := tm.Update(s)
|
||||
err := tm.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, tm.Admin) // Since this endpoint toggles the right, we should get a false for admin back.
|
||||
err = s.Commit()
|
||||
@ -148,7 +150,7 @@ func TestTeamMember_Update(t *testing.T) {
|
||||
Username: "user1",
|
||||
Admin: true,
|
||||
}
|
||||
err := tm.Update(s)
|
||||
err := tm.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, tm.Admin)
|
||||
err = s.Commit()
|
||||
|
@ -19,9 +19,10 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
@ -119,6 +120,11 @@ func GetTeamByID(s *xorm.Session, id int64) (team *Team, err error) {
|
||||
}
|
||||
|
||||
func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) {
|
||||
|
||||
if len(teams) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put the teams in a map to make assigning more info to it more efficient
|
||||
teamMap := make(map[int64]*Team, len(teams))
|
||||
var teamIDs []int64
|
||||
@ -177,7 +183,7 @@ func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) {
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the team"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /teams/{id} [get]
|
||||
func (t *Team) ReadOne(s *xorm.Session) (err error) {
|
||||
func (t *Team) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
team, err := GetTeamByID(s, t.ID)
|
||||
if team != nil {
|
||||
*t = *team
|
||||
@ -270,8 +276,10 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
metrics.UpdateCount(1, metrics.TeamCountKey)
|
||||
return
|
||||
return events.Dispatch(&TeamCreatedEvent{
|
||||
Team: t,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes a team
|
||||
@ -285,7 +293,7 @@ func (t *Team) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Failure 400 {object} web.HTTPError "Invalid team object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /teams/{id} [delete]
|
||||
func (t *Team) Delete(s *xorm.Session) (err error) {
|
||||
func (t *Team) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Delete the team
|
||||
_, err = s.ID(t.ID).Delete(&Team{})
|
||||
@ -311,8 +319,10 @@ func (t *Team) Delete(s *xorm.Session) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
metrics.UpdateCount(-1, metrics.TeamCountKey)
|
||||
return
|
||||
return events.Dispatch(&TeamDeletedEvent{
|
||||
Team: t,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// Update is the handler to create a team
|
||||
@ -328,7 +338,7 @@ func (t *Team) Delete(s *xorm.Session) (err error) {
|
||||
// @Failure 400 {object} web.HTTPError "Invalid team object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /teams/{id} [post]
|
||||
func (t *Team) Update(s *xorm.Session) (err error) {
|
||||
func (t *Team) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Check if we have a name
|
||||
if t.Name == "" {
|
||||
return ErrTeamNameCannotBeEmpty{}
|
||||
|
@ -62,13 +62,15 @@ func TestTeam_Create(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTeam_ReadOne(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
team := &Team{ID: 1}
|
||||
err := team.ReadOne(s)
|
||||
err := team.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "testteam1", team.Name)
|
||||
assert.Equal(t, "Lorem Ipsum", team.Description)
|
||||
@ -81,7 +83,7 @@ func TestTeam_ReadOne(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
team := &Team{ID: -1}
|
||||
err := team.ReadOne(s)
|
||||
err := team.ReadOne(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
})
|
||||
@ -91,7 +93,7 @@ func TestTeam_ReadOne(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
team := &Team{ID: 99999}
|
||||
err := team.ReadOne(s)
|
||||
err := team.ReadOne(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
})
|
||||
@ -113,6 +115,8 @@ func TestTeam_ReadAll(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTeam_Update(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -122,7 +126,7 @@ func TestTeam_Update(t *testing.T) {
|
||||
ID: 1,
|
||||
Name: "SomethingNew",
|
||||
}
|
||||
err := team.Update(s)
|
||||
err := team.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
@ -140,7 +144,7 @@ func TestTeam_Update(t *testing.T) {
|
||||
ID: 1,
|
||||
Name: "",
|
||||
}
|
||||
err := team.Update(s)
|
||||
err := team.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamNameCannotBeEmpty(err))
|
||||
})
|
||||
@ -153,13 +157,15 @@ func TestTeam_Update(t *testing.T) {
|
||||
ID: 9999,
|
||||
Name: "SomethingNew",
|
||||
}
|
||||
err := team.Update(s)
|
||||
err := team.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTeamDoesNotExist(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTeam_Delete(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@ -168,7 +174,7 @@ func TestTeam_Delete(t *testing.T) {
|
||||
team := &Team{
|
||||
ID: 1,
|
||||
}
|
||||
err := team.Delete(s)
|
||||
err := team.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
@ -20,6 +20,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
@ -30,5 +32,6 @@ func TestMain(m *testing.M) {
|
||||
user.InitTests()
|
||||
files.InitTests()
|
||||
models.SetupTests()
|
||||
events.Fake()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
@ -226,7 +226,7 @@ func InsertFromStructure(str []*models.NamespaceWithLists, user *user.User) (err
|
||||
return err
|
||||
}
|
||||
buckets := bucketsIn.([]*models.Bucket)
|
||||
err = buckets[0].Delete(s)
|
||||
err = buckets[0].Delete(s, user)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return err
|
||||
|
@ -20,6 +20,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
@ -37,5 +39,6 @@ func TestMain(m *testing.M) {
|
||||
files.InitTests()
|
||||
user.InitTests()
|
||||
models.SetupTests()
|
||||
events.Fake()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ func RenewToken(c echo.Context) (err error) {
|
||||
if typ == auth.AuthTypeLinkShare {
|
||||
share := &models.LinkSharing{}
|
||||
share.ID = int64(claims["id"].(float64))
|
||||
err := share.ReadOne(s)
|
||||
err := share.ReadOne(s, share)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
|
@ -147,7 +147,7 @@ func GetTaskAttachment(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Get the attachment incl file
|
||||
err = taskAttachment.ReadOne(s)
|
||||
err = taskAttachment.ReadOne(s, auth)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
|
@ -318,7 +318,7 @@ func (vcls *VikunjaCaldavListStorage) UpdateResource(rpath, content string) (*da
|
||||
}
|
||||
|
||||
// Update the task
|
||||
err = vTask.Update(s)
|
||||
err = vTask.Update(s, vcls.user)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return nil, err
|
||||
@ -354,7 +354,7 @@ func (vcls *VikunjaCaldavListStorage) DeleteResource(rpath string) error {
|
||||
}
|
||||
|
||||
// Delete it
|
||||
err = vcls.task.Delete(s)
|
||||
err = vcls.task.Delete(s, vcls.user)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return err
|
||||
@ -458,7 +458,7 @@ func (vcls *VikunjaCaldavListStorage) getListRessource(isCollection bool) (rr Vi
|
||||
log.Errorf("User %v tried to access a caldav resource (List %v) which they are not allowed to access", vcls.user.Username, vcls.list.ID)
|
||||
return rr, models.ErrUserDoesNotHaveAccessToList{ListID: vcls.list.ID}
|
||||
}
|
||||
err = vcls.list.ReadOne(s)
|
||||
err = vcls.list.ReadOne(s, vcls.user)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return
|
||||
|
@ -71,7 +71,7 @@ func setupMetrics(a *echo.Group) {
|
||||
}
|
||||
}
|
||||
|
||||
a.GET("/metrics", echo.WrapHandler(promhttp.Handler()))
|
||||
a.GET("/metrics", echo.WrapHandler(promhttp.HandlerFor(metrics.GetRegistry(), promhttp.HandlerOpts{})))
|
||||
}
|
||||
|
||||
func setupMetricsMiddleware(a *echo.Group) {
|
||||
|
27
pkg/user/events.go
Normal file
27
pkg/user/events.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 user
|
||||
|
||||
// CreatedEvent represents a CreatedEvent event
|
||||
type CreatedEvent struct {
|
||||
User *User
|
||||
}
|
||||
|
||||
// TopicName defines the name for CreatedEvent
|
||||
func (t *CreatedEvent) Name() string {
|
||||
return "user.created"
|
||||
}
|
45
pkg/user/listeners.go
Normal file
45
pkg/user/listeners.go
Normal file
@ -0,0 +1,45 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 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 user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
)
|
||||
|
||||
func RegisterListeners() {
|
||||
events.RegisterListener((&CreatedEvent{}).Name(), &IncreaseUserCounter{})
|
||||
}
|
||||
|
||||
///////
|
||||
// User Events
|
||||
|
||||
// IncreaseUserCounter represents a listener
|
||||
type IncreaseUserCounter struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the IncreaseUserCounter listener
|
||||
func (s *IncreaseUserCounter) Name() string {
|
||||
return "increase.user.counter"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event IncreaseUserCounter listens on is fired
|
||||
func (s *IncreaseUserCounter) Handle(payload message.Payload) (err error) {
|
||||
return keyvalue.IncrBy(metrics.UserCountKey, 1)
|
||||
}
|
@ -18,6 +18,7 @@ package user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
@ -37,4 +38,6 @@ func InitTests() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
events.Fake()
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ package user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"xorm.io/xorm"
|
||||
@ -70,15 +70,19 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update the metrics
|
||||
metrics.UpdateCount(1, metrics.ActiveUsersKey)
|
||||
|
||||
// Get the full new User
|
||||
newUserOut, err := GetUserByID(s, user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&CreatedEvent{
|
||||
User: newUserOut,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sendConfirmEmail(user)
|
||||
|
||||
return newUserOut, err
|
||||
|
Reference in New Issue
Block a user