Archived
feat: implement group EPUB upload and section splitting (US #29)
Backend: - Create hermes Django app with models: ReadingGroup, GroupBook, Section, ReadingSchedule, MemberProgress - EPUB section splitting service with automatic detection and reading time estimation - Section recommendation engine for 4-week meeting schedule - REST API endpoints for groups, books, sections, schedule, and member progress - Manual section adjustment (merge/split) support Frontend: - GroupsPage: list/create reading groups - GroupDetailPage: manage members, upload EPUB to group, view group books - GroupBookPage: section breakdown with merge/split controls, reading schedule, member progress - API client and TypeScript types for all group operations - i18n keys for English and Spanish Shared: - Group-related types and API endpoint constants in packages/shared
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# Generated by Django 5.1.7 on 2026-06-20 19:21
|
||||
|
||||
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'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('admin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='administered_groups', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Reading Group',
|
||||
'verbose_name_plural': 'Reading Groups',
|
||||
'db_table': 'groups_reading_group',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GroupBook',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(db_index=True, max_length=512)),
|
||||
('status', models.CharField(choices=[('active', 'Active'), ('replaced', 'Replaced')], db_index=True, default='active', max_length=16)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('ebook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_books', to='books.ebook')),
|
||||
('uploaded_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uploaded_group_books', to=settings.AUTH_USER_MODEL)),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_books', to='groups.readinggroup')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Group Book',
|
||||
'verbose_name_plural': 'Group Books',
|
||||
'db_table': 'groups_group_book',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReadingGroupMembership',
|
||||
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)),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='groups.readinggroup')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Reading Group Membership',
|
||||
'verbose_name_plural': 'Reading Group Memberships',
|
||||
'db_table': 'groups_membership',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='readinggroup',
|
||||
name='members',
|
||||
field=models.ManyToManyField(related_name='reading_groups', through='groups.ReadingGroupMembership', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReadingSchedule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('meeting_number', models.PositiveIntegerField()),
|
||||
('week_date', models.DateField()),
|
||||
('section_ids', models.JSONField(default=list, help_text='Ordered list of Section IDs for this meeting')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('group_book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='groups.groupbook')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Reading Schedule',
|
||||
'verbose_name_plural': 'Reading Schedules',
|
||||
'db_table': 'groups_reading_schedule',
|
||||
'ordering': ['meeting_number'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Section',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=512)),
|
||||
('order', models.PositiveIntegerField(db_index=True)),
|
||||
('start_chapter_index', models.PositiveIntegerField(default=0)),
|
||||
('end_chapter_index', models.PositiveIntegerField(default=0)),
|
||||
('estimated_reading_minutes', models.PositiveIntegerField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('group_book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sections', to='groups.groupbook')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Section',
|
||||
'verbose_name_plural': 'Sections',
|
||||
'db_table': 'groups_section',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MemberProgress',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('completed_sections', models.JSONField(default=list, help_text='Ordered list of completed Section IDs')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('group_book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='member_progress', to='groups.groupbook')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_progress', to=settings.AUTH_USER_MODEL)),
|
||||
('current_section', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='current_readers', to='groups.section')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Member Progress',
|
||||
'verbose_name_plural': 'Member Progress',
|
||||
'db_table': 'groups_member_progress',
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='groupbook',
|
||||
index=models.Index(fields=['group', 'status'], name='groups_grou_group_i_afdbf7_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='readinggroupmembership',
|
||||
unique_together={('user', 'group')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='readingschedule',
|
||||
index=models.Index(fields=['group_book', 'meeting_number'], name='groups_read_group_b_2fc416_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='readingschedule',
|
||||
unique_together={('group_book', 'meeting_number')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='section',
|
||||
index=models.Index(fields=['group_book', 'order'], name='groups_sect_group_b_e2810a_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='section',
|
||||
unique_together={('group_book', 'order')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='memberprogress',
|
||||
unique_together={('user', 'group_book')},
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user