You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

858 lines
19 KiB

package services
import (
"context"
"github.com/AchievementNetwork/stringset"
"github.com/gissleh/stufflog/database"
"github.com/gissleh/stufflog/internal/auth"
"github.com/gissleh/stufflog/internal/slerrors"
"github.com/gissleh/stufflog/models"
"golang.org/x/sync/errgroup"
)
// Loader loads the stuff.
type Loader struct {
DB database.Database
}
func (l *Loader) FindGroup(ctx context.Context, id string) (*models.GroupResult, error) {
group, err := l.DB.Groups().Find(ctx, id)
if err != nil {
return nil, err
}
if group.UserID != auth.UserID(ctx) {
return nil, slerrors.NotFound("Goal")
}
result := &models.GroupResult{Group: *group}
result.Items, err = l.DB.Items().List(ctx, models.ItemFilter{
UserID: auth.UserID(ctx),
GroupIDs: []string{group.ID},
})
if err != nil {
return nil, err
}
return result, nil
}
func (l *Loader) ListGroups(ctx context.Context, filter models.GroupFilter) ([]*models.GroupResult, error) {
filter.UserID = auth.UserID(ctx)
groups, err := l.DB.Groups().List(ctx, filter)
if err != nil {
return nil, err
}
groupIDs := make([]string, 0, len(groups))
for _, group := range groups {
groupIDs = append(groupIDs, group.ID)
}
items, err := l.DB.Items().List(ctx, models.ItemFilter{
UserID: auth.UserID(ctx),
GroupIDs: groupIDs,
})
results := make([]*models.GroupResult, len(groups))
for i, group := range groups {
results[i] = &models.GroupResult{Group: *group, Items: []*models.Item{}}
for _, item := range items {
if item.GroupID == group.ID {
results[i].Items = append(results[i].Items, item)
}
}
}
return results, nil
}
func (l *Loader) FindItem(ctx context.Context, id string) (*models.ItemResult, error) {
item, err := l.DB.Items().Find(ctx, id)
if err != nil {
return nil, err
}
if item.UserID != auth.UserID(ctx) {
return nil, slerrors.NotFound("Item")
}
result := &models.ItemResult{Item: *item}
result.Group, err = l.DB.Groups().Find(ctx, item.GroupID)
if err != nil {
return nil, err
}
return result, nil
}
func (l *Loader) ListItems(ctx context.Context, filter models.ItemFilter) ([]*models.ItemResult, error) {
filter.UserID = auth.UserID(ctx)
items, err := l.DB.Items().List(ctx, filter)
if err != nil {
return nil, err
}
groupIDs := make([]string, 0, len(items))
for _, item := range items {
groupIDs = append(groupIDs, item.GroupID)
}
groups, err := l.DB.Groups().List(ctx, models.GroupFilter{
UserID: auth.UserID(ctx),
IDs: groupIDs,
})
results := make([]*models.ItemResult, len(items))
for i, item := range items {
results[i] = &models.ItemResult{Item: *item}
for _, group := range groups {
if item.GroupID == group.ID {
results[i].Group = group
break
}
}
}
return results, nil
}
func (l *Loader) FindLog(ctx context.Context, id string) (*models.LogResult, error) {
log, err := l.DB.Logs().Find(ctx, id)
if err != nil {
return nil, err
}
if log.UserID != auth.UserID(ctx) {
return nil, slerrors.NotFound("Goal")
}
result := &models.LogResult{
Log: *log,
Task: nil,
}
task, err := l.DB.Tasks().Find(ctx, log.TaskID)
if err != nil {
return nil, err
}
project, err := l.DB.Projects().Find(ctx, task.ProjectID)
if err != nil {
return nil, err
}
result.Task = &models.TaskWithProject{
Task: *task,
Project: project,
}
result.Item, _ = l.DB.Items().Find(ctx, log.ItemID)
if log.SecondaryItemID != nil {
result.SecondaryItem, _ = l.DB.Items().Find(ctx, *log.SecondaryItemID)
}
return result, nil
}
func (l *Loader) ListLogs(ctx context.Context, filter models.LogFilter) ([]*models.LogResult, error) {
filter.UserID = auth.UserID(ctx)
logs, err := l.DB.Logs().List(ctx, filter)
if err != nil {
return nil, err
}
taskIDs := stringset.New()
itemIDs := stringset.New()
projectIDs := stringset.New()
for _, log := range logs {
taskIDs.Add(log.TaskID)
itemIDs.Add(log.ItemID)
if log.SecondaryItemID != nil {
itemIDs.Add(*log.SecondaryItemID)
}
}
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{
UserID: auth.UserID(ctx),
IDs: taskIDs.Strings(),
})
if err != nil {
return nil, err
}
for _, task := range tasks {
projectIDs.Add(task.ProjectID)
}
items, err := l.DB.Items().List(ctx, models.ItemFilter{
UserID: auth.UserID(ctx),
IDs: itemIDs.Strings(),
})
if err != nil {
return nil, err
}
projects, err := l.DB.Projects().List(ctx, models.ProjectFilter{
UserID: auth.UserID(ctx),
IDs: projectIDs.Strings(),
})
if err != nil {
return nil, err
}
results := make([]*models.LogResult, len(logs))
for i, log := range logs {
results[i] = &models.LogResult{
Log: *log,
Task: nil,
}
for _, task := range tasks {
if task.ID == log.TaskID {
results[i].Task = &models.TaskWithProject{Task: *task}
break
}
}
if results[i].Task != nil {
for _, project := range projects {
if project.ID == results[i].Task.ProjectID {
results[i].Task.Project = project
break
}
}
}
for _, item := range items {
if item.ID == log.ItemID {
results[i].Item = item
}
if log.SecondaryItemID != nil && item.ID == *log.SecondaryItemID {
results[i].SecondaryItem = item
}
}
}
return results, nil
}
func (l *Loader) FindProject(ctx context.Context, id string) (*models.ProjectResult, error) {
project, err := l.DB.Projects().Find(ctx, id)
if err != nil {
return nil, err
}
if project.UserID != auth.UserID(ctx) {
return nil, slerrors.NotFound("Goal")
}
result := &models.ProjectResult{Project: *project, Subtractions: []models.ProjectSubtraction{}}
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{
UserID: auth.UserID(ctx),
ProjectIDs: []string{project.ID},
})
taskIDs := make([]string, 0, len(tasks))
itemIDs := stringset.New()
for _, task := range tasks {
taskIDs = append(taskIDs, task.ID)
itemIDs.Add(task.ItemID)
}
logs, err := l.DB.Logs().List(ctx, models.LogFilter{
UserID: auth.UserID(ctx),
TaskIDs: taskIDs,
})
for _, log := range logs {
if log.SecondaryItemID != nil {
itemIDs.Add(*log.SecondaryItemID)
}
}
if err != nil {
return nil, err
}
items, err := l.DB.Items().List(ctx, models.ItemFilter{
UserID: auth.UserID(ctx),
IDs: itemIDs.Strings(),
})
if err != nil {
return nil, err
}
result.Tasks = make([]*models.TaskResult, len(tasks))
for i, task := range tasks {
result.Tasks[i] = &models.TaskResult{
Logs: []*models.LogWithSecondaryItem{},
}
result.Tasks[i].Task = *task
for _, log := range logs {
if log.TaskID == task.ID {
log := models.LogWithSecondaryItem{
Log: *log,
SecondaryItem: nil,
}
if log.SecondaryItemID != nil {
for _, item := range items {
if item.ID == *log.SecondaryItemID {
log.SecondaryItem = item
break
}
}
}
result.Tasks[i].Logs = append(result.Tasks[i].Logs, &log)
}
}
for _, item := range items {
if item.ID == task.ItemID {
result.Tasks[i].Item = item
break
}
}
for _, log := range result.Tasks[i].Logs {
if project.StartTime != nil && log.LoggedTime.Before(*project.StartTime) {
result.Subtractions = append(result.Subtractions, models.ProjectSubtraction{
Item: *result.Tasks[i].Item,
Amount: log.ItemAmount,
})
if log.SecondaryItemID != nil {
result.Subtractions = append(result.Subtractions, models.ProjectSubtraction{
Item: *log.SecondaryItem,
Amount: log.SecondaryItemAmount,
})
}
}
result.Tasks[i].CompletedAmount += log.Amount(log.ItemID)
}
}
result.SortTasks()
return result, nil
}
func (l *Loader) ListProjects(ctx context.Context, filter models.ProjectFilter) ([]*models.ProjectResult, error) {
filter.UserID = auth.UserID(ctx)
projects, err := l.DB.Projects().List(ctx, filter)
if err != nil {
return nil, err
}
projectIDs := make([]string, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
}
tasks, links, err := l.DB.Tasks().ListWithLinks(ctx, models.TaskFilter{
UserID: auth.UserID(ctx),
ProjectIDs: projectIDs,
})
if err != nil {
return nil, err
}
taskIDs := make([]string, 0, len(tasks))
itemIDs := stringset.New()
for _, task := range tasks {
taskIDs = append(taskIDs, task.ID)
itemIDs.Add(task.ItemID)
}
logs, err := l.DB.Logs().List(ctx, models.LogFilter{
UserID: auth.UserID(ctx),
TaskIDs: taskIDs,
})
if err != nil {
return nil, err
}
for _, log := range logs {
if log.SecondaryItemID != nil {
itemIDs.Add(*log.SecondaryItemID)
}
}
items, err := l.DB.Items().List(ctx, models.ItemFilter{
UserID: auth.UserID(ctx),
IDs: itemIDs.Strings(),
})
if err != nil {
return nil, err
}
results := make([]*models.ProjectResult, len(projects))
for i, project := range projects {
results[i] = &models.ProjectResult{Project: *project, Subtractions: []models.ProjectSubtraction{}}
results[i].Tasks = make([]*models.TaskResult, 0, 16)
for _, task := range tasks {
if task.ProjectID != project.ID {
foundLink := false
for _, link := range links {
if link.TaskID == task.ID && link.ProjectID == project.ID {
foundLink = true
break
}
}
if !foundLink {
continue
}
}
taskResult := &models.TaskResult{
Task: *task,
Logs: []*models.LogWithSecondaryItem{},
}
for _, log := range logs {
if log.TaskID == task.ID {
log := models.LogWithSecondaryItem{
Log: *log,
SecondaryItem: nil,
}
if log.SecondaryItemID != nil {
for _, item := range items {
if item.ID == *log.SecondaryItemID {
log.SecondaryItem = item
break
}
}
}
taskResult.Logs = append(taskResult.Logs, &log)
}
}
for _, item := range items {
if item.ID == task.ItemID {
taskResult.Item = item
break
}
}
for _, log := range taskResult.Logs {
if project.StartTime != nil && log.LoggedTime.Before(*project.StartTime) {
results[i].Subtractions = append(results[i].Subtractions, models.ProjectSubtraction{
Item: *taskResult.Item,
Amount: log.ItemAmount,
})
if log.SecondaryItem != nil {
results[i].Subtractions = append(results[i].Subtractions, models.ProjectSubtraction{
Item: *log.SecondaryItem,
Amount: log.SecondaryItemAmount,
})
}
}
taskResult.CompletedAmount += log.Amount(taskResult.ItemID)
}
results[i].Tasks = append(results[i].Tasks, taskResult)
}
results[i].SortTasks()
}
return results, nil
}
func (l *Loader) FindProjectGroup(ctx context.Context, id string) (*models.ProjectGroupResult, error) {
group, err := l.DB.ProjectGroups().Find(ctx, id)
if err != nil {
return nil, err
}
projects, err := l.ListProjects(ctx, models.ProjectFilter{
UserID: auth.UserID(ctx),
ProjectGroupIDs: []string{group.ID},
})
if err != nil {
return nil, err
}
result := &models.ProjectGroupResult{
ProjectGroup: *group,
Projects: projects,
}
result.RecountTasks()
return result, nil
}
func (l *Loader) ListProjectGroups(ctx context.Context) ([]*models.ProjectGroupResult, error) {
groups, err := l.DB.ProjectGroups().List(ctx, models.ProjectGroupFilter{UserID: auth.UserID(ctx)})
if err != nil {
return nil, err
}
ids := make([]string, 0, len(groups))
for _, group := range groups {
ids = append(ids, group.ID)
}
projects, err := l.ListProjects(ctx, models.ProjectFilter{
UserID: auth.UserID(ctx),
ProjectGroupIDs: ids,
})
results := make([]*models.ProjectGroupResult, 0, len(groups)+1)
for _, group := range groups {
matchingProjects := make([]*models.ProjectResult, 0, len(projects)/len(groups))
for _, project := range projects {
if *project.GroupID == group.ID {
matchingProjects = append(matchingProjects, project)
}
}
result := &models.ProjectGroupResult{
ProjectGroup: *group,
Projects: matchingProjects,
}
result.RecountTasks()
results = append(results, result)
}
ungroupedProjects, err := l.ListProjects(ctx, models.ProjectFilter{
UserID: auth.UserID(ctx),
Ungrouped: true,
})
if err != nil {
return nil, err
}
if len(ungroupedProjects) > 0 {
result := &models.ProjectGroupResult{
ProjectGroup: models.ProjectGroup{
ID: "META_UNGROUPED",
Name: "Ungrouped Projects",
Abbreviation: "OTHER",
CategoryNames: map[string]string{},
},
Projects: ungroupedProjects,
}
result.RecountTasks()
results = append(results, result)
}
return results, nil
}
func (l *Loader) FindTask(ctx context.Context, id string) (*models.TaskResult, error) {
task, err := l.DB.Tasks().Find(ctx, id)
if err != nil {
return nil, err
}
if task.UserID != auth.UserID(ctx) {
return nil, slerrors.NotFound("Goal")
}
result := &models.TaskResult{Task: *task}
result.Item, _ = l.DB.Items().Find(ctx, task.ItemID)
result.Project, _ = l.DB.Projects().Find(ctx, task.ProjectID)
logs, err := l.DB.Logs().List(ctx, models.LogFilter{
UserID: task.UserID,
TaskIDs: []string{task.ID},
})
if err != nil {
return nil, err
}
itemIds := stringset.New()
for _, log := range logs {
if log.SecondaryItemID != nil {
itemIds.Add(*log.SecondaryItemID)
}
}
items, err := l.DB.Items().List(ctx, models.ItemFilter{
UserID: task.UserID,
IDs: itemIds.Strings(),
})
for _, log := range logs {
result.CompletedAmount += log.Amount(result.ItemID)
log := models.LogWithSecondaryItem{
Log: *log,
SecondaryItem: nil,
}
if log.SecondaryItemID != nil {
for _, item := range items {
if item.ID == *log.SecondaryItemID {
log.SecondaryItem = item
break
}
}
}
result.Logs = append(result.Logs, &log)
}
return result, nil
}
func (l *Loader) ListTasks(ctx context.Context, filter models.TaskFilter) ([]*models.TaskResult, error) {
filter.UserID = auth.UserID(ctx)
tasks, err := l.DB.Tasks().List(ctx, filter)
if err != nil {
return nil, err
}
if len(tasks) == 0 {
return []*models.TaskResult{}, nil
}
taskIDs := make([]string, 0, len(tasks))
itemIDs := stringset.New()
projectIDs := stringset.New()
for _, task := range tasks {
taskIDs = append(taskIDs, task.ID)
itemIDs.Add(task.ItemID)
projectIDs.Add(task.ProjectID)
}
logs, err := l.DB.Logs().List(ctx, models.LogFilter{
UserID: auth.UserID(ctx),
TaskIDs: taskIDs,
})
if err != nil {
return nil, err
}
for _, log := range logs {
if log.SecondaryItemID != nil {
itemIDs.Add(*log.SecondaryItemID)
}
}
items, err := l.DB.Items().List(ctx, models.ItemFilter{
UserID: auth.UserID(ctx),
IDs: itemIDs.Strings(),
})
if err != nil {
return nil, err
}
projects, err := l.DB.Projects().List(ctx, models.ProjectFilter{
UserID: auth.UserID(ctx),
IDs: projectIDs.Strings(),
})
if err != nil {
return nil, err
}
results := make([]*models.TaskResult, 0, len(tasks))
for _, task := range tasks {
result := &models.TaskResult{
Task: *task,
Logs: []*models.LogWithSecondaryItem{},
}
for _, log := range logs {
if log.TaskID != task.ID {
continue
}
result.CompletedAmount += log.Amount(result.ItemID)
log := models.LogWithSecondaryItem{
Log: *log,
SecondaryItem: nil,
}
if log.SecondaryItemID != nil {
for _, item := range items {
if item.ID == *log.SecondaryItemID {
log.SecondaryItem = item
break
}
}
}
result.Logs = append(result.Logs, &log)
}
for _, item := range items {
if item.ID == task.ItemID {
result.Item = item
break
}
}
for _, project := range projects {
if project.ID == task.ProjectID {
result.Project = project
break
}
}
for _, log := range result.Logs {
result.CompletedAmount += log.Amount(result.ItemID)
}
results = append(results, result)
}
return results, nil
}
func (l *Loader) FindGoal(ctx context.Context, id string) (*models.GoalResult, error) {
goal, err := l.DB.Goals().Find(ctx, id)
if err != nil {
return nil, err
}
if goal.UserID != auth.UserID(ctx) {
return nil, slerrors.NotFound("Goal")
}
return l.populateGoals(ctx, goal)
}
func (l *Loader) ListGoals(ctx context.Context, filter models.GoalFilter) ([]*models.GoalResult, error) {
filter.UserID = auth.UserID(ctx)
goals, err := l.DB.Goals().List(ctx, filter)
if err != nil {
return nil, err
}
results := make([]*models.GoalResult, len(goals))
eg := errgroup.Group{}
for i := range results {
index := i // Required to avoid race condition.
eg.Go(func() error {
res, err := l.populateGoals(ctx, goals[index])
if err != nil {
return err
}
results[index] = res
return nil
})
}
err = eg.Wait()
if err != nil {
return nil, err
}
return results, nil
}
func (l *Loader) populateGoals(ctx context.Context, goal *models.Goal) (*models.GoalResult, error) {
userID := auth.UserID(ctx)
result := &models.GoalResult{
Goal: *goal,
Group: nil,
Items: nil,
Logs: nil,
CompletedAmount: 0,
}
result.Group, _ = l.DB.Groups().Find(ctx, goal.GroupID)
if result.Group != nil {
// Get items
items, err := l.DB.Items().List(ctx, models.ItemFilter{
UserID: userID,
GroupIDs: []string{goal.GroupID},
})
if err != nil {
return nil, err
}
itemIDs := make([]string, 0, len(items))
for _, item := range items {
result.Items = append(result.Items, &models.GoalResultItem{
Item: *item,
CompletedAmount: 0,
})
itemIDs = append(itemIDs, item.ID)
}
// Get logs
logs, err := l.DB.Logs().List(ctx, models.LogFilter{
UserID: userID,
ItemIDs: itemIDs,
MinTime: &goal.StartTime,
MaxTime: &goal.EndTime,
})
if err != nil {
return nil, err
}
// Get tasks
taskIDs := stringset.New()
for _, log := range logs {
taskIDs.Add(log.TaskID)
}
tasks, err := l.DB.Tasks().List(ctx, models.TaskFilter{
UserID: userID,
IDs: taskIDs.Strings(),
})
if err != nil {
return nil, err
}
projectIDs := stringset.New()
for _, task := range tasks {
projectIDs.Add(task.ProjectID)
}
projects, err := l.DB.Projects().List(ctx, models.ProjectFilter{
UserID: userID,
IDs: projectIDs.Strings(),
})
// Apply logs
result.Logs = make([]*models.GoalResultLog, 0, len(logs))
for _, log := range logs {
resultLog := &models.GoalResultLog{
LogResult: models.LogResult{Log: *log},
}
contributes := false
for _, task := range tasks {
if task.ID == log.TaskID {
resultLog.Task = &models.TaskWithProject{Task: *task}
for _, project := range projects {
if project.ID == task.ProjectID {
resultLog.Task.Project = project
break
}
}
break
}
}
for _, item := range result.Items {
amount := log.Amount(item.ID)
if amount > 0 && goal.Accepts(&item.Item, &resultLog.Task.Task) {
item.CompletedAmount += amount
if goal.Unweighted {
if item.GroupWeight > 0 {
result.CompletedAmount += amount
}
} else {
result.CompletedAmount += amount * item.GroupWeight
}
contributes = true
} else {
amount = 0
}
if item.ID == log.ItemID {
resultLog.Item = &item.Item
if amount > 0 {
resultLog.ItemCounted = true
}
if log.SecondaryItemID == nil {
break
}
}
if log.SecondaryItemID != nil && item.ID == *log.SecondaryItemID {
if amount > 0 {
resultLog.SecondaryItemCounted = true
}
resultLog.SecondaryItem = &item.Item
}
}
if contributes {
result.Logs = append(result.Logs, resultLog)
}
}
}
return result, nil
}