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:

  1. Gå till Instrumentpanel → Återkommande Fester → Skapa
  2. Ställ in dina festdetaljer (rutt, tider, svårighet)
  3. Konfigurera det återkommande mönstret (t.ex. “Varje lördag kl 8”)
  4. Ställ in startdatum och valfritt slutdatum
  5. 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!