Internal
Public Access
Implement comprehensive email verification and admin approval system for user registration. Users must verify their email before logging in, and admins must approve new users before they can access the system. Major features: - Email verification with UUID tokens (24hr expiry, one-time use) - Admin approval workflow via Django admin interface - Custom authentication backend enforcing verification and approval - Password change functionality for authenticated users - Enhanced profile page with proper styling and theme support - Collect first name, last name, and timezone during registration - Profile link added to header navigation Email notifications: - Verification email sent after registration - Admin notification when users need approval - Approval notification sent to users Security features: - Cryptographically secure UUID tokens - Token expiration and one-time use enforcement - Email enumeration protection - Custom JWT token validation for API access - Existing users auto-approved via data migration Templates added: - Email templates (verification, approval, admin notification) - Web templates (password change, verification pages) - Enhanced profile page with dark/light mode support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
207 lines
7.4 KiB
Python
207 lines
7.4 KiB
Python
"""
|
|
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)
|
|
sync_id = serializers.UUIDField(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', 'sync_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)
|
|
sync_id = serializers.UUIDField(read_only=True)
|
|
|
|
class Meta:
|
|
model = Tag
|
|
fields = ['id', 'sync_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)
|
|
sync_id = serializers.UUIDField(read_only=True)
|
|
task = serializers.UUIDField(source='task.sync_id', read_only=True)
|
|
|
|
class Meta:
|
|
model = TimeEntry
|
|
fields = ['id', 'sync_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)
|