Molti gruppi ciclistici hanno uscite regolari - il gruppo del pendolarismo del lunedì mattina, l’uscita avventurosa del primo sabato del mese, o la pedalata sociale del mercoledì sera. Fino ad ora, gli organizzatori dovevano creare manualmente ogni evento. Con la nostra nuova funzionalità Feste Ricorrenti, puoi impostare un programma una volta e lasciare che Party Onbici generi automaticamente le istanze.

Il Problema degli Eventi Singoli

I gruppi ciclistici comunitari hanno tipicamente programmi prevedibili:

  • “Ogni martedì e giovedì alle 6:30”
  • “La prima domenica di ogni mese”
  • “Ogni due sabati mattina”

Creare questi eventi manualmente è noioso, soggetto a errori e rende difficile mantenere percorsi e impostazioni coerenti tra le occorrenze.

Presentazione di iCalendar RRULE

Abbiamo costruito il nostro sistema di eventi ricorrenti sullo standard iCalendar RRULE (RFC 5545). Questo è lo stesso formato utilizzato da Google Calendar, Apple Calendar e Outlook - garantendo la compatibilità con l’ecosistema dei calendari più ampio.

Esempi di Modelli di Ricorrenza

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
  → Ogni lunedì, mercoledì e venerdì

RRULE:FREQ=MONTHLY;BYDAY=1SA
  → Il primo sabato di ogni mese

RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
  → Ogni due domeniche

RRULE:FREQ=DAILY;COUNT=10
  → Quotidianamente per 10 occorrenze

Il Modello dei Dati

Un RecurringParty funge da modello che genera istanze di 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)

Ogni istanza di Party generata si collega al suo genitore:

 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)

Generazione delle Istanze

Quando viene creata una festa ricorrente o il programma cambia, generiamo istanze per il periodo imminente:

 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

Generazione Automatica con Celery Beat

Un task Celery giornaliero mantiene aggiornate le istanze:

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

La programmazione di Celery Beat esegue questo a mezzanotte:

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

Interfaccia Utente

Creazione di una Festa Ricorrente

Il modulo utilizza il widget django-recurrence per costruire modelli 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",
        ]

Il widget fornisce un’interfaccia intuitiva per i modelli comuni:

  • Giornaliero / Settimanale / Mensile / Annuale
  • Giorni specifici della settimana
  • Modelli ordinali (primo, secondo, ultimo)
  • Intervalli personalizzati (ogni 2 settimane)
  • Condizioni di fine (fino a una data, dopo N occorrenze, o mai)

Gestione delle Istanze

Gli organizzatori possono visualizzare tutte le istanze generate e:

  • Modificare singole istanze - Cambiare percorso o orario per una singola occorrenza
  • Annullare istanze - Contrassegnare date specifiche come annullate
  • Rigenerare istanze - Attivare manualmente la generazione per i prossimi 30 giorni

Gestione dei Casi Particolari

Consapevolezza del Fuso Orario

Tutte le date vengono memorizzate nel fuso orario configurato dall’organizzatore e visualizzate nell’ora locale del visualizzatore:

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

Gestione di Festività ed Eccezioni

Gli utenti possono annullare singole istanze senza influenzare il modello ricorrente:

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

Le istanze annullate appaiono comunque nella lista con un indicatore visivo, evitando confusione.

Istanze Orfane

Se una festa ricorrente viene eliminata, le istanze generate possono essere opzionalmente conservate:

 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’Esperienza Utente

Creare un’uscita settimanale richiede solo pochi passaggi:

  1. Vai su Dashboard → Feste Ricorrenti → Crea
  2. Imposta i dettagli della tua festa (percorso, orari, difficoltà)
  3. Configura il modello di ricorrenza (es., “Ogni sabato alle 8”)
  4. Imposta la data di inizio e la data di fine opzionale
  5. Salva - le istanze vengono generate automaticamente!

Il sistema crea istanze della festa con 30 giorni di anticipo, e Celery continua a generarne di nuove man mano che passa il tempo.

Vantaggi per le Comunità Ciclistiche

  • Imposta e dimentica - Niente più creazione settimanale di eventi
  • Coerenza - Stesso percorso, stesso orario, stesse impostazioni
  • Flessibilità - Modifica o annulla singole occorrenze
  • Scoperta - I ciclisti possono trovare facilmente uscite di gruppo regolari
  • Integrazione calendario - Iscriviti agli eventi ricorrenti nella tua app calendario

Pronto a impostare la tua uscita regolare? Crea una festa ricorrente e lascia che gestiamo noi la programmazione!