feat(subscriptions): make sure all subscriptions are inherited properly
This commit is contained in:
parent
afe756e4c1
commit
ceaa9c0e03
@ -239,3 +239,17 @@
|
|||||||
position: 1
|
position: 1
|
||||||
updated: 2018-12-02 15:13:12
|
updated: 2018-12-02 15:13:12
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
|
-
|
||||||
|
id: 25
|
||||||
|
title: Test25
|
||||||
|
owner_id: 6
|
||||||
|
parent_project_id: 12
|
||||||
|
updated: 2018-12-02 15:13:12
|
||||||
|
created: 2018-12-01 15:13:12
|
||||||
|
-
|
||||||
|
id: 26
|
||||||
|
title: Test26
|
||||||
|
owner_id: 6
|
||||||
|
parent_project_id: 25
|
||||||
|
updated: 2018-12-02 15:13:12
|
||||||
|
created: 2018-12-01 15:13:12
|
||||||
|
@ -370,3 +370,9 @@
|
|||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 1
|
bucket_id: 1
|
||||||
position: 39
|
position: 39
|
||||||
|
- id: 39
|
||||||
|
title: 'task #39'
|
||||||
|
created_by_id: 1
|
||||||
|
project_id: 25
|
||||||
|
created: 2018-12-01 01:12:04
|
||||||
|
updated: 2018-12-01 01:12:04
|
||||||
|
@ -46,8 +46,9 @@ type Project struct {
|
|||||||
// The hex color of this project
|
// The hex color of this project
|
||||||
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
HexColor string `xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6"`
|
||||||
|
|
||||||
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
|
OwnerID int64 `xorm:"bigint INDEX not null" json:"-"`
|
||||||
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
|
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
|
||||||
|
ParentProject *Project `xorm:"-" json:"-"`
|
||||||
|
|
||||||
ChildProjects []*Project `xorm:"-" json:"child_projects"`
|
ChildProjects []*Project `xorm:"-" json:"child_projects"`
|
||||||
|
|
||||||
@ -513,6 +514,22 @@ func getSavedFilterProjects(s *xorm.Session, doer *user.User) (savedFiltersProje
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllParentProjects returns all parents of a given project
|
||||||
|
func (p *Project) GetAllParentProjects(s *xorm.Session) (err error) {
|
||||||
|
if p.ParentProjectID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, err := GetProjectSimpleByID(s, p.ParentProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ParentProject = parent
|
||||||
|
|
||||||
|
return parent.GetAllParentProjects(s)
|
||||||
|
}
|
||||||
|
|
||||||
// addProjectDetails adds owner user objects and project tasks to all projects in the slice
|
// addProjectDetails adds owner user objects and project tasks to all projects in the slice
|
||||||
func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err error) {
|
func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err error) {
|
||||||
if len(projects) == 0 {
|
if len(projects) == 0 {
|
||||||
@ -541,7 +558,7 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
|
|||||||
subscriptions, err := GetSubscriptions(s, SubscriptionEntityProject, projectIDs, a)
|
subscriptions, err := GetSubscriptions(s, SubscriptionEntityProject, projectIDs, a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("An error occurred while getting project subscriptions for a project: %s", err.Error())
|
log.Errorf("An error occurred while getting project subscriptions for a project: %s", err.Error())
|
||||||
subscriptions = make(map[int64]*Subscription)
|
subscriptions = make(map[int64][]*Subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range projects {
|
for _, p := range projects {
|
||||||
@ -558,8 +575,8 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
|
|||||||
}
|
}
|
||||||
p.IsFavorite = favs[p.ID]
|
p.IsFavorite = favs[p.ID]
|
||||||
|
|
||||||
if subscription, exists := subscriptions[p.ID]; exists {
|
if subscription, exists := subscriptions[p.ID]; exists && len(subscription) > 0 {
|
||||||
p.Subscription = subscription
|
p.Subscription = subscription[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,26 +163,26 @@ func (sb *Subscription) Delete(s *xorm.Session, auth web.Auth) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int64) (cond builder.Cond) {
|
func getSubscriberCondForEntities(entityType SubscriptionEntityType, entityIDs []int64) (cond builder.Cond) {
|
||||||
if entityType == SubscriptionEntityProject {
|
if entityType == SubscriptionEntityProject {
|
||||||
return builder.And(
|
return builder.And(
|
||||||
builder.Eq{"entity_id": entityID},
|
builder.In("entity_id", entityIDs),
|
||||||
builder.Eq{"entity_type": SubscriptionEntityProject},
|
builder.Eq{"entity_type": SubscriptionEntityProject},
|
||||||
)
|
)
|
||||||
// TODO: parent?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if entityType == SubscriptionEntityTask {
|
if entityType == SubscriptionEntityTask {
|
||||||
return builder.Or(
|
return builder.Or(
|
||||||
builder.And(
|
builder.And(
|
||||||
builder.Eq{"entity_id": entityID},
|
builder.In("entity_id", entityIDs),
|
||||||
builder.Eq{"entity_type": SubscriptionEntityTask},
|
builder.Eq{"entity_type": SubscriptionEntityTask},
|
||||||
),
|
),
|
||||||
builder.And(
|
builder.And(
|
||||||
builder.Eq{"entity_id": builder.
|
builder.Eq{"entity_id": builder.
|
||||||
Select("project_id").
|
Select("project_id").
|
||||||
From("tasks").
|
From("tasks").
|
||||||
Where(builder.Eq{"id": entityID}),
|
Where(builder.In("id", entityIDs)),
|
||||||
|
// TODO parent project
|
||||||
},
|
},
|
||||||
builder.Eq{"entity_type": SubscriptionEntityProject},
|
builder.Eq{"entity_type": SubscriptionEntityProject},
|
||||||
),
|
),
|
||||||
@ -195,73 +195,178 @@ func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int6
|
|||||||
// GetSubscription returns a matching subscription for an entity and user.
|
// 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
|
// 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 project the task belongs to.
|
// that task, if there is none it will look for a subscription on the project the task belongs to.
|
||||||
// TODO: check parent projects
|
|
||||||
func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) {
|
func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) {
|
||||||
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, a)
|
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, a)
|
||||||
if err != nil || len(subs) == 0 {
|
if err != nil || len(subs) == 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if sub, exists := subs[entityID]; exists {
|
if sub, exists := subs[entityID]; exists && len(sub) > 0 {
|
||||||
return sub, nil // Take exact match first, if available
|
return sub[0], nil // Take exact match first, if available
|
||||||
}
|
}
|
||||||
for _, sub := range subs {
|
for _, sub := range subs {
|
||||||
return sub, nil // For parents, take next available
|
if len(sub) > 0 {
|
||||||
|
return sub[0], nil // For parents, take next available
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSubscriptions returns a map of subscriptions to a set of given entity IDs
|
// GetSubscriptions returns a map of subscriptions to a set of given entity IDs
|
||||||
func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (projectsToSubscriptions map[int64]*Subscription, err error) {
|
func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (projectsToSubscriptions map[int64][]*Subscription, err error) {
|
||||||
u, is := a.(*user.User)
|
u, is := a.(*user.User)
|
||||||
if !is {
|
if u != nil && !is {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := entityType.validate(); err != nil {
|
if err := entityType.validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var entitiesFilter builder.Cond
|
switch entityType {
|
||||||
for _, eID := range entityIDs {
|
case SubscriptionEntityProject:
|
||||||
if entitiesFilter == nil {
|
return getSubscriptionsForProjects(s, entityIDs, u)
|
||||||
entitiesFilter = getSubscriberCondForEntity(entityType, eID)
|
case SubscriptionEntityTask:
|
||||||
continue
|
subs, err := getSubscriptionsForTasks(s, entityIDs, u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
entitiesFilter = entitiesFilter.Or(getSubscriberCondForEntity(entityType, eID))
|
|
||||||
|
// If the task does not have a subscription directly or from its project, get the one
|
||||||
|
// from the parent and return it instead.
|
||||||
|
for _, eID := range entityIDs {
|
||||||
|
if _, has := subs[eID]; has {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := GetTaskByIDSimple(s, eID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
projectSubscriptions, err := getSubscriptionsForProjects(s, []int64{task.ProjectID}, u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, subscription := range projectSubscriptions {
|
||||||
|
subs[eID] = subscription // The first project subscription is the subscription we're looking for
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSubscriptionsForProjects(s *xorm.Session, projectIDs []int64, u *user.User) (projectsToSubscriptions map[int64][]*Subscription, err error) {
|
||||||
|
origEntityIDs := projectIDs
|
||||||
|
var ps = make(map[int64]*Project)
|
||||||
|
|
||||||
|
for _, eID := range projectIDs {
|
||||||
|
ps[eID], err = GetProjectSimpleByID(s, eID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = ps[eID].GetAllParentProjects(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parentIDs := []int64{}
|
||||||
|
var parent = ps[eID].ParentProject
|
||||||
|
for parent != nil {
|
||||||
|
parentIDs = append(parentIDs, parent.ID)
|
||||||
|
parent = parent.ParentProject
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we have all parent ids
|
||||||
|
projectIDs = append(projectIDs, parentIDs...) // the child project id is already in there
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscriptions []*Subscription
|
var subscriptions []*Subscription
|
||||||
err = s.
|
if u != nil {
|
||||||
Where("user_id = ?", u.ID).
|
err = s.
|
||||||
And(entitiesFilter).
|
Where("user_id = ?", u.ID).
|
||||||
Find(&subscriptions)
|
And(getSubscriberCondForEntities(SubscriptionEntityProject, projectIDs)).
|
||||||
|
Find(&subscriptions)
|
||||||
|
} else {
|
||||||
|
err = s.
|
||||||
|
And(getSubscriberCondForEntities(SubscriptionEntityProject, projectIDs)).
|
||||||
|
Find(&subscriptions)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
projectsToSubscriptions = make(map[int64]*Subscription)
|
projectsToSubscriptions = make(map[int64][]*Subscription)
|
||||||
for _, sub := range subscriptions {
|
for _, sub := range subscriptions {
|
||||||
sub.Entity = sub.EntityType.String()
|
sub.Entity = sub.EntityType.String()
|
||||||
projectsToSubscriptions[sub.EntityID] = sub
|
projectsToSubscriptions[sub.EntityID] = append(projectsToSubscriptions[sub.EntityID], sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rearrange so that subscriptions trickle down
|
||||||
|
|
||||||
|
for _, eID := range origEntityIDs {
|
||||||
|
// If the current project does not have a subscription, climb up the tree until a project has one,
|
||||||
|
// then use that subscription for all child projects
|
||||||
|
_, has := projectsToSubscriptions[eID]
|
||||||
|
if !has {
|
||||||
|
var parent = ps[eID].ParentProject
|
||||||
|
for parent != nil {
|
||||||
|
sub, has := projectsToSubscriptions[parent.ID]
|
||||||
|
projectsToSubscriptions[eID] = sub
|
||||||
|
parent = parent.ParentProject
|
||||||
|
if has { // reached the top of the tree
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return projectsToSubscriptions, nil
|
return projectsToSubscriptions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSubscriptionsForTasks(s *xorm.Session, taskIDs []int64, u *user.User) (projectsToSubscriptions map[int64][]*Subscription, err error) {
|
||||||
|
var subscriptions []*Subscription
|
||||||
|
if u != nil {
|
||||||
|
err = s.
|
||||||
|
Where("user_id = ?", u.ID).
|
||||||
|
And(getSubscriberCondForEntities(SubscriptionEntityTask, taskIDs)).
|
||||||
|
Find(&subscriptions)
|
||||||
|
} else {
|
||||||
|
err = s.
|
||||||
|
And(getSubscriberCondForEntities(SubscriptionEntityTask, taskIDs)).
|
||||||
|
Find(&subscriptions)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectsToSubscriptions = make(map[int64][]*Subscription)
|
||||||
|
for _, sub := range subscriptions {
|
||||||
|
sub.Entity = sub.EntityType.String()
|
||||||
|
projectsToSubscriptions[sub.EntityID] = append(projectsToSubscriptions[sub.EntityID], sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType, entityID int64) (subscriptions []*Subscription, err error) {
|
func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType, entityID int64) (subscriptions []*Subscription, err error) {
|
||||||
if err := entityType.validate(); err != nil {
|
if err := entityType.validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cond := getSubscriberCondForEntity(entityType, entityID)
|
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, nil)
|
||||||
err = s.
|
|
||||||
Where(cond).
|
|
||||||
Find(&subscriptions)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userIDs := []int64{}
|
userIDs := []int64{}
|
||||||
for _, subscription := range subscriptions {
|
subscriptions = make([]*Subscription, len(subs))
|
||||||
userIDs = append(userIDs, subscription.UserID)
|
for _, subss := range subs {
|
||||||
|
for _, subscription := range subss {
|
||||||
|
userIDs = append(userIDs, subscription.UserID)
|
||||||
|
subscriptions = append(subscriptions, subscription)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := user.GetUsersByIDs(s, userIDs)
|
users, err := user.GetUsersByIDs(s, userIDs)
|
||||||
|
@ -260,19 +260,32 @@ func TestSubscriptionGet(t *testing.T) {
|
|||||||
s := db.NewSession()
|
s := db.NewSession()
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
// Project 6 belongs to project 6 where user 6 has subscribed to
|
// Project 25 belongs to project 12 where user 6 has subscribed to
|
||||||
sub, err := GetSubscription(s, SubscriptionEntityProject, 6, u)
|
sub, err := GetSubscription(s, SubscriptionEntityProject, 25, u)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, sub)
|
assert.NotNil(t, sub)
|
||||||
// assert.Equal(t, int64(2), sub.ID) // TODO
|
assert.Equal(t, int64(12), sub.EntityID)
|
||||||
|
assert.Equal(t, int64(3), sub.ID)
|
||||||
|
})
|
||||||
|
t.Run("project from parent's parent", func(t *testing.T) {
|
||||||
|
db.LoadAndAssertFixtures(t)
|
||||||
|
s := db.NewSession()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
// Project 26 belongs to project 25 which belongs to project 12 where user 6 has subscribed to
|
||||||
|
sub, err := GetSubscription(s, SubscriptionEntityProject, 26, u)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, sub)
|
||||||
|
assert.Equal(t, int64(12), sub.EntityID)
|
||||||
|
assert.Equal(t, int64(3), sub.ID)
|
||||||
})
|
})
|
||||||
t.Run("task from parent", func(t *testing.T) {
|
t.Run("task from parent", func(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
s := db.NewSession()
|
s := db.NewSession()
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
// Task 20 belongs to project 11 which belongs to project 6 where the user has subscribed
|
// Task 39 belongs to project 25 which belongs to project 12 where the user has subscribed
|
||||||
sub, err := GetSubscription(s, SubscriptionEntityTask, 20, u)
|
sub, err := GetSubscription(s, SubscriptionEntityTask, 39, u)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, sub)
|
assert.NotNil(t, sub)
|
||||||
// assert.Equal(t, int64(2), sub.ID) TODO
|
// assert.Equal(t, int64(2), sub.ID) TODO
|
||||||
|
Loading…
x
Reference in New Issue
Block a user