Subscriptions and notifications for namespaces, tasks and lists (#786)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/786 Co-authored-by: konrad <konrad@kola-entertainments.de> Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
parent
618b464ca3
commit
e7875ecb3b
25 changed files with 1714 additions and 23 deletions
|
@ -1,11 +1,10 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Errors"
|
||||
draft: false
|
||||
type: "doc"
|
||||
draft: false type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
---
|
||||
|
||||
# Errors
|
||||
|
@ -142,3 +141,10 @@ This document describes the different errors Vikunja can return.
|
|||
|-----------|------------------|-------------|
|
||||
| 11001 | 404 | The saved filter does not exist. |
|
||||
| 11002 | 412 | Saved filters are not available for link shares. |
|
||||
|
||||
## Subscriptions
|
||||
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|-------------|
|
||||
| 12001 | 412 | The subscription entity type is invalid. |
|
||||
| 12002 | 412 | The user is already subscribed to the entity itself or a parent entity. |
|
||||
|
|
|
@ -827,7 +827,7 @@ func (s *` + name + `) Name() string {
|
|||
return "` + listenerName + `"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event ` + name + ` listens on is fired
|
||||
// Handle 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)
|
||||
|
|
35
pkg/db/fixtures/subscriptions.yml
Normal file
35
pkg/db/fixtures/subscriptions.yml
Normal file
|
@ -0,0 +1,35 @@
|
|||
- id: 1
|
||||
entity_type: 3 # Task
|
||||
entity_id: 2
|
||||
user_id: 1
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 2
|
||||
entity_type: 1 # Namespace
|
||||
entity_id: 6
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 3
|
||||
entity_type: 2 # List
|
||||
entity_id: 12 # belongs to namespace 7
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 4
|
||||
entity_type: 3 # Task
|
||||
entity_id: 22 # belongs to list 13 which belongs to namespace 8
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 5
|
||||
entity_type: 1 # Namespace
|
||||
entity_id: 8
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 6
|
||||
entity_type: 2 # List
|
||||
entity_id: 13
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
||||
- id: 7
|
||||
entity_type: 3 # Task
|
||||
entity_id: 26
|
||||
user_id: 6
|
||||
created: 2021-02-01 15:13:12
|
49
pkg/migration/20210209204715.go
Normal file
49
pkg/migration/20210209204715.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 migration
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type subscriptions20210209204715 struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id"`
|
||||
EntityType int `xorm:"index not null" json:"-"`
|
||||
EntityID int64 `xorm:"bigint index not null" json:"entity_id"`
|
||||
UserID int64 `xorm:"bigint index not null" json:"-"`
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
}
|
||||
|
||||
func (subscriptions20210209204715) TableName() string {
|
||||
return "subscriptions"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210209204715",
|
||||
Description: "",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(subscriptions20210209204715{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1423,3 +1423,63 @@ func (err ErrSavedFilterNotAvailableForLinkShare) HTTPError() web.HTTPError {
|
|||
Message: "Saved filters are not available for link shares.",
|
||||
}
|
||||
}
|
||||
|
||||
// =============
|
||||
// Subscriptions
|
||||
// =============
|
||||
|
||||
// ErrUnknownSubscriptionEntityType represents an error where a subscription entity type is unknown
|
||||
type ErrUnknownSubscriptionEntityType struct {
|
||||
EntityType SubscriptionEntityType
|
||||
}
|
||||
|
||||
// IsErrUnknownSubscriptionEntityType checks if an error is ErrUnknownSubscriptionEntityType.
|
||||
func IsErrUnknownSubscriptionEntityType(err error) bool {
|
||||
_, ok := err.(*ErrUnknownSubscriptionEntityType)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrUnknownSubscriptionEntityType) Error() string {
|
||||
return fmt.Sprintf("Subscription entity type is unkowns [EntityType: %d]", err.EntityType)
|
||||
}
|
||||
|
||||
// ErrCodeUnknownSubscriptionEntityType holds the unique world-error code of this error
|
||||
const ErrCodeUnknownSubscriptionEntityType = 12001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUnknownSubscriptionEntityType) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeUnknownSubscriptionEntityType,
|
||||
Message: "The subscription entity type is invalid.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrSubscriptionAlreadyExists represents an error where a subscription entity already exists
|
||||
type ErrSubscriptionAlreadyExists struct {
|
||||
EntityID int64
|
||||
EntityType SubscriptionEntityType
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrSubscriptionAlreadyExists checks if an error is ErrSubscriptionAlreadyExists.
|
||||
func IsErrSubscriptionAlreadyExists(err error) bool {
|
||||
_, ok := err.(*ErrSubscriptionAlreadyExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrSubscriptionAlreadyExists) Error() string {
|
||||
return fmt.Sprintf("Subscription for this (entity_id, entity_type, user_id) already exists [EntityType: %d, EntityID: %d, UserID: %d]", err.EntityType, err.EntityID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeSubscriptionAlreadyExists holds the unique world-error code of this error
|
||||
const ErrCodeSubscriptionAlreadyExists = 12002
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrSubscriptionAlreadyExists) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeSubscriptionAlreadyExists,
|
||||
Message: "You're already subscribed.",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import (
|
|||
// TaskCreatedEvent represents an event where a task has been created
|
||||
type TaskCreatedEvent struct {
|
||||
Task *Task
|
||||
Doer web.Auth
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskCreatedEvent
|
||||
|
@ -39,7 +39,7 @@ func (t *TaskCreatedEvent) Name() string {
|
|||
// TaskUpdatedEvent represents an event where a task has been updated
|
||||
type TaskUpdatedEvent struct {
|
||||
Task *Task
|
||||
Doer web.Auth
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskUpdatedEvent
|
||||
|
@ -50,7 +50,7 @@ func (t *TaskUpdatedEvent) Name() string {
|
|||
// TaskDeletedEvent represents a TaskDeletedEvent event
|
||||
type TaskDeletedEvent struct {
|
||||
Task *Task
|
||||
Doer web.Auth
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskDeletedEvent
|
||||
|
@ -62,7 +62,7 @@ func (t *TaskDeletedEvent) Name() string {
|
|||
type TaskAssigneeCreatedEvent struct {
|
||||
Task *Task
|
||||
Assignee *user.User
|
||||
Doer web.Auth
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskAssigneeCreatedEvent
|
||||
|
@ -74,7 +74,7 @@ func (t *TaskAssigneeCreatedEvent) Name() string {
|
|||
type TaskCommentCreatedEvent struct {
|
||||
Task *Task
|
||||
Comment *TaskComment
|
||||
Doer web.Auth
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskCommentCreatedEvent
|
||||
|
@ -126,7 +126,7 @@ func (t *NamespaceDeletedEvent) Name() string {
|
|||
// ListCreatedEvent represents an event where a list has been created
|
||||
type ListCreatedEvent struct {
|
||||
List *List
|
||||
Doer web.Auth
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for ListCreatedEvent
|
||||
|
|
|
@ -68,6 +68,10 @@ type List struct {
|
|||
// True if a list is a favorite. Favorite lists show up in a separate namespace.
|
||||
IsFavorite bool `xorm:"default false" json:"is_favorite"`
|
||||
|
||||
// The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.
|
||||
// Will only returned when retreiving one list.
|
||||
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
|
||||
|
||||
// A timestamp when this list was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this list was last updated. You cannot change this value.
|
||||
|
@ -236,7 +240,8 @@ func (l *List) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
l.Subscription, err = GetSubscription(s, SubscriptionEntityList, l.ID, a)
|
||||
return
|
||||
}
|
||||
|
||||
// GetListSimpleByID gets a list with only the basic items, aka no tasks or user objects. Returns an error if the list does not exist.
|
||||
|
@ -622,7 +627,7 @@ func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
|
||||
return events.Dispatch(&ListCreatedEvent{
|
||||
List: l,
|
||||
Doer: a,
|
||||
Doer: doer,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -215,3 +215,34 @@ func TestList_ReadAll(t *testing.T) {
|
|||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestList_ReadOne(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
u := &user.User{ID: 1}
|
||||
l := &List{ID: 1}
|
||||
can, _, err := l.CanRead(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
err = l.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Test1", l.Title)
|
||||
})
|
||||
t.Run("with subscription", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
u := &user.User{ID: 6}
|
||||
l := &List{ID: 12}
|
||||
can, _, err := l.CanRead(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
err = l.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, l.Subscription)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -17,9 +17,14 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
)
|
||||
|
||||
|
@ -33,6 +38,10 @@ func RegisterListeners() {
|
|||
events.RegisterListener((&TaskDeletedEvent{}).Name(), &DecreaseTaskCounter{})
|
||||
events.RegisterListener((&TeamDeletedEvent{}).Name(), &DecreaseTeamCounter{})
|
||||
events.RegisterListener((&TeamCreatedEvent{}).Name(), &IncreaseTeamCounter{})
|
||||
events.RegisterListener((&TaskCommentCreatedEvent{}).Name(), &SendTaskCommentNotification{})
|
||||
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SendTaskAssignedNotification{})
|
||||
events.RegisterListener((&TaskDeletedEvent{}).Name(), &SendTaskDeletedNotification{})
|
||||
events.RegisterListener((&ListCreatedEvent{}).Name(), &SendListCreatedNotification{})
|
||||
}
|
||||
|
||||
//////
|
||||
|
@ -66,6 +75,143 @@ func (s *DecreaseTaskCounter) Handle(payload message.Payload) (err error) {
|
|||
return keyvalue.DecrBy(metrics.TaskCountKey, 1)
|
||||
}
|
||||
|
||||
// SendTaskCommentNotification represents a listener
|
||||
type SendTaskCommentNotification struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the SendTaskCommentNotification listener
|
||||
func (s *SendTaskCommentNotification) Name() string {
|
||||
return "send.task.comment.notification"
|
||||
}
|
||||
|
||||
// Handle is executed when the event SendTaskCommentNotification listens on is fired
|
||||
func (s *SendTaskCommentNotification) Handle(payload message.Payload) (err error) {
|
||||
event := &TaskCommentCreatedEvent{}
|
||||
err = json.Unmarshal(payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityTask, event.Task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Sending task comment notifications to %d subscribers for task %d", len(subscribers), event.Task.ID)
|
||||
|
||||
for _, subscriber := range subscribers {
|
||||
if subscriber.UserID == event.Doer.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
n := &TaskCommentNotification{
|
||||
Doer: event.Doer,
|
||||
Task: event.Task,
|
||||
Comment: event.Comment,
|
||||
}
|
||||
err = notifications.Notify(subscriber.User, n)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SendTaskAssignedNotification represents a listener
|
||||
type SendTaskAssignedNotification struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the SendTaskAssignedNotification listener
|
||||
func (s *SendTaskAssignedNotification) Name() string {
|
||||
return "send.task.assigned.notification"
|
||||
}
|
||||
|
||||
// Handle is executed when the event SendTaskAssignedNotification listens on is fired
|
||||
func (s *SendTaskAssignedNotification) Handle(payload message.Payload) (err error) {
|
||||
event := &TaskAssigneeCreatedEvent{}
|
||||
err = json.Unmarshal(payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityTask, event.Task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Sending task assigned notifications to %d subscribers for task %d", len(subscribers), event.Task.ID)
|
||||
|
||||
for _, subscriber := range subscribers {
|
||||
if subscriber.UserID == event.Doer.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
n := &TaskAssignedNotification{
|
||||
Doer: event.Doer,
|
||||
Task: event.Task,
|
||||
Assignee: event.Assignee,
|
||||
}
|
||||
err = notifications.Notify(subscriber.User, n)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendTaskDeletedNotification represents a listener
|
||||
type SendTaskDeletedNotification struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the SendTaskDeletedNotification listener
|
||||
func (s *SendTaskDeletedNotification) Name() string {
|
||||
return "send.task.deleted.notification"
|
||||
}
|
||||
|
||||
// Handle is executed when the event SendTaskDeletedNotification listens on is fired
|
||||
func (s *SendTaskDeletedNotification) Handle(payload message.Payload) (err error) {
|
||||
event := &TaskDeletedEvent{}
|
||||
err = json.Unmarshal(payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityTask, event.Task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Sending task deleted notifications to %d subscribers for task %d", len(subscribers), event.Task.ID)
|
||||
|
||||
for _, subscriber := range subscribers {
|
||||
if subscriber.UserID == event.Doer.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
n := &TaskDeletedNotification{
|
||||
Doer: event.Doer,
|
||||
Task: event.Task,
|
||||
}
|
||||
err = notifications.Notify(subscriber.User, n)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
///////
|
||||
// List Event Listeners
|
||||
|
||||
|
@ -91,6 +237,51 @@ func (s *DecreaseListCounter) Handle(payload message.Payload) (err error) {
|
|||
return keyvalue.DecrBy(metrics.ListCountKey, 1)
|
||||
}
|
||||
|
||||
// SendListCreatedNotification represents a listener
|
||||
type SendListCreatedNotification struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the SendListCreatedNotification listener
|
||||
func (s *SendListCreatedNotification) Name() string {
|
||||
return "send.list.created.notification"
|
||||
}
|
||||
|
||||
// Handle is executed when the event SendListCreatedNotification listens on is fired
|
||||
func (s *SendListCreatedNotification) Handle(payload message.Payload) (err error) {
|
||||
event := &ListCreatedEvent{}
|
||||
err = json.Unmarshal(payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityList, event.List.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Sending list created notifications to %d subscribers for list %d", len(subscribers), event.List.ID)
|
||||
|
||||
for _, subscriber := range subscribers {
|
||||
if subscriber.UserID == event.Doer.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
n := &ListCreatedNotification{
|
||||
Doer: event.Doer,
|
||||
List: event.List,
|
||||
}
|
||||
err = notifications.Notify(subscriber.User, n)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//////
|
||||
// Namespace events
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ func GetTables() []interface{} {
|
|||
&Bucket{},
|
||||
&UnsplashPhoto{},
|
||||
&SavedFilter{},
|
||||
&Subscription{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,10 @@ type Namespace struct {
|
|||
// The user who owns this namespace
|
||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||
|
||||
// The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.
|
||||
// Will only returned when retreiving one namespace.
|
||||
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
|
||||
|
||||
// A timestamp when this namespace was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this namespace was last updated. You cannot change this value.
|
||||
|
@ -166,6 +170,8 @@ func (n *Namespace) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
*n = *nn
|
||||
|
||||
n.Subscription, err = GetSubscription(s, SubscriptionEntityNamespace, n.ID, a)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -175,10 +181,11 @@ type NamespaceWithLists struct {
|
|||
Lists []*List `xorm:"-" json:"lists"`
|
||||
}
|
||||
|
||||
func makeNamespaceSliceFromMap(namespaces map[int64]*NamespaceWithLists, userMap map[int64]*user.User) []*NamespaceWithLists {
|
||||
func makeNamespaceSliceFromMap(namespaces map[int64]*NamespaceWithLists, userMap map[int64]*user.User, subscriptions map[int64]*Subscription) []*NamespaceWithLists {
|
||||
all := make([]*NamespaceWithLists, 0, len(namespaces))
|
||||
for _, n := range namespaces {
|
||||
n.Owner = userMap[n.OwnerID]
|
||||
n.Subscription = subscriptions[n.ID]
|
||||
all = append(all, n)
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
|
@ -289,6 +296,21 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
userIDs = append(userIDs, nsp.OwnerID)
|
||||
}
|
||||
|
||||
// Get all subscriptions
|
||||
subscriptions := []*Subscription{}
|
||||
err = s.
|
||||
Where("entity_type = ? AND user_id = ?", SubscriptionEntityNamespace, a.GetID()).
|
||||
In("entity_id", namespaceids).
|
||||
Find(&subscriptions)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
subscriptionsMap := make(map[int64]*Subscription)
|
||||
for _, sub := range subscriptions {
|
||||
sub.Entity = sub.EntityType.String()
|
||||
subscriptionsMap[sub.EntityID] = sub
|
||||
}
|
||||
|
||||
// Get all owners
|
||||
userMap := make(map[int64]*user.User)
|
||||
err = s.In("id", userIDs).Find(&userMap)
|
||||
|
@ -297,7 +319,7 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
}
|
||||
|
||||
if n.NamespacesOnly {
|
||||
all := makeNamespaceSliceFromMap(namespaces, userMap)
|
||||
all := makeNamespaceSliceFromMap(namespaces, userMap, subscriptionsMap)
|
||||
return all, len(all), numberOfTotalItems, nil
|
||||
}
|
||||
|
||||
|
@ -443,7 +465,7 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
|
||||
//////////////////////
|
||||
// Put it all together (and sort it)
|
||||
all := makeNamespaceSliceFromMap(namespaces, userMap)
|
||||
all := makeNamespaceSliceFromMap(namespaces, userMap, subscriptionsMap)
|
||||
return all, len(all), numberOfTotalItems, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -75,19 +75,31 @@ func TestNamespace_ReadOne(t *testing.T) {
|
|||
n := &Namespace{ID: 1}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err := n.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, n.Title, "testnamespace")
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("nonexistant", func(t *testing.T) {
|
||||
n := &Namespace{ID: 99999}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err := n.ReadOne(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("with subscription", func(t *testing.T) {
|
||||
n := &Namespace{ID: 8}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err := n.ReadOne(s, &user.User{ID: 6})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, n.Subscription)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
|
@ -45,3 +47,88 @@ func (n *ReminderDueNotification) ToMail() *notifications.Mail {
|
|||
func (n *ReminderDueNotification) ToDB() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TaskCommentNotification represents a TaskCommentNotification notification
|
||||
type TaskCommentNotification struct {
|
||||
Doer *user.User
|
||||
Task *Task
|
||||
Comment *TaskComment
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for TaskCommentNotification
|
||||
func (n *TaskCommentNotification) ToMail() *notifications.Mail {
|
||||
|
||||
mail := notifications.NewMail().
|
||||
From(n.Doer.GetName() + " via Vikunja <" + config.MailerFromEmail.GetString() + ">").
|
||||
Subject("Re: " + n.Task.Title)
|
||||
|
||||
lines := bufio.NewScanner(strings.NewReader(n.Comment.Comment))
|
||||
for lines.Scan() {
|
||||
mail.Line(lines.Text())
|
||||
}
|
||||
|
||||
return mail.
|
||||
Action("View Task", n.Task.GetFrontendURL())
|
||||
}
|
||||
|
||||
// ToDB returns the TaskCommentNotification notification in a format which can be saved in the db
|
||||
func (n *TaskCommentNotification) ToDB() interface{} {
|
||||
return n
|
||||
}
|
||||
|
||||
// TaskAssignedNotification represents a TaskAssignedNotification notification
|
||||
type TaskAssignedNotification struct {
|
||||
Doer *user.User
|
||||
Task *Task
|
||||
Assignee *user.User
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for TaskAssignedNotification
|
||||
func (n *TaskAssignedNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
Subject(n.Task.Title+"("+n.Task.GetFullIdentifier()+")"+" has been assigned to "+n.Assignee.GetName()).
|
||||
Line(n.Doer.GetName()+" has assigned this task to "+n.Assignee.GetName()).
|
||||
Action("View Task", n.Task.GetFrontendURL())
|
||||
}
|
||||
|
||||
// ToDB returns the TaskAssignedNotification notification in a format which can be saved in the db
|
||||
func (n *TaskAssignedNotification) ToDB() interface{} {
|
||||
return n
|
||||
}
|
||||
|
||||
// TaskDeletedNotification represents a TaskDeletedNotification notification
|
||||
type TaskDeletedNotification struct {
|
||||
Doer *user.User
|
||||
Task *Task
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for TaskDeletedNotification
|
||||
func (n *TaskDeletedNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
Subject(n.Task.Title + "(" + n.Task.GetFullIdentifier() + ")" + " has been delete").
|
||||
Line(n.Doer.GetName() + " has deleted the task " + n.Task.Title + "(" + n.Task.GetFullIdentifier() + ")")
|
||||
}
|
||||
|
||||
// ToDB returns the TaskDeletedNotification notification in a format which can be saved in the db
|
||||
func (n *TaskDeletedNotification) ToDB() interface{} {
|
||||
return n
|
||||
}
|
||||
|
||||
// ListCreatedNotification represents a ListCreatedNotification notification
|
||||
type ListCreatedNotification struct {
|
||||
Doer *user.User
|
||||
List *List
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for ListCreatedNotification
|
||||
func (n *ListCreatedNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
Subject(n.Doer.GetName()+` created the list "`+n.List.Title+`"`).
|
||||
Line(n.Doer.GetName()+` created the list "`+n.List.Title+`"`).
|
||||
Action("View List", config.ServiceFrontendurl.GetString()+"lists/")
|
||||
}
|
||||
|
||||
// ToDB returns the ListCreatedNotification notification in a format which can be saved in the db
|
||||
func (n *ListCreatedNotification) ToDB() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
|
282
pkg/models/subscription.go
Normal file
282
pkg/models/subscription.go
Normal file
|
@ -0,0 +1,282 @@
|
|||
// 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 (
|
||||
"time"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// SubscriptionEntityType represents all entities which can be subscribed to
|
||||
type SubscriptionEntityType int
|
||||
|
||||
const (
|
||||
SubscriptionEntityUnknown = iota
|
||||
SubscriptionEntityNamespace
|
||||
SubscriptionEntityList
|
||||
SubscriptionEntityTask
|
||||
)
|
||||
|
||||
const (
|
||||
entityNamespace = `namespace`
|
||||
entityList = `list`
|
||||
entityTask = `task`
|
||||
)
|
||||
|
||||
// Subscription represents a subscription for an entity
|
||||
type Subscription struct {
|
||||
// The numeric ID of the subscription
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id"`
|
||||
|
||||
EntityType SubscriptionEntityType `xorm:"index not null" json:"-"`
|
||||
Entity string `xorm:"-" json:"entity" param:"entity"`
|
||||
// The id of the entity to subscribe to.
|
||||
EntityID int64 `xorm:"bigint index not null" json:"entity_id" param:"entityID"`
|
||||
|
||||
// The user who made this subscription
|
||||
User *user.User `xorm:"-" json:"user"`
|
||||
UserID int64 `xorm:"bigint index not null" json:"-"`
|
||||
|
||||
// A timestamp when this subscription was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName gives us a better tabel name for the subscriptions table
|
||||
func (sb *Subscription) TableName() string {
|
||||
return "subscriptions"
|
||||
}
|
||||
|
||||
func getEntityTypeFromString(entityType string) SubscriptionEntityType {
|
||||
switch entityType {
|
||||
case entityNamespace:
|
||||
return SubscriptionEntityNamespace
|
||||
case entityList:
|
||||
return SubscriptionEntityList
|
||||
case entityTask:
|
||||
return SubscriptionEntityTask
|
||||
}
|
||||
|
||||
return SubscriptionEntityUnknown
|
||||
}
|
||||
|
||||
// String returns a human-readable string of an entity
|
||||
func (et SubscriptionEntityType) String() string {
|
||||
switch et {
|
||||
case SubscriptionEntityNamespace:
|
||||
return entityNamespace
|
||||
case SubscriptionEntityList:
|
||||
return entityList
|
||||
case SubscriptionEntityTask:
|
||||
return entityTask
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (et SubscriptionEntityType) validate() error {
|
||||
if et == SubscriptionEntityNamespace ||
|
||||
et == SubscriptionEntityList ||
|
||||
et == SubscriptionEntityTask {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ErrUnknownSubscriptionEntityType{EntityType: et}
|
||||
}
|
||||
|
||||
// Create subscribes the current user to an entity
|
||||
// @Summary Subscribes the current user to an entity.
|
||||
// @Description Subscribes the current user to an entity.
|
||||
// @tags subscriptions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param entity path string true "The entity the user subscribes to. Can be either `namespace`, `list` or `task`."
|
||||
// @Param entityID path string true "The numeric id of the entity to subscribe to."
|
||||
// @Success 200 {object} models.Subscription "The subscription"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
|
||||
// @Failure 412 {object} web.HTTPError "The subscription already exists."
|
||||
// @Failure 412 {object} web.HTTPError "The subscription entity is invalid."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /subscriptions/{entity}/{entityID} [put]
|
||||
func (sb *Subscription) Create(s *xorm.Session, auth web.Auth) (err error) {
|
||||
// Rights method alread does the validation of the entity type so we don't need to do that here
|
||||
|
||||
sb.UserID = auth.GetID()
|
||||
|
||||
sub, err := GetSubscription(s, sb.EntityType, sb.EntityID, auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sub != nil {
|
||||
return &ErrSubscriptionAlreadyExists{
|
||||
EntityID: sb.EntityID,
|
||||
EntityType: sb.EntityType,
|
||||
UserID: sb.UserID,
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.Insert(sb)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sb.User, err = user.GetFromAuth(auth)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete unsubscribes the current user to an entity
|
||||
// @Summary Unsubscribe the current user from an entity.
|
||||
// @Description Unsubscribes the current user to an entity.
|
||||
// @tags subscriptions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param entity path string true "The entity the user subscribed to. Can be either `namespace`, `list` or `task`."
|
||||
// @Param entityID path string true "The numeric id of the subscribed entity to."
|
||||
// @Success 200 {object} models.Subscription "The subscription"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to subscribe to this entity."
|
||||
// @Failure 404 {object} web.HTTPError "The subscription does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /subscriptions/{entity}/{entityID} [delete]
|
||||
func (sb *Subscription) Delete(s *xorm.Session, auth web.Auth) (err error) {
|
||||
sb.UserID = auth.GetID()
|
||||
|
||||
_, err = s.
|
||||
Where("entity_id = ? AND entity_type = ? AND user_id = ?", sb.EntityID, sb.EntityType, sb.UserID).
|
||||
Delete(&Subscription{})
|
||||
return
|
||||
}
|
||||
|
||||
func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int64) (cond builder.Cond) {
|
||||
if entityType == SubscriptionEntityNamespace {
|
||||
cond = builder.And(
|
||||
builder.Eq{"entity_id": entityID},
|
||||
builder.Eq{"entity_type": SubscriptionEntityNamespace},
|
||||
)
|
||||
}
|
||||
|
||||
if entityType == SubscriptionEntityList {
|
||||
cond = builder.Or(
|
||||
builder.And(
|
||||
builder.Eq{"entity_id": entityID},
|
||||
builder.Eq{"entity_type": SubscriptionEntityList},
|
||||
),
|
||||
builder.And(
|
||||
builder.Eq{"entity_id": builder.
|
||||
Select("namespace_id").
|
||||
From("list").
|
||||
Where(builder.Eq{"id": entityID}),
|
||||
},
|
||||
builder.Eq{"entity_type": SubscriptionEntityNamespace},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if entityType == SubscriptionEntityTask {
|
||||
cond = builder.Or(
|
||||
builder.And(
|
||||
builder.Eq{"entity_id": entityID},
|
||||
builder.Eq{"entity_type": SubscriptionEntityTask},
|
||||
),
|
||||
builder.And(
|
||||
builder.Eq{"entity_id": builder.
|
||||
Select("namespace_id").
|
||||
From("list").
|
||||
Join("INNER", "tasks", "list.id = tasks.list_id").
|
||||
Where(builder.Eq{"tasks.id": entityID}),
|
||||
},
|
||||
builder.Eq{"entity_type": SubscriptionEntityNamespace},
|
||||
),
|
||||
builder.And(
|
||||
builder.Eq{"entity_id": builder.
|
||||
Select("list_id").
|
||||
From("tasks").
|
||||
Where(builder.Eq{"id": entityID}),
|
||||
},
|
||||
builder.Eq{"entity_type": SubscriptionEntityList},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetSubscription returns a matching subscription for an entity and user.
|
||||
// It will return the next parent of a subscription. That means for tasks, it will first look for a subscription for
|
||||
// that task, if there is none it will look for a subscription on the list the task belongs to and if that also
|
||||
// doesn't exist it will check for a subscription for the namespace the list is belonging to.
|
||||
func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) {
|
||||
u, is := a.(*user.User)
|
||||
if !is {
|
||||
return
|
||||
}
|
||||
|
||||
if err := entityType.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscription = &Subscription{}
|
||||
cond := getSubscriberCondForEntity(entityType, entityID)
|
||||
exists, err := s.
|
||||
Where("user_id = ?", u.ID).
|
||||
And(cond).
|
||||
Get(subscription)
|
||||
if !exists {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscription.Entity = subscription.EntityType.String()
|
||||
|
||||
return subscription, err
|
||||
}
|
||||
|
||||
func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType, entityID int64) (subscriptions []*Subscription, err error) {
|
||||
if err := entityType.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cond := getSubscriberCondForEntity(entityType, entityID)
|
||||
err = s.
|
||||
Where(cond).
|
||||
Find(&subscriptions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userIDs := []int64{}
|
||||
for _, subscription := range subscriptions {
|
||||
userIDs = append(userIDs, subscription.UserID)
|
||||
}
|
||||
|
||||
users, err := user.GetUsersByIDs(s, userIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, subscription := range subscriptions {
|
||||
subscription.User = users[subscription.UserID]
|
||||
}
|
||||
return
|
||||
}
|
66
pkg/models/subscription_rights.go
Normal file
66
pkg/models/subscription_rights.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
// 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/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CanCreate checks if a user can subscribe to an entity
|
||||
func (sb *Subscription) CanCreate(s *xorm.Session, a web.Auth) (can bool, err error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, &ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
sb.EntityType = getEntityTypeFromString(sb.Entity)
|
||||
|
||||
switch sb.EntityType {
|
||||
case SubscriptionEntityNamespace:
|
||||
n := &Namespace{ID: sb.EntityID}
|
||||
can, _, err = n.CanRead(s, a)
|
||||
case SubscriptionEntityList:
|
||||
l := &List{ID: sb.EntityID}
|
||||
can, _, err = l.CanRead(s, a)
|
||||
case SubscriptionEntityTask:
|
||||
t := &Task{ID: sb.EntityID}
|
||||
can, _, err = t.CanRead(s, a)
|
||||
default:
|
||||
return false, &ErrUnknownSubscriptionEntityType{EntityType: sb.EntityType}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CanDelete checks if a user can delete a subscription
|
||||
func (sb *Subscription) CanDelete(s *xorm.Session, a web.Auth) (can bool, err error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, &ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
sb.EntityType = getEntityTypeFromString(sb.Entity)
|
||||
|
||||
realSb := &Subscription{}
|
||||
exists, err := s.
|
||||
Where("entity_id = ? AND entity_type = ? AND user_id = ?", sb.EntityID, sb.EntityType, a.GetID()).
|
||||
Get(realSb)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return exists, nil
|
||||
}
|
346
pkg/models/subscription_test.go
Normal file
346
pkg/models/subscription_test.go
Normal file
|
@ -0,0 +1,346 @@
|
|||
// 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 (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSubscriptionGetTypeFromString(t *testing.T) {
|
||||
t.Run("namespace", func(t *testing.T) {
|
||||
entityType := getEntityTypeFromString("namespace")
|
||||
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityNamespace), entityType)
|
||||
})
|
||||
t.Run("list", func(t *testing.T) {
|
||||
entityType := getEntityTypeFromString("list")
|
||||
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityList), entityType)
|
||||
})
|
||||
t.Run("task", func(t *testing.T) {
|
||||
entityType := getEntityTypeFromString("task")
|
||||
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityTask), entityType)
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
entityType := getEntityTypeFromString("someomejghsd")
|
||||
assert.Equal(t, SubscriptionEntityType(SubscriptionEntityUnknown), entityType)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSubscription_Create(t *testing.T) {
|
||||
u := &user.User{ID: 1}
|
||||
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "task",
|
||||
EntityID: 1,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
|
||||
err = sb.Create(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sb.User)
|
||||
|
||||
db.AssertExists(t, "subscriptions", map[string]interface{}{
|
||||
"entity_type": 3,
|
||||
"entity_id": 1,
|
||||
"user_id": u.ID,
|
||||
}, false)
|
||||
})
|
||||
t.Run("forbidden for link shares", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
linkShare := &LinkSharing{}
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "task",
|
||||
EntityID: 1,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, linkShare)
|
||||
assert.Error(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("noneixsting namespace", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "namespace",
|
||||
EntityID: 99999999,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("noneixsting list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "list",
|
||||
EntityID: 99999999,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListDoesNotExist(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("noneixsting task", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "task",
|
||||
EntityID: 99999999,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskDoesNotExist(err))
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("no rights to see namespace", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "namespace",
|
||||
EntityID: 6,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("no rights to see list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "list",
|
||||
EntityID: 20,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("no rights to see task", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "task",
|
||||
EntityID: 14,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("existing subscription for (entity_id, entity_type, user_id) ", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "task",
|
||||
EntityID: 2,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanCreate(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
|
||||
err = sb.Create(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrSubscriptionAlreadyExists(err))
|
||||
})
|
||||
|
||||
// TODO: Add tests to test triggering of notifications for subscribed things
|
||||
}
|
||||
|
||||
func TestSubscription_Delete(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
u := &user.User{ID: 1}
|
||||
sb := &Subscription{
|
||||
Entity: "task",
|
||||
EntityID: 2,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanDelete(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
|
||||
err = sb.Delete(s, u)
|
||||
assert.NoError(t, err)
|
||||
db.AssertMissing(t, "subscriptions", map[string]interface{}{
|
||||
"entity_type": 3,
|
||||
"entity_id": 2,
|
||||
"user_id": u.ID,
|
||||
})
|
||||
})
|
||||
t.Run("forbidden for link shares", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
linkShare := &LinkSharing{}
|
||||
|
||||
sb := &Subscription{
|
||||
Entity: "task",
|
||||
EntityID: 1,
|
||||
UserID: 1,
|
||||
}
|
||||
|
||||
can, err := sb.CanDelete(s, linkShare)
|
||||
assert.Error(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
t.Run("not owner of the subscription", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
u := &user.User{ID: 2}
|
||||
sb := &Subscription{
|
||||
Entity: "task",
|
||||
EntityID: 2,
|
||||
UserID: u.ID,
|
||||
}
|
||||
|
||||
can, err := sb.CanDelete(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSubscriptionGet(t *testing.T) {
|
||||
u := &user.User{ID: 6}
|
||||
|
||||
t.Run("test each individually", func(t *testing.T) {
|
||||
t.Run("namespace", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sub, err := GetSubscription(s, SubscriptionEntityNamespace, 6, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(2), sub.ID)
|
||||
})
|
||||
t.Run("list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sub, err := GetSubscription(s, SubscriptionEntityList, 12, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(3), sub.ID)
|
||||
})
|
||||
t.Run("task", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sub, err := GetSubscription(s, SubscriptionEntityTask, 22, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(4), sub.ID)
|
||||
})
|
||||
})
|
||||
t.Run("inherited", func(t *testing.T) {
|
||||
t.Run("list from namespace", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
// List 6 belongs to namespace 6 where user 6 has subscribed to
|
||||
sub, err := GetSubscription(s, SubscriptionEntityList, 6, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(2), sub.ID)
|
||||
})
|
||||
t.Run("task from namespace", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
// Task 20 belongs to list 11 which belongs to namespace 6 where the user has subscribed
|
||||
sub, err := GetSubscription(s, SubscriptionEntityTask, 20, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(2), sub.ID)
|
||||
})
|
||||
t.Run("task from list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
// Task 21 belongs to list 12 which the user has subscribed to
|
||||
sub, err := GetSubscription(s, SubscriptionEntityTask, 21, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, sub)
|
||||
assert.Equal(t, int64(3), sub.ID)
|
||||
})
|
||||
})
|
||||
t.Run("invalid type", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
_, err := GetSubscription(s, 2342, 21, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUnknownSubscriptionEntityType(err))
|
||||
})
|
||||
}
|
|
@ -225,10 +225,11 @@ func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *Li
|
|||
return err
|
||||
}
|
||||
|
||||
doer, _ := user.GetFromAuth(auth)
|
||||
err = events.Dispatch(&TaskAssigneeCreatedEvent{
|
||||
Task: t,
|
||||
Assignee: newAssignee,
|
||||
Doer: auth,
|
||||
Doer: doer,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -73,10 +73,11 @@ func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
err = events.Dispatch(&TaskCommentCreatedEvent{
|
||||
Task: &task,
|
||||
Comment: tc,
|
||||
Doer: a,
|
||||
Doer: doer,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -91,6 +91,10 @@ type Task struct {
|
|||
// True if a task is a favorite task. Favorite tasks show up in a separate "Important" list
|
||||
IsFavorite bool `xorm:"default false" json:"is_favorite"`
|
||||
|
||||
// The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.
|
||||
// Will only returned when retreiving one task.
|
||||
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
|
||||
|
||||
// A timestamp when this task was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this task was last updated. You cannot change this value.
|
||||
|
@ -119,6 +123,19 @@ func (Task) TableName() string {
|
|||
return "tasks"
|
||||
}
|
||||
|
||||
// GetFullIdentifier returns the task identifier if the task has one and the index prefixed with # otherwise.
|
||||
func (t *Task) GetFullIdentifier() string {
|
||||
if t.Identifier != "" {
|
||||
return t.Identifier
|
||||
}
|
||||
|
||||
return "#" + strconv.FormatInt(t.Index, 10)
|
||||
}
|
||||
|
||||
func (t *Task) GetFrontendURL() string {
|
||||
return config.ServiceFrontendurl.GetString() + "tasks/" + strconv.FormatInt(t.ID, 10)
|
||||
}
|
||||
|
||||
type taskFilterConcatinator string
|
||||
|
||||
const (
|
||||
|
@ -832,9 +849,10 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
|
||||
t.setIdentifier(l)
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
err = events.Dispatch(&TaskCreatedEvent{
|
||||
Task: t,
|
||||
Doer: a,
|
||||
Doer: doer,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1040,9 +1058,10 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
t.Updated = nt.Updated
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
err = events.Dispatch(&TaskUpdatedEvent{
|
||||
Task: t,
|
||||
Doer: a,
|
||||
Doer: doer,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1197,9 +1216,10 @@ func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
err = events.Dispatch(&TaskDeletedEvent{
|
||||
Task: t,
|
||||
Doer: a,
|
||||
Doer: doer,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -1241,5 +1261,6 @@ func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
|||
|
||||
*t = *taskMap[t.ID]
|
||||
|
||||
t.Subscription, err = GetSubscription(s, SubscriptionEntityTask, t.ID, a)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -467,4 +467,16 @@ func TestTask_ReadOne(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskDoesNotExist(err))
|
||||
})
|
||||
t.Run("with subscription", func(t *testing.T) {
|
||||
u = &user.User{ID: 6}
|
||||
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{ID: 22}
|
||||
err := task.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, task.Subscription)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ func SetupTests() {
|
|||
"users_namespace",
|
||||
"buckets",
|
||||
"saved_filters",
|
||||
"subscriptions",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -512,6 +512,15 @@ func registerAPIRoutes(a *echo.Group) {
|
|||
a.DELETE("/teams/:team/members/:user", teamMemberHandler.DeleteWeb)
|
||||
a.POST("/teams/:team/members/:user/admin", teamMemberHandler.UpdateWeb)
|
||||
|
||||
// Subscriptions
|
||||
subscriptionHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.Subscription{}
|
||||
},
|
||||
}
|
||||
a.PUT("/subscriptions/:entity/:entityID", subscriptionHandler.CreateWeb)
|
||||
a.DELETE("/subscriptions/:entity/:entityID", subscriptionHandler.DeleteWeb)
|
||||
|
||||
// Migrations
|
||||
m := a.Group("/migration")
|
||||
|
||||
|
|
|
@ -3991,6 +3991,128 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/subscriptions/{entity}/{entityID}": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Subscribes the current user to an entity.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"subscriptions"
|
||||
],
|
||||
"summary": "Subscribes the current user to an entity.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The entity the user subscribes to. Can be either ` + "`" + `namespace` + "`" + `, ` + "`" + `list` + "`" + ` or ` + "`" + `task` + "`" + `.",
|
||||
"name": "entity",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The numeric id of the entity to subscribe to.",
|
||||
"name": "entityID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The subscription",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to subscribe to this entity.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"412": {
|
||||
"description": "The subscription entity is invalid.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Unsubscribes the current user to an entity.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"subscriptions"
|
||||
],
|
||||
"summary": "Unsubscribe the current user from an entity.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The entity the user subscribed to. Can be either ` + "`" + `namespace` + "`" + `, ` + "`" + `list` + "`" + ` or ` + "`" + `task` + "`" + `.",
|
||||
"name": "entity",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The numeric id of the subscribed entity to.",
|
||||
"name": "entityID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The subscription",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to subscribe to this entity.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The subscription does not exist.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tasks/all": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -7039,6 +7161,10 @@ var doc = `{
|
|||
"description": "When this task starts.",
|
||||
"type": "string"
|
||||
},
|
||||
"subscription": {
|
||||
"description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.",
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
},
|
||||
"task_ids": {
|
||||
"description": "A list of task ids to update",
|
||||
"type": "array",
|
||||
|
@ -7201,6 +7327,10 @@ var doc = `{
|
|||
"description": "The user who created this list.",
|
||||
"$ref": "#/definitions/user.User"
|
||||
},
|
||||
"subscription": {
|
||||
"description": "The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one list.",
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the list. You'll see this in the namespace overview.",
|
||||
"type": "string",
|
||||
|
@ -7290,6 +7420,10 @@ var doc = `{
|
|||
"description": "The user who owns this namespace",
|
||||
"$ref": "#/definitions/user.User"
|
||||
},
|
||||
"subscription": {
|
||||
"description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.",
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
},
|
||||
"title": {
|
||||
"description": "The name of this namespace.",
|
||||
"type": "string",
|
||||
|
@ -7363,6 +7497,10 @@ var doc = `{
|
|||
"description": "The user who owns this namespace",
|
||||
"$ref": "#/definitions/user.User"
|
||||
},
|
||||
"subscription": {
|
||||
"description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.",
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
},
|
||||
"title": {
|
||||
"description": "The name of this namespace.",
|
||||
"type": "string",
|
||||
|
@ -7419,6 +7557,30 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.Subscription": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created": {
|
||||
"description": "A timestamp when this subscription was created. You cannot change this value.",
|
||||
"type": "string"
|
||||
},
|
||||
"entity": {
|
||||
"type": "string"
|
||||
},
|
||||
"entity_id": {
|
||||
"description": "The id of the entity to subscribe to.",
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"description": "The numeric ID of the subscription",
|
||||
"type": "integer"
|
||||
},
|
||||
"user": {
|
||||
"description": "The user who made this subscription",
|
||||
"$ref": "#/definitions/user.User"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Task": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7535,6 +7697,10 @@ var doc = `{
|
|||
"description": "When this task starts.",
|
||||
"type": "string"
|
||||
},
|
||||
"subscription": {
|
||||
"description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.",
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
},
|
||||
"title": {
|
||||
"description": "The task text. This is what you'll see in the list.",
|
||||
"type": "string",
|
||||
|
|
|
@ -3974,6 +3974,128 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/subscriptions/{entity}/{entityID}": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Subscribes the current user to an entity.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"subscriptions"
|
||||
],
|
||||
"summary": "Subscribes the current user to an entity.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The entity the user subscribes to. Can be either `namespace`, `list` or `task`.",
|
||||
"name": "entity",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The numeric id of the entity to subscribe to.",
|
||||
"name": "entityID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The subscription",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to subscribe to this entity.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"412": {
|
||||
"description": "The subscription entity is invalid.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Unsubscribes the current user to an entity.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"subscriptions"
|
||||
],
|
||||
"summary": "Unsubscribe the current user from an entity.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The entity the user subscribed to. Can be either `namespace`, `list` or `task`.",
|
||||
"name": "entity",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The numeric id of the subscribed entity to.",
|
||||
"name": "entityID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The subscription",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to subscribe to this entity.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The subscription does not exist.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tasks/all": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -7022,6 +7144,10 @@
|
|||
"description": "When this task starts.",
|
||||
"type": "string"
|
||||
},
|
||||
"subscription": {
|
||||
"description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.",
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
},
|
||||
"task_ids": {
|
||||
"description": "A list of task ids to update",
|
||||
"type": "array",
|
||||
|
@ -7184,6 +7310,10 @@
|
|||
"description": "The user who created this list.",
|
||||
"$ref": "#/definitions/user.User"
|
||||
},
|
||||
"subscription": {
|
||||
"description": "The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one list.",
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the list. You'll see this in the namespace overview.",
|
||||
"type": "string",
|
||||
|
@ -7273,6 +7403,10 @@
|
|||
"description": "The user who owns this namespace",
|
||||
"$ref": "#/definitions/user.User"
|
||||
},
|
||||
"subscription": {
|
||||
"description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.",
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
},
|
||||
"title": {
|
||||
"description": "The name of this namespace.",
|
||||
"type": "string",
|
||||
|
@ -7346,6 +7480,10 @@
|
|||
"description": "The user who owns this namespace",
|
||||
"$ref": "#/definitions/user.User"
|
||||
},
|
||||
"subscription": {
|
||||
"description": "The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one namespace.",
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
},
|
||||
"title": {
|
||||
"description": "The name of this namespace.",
|
||||
"type": "string",
|
||||
|
@ -7402,6 +7540,30 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.Subscription": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created": {
|
||||
"description": "A timestamp when this subscription was created. You cannot change this value.",
|
||||
"type": "string"
|
||||
},
|
||||
"entity": {
|
||||
"type": "string"
|
||||
},
|
||||
"entity_id": {
|
||||
"description": "The id of the entity to subscribe to.",
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"description": "The numeric ID of the subscription",
|
||||
"type": "integer"
|
||||
},
|
||||
"user": {
|
||||
"description": "The user who made this subscription",
|
||||
"$ref": "#/definitions/user.User"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Task": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7518,6 +7680,10 @@
|
|||
"description": "When this task starts.",
|
||||
"type": "string"
|
||||
},
|
||||
"subscription": {
|
||||
"description": "The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.\nWill only returned when retreiving one task.",
|
||||
"$ref": "#/definitions/models.Subscription"
|
||||
},
|
||||
"title": {
|
||||
"description": "The task text. This is what you'll see in the list.",
|
||||
"type": "string",
|
||||
|
|
|
@ -210,6 +210,11 @@ definitions:
|
|||
start_date:
|
||||
description: When this task starts.
|
||||
type: string
|
||||
subscription:
|
||||
$ref: '#/definitions/models.Subscription'
|
||||
description: |-
|
||||
The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.
|
||||
Will only returned when retreiving one task.
|
||||
task_ids:
|
||||
description: A list of task ids to update
|
||||
items:
|
||||
|
@ -330,6 +335,11 @@ definitions:
|
|||
owner:
|
||||
$ref: '#/definitions/user.User'
|
||||
description: The user who created this list.
|
||||
subscription:
|
||||
$ref: '#/definitions/models.Subscription'
|
||||
description: |-
|
||||
The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.
|
||||
Will only returned when retreiving one list.
|
||||
title:
|
||||
description: The title of the list. You'll see this in the namespace overview.
|
||||
maxLength: 250
|
||||
|
@ -395,6 +405,11 @@ definitions:
|
|||
owner:
|
||||
$ref: '#/definitions/user.User'
|
||||
description: The user who owns this namespace
|
||||
subscription:
|
||||
$ref: '#/definitions/models.Subscription'
|
||||
description: |-
|
||||
The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.
|
||||
Will only returned when retreiving one namespace.
|
||||
title:
|
||||
description: The name of this namespace.
|
||||
maxLength: 250
|
||||
|
@ -449,6 +464,11 @@ definitions:
|
|||
owner:
|
||||
$ref: '#/definitions/user.User'
|
||||
description: The user who owns this namespace
|
||||
subscription:
|
||||
$ref: '#/definitions/models.Subscription'
|
||||
description: |-
|
||||
The subscription status for the user reading this namespace. You can only read this property, use the subscription endpoints to modify it.
|
||||
Will only returned when retreiving one namespace.
|
||||
title:
|
||||
description: The name of this namespace.
|
||||
maxLength: 250
|
||||
|
@ -490,6 +510,23 @@ definitions:
|
|||
description: A timestamp when this filter was last updated. You cannot change this value.
|
||||
type: string
|
||||
type: object
|
||||
models.Subscription:
|
||||
properties:
|
||||
created:
|
||||
description: A timestamp when this subscription was created. You cannot change this value.
|
||||
type: string
|
||||
entity:
|
||||
type: string
|
||||
entity_id:
|
||||
description: The id of the entity to subscribe to.
|
||||
type: integer
|
||||
id:
|
||||
description: The numeric ID of the subscription
|
||||
type: integer
|
||||
user:
|
||||
$ref: '#/definitions/user.User'
|
||||
description: The user who made this subscription
|
||||
type: object
|
||||
models.Task:
|
||||
properties:
|
||||
assignees:
|
||||
|
@ -582,6 +619,11 @@ definitions:
|
|||
start_date:
|
||||
description: When this task starts.
|
||||
type: string
|
||||
subscription:
|
||||
$ref: '#/definitions/models.Subscription'
|
||||
description: |-
|
||||
The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it.
|
||||
Will only returned when retreiving one task.
|
||||
title:
|
||||
description: The task text. This is what you'll see in the list.
|
||||
maxLength: 250
|
||||
|
@ -3660,6 +3702,85 @@ paths:
|
|||
summary: Get an auth token for a share
|
||||
tags:
|
||||
- sharing
|
||||
/subscriptions/{entity}/{entityID}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Unsubscribes the current user to an entity.
|
||||
parameters:
|
||||
- description: The entity the user subscribed to. Can be either `namespace`, `list` or `task`.
|
||||
in: path
|
||||
name: entity
|
||||
required: true
|
||||
type: string
|
||||
- description: The numeric id of the subscribed entity to.
|
||||
in: path
|
||||
name: entityID
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The subscription
|
||||
schema:
|
||||
$ref: '#/definitions/models.Subscription'
|
||||
"403":
|
||||
description: The user does not have access to subscribe to this entity.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"404":
|
||||
description: The subscription does not exist.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Unsubscribe the current user from an entity.
|
||||
tags:
|
||||
- subscriptions
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Subscribes the current user to an entity.
|
||||
parameters:
|
||||
- description: The entity the user subscribes to. Can be either `namespace`, `list` or `task`.
|
||||
in: path
|
||||
name: entity
|
||||
required: true
|
||||
type: string
|
||||
- description: The numeric id of the entity to subscribe to.
|
||||
in: path
|
||||
name: entityID
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The subscription
|
||||
schema:
|
||||
$ref: '#/definitions/models.Subscription'
|
||||
"403":
|
||||
description: The user does not have access to subscribe to this entity.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"412":
|
||||
description: The subscription entity is invalid.
|
||||
schema:
|
||||
$ref: '#/definitions/web.HTTPError'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Subscribes the current user to an entity.
|
||||
tags:
|
||||
- subscriptions
|
||||
/tasks/{ID}:
|
||||
get:
|
||||
consumes:
|
||||
|
|
Loading…
Reference in a new issue