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:
Keith Smith
2025-12-18 17:41:29 -07:00
co-authored by Claude Sonnet 4.5
commit 6a0b35c39c
84 changed files with 7763 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
"""
Task serializers for KeepItGoing API.
"""
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Task, Tag, TimeEntry, TaskShare
User = get_user_model()
class TagSerializer(serializers.ModelSerializer):
"""Serializer for tags."""
task_count = serializers.SerializerMethodField()
class Meta:
model = Tag
fields = [
'id', 'name', 'description', 'color', 'icon', 'sort_order',
'is_archived', 'task_count', 'sync_id', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'sync_id', 'created_at', 'updated_at']
def get_task_count(self, obj):
return obj.tasks.filter(status__in=['pending', 'in_progress']).count()
def create(self, validated_data):
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
class TimeEntrySerializer(serializers.ModelSerializer):
"""Serializer for time entries."""
is_running = serializers.ReadOnlyField()
class Meta:
model = TimeEntry
fields = [
'id', 'task', 'started_at', 'ended_at', 'duration_seconds',
'notes', 'is_running', 'sync_id', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'sync_id', 'created_at', 'updated_at', 'duration_seconds']
def create(self, validated_data):
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
class TaskSerializer(serializers.ModelSerializer):
"""Serializer for tasks."""
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(),
many=True,
write_only=True,
required=False,
source='tags'
)
tag_sync_ids = serializers.SerializerMethodField()
subtasks = serializers.SerializerMethodField()
is_overdue = serializers.ReadOnlyField()
total_time_spent = serializers.ReadOnlyField()
parent_sync_id = serializers.UUIDField(source='parent.sync_id', read_only=True, allow_null=True)
class Meta:
model = Task
fields = [
'id', 'parent', 'parent_sync_id',
'title', 'description', 'status', 'priority', 'due_date', 'due_time',
'reminder_at', 'completed_at', 'recurrence', 'recurrence_rule',
'recurrence_end_date', 'tags', 'tag_ids', 'tag_sync_ids', 'subtasks',
'sort_order', 'is_overdue', 'total_time_spent', 'sync_id',
'created_at', 'updated_at'
]
read_only_fields = ['id', 'sync_id', 'created_at', 'updated_at', 'completed_at']
def get_tag_sync_ids(self, obj):
"""Return list of tag sync_ids for sync purposes."""
return [str(tag.sync_id) for tag in obj.tags.all()]
def get_subtasks(self, obj):
# Only include subtasks for top-level tasks to avoid recursion
if obj.parent is None:
subtasks = obj.subtasks.all()
return TaskSerializer(subtasks, many=True, context=self.context).data
return []
def create(self, validated_data):
tags = validated_data.pop('tags', [])
validated_data['user'] = self.context['request'].user
task = super().create(validated_data)
task.tags.set(tags)
return task
def update(self, instance, validated_data):
tags = validated_data.pop('tags', None)
task = super().update(instance, validated_data)
if tags is not None:
task.tags.set(tags)
return task
class TaskListSerializer(serializers.ModelSerializer):
"""Lightweight serializer for task lists."""
tags = TagSerializer(many=True, read_only=True)
subtask_count = serializers.SerializerMethodField()
is_overdue = serializers.ReadOnlyField()
class Meta:
model = Task
fields = [
'id', 'title', 'status', 'priority', 'due_date', 'due_time',
'tags', 'subtask_count', 'is_overdue', 'sort_order', 'sync_id', 'updated_at'
]
def get_subtask_count(self, obj):
return obj.subtasks.count()
class TaskSyncSerializer(serializers.ModelSerializer):
"""Serializer for tasks in sync responses - uses sync_id as id."""
id = serializers.UUIDField(source='sync_id', read_only=True)
tag_sync_ids = serializers.SerializerMethodField()
parent_sync_id = serializers.UUIDField(source='parent.sync_id', read_only=True, allow_null=True)
class Meta:
model = Task
fields = [
'id', 'parent', 'parent_sync_id',
'title', 'description', 'status', 'priority', 'due_date', 'due_time',
'reminder_at', 'completed_at', 'recurrence', 'recurrence_rule',
'recurrence_end_date', 'tag_sync_ids',
'sort_order', 'is_deleted', 'created_at', 'updated_at'
]
def get_tag_sync_ids(self, obj):
"""Return list of tag sync_ids for sync purposes."""
return [str(tag.sync_id) for tag in obj.tags.all()]
class TagSyncSerializer(serializers.ModelSerializer):
"""Serializer for tags in sync responses - uses sync_id as id."""
id = serializers.UUIDField(source='sync_id', read_only=True)
class Meta:
model = Tag
fields = ['id', 'name', 'description', 'color', 'icon', 'sort_order',
'is_archived', 'is_deleted', 'created_at', 'updated_at']
class TimeEntrySyncSerializer(serializers.ModelSerializer):
"""Serializer for time entries in sync responses - uses sync_id as id."""
id = serializers.UUIDField(source='sync_id', read_only=True)
task = serializers.UUIDField(source='task.sync_id', read_only=True)
class Meta:
model = TimeEntry
fields = ['id', 'task', 'started_at', 'ended_at', 'notes',
'is_deleted', 'created_at', 'updated_at']
class TaskShareSerializer(serializers.ModelSerializer):
"""Serializer for task sharing."""
shared_with_email = serializers.EmailField(write_only=True)
shared_with_username = serializers.CharField(source='shared_with.username', read_only=True)
class Meta:
model = TaskShare
fields = [
'id', 'task', 'tag', 'shared_with_email', 'shared_with_username',
'permission', 'sync_id', 'created_at'
]
read_only_fields = ['id', 'sync_id', 'created_at']
def validate_shared_with_email(self, value):
try:
user = User.objects.get(email=value)
if user == self.context['request'].user:
raise serializers.ValidationError("Cannot share with yourself.")
return value
except User.DoesNotExist:
raise serializers.ValidationError("User with this email does not exist.")
def validate(self, attrs):
if not attrs.get('task') and not attrs.get('tag'):
raise serializers.ValidationError("Must share either a task or a tag.")
if attrs.get('task') and attrs.get('tag'):
raise serializers.ValidationError("Cannot share both a task and a tag.")
return attrs
def create(self, validated_data):
email = validated_data.pop('shared_with_email')
validated_data['shared_with'] = User.objects.get(email=email)
validated_data['owner'] = self.context['request'].user
return super().create(validated_data)