Banyak grup bersepeda memiliki perjalanan reguler - kru komuter Senin pagi, perjalanan petualangan Sabtu pertama setiap bulan, atau putaran sosial Rabu malam. Sampai sekarang, penyelenggara harus membuat setiap acara secara manual. Dengan fitur Pesta Berulang kami yang baru, Anda dapat mengatur jadwal sekali dan membiarkan Party Onbici menghasilkan instansi secara otomatis.

Masalah dengan Acara Satu Kali

Grup bersepeda komunitas biasanya memiliki jadwal yang dapat diprediksi:

  • “Setiap Selasa dan Kamis jam 6:30 pagi”
  • “Minggu pertama setiap bulan”
  • “Setiap Sabtu kedua pagi”

Membuat acara-acara ini secara manual membosankan, rentan kesalahan, dan membuat sulit untuk mempertahankan rute dan pengaturan yang konsisten di seluruh kejadian.

Memperkenalkan iCalendar RRULE

Kami membangun sistem acara berulang kami berdasarkan standar iCalendar RRULE (RFC 5545). Ini adalah format yang sama yang digunakan oleh Google Calendar, Apple Calendar, dan Outlook - memastikan kompatibilitas dengan ekosistem kalender yang lebih luas.

Contoh Pola Pengulangan

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
  → Setiap Senin, Rabu, dan Jumat

RRULE:FREQ=MONTHLY;BYDAY=1SA
  → Sabtu pertama setiap bulan

RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
  → Setiap Minggu kedua

RRULE:FREQ=DAILY;COUNT=10
  → Harian untuk 10 kejadian

Model Data

RecurringParty berfungsi sebagai template yang menghasilkan instansi 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)

    # Konfigurasi jadwal
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    max_occurrences = models.PositiveIntegerField(null=True, blank=True)
    recurrence = RecurrenceField()  # pola RRULE

    # Field template (disalin ke setiap instansi)
    departure_time = models.TimeField()
    arrival_time = models.TimeField()
    origin = PointField()
    destination = PointField()
    route = models.JSONField()
    difficulty = models.CharField(choices=DIFFICULTY_CHOICES)
    # ... atribut pesta lainnya

    is_active = models.BooleanField(default=True)

Setiap instansi Party yang dihasilkan terhubung kembali ke induknya:

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

    # Dukungan pesta berulang
    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)

Pembuatan Instansi

Ketika pesta berulang dibuat atau jadwal berubah, kami menghasilkan instansi untuk periode mendatang:

 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]:
    """Hasilkan instansi pesta untuk kejadian mendatang."""
    occurrences = self.recurrence.between(
        datetime.now().date(),
        datetime.now().date() + timedelta(days=lookahead_days),
    )

    created = []
    for date in occurrences:
        # Lewati jika instansi sudah ada
        if self.instances.filter(scheduled_date=date).exists():
            continue

        # Buat instansi pesta baru dari 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,
            # ... salin field template lainnya
        )
        created.append(party)

    return created

Pembuatan Otomatis dengan Celery Beat

Tugas Celery harian menjaga instansi tetap terkini:

1
2
3
4
5
6
7
8
9
@shared_task
def generate_recurring_party_instances():
    """Hasilkan instansi pesta untuk semua pesta berulang aktif."""
    for recurring_party in RecurringParty.objects.filter(is_active=True):
        created = recurring_party.generate_instances(lookahead_days=30)
        if created:
            logger.info(
                f"Dihasilkan {len(created)} instansi untuk {recurring_party.name}"
            )

Jadwal Celery Beat menjalankan ini pada tengah malam:

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),  # Harian pada tengah malam
    },
}

Antarmuka Pengguna

Membuat Pesta Berulang

Form menggunakan widget django-recurrence untuk membangun pola RRULE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class RecurringPartyForm(forms.ModelForm):
    recurrence = RecurrenceField(
        label="Pola pengulangan",
        help_text="Atur kapan pesta ini harus berulang.",
        required=True,
    )

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

Widget menyediakan antarmuka intuitif untuk pola umum:

  • Harian / Mingguan / Bulanan / Tahunan
  • Hari-hari tertentu dalam seminggu
  • Pola ordinal (pertama, kedua, terakhir)
  • Interval kustom (setiap 2 minggu)
  • Kondisi akhir (sampai tanggal, setelah N kejadian, atau tidak pernah)

Mengelola Instansi

Penyelenggara dapat melihat semua instansi yang dihasilkan dan:

  • Edit instansi individual - Ubah rute atau waktu untuk satu kejadian
  • Batalkan instansi - Tandai tanggal tertentu sebagai dibatalkan
  • Regenerasi instansi - Trigger pembuatan secara manual untuk 30 hari ke depan

Menangani Kasus Tepi

Kesadaran Zona Waktu

Semua tanggal disimpan dalam zona waktu yang dikonfigurasi penyelenggara dan ditampilkan dalam waktu lokal penonton:

1
2
3
# Hasilkan instansi menggunakan tanggal sadar zona waktu
local_tz = get_current_timezone()
today = timezone.now().astimezone(local_tz).date()

Penanganan Hari Libur dan Pengecualian

Pengguna dapat membatalkan instansi individual tanpa mempengaruhi pola berulang:

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

Instansi yang dibatalkan masih muncul dalam daftar dengan indikator visual, mencegah kebingungan.

Instansi Yatim

Jika pesta berulang dihapus, instansi yang dihasilkan dapat secara opsional dipertahankan:

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

    recurring_party.delete()

Pengalaman Pengguna

Membuat perjalanan mingguan hanya membutuhkan beberapa langkah:

  1. Pergi ke Dashboard → Pesta Berulang → Buat
  2. Atur detail pesta Anda (rute, waktu, kesulitan)
  3. Konfigurasi pola pengulangan (mis., “Setiap Sabtu jam 8 pagi”)
  4. Atur tanggal mulai dan tanggal akhir opsional
  5. Simpan - instansi dihasilkan secara otomatis!

Sistem membuat instansi pesta 30 hari ke depan, dan Celery terus menghasilkan yang baru seiring waktu berlalu.

Manfaat untuk Komunitas Bersepeda

  • Atur dan lupakan - Tidak perlu lagi membuat acara mingguan
  • Konsistensi - Rute yang sama, waktu yang sama, pengaturan yang sama
  • Fleksibilitas - Edit atau batalkan kejadian individual
  • Penemuan - Pengendara dapat dengan mudah menemukan perjalanan grup reguler
  • Integrasi kalender - Berlangganan acara berulang di aplikasi kalender Anda

Siap mengatur perjalanan reguler Anda? Buat pesta berulang dan biarkan kami menangani penjadwalannya!