De nombreux groupes cyclistes ont des sorties régulières - l’équipe du trajet du lundi matin, la sortie aventure du premier samedi du mois, ou la balade sociale du mercredi soir. Jusqu’à présent, les organisateurs devaient créer manuellement chaque événement. Avec notre nouvelle fonctionnalité de Fêtes Récurrentes, vous pouvez configurer un calendrier une fois et laisser Party Onbici générer automatiquement les instances.

Le Problème des Événements Uniques

Les groupes cyclistes communautaires ont généralement des horaires prévisibles :

  • “Tous les mardis et jeudis à 6h30”
  • “Le premier dimanche de chaque mois”
  • “Un samedi matin sur deux”

Créer ces événements manuellement est fastidieux, sujet aux erreurs et rend difficile le maintien de routes et paramètres cohérents entre les occurrences.

Présentation de iCalendar RRULE

Nous avons construit notre système d’événements récurrents sur le standard iCalendar RRULE (RFC 5545). C’est le même format utilisé par Google Calendar, Apple Calendar et Outlook - garantissant la compatibilité avec l’écosystème calendrier plus large.

Exemples de Modèles de Récurrence

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
  → Tous les lundis, mercredis et vendredis

RRULE:FREQ=MONTHLY;BYDAY=1SA
  → Le premier samedi de chaque mois

RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
  → Un dimanche sur deux

RRULE:FREQ=DAILY;COUNT=10
  → Quotidiennement pendant 10 occurrences

Le Modèle de Données

Un RecurringParty sert de modèle qui génère des instances de Party :

 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)

Chaque instance de Party générée renvoie à son parent :

 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)

Génération d’Instances

Lorsqu’une fête récurrente est créée ou que le calendrier change, nous générons des instances pour la période à venir :

 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

Génération Automatique avec Celery Beat

Une tâche Celery quotidienne maintient les instances à jour :

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}"
            )

Le planificateur Celery Beat exécute ceci à minuit :

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
    },
}

Interface Utilisateur

Créer une Fête Récurrente

Le formulaire utilise le widget django-recurrence pour construire des modèles RRULE :

 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",
        ]

Le widget fournit une interface intuitive pour les modèles courants :

  • Quotidien / Hebdomadaire / Mensuel / Annuel
  • Jours spécifiques de la semaine
  • Modèles ordinaux (premier, deuxième, dernier)
  • Intervalles personnalisés (toutes les 2 semaines)
  • Conditions de fin (jusqu’à une date, après N occurrences, ou jamais)

Gestion des Instances

Les organisateurs peuvent voir toutes les instances générées et :

  • Modifier des instances individuelles - Changer l’itinéraire ou l’heure pour une occurrence
  • Annuler des instances - Marquer des dates spécifiques comme annulées
  • Régénérer des instances - Déclencher manuellement la génération pour les 30 prochains jours

Gestion des Cas Particuliers

Conscience des Fuseaux Horaires

Toutes les dates sont stockées dans le fuseau horaire configuré de l’organisateur et affichées dans l’heure locale du spectateur :

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

Gestion des Jours Fériés et Exceptions

Les utilisateurs peuvent annuler des instances individuelles sans affecter le modèle récurrent :

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

Les instances annulées apparaissent toujours dans la liste avec un indicateur visuel, évitant la confusion.

Instances Orphelines

Si une fête récurrente est supprimée, les instances générées peuvent optionnellement être préservées :

 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()

L’Expérience Utilisateur

Créer une sortie hebdomadaire ne prend que quelques étapes :

  1. Allez dans Tableau de bord → Fêtes Récurrentes → Créer
  2. Définissez les détails de votre fête (itinéraire, horaires, difficulté)
  3. Configurez le modèle de récurrence (ex., “Tous les samedis à 8h”)
  4. Définissez la date de début et la date de fin optionnelle
  5. Enregistrez - les instances sont générées automatiquement !

Le système crée des instances de fêtes 30 jours à l’avance, et Celery continue à en générer de nouvelles au fil du temps.

Avantages pour les Communautés Cyclistes

  • Configurez et oubliez - Plus de création d’événements hebdomadaires
  • Cohérence - Même itinéraire, même horaire, mêmes paramètres
  • Flexibilité - Modifiez ou annulez des occurrences individuelles
  • Découverte - Les cyclistes peuvent facilement trouver des sorties de groupe régulières
  • Intégration calendrier - Abonnez-vous aux événements récurrents dans votre application calendrier

Prêt à configurer votre sortie régulière ? Créez une fête récurrente et laissez-nous gérer la planification !