tracks/internal/services/todo_service.go

527 lines
13 KiB
Go
Raw Normal View History

Rewrite Tracks application in Golang This commit introduces a complete rewrite of the Tracks GTD application in Go (Golang), providing a modern, performant alternative to the Ruby on Rails implementation. ## Architecture & Technology Stack - Language: Go 1.21+ - Web Framework: Gin - ORM: GORM with SQLite/MySQL/PostgreSQL support - Authentication: JWT with bcrypt password hashing - Clean Architecture: Separated models, services, handlers, and middleware ## Implemented Features ### Core Models - User: Authentication and user management - Context: GTD contexts (@home, @work, etc.) - Project: Project grouping and tracking - Todo: Task management with state machine (active, completed, deferred, pending) - Tag: Flexible tagging system with polymorphic associations - Dependency: Todo dependencies with circular dependency detection - Preference: User preferences and settings - Note: Project notes - Attachment: File attachment support (model only) - RecurringTodo: Recurring task template (model only) ### API Endpoints **Authentication:** - POST /api/auth/login - User login - POST /api/auth/register - User registration - POST /api/auth/logout - User logout - GET /api/me - Get current user **Todos:** - GET /api/todos - List todos with filtering - POST /api/todos - Create todo - GET /api/todos/:id - Get todo details - PUT /api/todos/:id - Update todo - DELETE /api/todos/:id - Delete todo - POST /api/todos/:id/complete - Mark as completed - POST /api/todos/:id/activate - Mark as active - POST /api/todos/:id/defer - Defer to future date - POST /api/todos/:id/dependencies - Add dependency - DELETE /api/todos/:id/dependencies/:successor_id - Remove dependency **Projects:** - GET /api/projects - List projects - POST /api/projects - Create project - GET /api/projects/:id - Get project details - PUT /api/projects/:id - Update project - DELETE /api/projects/:id - Delete project - POST /api/projects/:id/complete - Complete project - POST /api/projects/:id/activate - Activate project - POST /api/projects/:id/hide - Hide project - POST /api/projects/:id/review - Mark as reviewed - GET /api/projects/:id/stats - Get project statistics **Contexts:** - GET /api/contexts - List contexts - POST /api/contexts - Create context - GET /api/contexts/:id - Get context details - PUT /api/contexts/:id - Update context - DELETE /api/contexts/:id - Delete context - POST /api/contexts/:id/hide - Hide context - POST /api/contexts/:id/activate - Activate context - POST /api/contexts/:id/close - Close context - GET /api/contexts/:id/stats - Get context statistics ### Business Logic **Todo State Management:** - Active: Ready to work on - Completed: Finished tasks - Deferred: Future actions (show_from date) - Pending: Blocked by dependencies **Dependency Management:** - Create blocking relationships between todos - Automatic state transitions when blocking todos complete - Circular dependency detection - Automatic unblocking when prerequisites complete **Tag System:** - Polymorphic tagging for todos and recurring todos - Automatic tag creation on first use - Tag cloud support **Project & Context Tracking:** - State management (active, hidden, closed/completed) - Statistics and health indicators - Review tracking for projects ### Infrastructure **Configuration:** - Environment-based configuration - Support for SQLite, MySQL, and PostgreSQL - Configurable JWT secrets and token expiry - Flexible server settings **Database:** - GORM for ORM - Automatic migrations - Connection pooling - Multi-database support **Authentication & Security:** - JWT-based authentication - Bcrypt password hashing - Secure cookie support - Token refresh mechanism **Docker Support:** - Multi-stage Dockerfile for optimized builds - Docker Compose with PostgreSQL - Volume mounting for data persistence - Production-ready configuration ## Project Structure ``` cmd/tracks/ # Application entry point internal/ config/ # Configuration management database/ # Database setup and migrations handlers/ # HTTP request handlers middleware/ # Authentication middleware models/ # Database models services/ # Business logic layer ``` ## Documentation - README_GOLANG.md: Comprehensive documentation - .env.example: Configuration template - API documentation included in README - Code comments for complex logic ## Future Work The following features from the original Rails app are not yet implemented: - Recurring todo instantiation logic - Email integration (Mailgun/CloudMailin) - Advanced statistics and analytics - Import/Export functionality (CSV, YAML, XML) - File upload handling for attachments - Mobile views - RSS/Atom feeds - iCalendar export ## Benefits Over Rails Version - Performance: Compiled binary, lower resource usage - Deployment: Single binary, no runtime dependencies - Type Safety: Compile-time type checking - Concurrency: Better handling of concurrent requests - Memory: Lower memory footprint - Portability: Easy cross-platform compilation ## Testing The code structure supports testing, though tests are not yet implemented. Future work includes adding unit and integration tests.
2025-11-05 10:46:59 +00:00
package services
import (
"errors"
"fmt"
"time"
"github.com/TracksApp/tracks/internal/database"
"github.com/TracksApp/tracks/internal/models"
"gorm.io/gorm"
)
// TodoService handles todo business logic
type TodoService struct{}
// NewTodoService creates a new TodoService
func NewTodoService() *TodoService {
return &TodoService{}
}
// CreateTodoRequest represents a todo creation request
type CreateTodoRequest struct {
Description string `json:"description" binding:"required"`
Notes string `json:"notes"`
ContextID uint `json:"context_id" binding:"required"`
ProjectID *uint `json:"project_id"`
DueDate *time.Time `json:"due_date"`
ShowFrom *time.Time `json:"show_from"`
Starred bool `json:"starred"`
TagNames []string `json:"tags"`
}
// UpdateTodoRequest represents a todo update request
type UpdateTodoRequest struct {
Description *string `json:"description"`
Notes *string `json:"notes"`
ContextID *uint `json:"context_id"`
ProjectID *uint `json:"project_id"`
DueDate *time.Time `json:"due_date"`
ShowFrom *time.Time `json:"show_from"`
Starred *bool `json:"starred"`
TagNames []string `json:"tags"`
}
// ListTodosFilter represents filters for listing todos
type ListTodosFilter struct {
State models.TodoState
ContextID *uint
ProjectID *uint
TagName *string
Starred *bool
Overdue *bool
DueToday *bool
ShowFrom *bool // If true, only show todos where show_from has passed
IncludeTags bool
}
// GetTodos returns todos for a user with optional filters
func (s *TodoService) GetTodos(userID uint, filter ListTodosFilter) ([]models.Todo, error) {
var todos []models.Todo
query := database.DB.Where("user_id = ?", userID)
// Apply filters
if filter.State != "" {
query = query.Where("state = ?", filter.State)
}
if filter.ContextID != nil {
query = query.Where("context_id = ?", *filter.ContextID)
}
if filter.ProjectID != nil {
query = query.Where("project_id = ?", *filter.ProjectID)
}
if filter.Starred != nil {
query = query.Where("starred = ?", *filter.Starred)
}
if filter.Overdue != nil && *filter.Overdue {
query = query.Where("due_date < ? AND state = ?", time.Now(), models.TodoStateActive)
}
if filter.DueToday != nil && *filter.DueToday {
today := time.Now().Truncate(24 * time.Hour)
tomorrow := today.Add(24 * time.Hour)
query = query.Where("due_date >= ? AND due_date < ?", today, tomorrow)
}
if filter.ShowFrom != nil && *filter.ShowFrom {
now := time.Now()
query = query.Where("show_from IS NULL OR show_from <= ?", now)
}
// Preload associations
query = query.Preload("Context").Preload("Project")
// Filter by tag
if filter.TagName != nil {
query = query.Joins("JOIN taggings ON taggings.taggable_id = todos.id AND taggings.taggable_type = ?", "Todo").
Joins("JOIN tags ON tags.id = taggings.tag_id").
Where("tags.name = ? AND tags.user_id = ?", *filter.TagName, userID)
}
// Order by created_at
query = query.Order("created_at ASC")
if err := query.Find(&todos).Error; err != nil {
return nil, err
}
Fix polymorphic tagging and add test script 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
2025-11-05 10:59:26 +00:00
// Load tags if requested
if filter.IncludeTags {
for i := range todos {
if err := s.loadTodoTags(&todos[i]); err != nil {
return nil, err
}
}
}
Rewrite Tracks application in Golang This commit introduces a complete rewrite of the Tracks GTD application in Go (Golang), providing a modern, performant alternative to the Ruby on Rails implementation. ## Architecture & Technology Stack - Language: Go 1.21+ - Web Framework: Gin - ORM: GORM with SQLite/MySQL/PostgreSQL support - Authentication: JWT with bcrypt password hashing - Clean Architecture: Separated models, services, handlers, and middleware ## Implemented Features ### Core Models - User: Authentication and user management - Context: GTD contexts (@home, @work, etc.) - Project: Project grouping and tracking - Todo: Task management with state machine (active, completed, deferred, pending) - Tag: Flexible tagging system with polymorphic associations - Dependency: Todo dependencies with circular dependency detection - Preference: User preferences and settings - Note: Project notes - Attachment: File attachment support (model only) - RecurringTodo: Recurring task template (model only) ### API Endpoints **Authentication:** - POST /api/auth/login - User login - POST /api/auth/register - User registration - POST /api/auth/logout - User logout - GET /api/me - Get current user **Todos:** - GET /api/todos - List todos with filtering - POST /api/todos - Create todo - GET /api/todos/:id - Get todo details - PUT /api/todos/:id - Update todo - DELETE /api/todos/:id - Delete todo - POST /api/todos/:id/complete - Mark as completed - POST /api/todos/:id/activate - Mark as active - POST /api/todos/:id/defer - Defer to future date - POST /api/todos/:id/dependencies - Add dependency - DELETE /api/todos/:id/dependencies/:successor_id - Remove dependency **Projects:** - GET /api/projects - List projects - POST /api/projects - Create project - GET /api/projects/:id - Get project details - PUT /api/projects/:id - Update project - DELETE /api/projects/:id - Delete project - POST /api/projects/:id/complete - Complete project - POST /api/projects/:id/activate - Activate project - POST /api/projects/:id/hide - Hide project - POST /api/projects/:id/review - Mark as reviewed - GET /api/projects/:id/stats - Get project statistics **Contexts:** - GET /api/contexts - List contexts - POST /api/contexts - Create context - GET /api/contexts/:id - Get context details - PUT /api/contexts/:id - Update context - DELETE /api/contexts/:id - Delete context - POST /api/contexts/:id/hide - Hide context - POST /api/contexts/:id/activate - Activate context - POST /api/contexts/:id/close - Close context - GET /api/contexts/:id/stats - Get context statistics ### Business Logic **Todo State Management:** - Active: Ready to work on - Completed: Finished tasks - Deferred: Future actions (show_from date) - Pending: Blocked by dependencies **Dependency Management:** - Create blocking relationships between todos - Automatic state transitions when blocking todos complete - Circular dependency detection - Automatic unblocking when prerequisites complete **Tag System:** - Polymorphic tagging for todos and recurring todos - Automatic tag creation on first use - Tag cloud support **Project & Context Tracking:** - State management (active, hidden, closed/completed) - Statistics and health indicators - Review tracking for projects ### Infrastructure **Configuration:** - Environment-based configuration - Support for SQLite, MySQL, and PostgreSQL - Configurable JWT secrets and token expiry - Flexible server settings **Database:** - GORM for ORM - Automatic migrations - Connection pooling - Multi-database support **Authentication & Security:** - JWT-based authentication - Bcrypt password hashing - Secure cookie support - Token refresh mechanism **Docker Support:** - Multi-stage Dockerfile for optimized builds - Docker Compose with PostgreSQL - Volume mounting for data persistence - Production-ready configuration ## Project Structure ``` cmd/tracks/ # Application entry point internal/ config/ # Configuration management database/ # Database setup and migrations handlers/ # HTTP request handlers middleware/ # Authentication middleware models/ # Database models services/ # Business logic layer ``` ## Documentation - README_GOLANG.md: Comprehensive documentation - .env.example: Configuration template - API documentation included in README - Code comments for complex logic ## Future Work The following features from the original Rails app are not yet implemented: - Recurring todo instantiation logic - Email integration (Mailgun/CloudMailin) - Advanced statistics and analytics - Import/Export functionality (CSV, YAML, XML) - File upload handling for attachments - Mobile views - RSS/Atom feeds - iCalendar export ## Benefits Over Rails Version - Performance: Compiled binary, lower resource usage - Deployment: Single binary, no runtime dependencies - Type Safety: Compile-time type checking - Concurrency: Better handling of concurrent requests - Memory: Lower memory footprint - Portability: Easy cross-platform compilation ## Testing The code structure supports testing, though tests are not yet implemented. Future work includes adding unit and integration tests.
2025-11-05 10:46:59 +00:00
return todos, nil
}
// GetTodo returns a single todo by ID
func (s *TodoService) GetTodo(userID, todoID uint) (*models.Todo, error) {
var todo models.Todo
if err := database.DB.
Where("id = ? AND user_id = ?", todoID, userID).
Preload("Context").
Preload("Project").
Preload("Attachments").
Preload("Predecessors.Predecessor").
Preload("Successors.Successor").
First(&todo).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("todo not found")
}
return nil, err
}
Fix polymorphic tagging and add test script 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
2025-11-05 10:59:26 +00:00
// Load tags manually through taggings
if err := s.loadTodoTags(&todo); err != nil {
return nil, err
}
Rewrite Tracks application in Golang This commit introduces a complete rewrite of the Tracks GTD application in Go (Golang), providing a modern, performant alternative to the Ruby on Rails implementation. ## Architecture & Technology Stack - Language: Go 1.21+ - Web Framework: Gin - ORM: GORM with SQLite/MySQL/PostgreSQL support - Authentication: JWT with bcrypt password hashing - Clean Architecture: Separated models, services, handlers, and middleware ## Implemented Features ### Core Models - User: Authentication and user management - Context: GTD contexts (@home, @work, etc.) - Project: Project grouping and tracking - Todo: Task management with state machine (active, completed, deferred, pending) - Tag: Flexible tagging system with polymorphic associations - Dependency: Todo dependencies with circular dependency detection - Preference: User preferences and settings - Note: Project notes - Attachment: File attachment support (model only) - RecurringTodo: Recurring task template (model only) ### API Endpoints **Authentication:** - POST /api/auth/login - User login - POST /api/auth/register - User registration - POST /api/auth/logout - User logout - GET /api/me - Get current user **Todos:** - GET /api/todos - List todos with filtering - POST /api/todos - Create todo - GET /api/todos/:id - Get todo details - PUT /api/todos/:id - Update todo - DELETE /api/todos/:id - Delete todo - POST /api/todos/:id/complete - Mark as completed - POST /api/todos/:id/activate - Mark as active - POST /api/todos/:id/defer - Defer to future date - POST /api/todos/:id/dependencies - Add dependency - DELETE /api/todos/:id/dependencies/:successor_id - Remove dependency **Projects:** - GET /api/projects - List projects - POST /api/projects - Create project - GET /api/projects/:id - Get project details - PUT /api/projects/:id - Update project - DELETE /api/projects/:id - Delete project - POST /api/projects/:id/complete - Complete project - POST /api/projects/:id/activate - Activate project - POST /api/projects/:id/hide - Hide project - POST /api/projects/:id/review - Mark as reviewed - GET /api/projects/:id/stats - Get project statistics **Contexts:** - GET /api/contexts - List contexts - POST /api/contexts - Create context - GET /api/contexts/:id - Get context details - PUT /api/contexts/:id - Update context - DELETE /api/contexts/:id - Delete context - POST /api/contexts/:id/hide - Hide context - POST /api/contexts/:id/activate - Activate context - POST /api/contexts/:id/close - Close context - GET /api/contexts/:id/stats - Get context statistics ### Business Logic **Todo State Management:** - Active: Ready to work on - Completed: Finished tasks - Deferred: Future actions (show_from date) - Pending: Blocked by dependencies **Dependency Management:** - Create blocking relationships between todos - Automatic state transitions when blocking todos complete - Circular dependency detection - Automatic unblocking when prerequisites complete **Tag System:** - Polymorphic tagging for todos and recurring todos - Automatic tag creation on first use - Tag cloud support **Project & Context Tracking:** - State management (active, hidden, closed/completed) - Statistics and health indicators - Review tracking for projects ### Infrastructure **Configuration:** - Environment-based configuration - Support for SQLite, MySQL, and PostgreSQL - Configurable JWT secrets and token expiry - Flexible server settings **Database:** - GORM for ORM - Automatic migrations - Connection pooling - Multi-database support **Authentication & Security:** - JWT-based authentication - Bcrypt password hashing - Secure cookie support - Token refresh mechanism **Docker Support:** - Multi-stage Dockerfile for optimized builds - Docker Compose with PostgreSQL - Volume mounting for data persistence - Production-ready configuration ## Project Structure ``` cmd/tracks/ # Application entry point internal/ config/ # Configuration management database/ # Database setup and migrations handlers/ # HTTP request handlers middleware/ # Authentication middleware models/ # Database models services/ # Business logic layer ``` ## Documentation - README_GOLANG.md: Comprehensive documentation - .env.example: Configuration template - API documentation included in README - Code comments for complex logic ## Future Work The following features from the original Rails app are not yet implemented: - Recurring todo instantiation logic - Email integration (Mailgun/CloudMailin) - Advanced statistics and analytics - Import/Export functionality (CSV, YAML, XML) - File upload handling for attachments - Mobile views - RSS/Atom feeds - iCalendar export ## Benefits Over Rails Version - Performance: Compiled binary, lower resource usage - Deployment: Single binary, no runtime dependencies - Type Safety: Compile-time type checking - Concurrency: Better handling of concurrent requests - Memory: Lower memory footprint - Portability: Easy cross-platform compilation ## Testing The code structure supports testing, though tests are not yet implemented. Future work includes adding unit and integration tests.
2025-11-05 10:46:59 +00:00
return &todo, nil
}
Fix polymorphic tagging and add test script 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
2025-11-05 10:59:26 +00:00
// loadTodoTags loads tags for a todo through the polymorphic taggings
func (s *TodoService) loadTodoTags(todo *models.Todo) error {
var tags []models.Tag
err := database.DB.
Joins("JOIN taggings ON taggings.tag_id = tags.id").
Where("taggings.taggable_id = ? AND taggings.taggable_type = ?", todo.ID, "Todo").
Find(&tags).Error
if err != nil {
return err
}
todo.Tags = tags
return nil
}
Rewrite Tracks application in Golang This commit introduces a complete rewrite of the Tracks GTD application in Go (Golang), providing a modern, performant alternative to the Ruby on Rails implementation. ## Architecture & Technology Stack - Language: Go 1.21+ - Web Framework: Gin - ORM: GORM with SQLite/MySQL/PostgreSQL support - Authentication: JWT with bcrypt password hashing - Clean Architecture: Separated models, services, handlers, and middleware ## Implemented Features ### Core Models - User: Authentication and user management - Context: GTD contexts (@home, @work, etc.) - Project: Project grouping and tracking - Todo: Task management with state machine (active, completed, deferred, pending) - Tag: Flexible tagging system with polymorphic associations - Dependency: Todo dependencies with circular dependency detection - Preference: User preferences and settings - Note: Project notes - Attachment: File attachment support (model only) - RecurringTodo: Recurring task template (model only) ### API Endpoints **Authentication:** - POST /api/auth/login - User login - POST /api/auth/register - User registration - POST /api/auth/logout - User logout - GET /api/me - Get current user **Todos:** - GET /api/todos - List todos with filtering - POST /api/todos - Create todo - GET /api/todos/:id - Get todo details - PUT /api/todos/:id - Update todo - DELETE /api/todos/:id - Delete todo - POST /api/todos/:id/complete - Mark as completed - POST /api/todos/:id/activate - Mark as active - POST /api/todos/:id/defer - Defer to future date - POST /api/todos/:id/dependencies - Add dependency - DELETE /api/todos/:id/dependencies/:successor_id - Remove dependency **Projects:** - GET /api/projects - List projects - POST /api/projects - Create project - GET /api/projects/:id - Get project details - PUT /api/projects/:id - Update project - DELETE /api/projects/:id - Delete project - POST /api/projects/:id/complete - Complete project - POST /api/projects/:id/activate - Activate project - POST /api/projects/:id/hide - Hide project - POST /api/projects/:id/review - Mark as reviewed - GET /api/projects/:id/stats - Get project statistics **Contexts:** - GET /api/contexts - List contexts - POST /api/contexts - Create context - GET /api/contexts/:id - Get context details - PUT /api/contexts/:id - Update context - DELETE /api/contexts/:id - Delete context - POST /api/contexts/:id/hide - Hide context - POST /api/contexts/:id/activate - Activate context - POST /api/contexts/:id/close - Close context - GET /api/contexts/:id/stats - Get context statistics ### Business Logic **Todo State Management:** - Active: Ready to work on - Completed: Finished tasks - Deferred: Future actions (show_from date) - Pending: Blocked by dependencies **Dependency Management:** - Create blocking relationships between todos - Automatic state transitions when blocking todos complete - Circular dependency detection - Automatic unblocking when prerequisites complete **Tag System:** - Polymorphic tagging for todos and recurring todos - Automatic tag creation on first use - Tag cloud support **Project & Context Tracking:** - State management (active, hidden, closed/completed) - Statistics and health indicators - Review tracking for projects ### Infrastructure **Configuration:** - Environment-based configuration - Support for SQLite, MySQL, and PostgreSQL - Configurable JWT secrets and token expiry - Flexible server settings **Database:** - GORM for ORM - Automatic migrations - Connection pooling - Multi-database support **Authentication & Security:** - JWT-based authentication - Bcrypt password hashing - Secure cookie support - Token refresh mechanism **Docker Support:** - Multi-stage Dockerfile for optimized builds - Docker Compose with PostgreSQL - Volume mounting for data persistence - Production-ready configuration ## Project Structure ``` cmd/tracks/ # Application entry point internal/ config/ # Configuration management database/ # Database setup and migrations handlers/ # HTTP request handlers middleware/ # Authentication middleware models/ # Database models services/ # Business logic layer ``` ## Documentation - README_GOLANG.md: Comprehensive documentation - .env.example: Configuration template - API documentation included in README - Code comments for complex logic ## Future Work The following features from the original Rails app are not yet implemented: - Recurring todo instantiation logic - Email integration (Mailgun/CloudMailin) - Advanced statistics and analytics - Import/Export functionality (CSV, YAML, XML) - File upload handling for attachments - Mobile views - RSS/Atom feeds - iCalendar export ## Benefits Over Rails Version - Performance: Compiled binary, lower resource usage - Deployment: Single binary, no runtime dependencies - Type Safety: Compile-time type checking - Concurrency: Better handling of concurrent requests - Memory: Lower memory footprint - Portability: Easy cross-platform compilation ## Testing The code structure supports testing, though tests are not yet implemented. Future work includes adding unit and integration tests.
2025-11-05 10:46:59 +00:00
// CreateTodo creates a new todo
func (s *TodoService) CreateTodo(userID uint, req CreateTodoRequest) (*models.Todo, error) {
// Verify context belongs to user
var context models.Context
if err := database.DB.Where("id = ? AND user_id = ?", req.ContextID, userID).First(&context).Error; err != nil {
return nil, fmt.Errorf("context not found")
}
// Verify project belongs to user if provided
if req.ProjectID != nil {
var project models.Project
if err := database.DB.Where("id = ? AND user_id = ?", *req.ProjectID, userID).First(&project).Error; err != nil {
return nil, fmt.Errorf("project not found")
}
}
// Determine initial state
state := models.TodoStateActive
if req.ShowFrom != nil && req.ShowFrom.After(time.Now()) {
state = models.TodoStateDeferred
}
// Create todo
todo := models.Todo{
UserID: userID,
Description: req.Description,
Notes: req.Notes,
ContextID: req.ContextID,
ProjectID: req.ProjectID,
DueDate: req.DueDate,
ShowFrom: req.ShowFrom,
Starred: req.Starred,
State: state,
}
// Begin transaction
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Create(&todo).Error; err != nil {
tx.Rollback()
return nil, err
}
// Handle tags
if len(req.TagNames) > 0 {
tagService := NewTagService()
if err := tagService.SetTodoTags(tx, userID, todo.ID, req.TagNames); err != nil {
tx.Rollback()
return nil, err
}
}
if err := tx.Commit().Error; err != nil {
return nil, err
}
// Reload with associations
return s.GetTodo(userID, todo.ID)
}
// UpdateTodo updates a todo
func (s *TodoService) UpdateTodo(userID, todoID uint, req UpdateTodoRequest) (*models.Todo, error) {
todo, err := s.GetTodo(userID, todoID)
if err != nil {
return nil, err
}
// Begin transaction
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// Update fields
if req.Description != nil {
todo.Description = *req.Description
}
if req.Notes != nil {
todo.Notes = *req.Notes
}
if req.ContextID != nil {
// Verify context
var context models.Context
if err := tx.Where("id = ? AND user_id = ?", *req.ContextID, userID).First(&context).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("context not found")
}
todo.ContextID = *req.ContextID
}
if req.ProjectID != nil {
// Verify project
var project models.Project
if err := tx.Where("id = ? AND user_id = ?", *req.ProjectID, userID).First(&project).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("project not found")
}
todo.ProjectID = req.ProjectID
}
if req.DueDate != nil {
todo.DueDate = req.DueDate
}
if req.ShowFrom != nil {
todo.ShowFrom = req.ShowFrom
}
if req.Starred != nil {
todo.Starred = *req.Starred
}
if err := tx.Save(&todo).Error; err != nil {
tx.Rollback()
return nil, err
}
// Handle tags
if req.TagNames != nil {
tagService := NewTagService()
if err := tagService.SetTodoTags(tx, userID, todo.ID, req.TagNames); err != nil {
tx.Rollback()
return nil, err
}
}
if err := tx.Commit().Error; err != nil {
return nil, err
}
// Reload with associations
return s.GetTodo(userID, todoID)
}
// DeleteTodo deletes a todo
func (s *TodoService) DeleteTodo(userID, todoID uint) error {
todo, err := s.GetTodo(userID, todoID)
if err != nil {
return err
}
return database.DB.Delete(&todo).Error
}
// CompleteTodo marks a todo as completed
func (s *TodoService) CompleteTodo(userID, todoID uint) (*models.Todo, error) {
todo, err := s.GetTodo(userID, todoID)
if err != nil {
return nil, err
}
if err := todo.Complete(); err != nil {
return nil, err
}
tx := database.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Save(&todo).Error; err != nil {
tx.Rollback()
return nil, err
}
// Unblock any pending successors
if err := s.checkAndUnblockSuccessors(tx, todoID); err != nil {
tx.Rollback()
return nil, err
}
if err := tx.Commit().Error; err != nil {
return nil, err
}
return s.GetTodo(userID, todoID)
}
// ActivateTodo marks a todo as active
func (s *TodoService) ActivateTodo(userID, todoID uint) (*models.Todo, error) {
todo, err := s.GetTodo(userID, todoID)
if err != nil {
return nil, err
}
// Check if there are incomplete predecessors
hasIncompletePredecessors, err := s.hasIncompletePredecessors(todoID)
if err != nil {
return nil, err
}
if hasIncompletePredecessors {
return nil, fmt.Errorf("cannot activate todo with incomplete predecessors")
}
if err := todo.Activate(); err != nil {
return nil, err
}
if err := database.DB.Save(&todo).Error; err != nil {
return nil, err
}
return s.GetTodo(userID, todoID)
}
// DeferTodo marks a todo as deferred
func (s *TodoService) DeferTodo(userID, todoID uint, showFrom time.Time) (*models.Todo, error) {
todo, err := s.GetTodo(userID, todoID)
if err != nil {
return nil, err
}
if err := todo.Defer(showFrom); err != nil {
return nil, err
}
if err := database.DB.Save(&todo).Error; err != nil {
return nil, err
}
return s.GetTodo(userID, todoID)
}
// AddDependency adds a dependency between two todos
func (s *TodoService) AddDependency(userID, predecessorID, successorID uint) error {
// Verify both todos belong to user
if _, err := s.GetTodo(userID, predecessorID); err != nil {
return fmt.Errorf("predecessor not found")
}
if _, err := s.GetTodo(userID, successorID); err != nil {
return fmt.Errorf("successor not found")
}
// Check for circular dependencies
if err := s.checkCircularDependency(predecessorID, successorID); err != nil {
return err
}
// Create dependency
dependency := models.Dependency{
PredecessorID: predecessorID,
SuccessorID: successorID,
RelationshipType: models.DependencyTypeBlocks,
}
if err := database.DB.Create(&dependency).Error; err != nil {
return err
}
// Block the successor if predecessor is not complete
successor, _ := s.GetTodo(userID, successorID)
predecessor, _ := s.GetTodo(userID, predecessorID)
if !predecessor.IsCompleted() && !successor.IsPending() {
successor.Block()
database.DB.Save(successor)
}
return nil
}
// RemoveDependency removes a dependency
func (s *TodoService) RemoveDependency(userID, predecessorID, successorID uint) error {
// Verify both todos belong to user
if _, err := s.GetTodo(userID, predecessorID); err != nil {
return err
}
if _, err := s.GetTodo(userID, successorID); err != nil {
return err
}
if err := database.DB.
Where("predecessor_id = ? AND successor_id = ?", predecessorID, successorID).
Delete(&models.Dependency{}).Error; err != nil {
return err
}
// Check if successor should be unblocked
s.checkAndUnblockSuccessors(database.DB, successorID)
return nil
}
// Helper functions
func (s *TodoService) hasIncompletePredecessors(todoID uint) (bool, error) {
var count int64
err := database.DB.Model(&models.Dependency{}).
Joins("JOIN todos ON todos.id = dependencies.predecessor_id").
Where("dependencies.successor_id = ? AND todos.state != ?", todoID, models.TodoStateCompleted).
Count(&count).Error
return count > 0, err
}
func (s *TodoService) checkAndUnblockSuccessors(tx *gorm.DB, todoID uint) error {
var successors []models.Todo
// Get all successors
if err := tx.
Joins("JOIN dependencies ON dependencies.successor_id = todos.id").
Where("dependencies.predecessor_id = ? AND todos.state = ?", todoID, models.TodoStatePending).
Find(&successors).Error; err != nil {
return err
}
// For each successor, check if all predecessors are complete
for _, successor := range successors {
hasIncompletePredecessors, err := s.hasIncompletePredecessors(successor.ID)
if err != nil {
return err
}
if !hasIncompletePredecessors {
successor.Unblock()
if err := tx.Save(&successor).Error; err != nil {
return err
}
}
}
return nil
}
func (s *TodoService) checkCircularDependency(predecessorID, successorID uint) error {
// Simple check: ensure successor is not already a predecessor of the predecessor
visited := make(map[uint]bool)
return s.dfsCheckCircular(successorID, predecessorID, visited)
}
func (s *TodoService) dfsCheckCircular(currentID, targetID uint, visited map[uint]bool) error {
if currentID == targetID {
return fmt.Errorf("circular dependency detected")
}
if visited[currentID] {
return nil
}
visited[currentID] = true
var successors []models.Dependency
if err := database.DB.Where("predecessor_id = ?", currentID).Find(&successors).Error; err != nil {
return err
}
for _, dep := range successors {
if err := s.dfsCheckCircular(dep.SuccessorID, targetID, visited); err != nil {
return err
}
}
return nil
}