Veel fietsgroepen hebben regelmatige ritten - de maandagochtend woon-werkverkeer crew, de avontuurlijke rit op de eerste zaterdag van de maand, of de woensdagavond sociale ronde. Tot nu toe moesten organisatoren elk evenement handmatig aanmaken. Met onze nieuwe Terugkerende Feesten functie kunt u een schema eenmalig instellen en Party Onbici automatisch instanties laten genereren.

Het Probleem met Eenmalige Evenementen

Community fietsgroepen hebben doorgaans voorspelbare schema’s:

  • “Elke dinsdag en donderdag om 6:30 uur”
  • “Eerste zondag van elke maand”
  • “Om de zaterdag ’s ochtends”

Het handmatig aanmaken van deze evenementen is vervelend, foutgevoelig en maakt het moeilijk om consistente routes en instellingen over gebeurtenissen te behouden.

Introductie van iCalendar RRULE

We hebben ons systeem voor terugkerende evenementen gebouwd op de iCalendar RRULE-standaard (RFC 5545). Dit is hetzelfde formaat dat wordt gebruikt door Google Calendar, Apple Calendar en Outlook - wat compatibiliteit met het bredere kalenderecosysteem garandeert.

Voorbeeld Herhalingspatronen

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
  → Elke maandag, woensdag en vrijdag

RRULE:FREQ=MONTHLY;BYDAY=1SA
  → Eerste zaterdag van elke maand

RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
  → Om de andere zondag

RRULE:FREQ=DAILY;COUNT=10
  → Dagelijks voor 10 gebeurtenissen

Het Datamodel

Een RecurringParty dient als sjabloon die Party instanties genereert:

 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)

    # Schema configuratie
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    max_occurrences = models.PositiveIntegerField(null=True, blank=True)
    recurrence = RecurrenceField()  # RRULE patroon

    # Sjabloonvelden (gekopieerd naar elke instantie)
    departure_time = models.TimeField()
    arrival_time = models.TimeField()
    origin = PointField()
    destination = PointField()
    route = models.JSONField()
    difficulty = models.CharField(choices=DIFFICULTY_CHOICES)
    # ... andere feestattributen

    is_active = models.BooleanField(default=True)

Elke gegenereerde Party instantie linkt terug naar zijn ouder:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Party(models.Model):
    # Bestaande velden...

    # Ondersteuning voor terugkerend feest
    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)

Instantie Generatie

Wanneer een terugkerend feest wordt aangemaakt of het schema verandert, genereren we instanties voor de komende periode:

 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]:
    """Genereer feestinstanties voor aankomende gebeurtenissen."""
    occurrences = self.recurrence.between(
        datetime.now().date(),
        datetime.now().date() + timedelta(days=lookahead_days),
    )

    created = []
    for date in occurrences:
        # Sla over als instantie al bestaat
        if self.instances.filter(scheduled_date=date).exists():
            continue

        # Maak nieuwe feestinstantie van sjabloon
        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,
            # ... kopieer andere sjabloonvelden
        )
        created.append(party)

    return created

Automatische Generatie met Celery Beat

Een dagelijkse Celery taak houdt instanties up-to-date:

1
2
3
4
5
6
7
8
9
@shared_task
def generate_recurring_party_instances():
    """Genereer feestinstanties voor alle actieve terugkerende feesten."""
    for recurring_party in RecurringParty.objects.filter(is_active=True):
        created = recurring_party.generate_instances(lookahead_days=30)
        if created:
            logger.info(
                f"{len(created)} instanties gegenereerd voor {recurring_party.name}"
            )

Het Celery Beat schema voert dit uit om middernacht:

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),  # Dagelijks om middernacht
    },
}

Gebruikersinterface

Een Terugkerend Feest Aanmaken

Het formulier gebruikt de django-recurrence widget voor het bouwen van RRULE patronen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class RecurringPartyForm(forms.ModelForm):
    recurrence = RecurrenceField(
        label="Herhalingspatroon",
        help_text="Stel in wanneer dit feest moet herhalen.",
        required=True,
    )

    class Meta:
        model = RecurringParty
        fields = [
            "name", "start_date", "end_date", "recurrence",
            "departure_time", "arrival_time",
            "origin", "destination", "description",
            "difficulty", "gender", "privacy",
        ]

De widget biedt een intuïtieve interface voor veelvoorkomende patronen:

  • Dagelijks / Wekelijks / Maandelijks / Jaarlijks
  • Specifieke dagen van de week
  • Ordinale patronen (eerste, tweede, laatste)
  • Aangepaste intervallen (elke 2 weken)
  • Eindvoorwaarden (tot datum, na N gebeurtenissen, of nooit)

Instanties Beheren

Organisatoren kunnen alle gegenereerde instanties bekijken en:

  • Individuele instanties bewerken - Wijzig route of tijd voor één gebeurtenis
  • Instanties annuleren - Markeer specifieke data als geannuleerd
  • Instanties regenereren - Handmatig generatie triggeren voor de volgende 30 dagen

Randgevallen Afhandelen

Tijdzone Bewustzijn

Alle datums worden opgeslagen in de geconfigureerde tijdzone van de organisator en weergegeven in de lokale tijd van de kijker:

1
2
3
# Genereer instanties met tijdzone-bewuste datums
local_tz = get_current_timezone()
today = timezone.now().astimezone(local_tz).date()

Feestdagen en Uitzonderingen Afhandelen

Gebruikers kunnen individuele instanties annuleren zonder het terugkerende patroon te beïnvloeden:

1
2
3
party = recurring_party.instances.get(scheduled_date=holiday_date)
party.is_cancelled = True
party.save()

Geannuleerde instanties verschijnen nog steeds in de lijst met een visuele indicator, waardoor verwarring wordt voorkomen.

Verweesd Instanties

Als een terugkerend feest wordt verwijderd, kunnen gegenereerde instanties optioneel behouden blijven:

 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:
        # Soft-delete toekomstige instanties
        recurring_party.instances.filter(
            scheduled_date__gte=date.today(),
            deleted_at__isnull=True,
        ).update(deleted_at=timezone.now())

    recurring_party.delete()

De Gebruikerservaring

Een wekelijkse rit aanmaken duurt slechts enkele stappen:

  1. Ga naar Dashboard → Terugkerende Feesten → Aanmaken
  2. Stel uw feestdetails in (route, tijden, moeilijkheid)
  3. Configureer het herhalingspatroon (bijv. “Elke zaterdag om 8 uur”)
  4. Stel de startdatum en optionele einddatum in
  5. Opslaan - instanties worden automatisch gegenereerd!

Het systeem maakt feestinstanties 30 dagen vooruit aan, en Celery blijft nieuwe genereren naarmate de tijd verstrijkt.

Voordelen voor Fietsgemeenschappen

  • Instellen en vergeten - Geen wekelijkse evenementcreatie meer
  • Consistentie - Dezelfde route, dezelfde tijd, dezelfde instellingen
  • Flexibiliteit - Bewerk of annuleer individuele gebeurtenissen
  • Ontdekking - Rijders kunnen gemakkelijk reguliere groepsritten vinden
  • Kalenderintegratie - Abonneer op terugkerende evenementen in uw kalenderapp

Klaar om uw reguliere rit in te stellen? Maak een terugkerend feest aan en laat ons de planning afhandelen!