mirror of
https://github.com/TracksApp/tracks.git
synced 2025-12-16 15:20:13 +01:00
This commit fixes the tag loading issue and adds comprehensive testing. ## Bug Fix: Polymorphic Tag Loading Fixed issue with many-to-many tag relationships not working correctly with polymorphic associations. The problem was that GORM doesn't support using `many2many` with polymorphic relationships directly. **Changes:** - Modified `internal/models/todo.go`: Changed Tags field to use `gorm:"-"` to skip GORM handling - Modified `internal/models/recurring_todo.go`: Same fix for recurring todos - Modified `internal/services/todo_service.go`: Added `loadTodoTags()` helper function to manually load tags through the taggings join table **How it works now:** 1. Tags are no longer automatically loaded by GORM 2. Manual loading via JOIN query: `tags JOIN taggings ON tag_id WHERE taggable_id AND taggable_type` 3. Called after loading todos in both `GetTodo()` and `GetTodos()` ## Testing Added `test_api.sh` - comprehensive integration test script that tests: 1. Health check 2. User registration 3. Authentication 4. Context creation 5. Project creation 6. Todo creation with tags 7. Listing todos with filters 8. Completing todos 9. Project statistics All tests pass successfully! ## Files Changed - `internal/models/todo.go`: Fix tag relationship - `internal/models/recurring_todo.go`: Fix tag relationship - `internal/services/todo_service.go`: Add manual tag loading - `test_api.sh`: New integration test script - `go.sum`: Updated with exact dependency versions
171 lines
4.7 KiB
Go
171 lines
4.7 KiB
Go
package models
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// TodoState represents the state of a todo
|
|
type TodoState string
|
|
|
|
const (
|
|
TodoStateActive TodoState = "active"
|
|
TodoStateCompleted TodoState = "completed"
|
|
TodoStateDeferred TodoState = "deferred"
|
|
TodoStatePending TodoState = "pending"
|
|
)
|
|
|
|
// Todo represents a task/action item
|
|
type Todo struct {
|
|
ID uint `gorm:"primaryKey" json:"id"`
|
|
UserID uint `gorm:"not null;index" json:"user_id"`
|
|
ContextID uint `gorm:"not null;index" json:"context_id"`
|
|
ProjectID *uint `gorm:"index" json:"project_id"`
|
|
RecurringTodoID *uint `gorm:"index" json:"recurring_todo_id"`
|
|
Description string `gorm:"not null;size:300" json:"description"`
|
|
Notes string `gorm:"type:text" json:"notes"`
|
|
State TodoState `gorm:"type:varchar(20);default:'active';index" json:"state"`
|
|
DueDate *time.Time `json:"due_date"`
|
|
ShowFrom *time.Time `json:"show_from"`
|
|
CompletedAt *time.Time `json:"completed_at"`
|
|
Starred bool `gorm:"default:false" json:"starred"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
|
|
// Associations
|
|
User User `gorm:"foreignKey:UserID" json:"-"`
|
|
Context Context `gorm:"foreignKey:ContextID" json:"context,omitempty"`
|
|
Project *Project `gorm:"foreignKey:ProjectID" json:"project,omitempty"`
|
|
RecurringTodo *RecurringTodo `gorm:"foreignKey:RecurringTodoID" json:"recurring_todo,omitempty"`
|
|
Taggings []Tagging `gorm:"polymorphic:Taggable" json:"-"`
|
|
Tags []Tag `gorm:"-" json:"tags,omitempty"`
|
|
Attachments []Attachment `gorm:"foreignKey:TodoID" json:"attachments,omitempty"`
|
|
|
|
// Dependencies
|
|
Predecessors []Dependency `gorm:"foreignKey:SuccessorID" json:"predecessors,omitempty"`
|
|
Successors []Dependency `gorm:"foreignKey:PredecessorID" json:"successors,omitempty"`
|
|
}
|
|
|
|
// BeforeCreate sets default values
|
|
func (t *Todo) BeforeCreate(tx *gorm.DB) error {
|
|
if t.State == "" {
|
|
t.State = TodoStateActive
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsActive returns true if the todo is active
|
|
func (t *Todo) IsActive() bool {
|
|
return t.State == TodoStateActive
|
|
}
|
|
|
|
// IsCompleted returns true if the todo is completed
|
|
func (t *Todo) IsCompleted() bool {
|
|
return t.State == TodoStateCompleted
|
|
}
|
|
|
|
// IsDeferred returns true if the todo is deferred
|
|
func (t *Todo) IsDeferred() bool {
|
|
return t.State == TodoStateDeferred
|
|
}
|
|
|
|
// IsPending returns true if the todo is pending (blocked)
|
|
func (t *Todo) IsPending() bool {
|
|
return t.State == TodoStatePending
|
|
}
|
|
|
|
// Complete transitions the todo to completed state
|
|
func (t *Todo) Complete() error {
|
|
if t.IsCompleted() {
|
|
return errors.New("todo is already completed")
|
|
}
|
|
|
|
now := time.Now()
|
|
t.State = TodoStateCompleted
|
|
t.CompletedAt = &now
|
|
|
|
return nil
|
|
}
|
|
|
|
// Activate transitions the todo to active state
|
|
func (t *Todo) Activate() error {
|
|
if t.IsActive() {
|
|
return errors.New("todo is already active")
|
|
}
|
|
|
|
// Can't activate if it has incomplete predecessors
|
|
// This check should be done by the service layer
|
|
|
|
t.State = TodoStateActive
|
|
t.CompletedAt = nil
|
|
|
|
return nil
|
|
}
|
|
|
|
// Defer transitions the todo to deferred state
|
|
func (t *Todo) Defer(showFrom time.Time) error {
|
|
if !t.IsActive() {
|
|
return errors.New("can only defer active todos")
|
|
}
|
|
|
|
t.State = TodoStateDeferred
|
|
t.ShowFrom = &showFrom
|
|
|
|
return nil
|
|
}
|
|
|
|
// Block transitions the todo to pending state
|
|
func (t *Todo) Block() error {
|
|
if t.IsCompleted() {
|
|
return errors.New("cannot block completed todo")
|
|
}
|
|
|
|
t.State = TodoStatePending
|
|
|
|
return nil
|
|
}
|
|
|
|
// Unblock transitions the todo from pending to active
|
|
func (t *Todo) Unblock() error {
|
|
if !t.IsPending() {
|
|
return errors.New("todo is not pending")
|
|
}
|
|
|
|
t.State = TodoStateActive
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsDue returns true if the todo has a due date that has passed
|
|
func (t *Todo) IsDue() bool {
|
|
if t.DueDate == nil {
|
|
return false
|
|
}
|
|
return t.DueDate.Before(time.Now())
|
|
}
|
|
|
|
// IsOverdue returns true if the todo is active and past due
|
|
func (t *Todo) IsOverdue() bool {
|
|
return t.IsActive() && t.IsDue()
|
|
}
|
|
|
|
// ShouldShow returns true if the todo should be displayed (not deferred or show_from has passed)
|
|
func (t *Todo) ShouldShow() bool {
|
|
if t.ShowFrom == nil {
|
|
return true
|
|
}
|
|
return t.ShowFrom.Before(time.Now()) || t.ShowFrom.Equal(time.Now())
|
|
}
|
|
|
|
// IsStale returns true if the todo is old based on the staleness threshold
|
|
func (t *Todo) IsStale(stalenessThresholdDays int) bool {
|
|
if t.IsCompleted() {
|
|
return false
|
|
}
|
|
|
|
threshold := time.Now().AddDate(0, 0, -stalenessThresholdDays)
|
|
return t.CreatedAt.Before(threshold)
|
|
}
|