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:
@@ -0,0 +1,104 @@
|
||||
# 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='Tag',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('color', models.CharField(default='#6B7280', max_length=7)),
|
||||
('sync_id', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'tags',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Task',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=500)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
|
||||
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('urgent', 'Urgent')], default='medium', max_length=20)),
|
||||
('due_date', models.DateField(blank=True, null=True)),
|
||||
('due_time', models.TimeField(blank=True, null=True)),
|
||||
('reminder_at', models.DateTimeField(blank=True, null=True)),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('recurrence', models.CharField(choices=[('none', 'None'), ('daily', 'Daily'), ('weekly', 'Weekly'), ('biweekly', 'Bi-weekly'), ('monthly', 'Monthly'), ('yearly', 'Yearly'), ('custom', 'Custom')], default='none', max_length=20)),
|
||||
('recurrence_rule', models.CharField(blank=True, max_length=500)),
|
||||
('recurrence_end_date', models.DateField(blank=True, null=True)),
|
||||
('sort_order', models.IntegerField(default=0)),
|
||||
('sync_id', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'tasks',
|
||||
'ordering': ['sort_order', '-priority', 'due_date', 'created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TaskGroup',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('color', models.CharField(default='#3B82F6', max_length=7)),
|
||||
('icon', models.CharField(blank=True, max_length=50)),
|
||||
('sort_order', models.IntegerField(default=0)),
|
||||
('is_archived', models.BooleanField(default=False)),
|
||||
('sync_id', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'task_groups',
|
||||
'ordering': ['sort_order', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TaskShare',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('permission', models.CharField(choices=[('viewer', 'Viewer'), ('editor', 'Editor')], default='viewer', max_length=20)),
|
||||
('sync_id', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'task_shares',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TimeEntry',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('started_at', models.DateTimeField()),
|
||||
('ended_at', models.DateTimeField(blank=True, null=True)),
|
||||
('duration_seconds', models.IntegerField(blank=True, null=True)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('sync_id', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'time_entries',
|
||||
'ordering': ['-started_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,86 @@
|
||||
# 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 = [
|
||||
('tasks', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subtasks', to='tasks.task'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(blank=True, related_name='tasks', to='tasks.tag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='tasks.tag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='taskshare',
|
||||
name='group',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shares', to='tasks.tag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='taskshare',
|
||||
name='owner',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shares_given', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='taskshare',
|
||||
name='shared_with',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shares_received', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='taskshare',
|
||||
name='task',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shares', to='tasks.task'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='timeentry',
|
||||
name='task',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='time_entries', to='tasks.task'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='timeentry',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='time_entries', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='tag',
|
||||
unique_together={('user', 'name')},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='taskshare',
|
||||
constraint=models.CheckConstraint(condition=models.Q(models.Q(('group__isnull', True), ('task__isnull', False)), models.Q(('group__isnull', False), ('task__isnull', True)), _connector='OR'), name='share_task_or_group_not_both'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,135 @@
|
||||
# Migration to merge TaskGroup into Tag and support multiple tags per task
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def migrate_groups_to_tags(apps, schema_editor):
|
||||
"""Migrate TaskGroup data to Tag and convert task.group_id to task_tags."""
|
||||
TaskGroup = apps.get_model('tasks', 'TaskGroup')
|
||||
Tag = apps.get_model('tasks', 'Tag')
|
||||
Task = apps.get_model('tasks', 'Task')
|
||||
|
||||
# First, migrate all TaskGroups to Tags
|
||||
group_to_tag_map = {}
|
||||
for group in TaskGroup.objects.all():
|
||||
# Check if a tag with the same sync_id already exists
|
||||
existing_tag = Tag.objects.filter(sync_id=group.sync_id).first()
|
||||
if existing_tag:
|
||||
# Update existing tag with group data
|
||||
existing_tag.description = group.description
|
||||
existing_tag.icon = group.icon
|
||||
existing_tag.sort_order = group.sort_order
|
||||
existing_tag.is_archived = group.is_archived
|
||||
existing_tag.save()
|
||||
group_to_tag_map[group.id] = existing_tag
|
||||
else:
|
||||
# Create new tag from group
|
||||
tag = Tag.objects.create(
|
||||
user=group.user,
|
||||
name=group.name,
|
||||
description=group.description,
|
||||
color=group.color,
|
||||
icon=group.icon,
|
||||
sort_order=group.sort_order,
|
||||
is_archived=group.is_archived,
|
||||
sync_id=group.sync_id,
|
||||
)
|
||||
group_to_tag_map[group.id] = tag
|
||||
|
||||
# Convert task.group_id to task_tags entries
|
||||
for task in Task.objects.exclude(group_id__isnull=True):
|
||||
if task.group_id in group_to_tag_map:
|
||||
tag = group_to_tag_map[task.group_id]
|
||||
task.tags.add(tag)
|
||||
|
||||
|
||||
def reverse_migration(apps, schema_editor):
|
||||
"""Reverse migration - not fully reversible, just a placeholder."""
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tasks', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Step 1: Add new fields to Tag model
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='icon',
|
||||
field=models.CharField(blank=True, default='', max_length=50),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='sort_order',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='is_archived',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
|
||||
# Step 2: Remove unique_together constraint on Tag (user, name)
|
||||
# to allow migration of groups with same names
|
||||
migrations.AlterUniqueTogether(
|
||||
name='tag',
|
||||
unique_together=set(),
|
||||
),
|
||||
|
||||
# Step 3: Change Tag ordering
|
||||
migrations.AlterModelOptions(
|
||||
name='tag',
|
||||
options={'ordering': ['sort_order', 'name']},
|
||||
),
|
||||
|
||||
# Step 4: Run data migration
|
||||
migrations.RunPython(migrate_groups_to_tags, reverse_migration),
|
||||
|
||||
# Step 5: Remove group field from Task
|
||||
migrations.RemoveField(
|
||||
model_name='task',
|
||||
name='group',
|
||||
),
|
||||
|
||||
# Step 6: Update TaskShare - remove old constraint first
|
||||
migrations.RemoveConstraint(
|
||||
model_name='taskshare',
|
||||
name='share_task_or_group_not_both',
|
||||
),
|
||||
|
||||
# Step 7: Rename group field to tag in TaskShare
|
||||
migrations.RenameField(
|
||||
model_name='taskshare',
|
||||
old_name='group',
|
||||
new_name='tag',
|
||||
),
|
||||
|
||||
# Step 8: Add new constraint for TaskShare
|
||||
migrations.AddConstraint(
|
||||
model_name='taskshare',
|
||||
constraint=models.CheckConstraint(
|
||||
check=models.Q(
|
||||
models.Q(('tag__isnull', True), ('task__isnull', False)),
|
||||
models.Q(('tag__isnull', False), ('task__isnull', True)),
|
||||
_connector='OR'
|
||||
),
|
||||
name='share_task_or_tag_not_both'
|
||||
),
|
||||
),
|
||||
|
||||
# Step 9: Delete TaskGroup model
|
||||
migrations.DeleteModel(
|
||||
name='TaskGroup',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-13 00:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tasks', '0003_merge_groups_to_tags'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='color',
|
||||
field=models.CharField(default='#3B82F6', max_length=7),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='name',
|
||||
field=models.CharField(max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='taskshare',
|
||||
name='tag',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shares', to='tasks.tag'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-13 06:45
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def fix_tasks_tags_table(apps, schema_editor):
|
||||
"""
|
||||
Fix the tasks_tags junction table to reference the correct 'tags' table
|
||||
instead of the old 'task_groups' table.
|
||||
"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Drop and recreate the tasks_tags table with correct foreign keys
|
||||
schema_editor.execute(
|
||||
"DROP TABLE IF EXISTS tasks_tags;"
|
||||
)
|
||||
|
||||
schema_editor.execute(
|
||||
"""
|
||||
CREATE TABLE tasks_tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id CHAR(32) NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
tag_id CHAR(32) NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
UNIQUE (task_id, tag_id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
# Create indexes for performance
|
||||
schema_editor.execute(
|
||||
"CREATE INDEX tasks_tags_task_id_idx ON tasks_tags(task_id);"
|
||||
)
|
||||
schema_editor.execute(
|
||||
"CREATE INDEX tasks_tags_tag_id_idx ON tasks_tags(tag_id);"
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tasks', '0004_alter_tag_color_alter_tag_name_alter_taskshare_tag'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(fix_tasks_tags_table, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-13 07:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def fix_task_shares_table(apps, schema_editor):
|
||||
"""
|
||||
Fix the task_shares table to reference the correct 'tags' table
|
||||
instead of the old 'task_groups' table.
|
||||
"""
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Drop and recreate the task_shares table with correct foreign keys
|
||||
schema_editor.execute(
|
||||
"DROP TABLE IF EXISTS task_shares;"
|
||||
)
|
||||
|
||||
schema_editor.execute(
|
||||
"""
|
||||
CREATE TABLE task_shares (
|
||||
id CHAR(32) NOT NULL PRIMARY KEY,
|
||||
permission VARCHAR(20) NOT NULL,
|
||||
sync_id CHAR(32) NOT NULL UNIQUE,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
owner_id CHAR(32) NOT NULL REFERENCES users(id) DEFERRABLE INITIALLY DEFERRED,
|
||||
shared_with_id CHAR(32) NOT NULL REFERENCES users(id) DEFERRABLE INITIALLY DEFERRED,
|
||||
task_id CHAR(32) NULL REFERENCES tasks(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
tag_id CHAR(32) NULL REFERENCES tags(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
|
||||
CONSTRAINT share_task_or_tag_not_both CHECK (
|
||||
(tag_id IS NULL AND task_id IS NOT NULL) OR
|
||||
(tag_id IS NOT NULL AND task_id IS NULL)
|
||||
)
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
# Create indexes for foreign keys
|
||||
schema_editor.execute(
|
||||
"CREATE INDEX task_shares_owner_id_idx ON task_shares(owner_id);"
|
||||
)
|
||||
schema_editor.execute(
|
||||
"CREATE INDEX task_shares_shared_with_id_idx ON task_shares(shared_with_id);"
|
||||
)
|
||||
schema_editor.execute(
|
||||
"CREATE INDEX task_shares_task_id_idx ON task_shares(task_id);"
|
||||
)
|
||||
schema_editor.execute(
|
||||
"CREATE INDEX task_shares_tag_id_idx ON task_shares(tag_id);"
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tasks', '0005_fix_tasks_tags_table'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(fix_task_shares_table, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-14 03:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tasks', '0006_fix_task_shares_table'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='is_deleted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='is_deleted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='timeentry',
|
||||
name='is_deleted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user