Archived
feat: implement reading pace notifications (US #33)
Backend: - New hermes app with ReadingGroup, GroupMeeting, GroupMembership models - NotificationPreference (per-user: enable/disable, frequency daily/weekly) - PaceNotification model for tracking sent/dismissed notifications - Pace calculation service comparing current progress vs meeting targets - API endpoints: pace status, notification preferences CRUD, reminders, dismiss - Ahead/behind/on_track/completed status determination Frontend: - PaceNotification component for in-app alerts (behind/ahead/on_track statuses) - ReadingPaceBanner — persistent banner in reader view - Notification settings in Settings page (enable/disable, frequency, reminders) - API client module (groupsApi) for all notification endpoints
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# Generated by Django 5.1.7 on 2026-06-20 19:22
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('books', '0003_readingprogress_epub_location'),
|
||||
('users', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='NotificationPreference',
|
||||
fields=[
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='notification_preference', serialize=False, to=settings.AUTH_USER_MODEL)),
|
||||
('pace_notifications_enabled', models.BooleanField(default=True)),
|
||||
('reminder_enabled', models.BooleanField(default=True)),
|
||||
('frequency', models.CharField(choices=[('daily', 'Daily'), ('weekly', 'Weekly')], default='daily', max_length=16)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Notification Preference',
|
||||
'verbose_name_plural': 'Notification Preferences',
|
||||
'db_table': 'groups_notification_preference',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReadingGroup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(db_index=True, max_length=256)),
|
||||
('description', models.TextField(blank=True, default='')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('book', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reading_groups', to='books.book')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_groups', to=settings.AUTH_USER_MODEL)),
|
||||
('ebook', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reading_groups', to='books.ebook')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Reading Group',
|
||||
'verbose_name_plural': 'Reading Groups',
|
||||
'db_table': 'groups_reading_group',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GroupMeeting',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(blank=True, default='', max_length=512)),
|
||||
('scheduled_at', models.DateTimeField(db_index=True)),
|
||||
('target_section', models.PositiveIntegerField(default=0, help_text='Target chapter/section index to reach by this meeting')),
|
||||
('target_section_label', models.CharField(blank=True, default='', help_text="Human-readable label for the target section (e.g. 'Section 3')", max_length=512)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meetings', to='groups.readinggroup')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Group Meeting',
|
||||
'verbose_name_plural': 'Group Meetings',
|
||||
'db_table': 'groups_meeting',
|
||||
'ordering': ['scheduled_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PaceNotification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('sent', 'Sent'), ('dismissed', 'Dismissed')], default='pending', max_length=16)),
|
||||
('current_section', models.PositiveIntegerField(default=0)),
|
||||
('target_section', models.PositiveIntegerField(default=0)),
|
||||
('ahead_behind_delta', models.IntegerField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('ebook', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pace_notifications', to='books.ebook')),
|
||||
('meeting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pace_notifications', to='groups.groupmeeting')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pace_notifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Pace Notification',
|
||||
'verbose_name_plural': 'Pace Notifications',
|
||||
'db_table': 'groups_pace_notification',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['user', 'meeting', 'ebook'], name='groups_pace_user_id_18e929_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GroupMembership',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('admin', 'Admin'), ('member', 'Member')], default='member', max_length=16)),
|
||||
('joined_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_memberships', to=settings.AUTH_USER_MODEL)),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='groups.readinggroup')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Group Membership',
|
||||
'verbose_name_plural': 'Group Memberships',
|
||||
'db_table': 'groups_membership',
|
||||
'indexes': [models.Index(fields=['user', 'group'], name='groups_memb_user_id_9cda7e_idx')],
|
||||
'unique_together': {('user', 'group')},
|
||||
},
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user