From 5cc420b2893fea0253d111c342439c8a0130a2fe Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 17 Jul 2024 12:10:34 +0200 Subject: [PATCH] fix(filters): add task to buckets of saved filters when creating the task --- pkg/models/listeners.go | 87 ++++++++++++++++++++++++++++++++++++- pkg/models/saved_filters.go | 54 +++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/pkg/models/listeners.go b/pkg/models/listeners.go index bb6352e69..b2811aa1e 100644 --- a/pkg/models/listeners.go +++ b/pkg/models/listeners.go @@ -23,7 +23,6 @@ import ( "time" "code.vikunja.io/api/pkg/config" - "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/log" @@ -66,6 +65,7 @@ func RegisterListeners() { events.RegisterListener((&TaskAttachmentDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{}) events.RegisterListener((&TaskRelationCreatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{}) events.RegisterListener((&TaskRelationDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{}) + events.RegisterListener((&TaskCreatedEvent{}).Name(), &UpdateTaskInSavedFilterViews{}) if config.TypesenseEnabled.GetBool() { events.RegisterListener((&TaskDeletedEvent{}).Name(), &RemoveTaskFromTypesense{}) events.RegisterListener((&TaskCreatedEvent{}).Name(), &AddTaskToTypesense{}) @@ -671,6 +671,91 @@ func (s *DecreaseAttachmentCounter) Handle(_ *message.Message) (err error) { return keyvalue.DecrBy(metrics.AttachmentsCountKey, 1) } +// UpdateTaskInSavedFilterViews represents a listener +type UpdateTaskInSavedFilterViews struct { +} + +// Name defines the name for the UpdateTaskInSavedFilterViews listener +func (l *UpdateTaskInSavedFilterViews) Name() string { + return "task.set.saved.filter.views" +} + +// Handle is executed when the event UpdateTaskInSavedFilterViews listens on is fired +func (l *UpdateTaskInSavedFilterViews) Handle(msg *message.Message) (err error) { + event := &TaskCreatedEvent{} + err = json.Unmarshal(msg.Payload, event) + if err != nil { + return err + } + + // This operation is potentially very resource-heavy, because we don't know if a task is included + // in a filter until we evaluate that filter. We need to evaluate each filter individually - since + // there can be many filters, this can take a while to execute. + // For this reason, we do this in an asynchronous event listener. + + s := db.NewSession() + defer s.Close() + + // Get all saved filters with a manual kanban view + kanbanFilterViews := []*ProjectView{} + err = s.Where("project_id < 0 and view_kind = ? and bucket_configuration_mode = ?", ProjectViewKindKanban, BucketConfigurationModeManual). + Find(&kanbanFilterViews) + if err != nil { + return err + } + + filterIDs := []int64{} + for _, view := range kanbanFilterViews { + filterIDs = append(filterIDs, getSavedFilterIDFromProjectID(view.ProjectID)) + } + + filters := map[int64]*SavedFilter{} + err = s.In("id", filterIDs).Find(&filters) + if err != nil { + return err + } + + u, err := user.GetUserByID(s, event.Doer.GetID()) + if err != nil { + return err + } + doerTimezone := u.Timezone + + taskBuckets := []*TaskBucket{} + taskPositions := []*TaskPosition{} + + for _, view := range kanbanFilterViews { + filter, exists := filters[getSavedFilterIDFromProjectID(view.ProjectID)] + if !exists { + log.Debugf("Did not find filter for view %d", view.ID) + continue + } + + taskBucket, taskPosition, err := addTaskToFilter(s, filter, view, doerTimezone, event.Task) + if err != nil { + return err + } + + if taskBucket != nil && taskPosition != nil { + taskBuckets = append(taskBuckets, taskBucket) + taskPositions = append(taskPositions, taskPosition) + } + } + + if len(taskBuckets) > 0 || len(taskPositions) > 0 { + _, err = s.Insert(taskBuckets) + if err != nil { + return + } + _, err = s.Insert(taskPositions) + if err != nil { + return + } + } + + return nil +} + /////// // Project Event Listeners diff --git a/pkg/models/saved_filters.go b/pkg/models/saved_filters.go index e58d2f5bd..f3ef5af30 100644 --- a/pkg/models/saved_filters.go +++ b/pkg/models/saved_filters.go @@ -18,8 +18,11 @@ package models import ( "time" + "xorm.io/builder" + "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/user" + "code.vikunja.io/web" "xorm.io/xorm" ) @@ -214,3 +217,54 @@ func (sf *SavedFilter) Delete(s *xorm.Session, _ web.Auth) error { Delete(sf) return err } + +func addTaskToFilter(s *xorm.Session, filter *SavedFilter, view *ProjectView, fallbackTimezone string, task *Task) (taskBucket *TaskBucket, taskPosition *TaskPosition, err error) { + + filterString := filter.Filters.Filter + + if filter.Filters.FilterTimezone == "" { + filter.Filters.FilterTimezone = fallbackTimezone + } + + parsedFilters, err := getTaskFiltersFromFilterString(filterString, filter.Filters.FilterTimezone) + if err != nil { + log.Errorf("Could not parse filter string '%s' from view %d and saved filter %d: %v", filterString, view.ID, filter.ID, err) + return + } + + filterCond, err := convertFiltersToDBFilterCond(parsedFilters, filter.Filters.FilterIncludeNulls) + if err != nil { + log.Errorf("Could not convert filter string '%s' from view %d and saved filter %d to db conditions: %v", filterString, view.ID, filter.ID, err) + return + } + + taskIsInCurrentFilterAndView, err := s.Where(builder.And( + filterCond, + builder.Eq{"id": task.ID}, + )).Exist(&Task{}) + if !taskIsInCurrentFilterAndView { + return + } + if err != nil { + return nil, nil, err + } + + bucketID, err := getDefaultBucketID(s, view) + if err != nil { + return nil, nil, err + } + + taskBucket = &TaskBucket{ + BucketID: bucketID, + TaskID: task.ID, + ProjectViewID: view.ID, + } + + taskPosition = &TaskPosition{ + TaskID: task.ID, + ProjectViewID: view.ID, + Position: calculateDefaultPosition(task.Index, task.Position), + } + + return +}