Internal
Public Access
Initial commit: KeepItGoing task management server
Features: - Django-based REST API with web interface - Task management with tags, priorities, and due dates - Time tracking with start/stop timers - Subtasks support - Task filtering (all, today, upcoming, overdue, completed) - Tag-based organization with color coding - Sorting by due date and priority - Auto-assign tags when filtering - Responsive 3-pane layout (sidebar, task list, detail panel) - Task sharing between users - Mobile-responsive design with dark mode support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
+283
@@ -0,0 +1,283 @@
|
||||
"""
|
||||
Task models for KeepItGoing.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class SoftDeleteManager(models.Manager):
|
||||
"""Manager that excludes soft-deleted items by default."""
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(is_deleted=False)
|
||||
|
||||
|
||||
class AllObjectsManager(models.Manager):
|
||||
"""Manager that includes soft-deleted items."""
|
||||
pass
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
"""
|
||||
Tags for organizing and categorizing tasks.
|
||||
Supports multiple tags per task.
|
||||
"""
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='tags'
|
||||
)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
color = models.CharField(max_length=7, default='#3B82F6') # Hex color
|
||||
icon = models.CharField(max_length=50, blank=True) # Icon name
|
||||
sort_order = models.IntegerField(default=0)
|
||||
is_archived = models.BooleanField(default=False)
|
||||
|
||||
# Sync fields
|
||||
sync_id = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# Managers
|
||||
objects = SoftDeleteManager()
|
||||
all_objects = AllObjectsManager()
|
||||
|
||||
class Meta:
|
||||
db_table = 'tags'
|
||||
ordering = ['sort_order', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
"""
|
||||
Main task model with support for subtasks, recurrence, and time tracking.
|
||||
"""
|
||||
STATUS_CHOICES = [
|
||||
('pending', 'Pending'),
|
||||
('in_progress', 'In Progress'),
|
||||
('completed', 'Completed'),
|
||||
('cancelled', 'Cancelled'),
|
||||
]
|
||||
|
||||
PRIORITY_CHOICES = [
|
||||
('low', 'Low'),
|
||||
('medium', 'Medium'),
|
||||
('high', 'High'),
|
||||
('urgent', 'Urgent'),
|
||||
]
|
||||
|
||||
RECURRENCE_CHOICES = [
|
||||
('none', 'None'),
|
||||
('daily', 'Daily'),
|
||||
('weekly', 'Weekly'),
|
||||
('biweekly', 'Bi-weekly'),
|
||||
('monthly', 'Monthly'),
|
||||
('yearly', 'Yearly'),
|
||||
('custom', 'Custom'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='tasks'
|
||||
)
|
||||
|
||||
# Hierarchy
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='subtasks'
|
||||
)
|
||||
|
||||
# Core fields
|
||||
title = models.CharField(max_length=500)
|
||||
description = models.TextField(blank=True)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
||||
priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium')
|
||||
|
||||
# Dates and times
|
||||
due_date = models.DateField(null=True, blank=True)
|
||||
due_time = models.TimeField(null=True, blank=True)
|
||||
reminder_at = models.DateTimeField(null=True, blank=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Recurrence (RRULE format for custom)
|
||||
recurrence = models.CharField(max_length=20, choices=RECURRENCE_CHOICES, default='none')
|
||||
recurrence_rule = models.CharField(max_length=500, blank=True) # RRULE string
|
||||
recurrence_end_date = models.DateField(null=True, blank=True)
|
||||
|
||||
# Tags (multiple per task)
|
||||
tags = models.ManyToManyField(Tag, blank=True, related_name='tasks')
|
||||
|
||||
# Ordering
|
||||
sort_order = models.IntegerField(default=0)
|
||||
|
||||
# Sync fields
|
||||
sync_id = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# Managers
|
||||
objects = SoftDeleteManager()
|
||||
all_objects = AllObjectsManager()
|
||||
|
||||
class Meta:
|
||||
db_table = 'tasks'
|
||||
ordering = ['sort_order', '-priority', 'due_date', 'created_at']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Auto-set completed_at when status changes to completed
|
||||
if self.status == 'completed' and self.completed_at is None:
|
||||
from django.utils import timezone
|
||||
self.completed_at = timezone.now()
|
||||
elif self.status != 'completed':
|
||||
self.completed_at = None
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def is_overdue(self):
|
||||
"""Check if task is overdue."""
|
||||
if self.due_date and self.status not in ('completed', 'cancelled'):
|
||||
from django.utils import timezone
|
||||
return self.due_date < timezone.now().date()
|
||||
return False
|
||||
|
||||
@property
|
||||
def total_time_spent(self):
|
||||
"""Calculate total time spent on this task in seconds."""
|
||||
total_seconds = sum(
|
||||
entry.duration_seconds or 0
|
||||
for entry in self.time_entries.all()
|
||||
)
|
||||
return total_seconds
|
||||
|
||||
@property
|
||||
def total_time_formatted(self):
|
||||
"""Return total time spent formatted as H:MM:SS."""
|
||||
seconds = self.total_time_spent
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
secs = seconds % 60
|
||||
return f"{hours}:{minutes:02d}:{secs:02d}"
|
||||
|
||||
|
||||
class TimeEntry(models.Model):
|
||||
"""
|
||||
Time tracking entries for tasks.
|
||||
"""
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='time_entries')
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='time_entries'
|
||||
)
|
||||
|
||||
started_at = models.DateTimeField()
|
||||
ended_at = models.DateTimeField(null=True, blank=True)
|
||||
duration_seconds = models.IntegerField(null=True, blank=True)
|
||||
notes = models.TextField(blank=True)
|
||||
|
||||
# Sync fields
|
||||
sync_id = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# Managers
|
||||
objects = SoftDeleteManager()
|
||||
all_objects = AllObjectsManager()
|
||||
|
||||
class Meta:
|
||||
db_table = 'time_entries'
|
||||
ordering = ['-started_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.task.title} - {self.started_at}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Calculate duration if ended
|
||||
if self.ended_at and self.started_at:
|
||||
delta = self.ended_at - self.started_at
|
||||
self.duration_seconds = int(delta.total_seconds())
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
"""Check if this time entry is currently running."""
|
||||
return self.ended_at is None
|
||||
|
||||
|
||||
class TaskShare(models.Model):
|
||||
"""
|
||||
Sharing tasks or tags with other users.
|
||||
"""
|
||||
PERMISSION_CHOICES = [
|
||||
('viewer', 'Viewer'),
|
||||
('editor', 'Editor'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='shares_given'
|
||||
)
|
||||
shared_with = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='shares_received'
|
||||
)
|
||||
|
||||
# Can share either a task or a tag
|
||||
task = models.ForeignKey(
|
||||
Task,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='shares'
|
||||
)
|
||||
tag = models.ForeignKey(
|
||||
Tag,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='shares'
|
||||
)
|
||||
|
||||
permission = models.CharField(max_length=20, choices=PERMISSION_CHOICES, default='viewer')
|
||||
|
||||
# Sync fields
|
||||
sync_id = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'task_shares'
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
check=(
|
||||
models.Q(task__isnull=False, tag__isnull=True) |
|
||||
models.Q(task__isnull=True, tag__isnull=False)
|
||||
),
|
||||
name='share_task_or_tag_not_both'
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
shared_item = self.task or self.tag
|
||||
return f"{shared_item} shared with {self.shared_with.email}"
|
||||
Reference in New Issue
Block a user