Add EventSeries (club-scoped): an RFC-5545 rrule + dtstart + duration and a template (kind/title/location/opponent + audience M2M). Concrete Event rows are materialised occurrences carrying a series FK plus detached/cancelled flags; the series tracks excluded_dates (EXDATEs) and a generated_until horizon watermark. Recurrence service: - occurrence_datetimes expands the rrule (via python-dateutil) up to a horizon, minus EXDATEs. - generate_occurrences materialises missing rows, copies the template + audience (so attendance syncs through the existing signals), and is idempotent. - cancel_occurrence adds an EXDATE and deletes (or soft-cancels) one occurrence so it isn't regenerated; detach_occurrence marks an occurrence as independently edited; propagate_series re-applies the template to non-detached future occurrences. - extend_event_series management command rolls the horizon forward. Register EventSeries in the admin and surface series/detached/cancelled on the Event admin. Add python-dateutil. Full suite at 100% coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
58 lines
3.7 KiB
Python
58 lines
3.7 KiB
Python
# Generated by Django 6.0.6 on 2026-07-12 15:47
|
|
|
|
import django.db.models.deletion
|
|
import uuid
|
|
from django.db import migrations, models
|
|
|
|
|
|
class Migration(migrations.Migration):
|
|
|
|
dependencies = [
|
|
('club', '0008_alter_clubmembership_unique_together_and_more'),
|
|
('events', '0004_remove_event_team'),
|
|
('members', '0002_alter_familymembership_unique_together_and_more'),
|
|
('teams', '0001_initial'),
|
|
]
|
|
|
|
operations = [
|
|
migrations.AddField(
|
|
model_name='event',
|
|
name='cancelled',
|
|
field=models.BooleanField(default=False, verbose_name='cancelled'),
|
|
),
|
|
migrations.AddField(
|
|
model_name='event',
|
|
name='detached',
|
|
field=models.BooleanField(default=False, help_text='Edited independently; excluded from series-wide updates and regeneration.', verbose_name='detached'),
|
|
),
|
|
migrations.CreateModel(
|
|
name='EventSeries',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('rrule', models.CharField(help_text='RFC 5545 RRULE, e.g. FREQ=WEEKLY;BYDAY=MO,WE.', max_length=255, verbose_name='recurrence rule')),
|
|
('dtstart', models.DateTimeField(verbose_name='first occurrence')),
|
|
('duration', models.DurationField(blank=True, help_text="Length of each occurrence; sets each event's end.", null=True, verbose_name='duration')),
|
|
('excluded_dates', models.JSONField(blank=True, default=list, help_text='ISO start datetimes of occurrences removed from the series (EXDATEs).', verbose_name='excluded dates')),
|
|
('generated_until', models.DateTimeField(blank=True, help_text='Occurrences have been materialised up to this point.', null=True, verbose_name='generated until')),
|
|
('kind', models.CharField(choices=[('training', 'training'), ('match', 'match'), ('tournament', 'tournament'), ('meeting', 'meeting'), ('social', 'social'), ('other', 'other')], default='other', max_length=10, verbose_name='kind')),
|
|
('title', models.CharField(max_length=255, verbose_name='title')),
|
|
('club', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='club.club')),
|
|
('excluded_members', models.ManyToManyField(blank=True, related_name='excluded_from_event_series', to='members.member', verbose_name='excluded members')),
|
|
('invited_members', models.ManyToManyField(blank=True, related_name='invited_to_event_series', to='members.member', verbose_name='invited members')),
|
|
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='event_series', to='events.location', verbose_name='location')),
|
|
('opponent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='event_series', to='events.opponent', verbose_name='opponent')),
|
|
('teams', models.ManyToManyField(blank=True, related_name='event_series', to='teams.team', verbose_name='teams')),
|
|
],
|
|
options={
|
|
'verbose_name': 'event series',
|
|
'verbose_name_plural': 'event series',
|
|
'ordering': ['title'],
|
|
},
|
|
),
|
|
migrations.AddField(
|
|
model_name='event',
|
|
name='series',
|
|
field=models.ForeignKey(blank=True, help_text='The recurring series this occurrence belongs to; blank for one-off events.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='occurrences', to='events.eventseries', verbose_name='series'),
|
|
),
|
|
]
|