Wiele grup rowerowych ma regularne przejazdy - poniedziałkowa poranna ekipa dojazdowa, przygodowy przejazd w pierwszą sobotę miesiąca lub środowy wieczorny przejazd towarzyski. Do tej pory organizatorzy musieli ręcznie tworzyć każde wydarzenie. Dzięki naszej nowej funkcji Powtarzających się Imprez możesz ustawić harmonogram raz i pozwolić Party Onbici automatycznie generować instancje.
Problem z jednorazowymi wydarzeniami
Społecznościowe grupy rowerowe zazwyczaj mają przewidywalne harmonogramy:
- “Co wtorek i czwartek o 6:30”
- “Pierwsza niedziela każdego miesiąca”
- “Co drugą sobotę rano”
Ręczne tworzenie tych wydarzeń jest żmudne, podatne na błędy i utrudnia utrzymanie spójnych tras i ustawień między wystąpieniami.
Przedstawiamy iCalendar RRULE
Zbudowaliśmy nasz system powtarzających się wydarzeń na standardzie iCalendar RRULE (RFC 5545). To ten sam format używany przez Google Calendar, Apple Calendar i Outlook - zapewniający kompatybilność z szerszym ekosystemem kalendarzy.
Przykładowe wzorce powtarzania
1
2
3
4
5
6
7
8
9
10
11
| RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
→ W każdy poniedziałek, środę i piątek
RRULE:FREQ=MONTHLY;BYDAY=1SA
→ Pierwsza sobota każdego miesiąca
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
→ Co drugą niedzielę
RRULE:FREQ=DAILY;COUNT=10
→ Codziennie przez 10 wystąpień
|
Model danych
RecurringParty służy jako szablon generujący instancje 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)
# Konfiguracja harmonogramu
start_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
max_occurrences = models.PositiveIntegerField(null=True, blank=True)
recurrence = RecurrenceField() # wzorzec RRULE
# Pola szablonu (kopiowane do każdej instancji)
departure_time = models.TimeField()
arrival_time = models.TimeField()
origin = PointField()
destination = PointField()
route = models.JSONField()
difficulty = models.CharField(choices=DIFFICULTY_CHOICES)
# ... inne atrybuty imprezy
is_active = models.BooleanField(default=True)
|
Każda wygenerowana instancja Party łączy się ze swoim rodzicem:
1
2
3
4
5
6
7
8
9
10
11
12
| class Party(models.Model):
# Istniejące pola...
# Wsparcie dla powtarzającej się imprezy
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)
|
Generowanie instancji
Gdy powtarzająca się impreza jest tworzona lub harmonogram się zmienia, generujemy instancje na nadchodzący okres:
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]:
"""Generuj instancje imprezy dla nadchodzących wystąpień."""
occurrences = self.recurrence.between(
datetime.now().date(),
datetime.now().date() + timedelta(days=lookahead_days),
)
created = []
for date in occurrences:
# Pomiń jeśli instancja już istnieje
if self.instances.filter(scheduled_date=date).exists():
continue
# Utwórz nową instancję imprezy z szablonu
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,
# ... skopiuj inne pola szablonu
)
created.append(party)
return created
|
Automatyczne generowanie z Celery Beat
Codzienne zadanie Celery utrzymuje instancje aktualne:
1
2
3
4
5
6
7
8
9
| @shared_task
def generate_recurring_party_instances():
"""Generuj instancje imprezy dla wszystkich aktywnych powtarzających się imprez."""
for recurring_party in RecurringParty.objects.filter(is_active=True):
created = recurring_party.generate_instances(lookahead_days=30)
if created:
logger.info(
f"Wygenerowano {len(created)} instancji dla {recurring_party.name}"
)
|
Harmonogram Celery Beat uruchamia to o północy:
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), # Codziennie o północy
},
}
|
Interfejs użytkownika
Tworzenie powtarzającej się imprezy
Formularz używa widgetu django-recurrence do budowania wzorców RRULE:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class RecurringPartyForm(forms.ModelForm):
recurrence = RecurrenceField(
label="Wzorzec powtarzania",
help_text="Ustaw kiedy ta impreza powinna się powtarzać.",
required=True,
)
class Meta:
model = RecurringParty
fields = [
"name", "start_date", "end_date", "recurrence",
"departure_time", "arrival_time",
"origin", "destination", "description",
"difficulty", "gender", "privacy",
]
|
Widget zapewnia intuicyjny interfejs dla typowych wzorców:
- Codziennie / Tygodniowo / Miesięcznie / Rocznie
- Określone dni tygodnia
- Wzorce porządkowe (pierwszy, drugi, ostatni)
- Niestandardowe interwały (co 2 tygodnie)
- Warunki zakończenia (do daty, po N wystąpieniach lub nigdy)
Zarządzanie instancjami
Organizatorzy mogą przeglądać wszystkie wygenerowane instancje i:
- Edytować pojedyncze instancje - Zmienić trasę lub czas dla jednego wystąpienia
- Anulować instancje - Oznaczyć określone daty jako anulowane
- Regenerować instancje - Ręcznie wywołać generowanie na następne 30 dni
Obsługa przypadków brzegowych
Świadomość strefy czasowej
Wszystkie daty są przechowywane w skonfigurowanej strefie czasowej organizatora i wyświetlane w lokalnym czasie widza:
1
2
3
| # Generuj instancje używając dat świadomych strefy czasowej
local_tz = get_current_timezone()
today = timezone.now().astimezone(local_tz).date()
|
Obsługa świąt i wyjątków
Użytkownicy mogą anulować pojedyncze instancje bez wpływu na wzorzec powtarzania:
1
2
3
| party = recurring_party.instances.get(scheduled_date=holiday_date)
party.is_cancelled = True
party.save()
|
Anulowane instancje nadal pojawiają się na liście z wizualnym wskaźnikiem, zapobiegając nieporozumieniom.
Osierocone instancje
Jeśli powtarzająca się impreza zostanie usunięta, wygenerowane instancje mogą być opcjonalnie zachowane:
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 przyszłych instancji
recurring_party.instances.filter(
scheduled_date__gte=date.today(),
deleted_at__isnull=True,
).update(deleted_at=timezone.now())
recurring_party.delete()
|
Doświadczenie użytkownika
Tworzenie tygodniowego przejazdu wymaga tylko kilku kroków:
- Przejdź do Panel → Powtarzające się imprezy → Utwórz
- Ustaw szczegóły imprezy (trasa, czasy, trudność)
- Skonfiguruj wzorzec powtarzania (np. “Co sobotę o 8 rano”)
- Ustaw datę rozpoczęcia i opcjonalną datę zakończenia
- Zapisz - instancje są generowane automatycznie!
System tworzy instancje imprez z 30-dniowym wyprzedzeniem, a Celery generuje nowe w miarę upływu czasu.
Korzyści dla społeczności rowerowych
- Ustaw i zapomnij - Koniec z cotygodniowym tworzeniem wydarzeń
- Spójność - Ta sama trasa, ten sam czas, te same ustawienia
- Elastyczność - Edytuj lub anuluj pojedyncze wystąpienia
- Odkrywanie - Rowerzyści mogą łatwo znaleźć regularne przejazdy grupowe
- Integracja z kalendarzem - Subskrybuj powtarzające się wydarzenia w aplikacji kalendarza
Gotowy na ustawienie regularnego przejazdu? Utwórz powtarzającą się imprezę i pozwól nam zająć się planowaniem!