Många cykelgrupper har regelbundna turer - måndagsmorgonens pendlargäng, äventyrsresan första lördagen i månaden, eller onsdagskvällens sociala sväng. Hittills har arrangörer behövt skapa varje evenemang manuellt. Med vår nya funktion för Återkommande Fester kan du ställa in ett schema en gång och låta Party Onbici generera instanser automatiskt.
Problemet med engångsevenemang
Cykelgrupper i gemenskapen har vanligtvis förutsägbara scheman:
- “Varje tisdag och torsdag kl 6:30”
- “Första söndagen varje månad”
- “Varannan lördag morgon”
Att manuellt skapa dessa evenemang är tråkigt, felbenäget och gör det svårt att upprätthålla konsekventa rutter och inställningar över förekomster.
Introduktion av iCalendar RRULE
Vi byggde vårt system för återkommande evenemang på iCalendar RRULE-standarden (RFC 5545). Detta är samma format som används av Google Calendar, Apple Calendar och Outlook - vilket säkerställer kompatibilitet med det bredare kalenderekosystemet.
Exempel på återkommande mönster
1
2
3
4
5
6
7
8
9
10
11
| RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
→ Varje måndag, onsdag och fredag
RRULE:FREQ=MONTHLY;BYDAY=1SA
→ Första lördagen varje månad
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
→ Varannan söndag
RRULE:FREQ=DAILY;COUNT=10
→ Dagligen i 10 förekomster
|
Datamodellen
En RecurringParty fungerar som en mall som genererar Party-instanser:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class RecurringParty(models.Model):
name = models.CharField(max_length=255)
# Schemakonfiguration
start_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
max_occurrences = models.PositiveIntegerField(null=True, blank=True)
recurrence = RecurrenceField() # RRULE-mönster
# Mallfält (kopieras till varje instans)
departure_time = models.TimeField()
arrival_time = models.TimeField()
origin = PointField()
destination = PointField()
route = models.JSONField()
difficulty = models.CharField(choices=DIFFICULTY_CHOICES)
# ... andra festattribut
is_active = models.BooleanField(default=True)
|
Varje genererad Party-instans länkar tillbaka till sin förälder:
1
2
3
4
5
6
7
8
9
10
11
12
| class Party(models.Model):
# Befintliga fält...
# Stöd för återkommande fest
scheduled_date = models.DateField(null=True, blank=True)
recurring_parent = models.ForeignKey(
RecurringParty,
on_delete=models.SET_NULL,
null=True,
related_name="instances"
)
is_cancelled = models.BooleanField(default=False)
|
Instansgenerering
När en återkommande fest skapas eller schemat ändras, genererar vi instanser för den kommande perioden:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| def generate_instances(self, lookahead_days: int = 30) -> list[Party]:
"""Generera festinstanser för kommande förekomster."""
occurrences = self.recurrence.between(
datetime.now().date(),
datetime.now().date() + timedelta(days=lookahead_days),
)
created = []
for date in occurrences:
# Hoppa över om instans redan finns
if self.instances.filter(scheduled_date=date).exists():
continue
# Skapa ny festinstans från mall
party = Party.objects.create(
name=f"{self.name} - {date.strftime('%B %d')}",
scheduled_date=date,
recurring_parent=self,
departure_time=self.departure_time,
arrival_time=self.arrival_time,
origin=self.origin,
destination=self.destination,
route=self.route,
# ... kopiera andra mallfält
)
created.append(party)
return created
|
Automatisk generering med Celery Beat
En daglig Celery-uppgift håller instanser uppdaterade:
1
2
3
4
5
6
7
8
9
| @shared_task
def generate_recurring_party_instances():
"""Generera festinstanser för alla aktiva återkommande fester."""
for recurring_party in RecurringParty.objects.filter(is_active=True):
created = recurring_party.generate_instances(lookahead_days=30)
if created:
logger.info(
f"Genererade {len(created)} instanser för {recurring_party.name}"
)
|
Celery Beat-schemat kör detta vid midnatt:
1
2
3
4
5
6
| CELERY_BEAT_SCHEDULE = {
"generate-recurring-party-instances": {
"task": "party_onbici.apps.party.tasks.generate_recurring_party_instances",
"schedule": crontab(hour=0, minute=0), # Dagligen vid midnatt
},
}
|
Användargränssnitt
Skapa en återkommande fest
Formuläret använder django-recurrence-widget för att bygga RRULE-mönster:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class RecurringPartyForm(forms.ModelForm):
recurrence = RecurrenceField(
label="Återkommande mönster",
help_text="Ställ in när denna fest ska upprepas.",
required=True,
)
class Meta:
model = RecurringParty
fields = [
"name", "start_date", "end_date", "recurrence",
"departure_time", "arrival_time",
"origin", "destination", "description",
"difficulty", "gender", "privacy",
]
|
Widgeten ger ett intuitivt gränssnitt för vanliga mönster:
- Dagligen / Veckovis / Månadsvis / Årligen
- Specifika veckodagar
- Ordinalsmönster (första, andra, sista)
- Anpassade intervall (varannan vecka)
- Slutvillkor (till datum, efter N förekomster, eller aldrig)
Hantera instanser
Arrangörer kan visa alla genererade instanser och:
- Redigera enskilda instanser - Ändra rutt eller tid för en förekomst
- Avbryt instanser - Markera specifika datum som avbrutna
- Återgenerera instanser - Manuellt trigga generering för de nästa 30 dagarna
Hantering av kantfall
Tidszonsmedvetenhet
Alla datum lagras i arrangörens konfigurerade tidszon och visas i visarens lokala tid:
1
2
3
| # Generera instanser med tidszonsmedvetna datum
local_tz = get_current_timezone()
today = timezone.now().astimezone(local_tz).date()
|
Hantering av helgdagar och undantag
Användare kan avbryta enskilda instanser utan att påverka det återkommande mönstret:
1
2
3
| party = recurring_party.instances.get(scheduled_date=holiday_date)
party.is_cancelled = True
party.save()
|
Avbrutna instanser visas fortfarande i listan med en visuell indikator, vilket förhindrar förvirring.
Föräldralösa instanser
Om en återkommande fest tas bort kan genererade instanser valfritt bevaras:
1
2
3
4
5
6
7
8
9
10
11
| def form_valid(self, form):
delete_instances = request.POST.get("delete_instances") == "true"
if delete_instances:
# Mjukradera framtida instanser
recurring_party.instances.filter(
scheduled_date__gte=date.today(),
deleted_at__isnull=True,
).update(deleted_at=timezone.now())
recurring_party.delete()
|
Användarupplevelsen
Att skapa en veckotur tar bara några steg:
- Gå till Instrumentpanel → Återkommande Fester → Skapa
- Ställ in dina festdetaljer (rutt, tider, svårighet)
- Konfigurera det återkommande mönstret (t.ex. “Varje lördag kl 8”)
- Ställ in startdatum och valfritt slutdatum
- Spara - instanser genereras automatiskt!
Systemet skapar festinstanser 30 dagar framåt, och Celery fortsätter generera nya allteftersom tiden går.
Fördelar för cykelgemenskaper
- Ställ in och glöm - Inget mer veckovis skapande av evenemang
- Konsekvens - Samma rutt, samma tid, samma inställningar
- Flexibilitet - Redigera eller avbryt enskilda förekomster
- Upptäckbarhet - Cyklister kan enkelt hitta regelbundna gruppturer
- Kalenderintegration - Prenumerera på återkommande evenemang i din kalenderapp
Redo att ställa in din regelbundna tur? Skapa en återkommande fest och låt oss hantera schemaläggningen!