Viele Radgruppen haben regelmäßige Fahrten - die Montag-Morgen-Pendlergruppe, die Abenteuerfahrt am ersten Samstag des Monats oder die gesellige Mittwochabend-Runde. Bisher mussten Organisatoren jedes Event manuell erstellen. Mit unserer neuen Funktion für Wiederkehrende Partys können Sie einmal einen Zeitplan einrichten und Party Onbici automatisch Instanzen generieren lassen.

Das Problem mit Einzelevents

Gemeinschaftliche Radgruppen haben typischerweise vorhersehbare Zeitpläne:

  • “Jeden Dienstag und Donnerstag um 6:30 Uhr”
  • “Jeden ersten Sonntag im Monat”
  • “Jeden zweiten Samstag morgens”

Diese Events manuell zu erstellen ist mühsam, fehleranfällig und erschwert die Aufrechterhaltung konsistenter Routen und Einstellungen zwischen den Terminen.

Einführung von iCalendar RRULE

Wir haben unser System für wiederkehrende Events auf dem iCalendar RRULE-Standard (RFC 5545) aufgebaut. Dies ist dasselbe Format, das von Google Calendar, Apple Calendar und Outlook verwendet wird - und gewährleistet Kompatibilität mit dem breiteren Kalender-Ökosystem.

Beispiele für Wiederholungsmuster

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
  → Jeden Montag, Mittwoch und Freitag

RRULE:FREQ=MONTHLY;BYDAY=1SA
  → Jeden ersten Samstag im Monat

RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
  → Jeden zweiten Sonntag

RRULE:FREQ=DAILY;COUNT=10
  → Täglich für 10 Termine

Das Datenmodell

Eine RecurringParty dient als Vorlage, die Party-Instanzen generiert:

 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)

    # Schedule configuration
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    max_occurrences = models.PositiveIntegerField(null=True, blank=True)
    recurrence = RecurrenceField()  # RRULE pattern

    # Template fields (copied to each instance)
    departure_time = models.TimeField()
    arrival_time = models.TimeField()
    origin = PointField()
    destination = PointField()
    route = models.JSONField()
    difficulty = models.CharField(choices=DIFFICULTY_CHOICES)
    # ... other party attributes

    is_active = models.BooleanField(default=True)

Jede generierte Party-Instanz verweist zurück auf ihr Elternelement:

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

    # Recurring party support
    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)

Instanzgenerierung

Wenn eine wiederkehrende Party erstellt wird oder sich der Zeitplan ändert, generieren wir Instanzen für den kommenden Zeitraum:

 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]:
    """Generate party instances for upcoming occurrences."""
    occurrences = self.recurrence.between(
        datetime.now().date(),
        datetime.now().date() + timedelta(days=lookahead_days),
    )

    created = []
    for date in occurrences:
        # Skip if instance already exists
        if self.instances.filter(scheduled_date=date).exists():
            continue

        # Create new party instance from template
        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,
            # ... copy other template fields
        )
        created.append(party)

    return created

Automatische Generierung mit Celery Beat

Eine tägliche Celery-Aufgabe hält die Instanzen aktuell:

1
2
3
4
5
6
7
8
9
@shared_task
def generate_recurring_party_instances():
    """Generate party instances for all active recurring parties."""
    for recurring_party in RecurringParty.objects.filter(is_active=True):
        created = recurring_party.generate_instances(lookahead_days=30)
        if created:
            logger.info(
                f"Generated {len(created)} instances for {recurring_party.name}"
            )

Der Celery Beat-Zeitplan führt dies um Mitternacht aus:

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),  # Daily at midnight
    },
}

Benutzeroberfläche

Erstellen einer Wiederkehrenden Party

Das Formular verwendet das django-recurrence Widget zum Erstellen von RRULE-Mustern:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class RecurringPartyForm(forms.ModelForm):
    recurrence = RecurrenceField(
        label="Recurrence pattern",
        help_text="Set up when this party should repeat.",
        required=True,
    )

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

Das Widget bietet eine intuitive Oberfläche für gängige Muster:

  • Täglich / Wöchentlich / Monatlich / Jährlich
  • Bestimmte Wochentage
  • Ordinale Muster (erster, zweiter, letzter)
  • Benutzerdefinierte Intervalle (alle 2 Wochen)
  • Endbedingungen (bis zu einem Datum, nach N Terminen oder nie)

Verwaltung von Instanzen

Organisatoren können alle generierten Instanzen einsehen und:

  • Einzelne Instanzen bearbeiten - Route oder Zeit für einen Termin ändern
  • Instanzen absagen - Bestimmte Termine als abgesagt markieren
  • Instanzen neu generieren - Generierung für die nächsten 30 Tage manuell auslösen

Umgang mit Sonderfällen

Zeitzonen-Bewusstsein

Alle Daten werden in der konfigurierten Zeitzone des Organisators gespeichert und in der lokalen Zeit des Betrachters angezeigt:

1
2
3
# Generate instances using timezone-aware dates
local_tz = get_current_timezone()
today = timezone.now().astimezone(local_tz).date()

Feiertags- und Ausnahmebehandlung

Benutzer können einzelne Instanzen absagen, ohne das wiederkehrende Muster zu beeinflussen:

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

Abgesagte Instanzen erscheinen weiterhin in der Liste mit einem visuellen Indikator, um Verwirrung zu vermeiden.

Verwaiste Instanzen

Wenn eine wiederkehrende Party gelöscht wird, können generierte Instanzen optional erhalten bleiben:

 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 future instances
        recurring_party.instances.filter(
            scheduled_date__gte=date.today(),
            deleted_at__isnull=True,
        ).update(deleted_at=timezone.now())

    recurring_party.delete()

Das Benutzererlebnis

Das Erstellen einer wöchentlichen Fahrt dauert nur wenige Schritte:

  1. Gehen Sie zu Dashboard → Wiederkehrende Partys → Erstellen
  2. Legen Sie Ihre Party-Details fest (Route, Zeiten, Schwierigkeit)
  3. Konfigurieren Sie das Wiederholungsmuster (z.B. “Jeden Samstag um 8 Uhr”)
  4. Legen Sie das Startdatum und optionales Enddatum fest
  5. Speichern - Instanzen werden automatisch generiert!

Das System erstellt Party-Instanzen 30 Tage im Voraus, und Celery generiert kontinuierlich neue, während die Zeit vergeht.

Vorteile für Rad-Communities

  • Einrichten und vergessen - Keine wöchentliche Event-Erstellung mehr
  • Konsistenz - Gleiche Route, gleiche Zeit, gleiche Einstellungen
  • Flexibilität - Einzelne Termine bearbeiten oder absagen
  • Entdeckung - Radfahrer können leicht regelmäßige Gruppenfahrten finden
  • Kalender-Integration - Abonnieren Sie wiederkehrende Events in Ihrer Kalender-App

Bereit, Ihre regelmäßige Fahrt einzurichten? Erstellen Sie eine wiederkehrende Party und lassen Sie uns die Planung übernehmen!