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
View File
+28
View File
@@ -0,0 +1,28 @@
"""
Notification admin configuration.
"""
from django.contrib import admin
from .models import Notification, ScheduledReminder
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
"""Admin for Notification."""
list_display = ['title', 'user', 'notification_type', 'is_read', 'created_at']
list_filter = ['notification_type', 'is_read', 'created_at']
search_fields = ['title', 'message', 'user__email']
ordering = ['-created_at']
raw_id_fields = ['user', 'task']
@admin.register(ScheduledReminder)
class ScheduledReminderAdmin(admin.ModelAdmin):
"""Admin for ScheduledReminder."""
list_display = ['task', 'remind_at', 'is_sent', 'sent_at', 'created_at']
list_filter = ['is_sent', 'remind_at']
search_fields = ['task__title']
ordering = ['remind_at']
raw_id_fields = ['task']
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class NotificationsConfig(AppConfig):
name = 'notifications'
+45
View File
@@ -0,0 +1,45 @@
# Generated by Django 5.2.9 on 2025-12-10 00:05
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('notification_type', models.CharField(choices=[('reminder', 'Task Reminder'), ('due_soon', 'Due Soon'), ('overdue', 'Overdue'), ('shared', 'Task Shared'), ('comment', 'Comment')], max_length=20)),
('title', models.CharField(max_length=255)),
('message', models.TextField()),
('is_read', models.BooleanField(default=False)),
('read_at', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'notifications',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='ScheduledReminder',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('remind_at', models.DateTimeField()),
('is_sent', models.BooleanField(default=False)),
('sent_at', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'scheduled_reminders',
'ordering': ['remind_at'],
},
),
]
+22
View File
@@ -0,0 +1,22 @@
# Generated by Django 5.2.9 on 2025-12-10 00:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('notifications', '0001_initial'),
('tasks', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='notification',
name='task',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='tasks.task'),
),
]
+29
View File
@@ -0,0 +1,29 @@
# Generated by Django 5.2.9 on 2025-12-10 00:05
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('notifications', '0002_initial'),
('tasks', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='notification',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='scheduledreminder',
name='task',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_reminders', to='tasks.task'),
),
]
+70
View File
@@ -0,0 +1,70 @@
"""
Notification models for KeepItGoing.
"""
import uuid
from django.conf import settings
from django.db import models
class Notification(models.Model):
"""
Stores notifications for users.
"""
NOTIFICATION_TYPES = [
('reminder', 'Task Reminder'),
('due_soon', 'Due Soon'),
('overdue', 'Overdue'),
('shared', 'Task Shared'),
('comment', 'Comment'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='notifications'
)
notification_type = models.CharField(max_length=20, choices=NOTIFICATION_TYPES)
title = models.CharField(max_length=255)
message = models.TextField()
task = models.ForeignKey(
'tasks.Task',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='notifications'
)
is_read = models.BooleanField(default=False)
read_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'notifications'
ordering = ['-created_at']
def __str__(self):
return f"{self.notification_type}: {self.title}"
class ScheduledReminder(models.Model):
"""
Tracks scheduled reminders for tasks.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
task = models.ForeignKey(
'tasks.Task',
on_delete=models.CASCADE,
related_name='scheduled_reminders'
)
remind_at = models.DateTimeField()
is_sent = models.BooleanField(default=False)
sent_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'scheduled_reminders'
ordering = ['remind_at']
def __str__(self):
return f"Reminder for {self.task.title} at {self.remind_at}"
+29
View File
@@ -0,0 +1,29 @@
"""
Notification serializers for KeepItGoing API.
"""
from rest_framework import serializers
from .models import Notification, ScheduledReminder
class NotificationSerializer(serializers.ModelSerializer):
"""Serializer for notifications."""
task_title = serializers.CharField(source='task.title', read_only=True)
class Meta:
model = Notification
fields = [
'id', 'notification_type', 'title', 'message', 'task',
'task_title', 'is_read', 'read_at', 'created_at'
]
read_only_fields = ['id', 'created_at']
class ScheduledReminderSerializer(serializers.ModelSerializer):
"""Serializer for scheduled reminders."""
class Meta:
model = ScheduledReminder
fields = ['id', 'task', 'remind_at', 'is_sent', 'sent_at', 'created_at']
read_only_fields = ['id', 'is_sent', 'sent_at', 'created_at']
+235
View File
@@ -0,0 +1,235 @@
"""
Celery tasks for notifications.
"""
from celery import shared_task
from django.utils import timezone
from datetime import timedelta
@shared_task
def send_due_reminders():
"""
Check for tasks due soon and send reminders.
Runs every 5 minutes via Celery Beat.
"""
from tasks.models import Task
from users.models import DeviceToken
from .models import Notification, ScheduledReminder
now = timezone.now()
# Find scheduled reminders that need to be sent
reminders = ScheduledReminder.objects.filter(
remind_at__lte=now,
is_sent=False,
task__status__in=['pending', 'in_progress']
).select_related('task', 'task__user')
for reminder in reminders:
task = reminder.task
user = task.user
# Create notification
Notification.objects.create(
user=user,
notification_type='reminder',
title=f'Reminder: {task.title}',
message=f'Task "{task.title}" is due soon.',
task=task,
)
# Send push notifications
send_push_to_user.delay(
user_id=str(user.id),
title=f'Reminder: {task.title}',
body=f'Task is due soon.',
data={'task_id': str(task.id)}
)
# Mark as sent
reminder.is_sent = True
reminder.sent_at = now
reminder.save()
@shared_task
def check_overdue_tasks():
"""
Check for overdue tasks and notify users.
Runs daily.
"""
from tasks.models import Task
from .models import Notification
today = timezone.now().date()
# Find overdue tasks that haven't been notified today
overdue_tasks = Task.objects.filter(
due_date__lt=today,
status__in=['pending', 'in_progress']
).select_related('user')
for task in overdue_tasks:
# Check if we already sent an overdue notification today
existing = Notification.objects.filter(
user=task.user,
task=task,
notification_type='overdue',
created_at__date=today
).exists()
if not existing:
Notification.objects.create(
user=task.user,
notification_type='overdue',
title=f'Overdue: {task.title}',
message=f'Task "{task.title}" is overdue.',
task=task,
)
send_push_to_user.delay(
user_id=str(task.user.id),
title=f'Overdue: {task.title}',
body='This task is overdue.',
data={'task_id': str(task.id)}
)
@shared_task
def send_push_to_user(user_id, title, body, data=None):
"""
Send push notification to all user devices.
"""
from django.contrib.auth import get_user_model
from users.models import DeviceToken
User = get_user_model()
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
return
if not user.push_notifications:
return
tokens = DeviceToken.objects.filter(user=user, is_active=True)
for token in tokens:
if token.platform == 'android':
send_fcm_notification.delay(token.token, title, body, data)
elif token.platform == 'web':
send_web_push_notification.delay(token.token, title, body, data)
elif token.platform == 'desktop':
# Desktop notifications are handled via WebSocket
pass
@shared_task
def send_fcm_notification(token, title, body, data=None):
"""Send FCM notification to Android device."""
from django.conf import settings
# Only send if FCM is configured
if not getattr(settings, 'FCM_CREDENTIALS_PATH', None):
return
try:
import firebase_admin
from firebase_admin import credentials, messaging
# Initialize Firebase if not already done
if not firebase_admin._apps:
cred = credentials.Certificate(settings.FCM_CREDENTIALS_PATH)
firebase_admin.initialize_app(cred)
message = messaging.Message(
notification=messaging.Notification(
title=title,
body=body,
),
data=data or {},
token=token,
)
messaging.send(message)
except Exception as e:
# Log error but don't fail
print(f"FCM notification failed: {e}")
@shared_task
def send_web_push_notification(subscription_info, title, body, data=None):
"""Send Web Push notification."""
from django.conf import settings
vapid_private_key = getattr(settings, 'VAPID_PRIVATE_KEY', None)
vapid_email = getattr(settings, 'VAPID_ADMIN_EMAIL', None)
if not vapid_private_key or not vapid_email:
return
try:
from pywebpush import webpush, WebPushException
import json
webpush(
subscription_info=json.loads(subscription_info),
data=json.dumps({
'title': title,
'body': body,
'data': data or {}
}),
vapid_private_key=vapid_private_key,
vapid_claims={'sub': f'mailto:{vapid_email}'}
)
except Exception as e:
print(f"Web push notification failed: {e}")
@shared_task
def schedule_task_reminder(task_id):
"""
Schedule a reminder for a task based on its due date and user preferences.
Called when a task is created or updated.
"""
from tasks.models import Task
from .models import ScheduledReminder
try:
task = Task.objects.get(id=task_id)
except Task.DoesNotExist:
return
# Remove existing scheduled reminders for this task
ScheduledReminder.objects.filter(task=task, is_sent=False).delete()
# Only schedule if task has a reminder time or due date
if task.reminder_at:
ScheduledReminder.objects.create(
task=task,
remind_at=task.reminder_at
)
elif task.due_date:
# Default reminder based on user preference
reminder_minutes = task.user.default_reminder_minutes
if task.due_time:
from datetime import datetime
due_datetime = datetime.combine(task.due_date, task.due_time)
due_datetime = timezone.make_aware(due_datetime)
else:
# Default to 9 AM on due date
due_datetime = timezone.make_aware(
timezone.datetime.combine(task.due_date, timezone.datetime.min.time())
).replace(hour=9)
remind_at = due_datetime - timedelta(minutes=reminder_minutes)
if remind_at > timezone.now():
ScheduledReminder.objects.create(
task=task,
remind_at=remind_at
)
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+15
View File
@@ -0,0 +1,15 @@
"""
Notification URLs for KeepItGoing.
"""
from django.urls import path
from . import views
# API URLs (to be included under /api/notifications/)
api_urlpatterns = [
path('', views.NotificationListAPIView.as_view(), name='api-notification-list'),
path('<uuid:notification_id>/read/', views.mark_read, name='api-notification-read'),
path('read-all/', views.mark_all_read, name='api-notification-read-all'),
path('unread-count/', views.unread_count, name='api-notification-unread-count'),
path('<uuid:notification_id>/', views.delete_notification, name='api-notification-delete'),
]
+96
View File
@@ -0,0 +1,96 @@
"""
Notification views for KeepItGoing.
"""
from django.utils import timezone
from rest_framework import generics, permissions, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from .models import Notification
from .serializers import NotificationSerializer
class NotificationListAPIView(generics.ListAPIView):
"""API endpoint for listing notifications."""
serializer_class = NotificationSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
queryset = Notification.objects.filter(user=self.request.user)
# Filter by read status
is_read = self.request.query_params.get('is_read')
if is_read is not None:
queryset = queryset.filter(is_read=is_read.lower() == 'true')
return queryset
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
def mark_read(request, notification_id):
"""Mark a notification as read."""
try:
notification = Notification.objects.get(
id=notification_id,
user=request.user
)
except Notification.DoesNotExist:
return Response(
{'error': 'Notification not found'},
status=status.HTTP_404_NOT_FOUND
)
notification.is_read = True
notification.read_at = timezone.now()
notification.save()
return Response(NotificationSerializer(notification).data)
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
def mark_all_read(request):
"""Mark all notifications as read."""
Notification.objects.filter(
user=request.user,
is_read=False
).update(
is_read=True,
read_at=timezone.now()
)
return Response({'status': 'All notifications marked as read'})
@api_view(['GET'])
@permission_classes([permissions.IsAuthenticated])
def unread_count(request):
"""Get count of unread notifications."""
count = Notification.objects.filter(
user=request.user,
is_read=False
).count()
return Response({'unread_count': count})
@api_view(['DELETE'])
@permission_classes([permissions.IsAuthenticated])
def delete_notification(request, notification_id):
"""Delete a notification."""
try:
notification = Notification.objects.get(
id=notification_id,
user=request.user
)
except Notification.DoesNotExist:
return Response(
{'error': 'Notification not found'},
status=status.HTTP_404_NOT_FOUND
)
notification.delete()
return Response(status=status.HTTP_204_NO_CONTENT)