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:
parent
a71aa0c898
commit
0ab9ce9ec4
70 changed files with 1636 additions and 283 deletions
|
@ -1,5 +1,5 @@
|
|||
run:
|
||||
timeout: 5m
|
||||
timeout: 15m
|
||||
tests: true
|
||||
|
||||
linters:
|
||||
|
|
|
@ -136,6 +136,10 @@ log:
|
|||
http: "stdout"
|
||||
# Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
|
||||
echo: "off"
|
||||
# Whether or not to log events. Useful for debugging. Possible values are stdout, stderr, file or off to disable events logging.
|
||||
events: "stdout"
|
||||
# The log level for event log messages. Possible values (case-insensitive) are ERROR, INFO, DEBUG.
|
||||
eventslevel: "info"
|
||||
|
||||
ratelimit:
|
||||
# whether or not to enable the rate limit
|
||||
|
|
195
docs/content/doc/development/events-and-listeners.md
Normal file
195
docs/content/doc/development/events-and-listeners.md
Normal file
|
@ -0,0 +1,195 @@
|
|||
---
|
||||
date: 2018-10-13T19:26:34+02:00
|
||||
title: "Events and Listeners"
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "development"
|
||||
---
|
||||
|
||||
# Events and Listeners
|
||||
|
||||
Vikunja provides a simple observer pattern mechanism through events and listeners.
|
||||
The basic principle of events is always the same: Something happens (=An event is fired) and something reacts to it (=A listener is called).
|
||||
|
||||
Vikunja supports this principle through the `events` package.
|
||||
It is built upon the excellent [watermill](https://watermill.io) library.
|
||||
|
||||
Currently, it only supports dispatching events through Go Channels which makes it configuration-less.
|
||||
More methods of dispatching events (like kafka or rabbitmq) are available in watermill and could be enabled with a PR.
|
||||
|
||||
This document explains how events and listeners work in Vikunja, how to use them and how to create new ones.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Events
|
||||
|
||||
### Definition
|
||||
|
||||
Each event has to implement this interface:
|
||||
|
||||
```golang
|
||||
type Event interface {
|
||||
Name() string
|
||||
}
|
||||
```
|
||||
|
||||
An event can contain whatever data you need.
|
||||
|
||||
When an event is dispatched, all of the data it contains will be marshaled into json for dispatching.
|
||||
You then get the event with all its data back in the listener, see below.
|
||||
|
||||
#### Naming Convention
|
||||
|
||||
Event names should roughly have the entity they're dealing with on the left and the action on the right of the name, separated by `.`.
|
||||
There's no limit to how "deep" or specifig an event name can be.
|
||||
|
||||
The name should have the most general concept it's describing at the left, getting more specific on the right of it.
|
||||
|
||||
#### Location
|
||||
|
||||
All events for a package should be declared in the `events.go` file of that package.
|
||||
|
||||
### Creating a New Event
|
||||
|
||||
The easiest way to create a new event is to generate it with mage:
|
||||
|
||||
```
|
||||
mage dev:make-event <event-name> <package>
|
||||
```
|
||||
|
||||
The function takes the name of the event as the first argument and the package where the event should be created as the second argument.
|
||||
Events will be appended to the `pkg/<module>/events.go` file.
|
||||
Both parameters are mandatory.
|
||||
|
||||
The event type name is automatically camel-cased and gets the `Event` suffix if the provided name does not already have one.
|
||||
The event name is derived from the type name and stripped of the `.event` suffix.
|
||||
|
||||
The generated event will look something like the example below.
|
||||
|
||||
### Dispatching events
|
||||
|
||||
To dispatch an event, simply call the `events.Dispatch` method and pass in the event as parameter.
|
||||
|
||||
### Example
|
||||
|
||||
The `TaskCreatedEvent` is declared in the `pkg/models/events.go` file as follows:
|
||||
|
||||
```golang
|
||||
// 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"
|
||||
}
|
||||
```
|
||||
|
||||
It is dispatched in the `createTask` function of the `models` package:
|
||||
|
||||
```golang
|
||||
func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err error) {
|
||||
|
||||
// ...
|
||||
|
||||
err = events.Dispatch(&TaskCreatedEvent{
|
||||
Task: t,
|
||||
Doer: a,
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, the curent task and doer are injected into it.
|
||||
|
||||
### Special Events
|
||||
|
||||
#### `BootedEvent`
|
||||
|
||||
Once Vikunja is fully initialized, right before the api web server is started, this event is fired.
|
||||
|
||||
## Listeners
|
||||
|
||||
A listener is a piece of code that gets executed asynchronously when an event is dispatched.
|
||||
|
||||
A single event can have multiple listeners who are independent of each other.
|
||||
|
||||
### Definition
|
||||
|
||||
All listeners must implement this interface:
|
||||
|
||||
```golang
|
||||
// Listener represents something that listens to events
|
||||
type Listener interface {
|
||||
Handle(payload message.Payload) error
|
||||
Name() string
|
||||
}
|
||||
```
|
||||
|
||||
The `Handle` method is executed when the event this listener listens on is dispatched.
|
||||
* As the single parameter, it gets the payload of the event, which is the event struct when it was dispatched decoded as json object and passed as a slice of bytes.
|
||||
To use it you'll need to unmarshal it. Unfortunately there's no way to pass an already populated event object to the function because we would not know what type it has when parsing it.
|
||||
* If the handler returns an error, the listener is retried 5 times, with an exponentional back-off period in between retries.
|
||||
If it still fails after the fifth retry, the event is nack'd and it's up to the event dispatcher to resend it.
|
||||
You can learn more about this mechanism in the [watermill documentation](https://watermill.io/docs/middlewares/#retry).
|
||||
|
||||
The `Name` method needs to return a unique listener name for this listener.
|
||||
It should follow the same convention as event names, see above.
|
||||
|
||||
### Creating a New Listener
|
||||
|
||||
The easiest way to create a new listener for an event is with mage:
|
||||
|
||||
```
|
||||
mage dev:make-listener <listener-name> <event-name> <package>
|
||||
```
|
||||
|
||||
This will create a new listener type in the `pkg/<package>/listners.go` file and implement the `Handle` and `Name` methods.
|
||||
It will also pre-generate some boilerplate code to unmarshal the event from the payload.
|
||||
|
||||
Furthermore, it will register the listener for its event in the `RegisterListeners()` method of the same file.
|
||||
This function is called at startup and has to contain all events you want to listen for.
|
||||
|
||||
### Listening for Events
|
||||
|
||||
To listen for an event, you need to register the listener for the event it should be called for.
|
||||
This usually happens in the `RegisterListeners()` method in `pkg/<package>/listners.go` which is called at start up.
|
||||
|
||||
The listener will never be executed if it hasn't been registered.
|
||||
|
||||
See the example below.
|
||||
|
||||
### Example
|
||||
|
||||
```golang
|
||||
// RegisterListeners registers all event listeners
|
||||
func RegisterListeners() {
|
||||
events.RegisterListener((&ListCreatedEvent{}).Name(), &IncreaseListCounter{})
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
When testing, you should call the `events.Fake()` method in the `TestMain` function of the package you want to test.
|
||||
This prevents any events from being fired and lets you assert an event has been dispatched like so:
|
||||
|
||||
```golang
|
||||
events.AssertDispatched(t, &TaskCreatedEvent{})
|
||||
```
|
|
@ -446,6 +446,18 @@ Echo has its own logging which usually is unnessecary, which is why it is disabl
|
|||
|
||||
Default: `off`
|
||||
|
||||
### events
|
||||
|
||||
Whether or not to log events. Useful for debugging. Possible values are stdout, stderr, file or off to disable events logging.
|
||||
|
||||
Default: `stdout`
|
||||
|
||||
### eventslevel
|
||||
|
||||
The log level for event log messages. Possible values (case-insensitive) are ERROR, INFO, DEBUG.
|
||||
|
||||
Default: `info`
|
||||
|
||||
---
|
||||
|
||||
## ratelimit
|
||||
|
|
2
docs/themes/vikunja
vendored
2
docs/themes/vikunja
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 958219fc84db455ed58d7a4380bbffc8d04fd5cf
|
||||
Subproject commit 1ebcbbb645ad20ea683feef2804314a6c658799b
|
14
go.mod
14
go.mod
|
@ -18,9 +18,9 @@ module code.vikunja.io/api
|
|||
|
||||
require (
|
||||
4d63.com/tz v1.2.0
|
||||
code.vikunja.io/web v0.0.0-20201223143420-588abb73703a
|
||||
dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363 // indirect
|
||||
code.vikunja.io/web v0.0.0-20210131201003-26386be9a9ae
|
||||
gitea.com/xorm/xorm-redis-cache v0.2.0
|
||||
github.com/ThreeDotsLabs/watermill v1.1.1
|
||||
github.com/adlio/trello v1.8.0
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
|
||||
|
@ -28,7 +28,6 @@ require (
|
|||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
|
||||
github.com/client9/misspell v0.3.4
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/coreos/go-oidc/v3 v3.0.0
|
||||
github.com/cweill/gotests v1.6.0
|
||||
github.com/d4l3k/messagediff v1.2.1
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
|
@ -37,7 +36,7 @@ require (
|
|||
github.com/fzipp/gocyclo v0.3.1
|
||||
github.com/gabriel-vasile/mimetype v1.1.2
|
||||
github.com/getsentry/sentry-go v0.9.0
|
||||
github.com/go-errors/errors v1.1.1
|
||||
github.com/go-errors/errors v1.1.1 // indirect
|
||||
github.com/go-redis/redis/v8 v8.4.11
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.5.0
|
||||
|
@ -82,8 +81,9 @@ require (
|
|||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
|
||||
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 // indirect
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
|
@ -94,9 +94,9 @@ require (
|
|||
honnef.co/go/tools v0.0.1-2020.1.5
|
||||
src.techknowlogick.com/xgo v1.2.1-0.20201205054505-b97762e7a76b
|
||||
src.techknowlogick.com/xormigrate v1.4.0
|
||||
xorm.io/builder v0.3.7
|
||||
xorm.io/builder v0.3.8
|
||||
xorm.io/core v0.7.3
|
||||
xorm.io/xorm v1.0.5
|
||||
xorm.io/xorm v1.0.7
|
||||
)
|
||||
|
||||
replace (
|
||||
|
|
80
go.sum
80
go.sum
|
@ -38,14 +38,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
|||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
code.vikunja.io/web v0.0.0-20201218134444-505d0e77fac7 h1:iS3TFA+y1If6DEbqzad5Ge7TI1NxZr9BevC/dU4ygEo=
|
||||
code.vikunja.io/web v0.0.0-20201218134444-505d0e77fac7/go.mod h1:vDWiCtftF6LNCCrem7mjstPWMgzLUvMW/L4YwIQ1Voo=
|
||||
code.vikunja.io/web v0.0.0-20201222144643-6fa2fb587215 h1:O5zMWgcnVDVLaQUawgdsv/jX/4SUUAvSedvRR+5+x2o=
|
||||
code.vikunja.io/web v0.0.0-20201222144643-6fa2fb587215/go.mod h1:OgFO06HN1KpA4S7Dw/QAIeygiUPSeGJJn1ykz/sjZdU=
|
||||
code.vikunja.io/web v0.0.0-20201223143420-588abb73703a h1:LaWCucY5Pp30EIMgGOvdVFNss5OhIAwrAO8PuFVRUfw=
|
||||
code.vikunja.io/web v0.0.0-20201223143420-588abb73703a/go.mod h1:OgFO06HN1KpA4S7Dw/QAIeygiUPSeGJJn1ykz/sjZdU=
|
||||
dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363 h1:o4lAkfETerCnr1kF9/qwkwjICnU+YLHNDCM8h2xj7as=
|
||||
dmitri.shuralyov.com/go/generated v0.0.0-20170818220700-b1254a446363/go.mod h1:WG7q7swWsS2f9PYpt5DoEP/EBYWx8We5UoRltn9vJl8=
|
||||
code.vikunja.io/web v0.0.0-20210131201003-26386be9a9ae h1:qqgwoWjKrpIOdrIR0FPawiHLZTRYwS9MBgwH1eZJJqA=
|
||||
code.vikunja.io/web v0.0.0-20210131201003-26386be9a9ae/go.mod h1:OgFO06HN1KpA4S7Dw/QAIeygiUPSeGJJn1ykz/sjZdU=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
|
@ -72,6 +66,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
|
|||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/ThreeDotsLabs/watermill v1.1.1 h1:+9NXqWQvplzxBru2CIInvVOZeKUnM+Nysg42fInl5sY=
|
||||
github.com/ThreeDotsLabs/watermill v1.1.1/go.mod h1:Qd1xNFxolCAHCzcMrm6RnjW0manbvN+DJVWc1MWRFlI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
|
@ -111,7 +107,10 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBW
|
|||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2 h1:t8KYCwSKsOEZBFELI4Pn/phbp38iJ1RRAkDFNin1aak=
|
||||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c=
|
||||
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
|
@ -134,7 +133,6 @@ github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
|
|||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
|
@ -147,8 +145,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cweill/gotests v1.5.3 h1:k3t4wW/x/YNixWZJhUIn+mivmK5iV1tJVOwVYkx0UcU=
|
||||
github.com/cweill/gotests v1.5.3/go.mod h1:XZYOJkGVkCRoymaIzmp9Wyi3rUgfA3oOnkuljYrjFV8=
|
||||
github.com/cweill/gotests v1.6.0 h1:KJx+/p4EweijYzqPb4Y/8umDCip1Cv6hEVyOx0mE9W8=
|
||||
github.com/cweill/gotests v1.6.0/go.mod h1:CaRYbxQZGQOxXDvM9l0XJVV2Tjb2E5H53vq+reR2GrA=
|
||||
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
|
||||
|
@ -211,6 +207,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
|
|||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
|
||||
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
|
||||
|
@ -241,18 +239,6 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
|||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-redis/redis/v8 v8.4.2 h1:gKRo1KZ+O3kXRfxeRblV5Tr470d2YJZJVIAv2/S8960=
|
||||
github.com/go-redis/redis/v8 v8.4.2/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M=
|
||||
github.com/go-redis/redis/v8 v8.4.4 h1:fGqgxCTR1sydaKI00oQf3OmkU/DIe/I/fYXvGklCIuc=
|
||||
github.com/go-redis/redis/v8 v8.4.4/go.mod h1:nA0bQuF0i5JFx4Ta9RZxGKXFrQ8cRWntra97f0196iY=
|
||||
github.com/go-redis/redis/v8 v8.4.6 h1:a4i+zYK6Mq2A2qAz0R6n6xo9dfnh7HdlRU/QvrlpfPI=
|
||||
github.com/go-redis/redis/v8 v8.4.6/go.mod h1:nA0bQuF0i5JFx4Ta9RZxGKXFrQ8cRWntra97f0196iY=
|
||||
github.com/go-redis/redis/v8 v8.4.7 h1:l0/Hkj3HLA46eJSvHGLhnF+KHBrmsyipK84ycJXFZxw=
|
||||
github.com/go-redis/redis/v8 v8.4.7/go.mod h1:nA0bQuF0i5JFx4Ta9RZxGKXFrQ8cRWntra97f0196iY=
|
||||
github.com/go-redis/redis/v8 v8.4.8 h1:sEG4g6Jq4hvQzbrNsVDNTDdxFCUnFC0jxuOp6tgALlA=
|
||||
github.com/go-redis/redis/v8 v8.4.8/go.mod h1:/cTZsrSn1DPqRuOnSDuyH2OSvd9iX0iUGT0s7hYGIAg=
|
||||
github.com/go-redis/redis/v8 v8.4.9 h1:ixEQSxNnzo6zh/dmoZIHl9DmyX3mHV5a2p6OasPR93k=
|
||||
github.com/go-redis/redis/v8 v8.4.9/go.mod h1:d5yY/TlkQyYBSBHnXUmnf1OrHbyQere5JV4dLKwvXmo=
|
||||
github.com/go-redis/redis/v8 v8.4.10 h1:fWdl0RBmVibUDOp8bqz1e2Yy9dShOeIeWsiAifYk06Y=
|
||||
github.com/go-redis/redis/v8 v8.4.10/go.mod h1:d5yY/TlkQyYBSBHnXUmnf1OrHbyQere5JV4dLKwvXmo=
|
||||
github.com/go-redis/redis/v8 v8.4.11 h1:t2lToev01VTrqYQcv+QFbxtGgcf64K+VUMgf9Ap6A/E=
|
||||
github.com/go-redis/redis/v8 v8.4.11/go.mod h1:d5yY/TlkQyYBSBHnXUmnf1OrHbyQere5JV4dLKwvXmo=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
|
@ -261,8 +247,6 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
|
|||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.4.1 h1:Qz9y0wUOXPHzKhK6C79A/menChtEu/xd0Dn5ngVyMD0=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.4.1/go.mod h1:P4L3WxgOsCLbAeUC50qX5rdj1ULZfUMqgCbqah3OH5U=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.5.0 h1:fFJGHhFdcwy48oTLHvr0WRQ09rGiZE+as9ElvbRWS+c=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.5.0/go.mod h1:P4L3WxgOsCLbAeUC50qX5rdj1ULZfUMqgCbqah3OH5U=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
|
@ -346,12 +330,12 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
|
|||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20201107091007-3b93a8888063 h1:dKprcOvlsvqfWn/iGvz+oYuC2axESeSMuF8dDrWMNsE=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20201107091007-3b93a8888063/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20210104184537-8eed68eb605f h1:wHGrcNkjqm/QJeljJ9bkFWtND9I0ASarwpBlRcwRymk=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20210104184537-8eed68eb605f/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
|
@ -369,10 +353,12 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt
|
|||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
|
@ -390,8 +376,6 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m
|
|||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/iancoleman/strcase v0.1.2 h1:gnomlvw9tnV3ITTAxzKSgTF+8kFWcU/f+TgttpXGz1U=
|
||||
github.com/iancoleman/strcase v0.1.2/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
||||
github.com/iancoleman/strcase v0.1.3 h1:dJBk1m2/qjL1twPLf68JND55vvivMupZ4wIzE8CTdBw=
|
||||
github.com/iancoleman/strcase v0.1.3/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
|
@ -526,9 +510,9 @@ github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
|||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4 h1:uj4xhotfY92Y1Oa6n6HUiFn87CdoEHYUlTy0+IgbLrs=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
|
||||
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
|
||||
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
|
@ -570,8 +554,6 @@ github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/
|
|||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
|
||||
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
|
@ -612,6 +594,7 @@ github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
|||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
|
||||
|
@ -859,8 +842,6 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/otel v0.14.0 h1:YFBEfjCk9MTjaytCNSUkp9Q8lF7QJezA06T71FbQxLQ=
|
||||
go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw=
|
||||
go.opentelemetry.io/otel v0.15.0 h1:CZFy2lPhxd4HlhZnYK8gRyDotksO3Ip9rBweY1vVYJw=
|
||||
go.opentelemetry.io/otel v0.15.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA=
|
||||
go.opentelemetry.io/otel v0.16.0 h1:uIWEbdeb4vpKPGITLsRVUS44L5oDbDUCZxn8lkxhmgw=
|
||||
go.opentelemetry.io/otel v0.16.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
|
@ -965,7 +946,6 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
|
@ -990,18 +970,6 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7O
|
|||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210112200429-01de73cf58bd h1:0n2rzLq6xLtV9OFaT0BF2syUkjOwRrJ1zvXY5hH7Kkc=
|
||||
golang.org/x/oauth2 v0.0.0-20210112200429-01de73cf58bd/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423 h1:/hEknzWkMPCjTo7StMHRrBRa8YBbXuBWfck8680k3RE=
|
||||
golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3 h1:BaN3BAqnopnKjvl+15DYP6LLrbBHfbfmlFYzmFj/Q9Q=
|
||||
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210125201302-af13f521f196 h1:w0u30BeG/TALEc6xVf1Klaz2+etRR4K6jxhRkWCqt4g=
|
||||
golang.org/x/oauth2 v0.0.0-20210125201302-af13f521f196/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210126194326-f9ce19ea3013 h1:55H5j7lotzuFCEOKDsMch+fRNUQ9DgtyHOUP31FNqKc=
|
||||
golang.org/x/oauth2 v0.0.0-20210126194326-f9ce19ea3013/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c h1:HiAZXo96zOhVhtFHchj/ojzoxCFiPrp9/j0GtS38V3g=
|
||||
golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -1080,10 +1048,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuF
|
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
|
||||
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0 h1:n+DPcgTwkgWzIFpLmoimYR2K2b0Ga5+Os4kayIN0vGo=
|
||||
golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq0iqbV3ZA+u6+0TlNCo=
|
||||
golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
@ -1099,6 +1065,8 @@ golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -1122,8 +1090,6 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
|
|||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628034336-212fb13d595e h1:ZlQjfVdpDxeqxRfmO30CdqWWzTvgRCj0MxaUVfxEG1k=
|
||||
golang.org/x/tools v0.0.0-20190628034336-212fb13d595e/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
|
@ -1336,11 +1302,13 @@ src.techknowlogick.com/xormigrate v1.4.0 h1:gAfLoDwcVfMiFhSXg5Qwm7LNnG1iUbBVDUNf
|
|||
src.techknowlogick.com/xormigrate v1.4.0/go.mod h1:xCtbAK00lJ0v4zP2O6VBVMG3RHm7W5Yo1Dz0r9kL/ho=
|
||||
xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
|
||||
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/builder v0.3.8 h1:P/wPgRqa9kX5uE0aA1/ukJ23u9KH0aSRpHLwDKXigSE=
|
||||
xorm.io/builder v0.3.8/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/core v0.7.3 h1:W8ws1PlrnkS1CZU1YWaYLMQcQilwAmQXU0BJDJon+H0=
|
||||
xorm.io/core v0.7.3/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM=
|
||||
xorm.io/xorm v1.0.1 h1:/lITxpJtkZauNpdzj+L9CN/3OQxZaABrbergMcJu+Cw=
|
||||
xorm.io/xorm v1.0.1/go.mod h1:o4vnEsQ5V2F1/WK6w4XTwmiWJeGj82tqjAnHe44wVHY=
|
||||
xorm.io/xorm v1.0.2 h1:kZlCh9rqd1AzGwWitcrEEqHE1h1eaZE/ujU5/2tWEtg=
|
||||
xorm.io/xorm v1.0.2/go.mod h1:o4vnEsQ5V2F1/WK6w4XTwmiWJeGj82tqjAnHe44wVHY=
|
||||
xorm.io/xorm v1.0.5 h1:LRr5PfOUb4ODPR63YwbowkNDwcolT2LnkwP/TUaMaB0=
|
||||
xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
|
||||
xorm.io/xorm v1.0.7 h1:26yBTDVI+CfQpVz2Y88fISh+aiJXIPP4eNoTJlwzsC4=
|
||||
xorm.io/xorm v1.0.7/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
|
||||
|
|
159
magefile.go
159
magefile.go
|
@ -24,6 +24,7 @@ import (
|
|||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
@ -60,13 +61,15 @@ var (
|
|||
|
||||
// Aliases are mage aliases of targets
|
||||
Aliases = map[string]interface{}{
|
||||
"build": Build.Build,
|
||||
"do-the-swag": DoTheSwag,
|
||||
"check:got-swag": Check.GotSwag,
|
||||
"release:os-package": Release.OsPackage,
|
||||
"dev:create-migration": Dev.CreateMigration,
|
||||
"generate-docs": GenerateDocs,
|
||||
"check:golangci-fix": Check.GolangciFix,
|
||||
"build": Build.Build,
|
||||
"do-the-swag": DoTheSwag,
|
||||
"check:got-swag": Check.GotSwag,
|
||||
"release:os-package": Release.OsPackage,
|
||||
"dev:make-migration": Dev.MakeMigration,
|
||||
"dev:make-event": Dev.MakeEvent,
|
||||
"dev:make-listener": Dev.MakeListener,
|
||||
"generate-docs": GenerateDocs,
|
||||
"check:golangci-fix": Check.GolangciFix,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -294,6 +297,25 @@ func moveFile(src, dst string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func appendToFile(filename, content string) error {
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString(content)
|
||||
return err
|
||||
}
|
||||
|
||||
const InfoColor = "\033[1;32m%s\033[0m"
|
||||
|
||||
func printSuccess(text string, args ...interface{}) {
|
||||
text = fmt.Sprintf(text, args...)
|
||||
fmt.Printf(InfoColor+"\n", text)
|
||||
}
|
||||
|
||||
// Formats the code using go fmt
|
||||
func Fmt() {
|
||||
mg.Deps(initVars)
|
||||
|
@ -695,7 +717,7 @@ func (Release) Packages() error {
|
|||
type Dev mg.Namespace
|
||||
|
||||
// Creates a new bare db migration skeleton in pkg/migration with the current date
|
||||
func (Dev) CreateMigration() error {
|
||||
func (Dev) MakeMigration() error {
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Enter the name of the struct: ")
|
||||
|
@ -747,14 +769,129 @@ func init() {
|
|||
})
|
||||
}
|
||||
`
|
||||
f, err := os.Create(RootPath + "/pkg/migration/" + date + ".go")
|
||||
filename := "/pkg/migration/" + date + ".go"
|
||||
f, err := os.Create(RootPath + filename)
|
||||
defer f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.WriteString(migration)
|
||||
return err
|
||||
if _, err := f.WriteString(migration); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printSuccess("Migration has been created at %s!", filename)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new event. Takes the name of the event as the first argument and the module where the event should be created as the second argument. Events will be appended to the pkg/<module>/events.go file.
|
||||
func (Dev) MakeEvent(name, module string) error {
|
||||
|
||||
name = strcase.ToCamel(name)
|
||||
|
||||
if !strings.HasSuffix(name, "Event") {
|
||||
name += "Event"
|
||||
}
|
||||
|
||||
eventName := strings.ReplaceAll(strcase.ToDelimited(name, '.'), ".event", "")
|
||||
|
||||
newEventCode := `
|
||||
// ` + name + ` represents a ` + name + ` event
|
||||
type ` + name + ` struct {
|
||||
}
|
||||
|
||||
// Name defines the name for ` + name + `
|
||||
func (t *` + name + `) Name() string {
|
||||
return "` + eventName + `"
|
||||
}
|
||||
`
|
||||
filename := "./pkg/" + module + "/events.go"
|
||||
if err := appendToFile(filename, newEventCode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printSuccess("The new event has been created successfully! Head over to %s and adjust its content.", filename)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new listener for an event. Takes the name of the listener, the name of the event to listen to and the module where everything should be placed as parameters.
|
||||
func (Dev) MakeListener(name, event, module string) error {
|
||||
name = strcase.ToCamel(name)
|
||||
listenerName := strcase.ToDelimited(name, '.')
|
||||
listenerCode := `
|
||||
// ` + name + ` represents a listener
|
||||
type ` + name + ` struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the ` + name + ` listener
|
||||
func (s *` + name + `) Name() string {
|
||||
return "` + listenerName + `"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event ` + name + ` listens on is fired
|
||||
func (s *` + name + `) Handle(payload message.Payload) (err error) {
|
||||
event := &` + event + `{}
|
||||
err = json.Unmarshal(payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
`
|
||||
filename := "./pkg/" + module + "/listeners.go"
|
||||
|
||||
//////
|
||||
// Register the listener
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var idx int64 = 0
|
||||
for scanner.Scan() {
|
||||
if scanner.Text() == "}" {
|
||||
//idx -= int64(len(scanner.Text()))
|
||||
break
|
||||
}
|
||||
idx += int64(len(scanner.Bytes()) + 1)
|
||||
}
|
||||
file.Close()
|
||||
|
||||
registerListenerCode := ` events.RegisterListener((&` + event + `{}).Name(), &` + name + `{})
|
||||
`
|
||||
|
||||
f, err := os.OpenFile(filename, os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.Seek(idx, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
remainder, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Seek(idx, 0)
|
||||
f.Write([]byte(registerListenerCode))
|
||||
f.Write(remainder)
|
||||
|
||||
///////
|
||||
// Append the listener code
|
||||
if err := appendToFile(filename, listenerCode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printSuccess("The new listener has been created successfully! Head over to %s and adjust its content.", filename)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type configOption struct {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue