กลุ่มนักปั่นจักรยานหลายกลุ่มมีการปั่นประจำ - ทีมปั่นเช้าวันจันทร์ การปั่นผจญภัยวันเสาร์แรกของเดือน หรือการปั่นสังสรรค์เย็นวันพุธ จนถึงตอนนี้ ผู้จัดต้องสร้างแต่ละกิจกรรมด้วยตนเอง ด้วยฟีเจอร์ปาร์ตี้ที่เกิดซ้ำใหม่ของเรา คุณสามารถตั้งค่าตารางเวลาครั้งเดียวและให้ Party Onbici สร้างอินสแตนซ์โดยอัตโนมัติ

ปัญหาของกิจกรรมครั้งเดียว

กลุ่มปั่นจักรยานชุมชนมักมีตารางที่คาดเดาได้:

  • “ทุกวันอังคารและพฤหัสบดี เวลา 6:30 น.”
  • “วันอาทิตย์แรกของทุกเดือน”
  • “ทุกวันเสาร์สัปดาห์เว้นสัปดาห์ตอนเช้า”

การสร้างกิจกรรมเหล่านี้ด้วยตนเองน่าเบื่อ เกิดข้อผิดพลาดได้ง่าย และทำให้ยากต่อการรักษาเส้นทางและการตั้งค่าที่สอดคล้องกันในแต่ละครั้ง

แนะนำ iCalendar RRULE

เราสร้างระบบกิจกรรมที่เกิดซ้ำบนมาตรฐาน iCalendar RRULE (RFC 5545) นี่คือรูปแบบเดียวกับที่ Google Calendar, Apple Calendar และ Outlook ใช้ - รับประกันความเข้ากันได้กับระบบนิเวศปฏิทินที่กว้างขึ้น

ตัวอย่างรูปแบบการเกิดซ้ำ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
  → ทุกวันจันทร์ พุธ และศุกร์

RRULE:FREQ=MONTHLY;BYDAY=1SA
  → วันเสาร์แรกของทุกเดือน

RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
  → ทุกวันอาทิตย์สัปดาห์เว้นสัปดาห์

RRULE:FREQ=DAILY;COUNT=10
  → ทุกวันเป็นเวลา 10 ครั้ง

โมเดลข้อมูล

RecurringParty ทำหน้าที่เป็นเทมเพลตที่สร้างอินสแตนซ์ 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)

    # การกำหนดค่าตารางเวลา
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    max_occurrences = models.PositiveIntegerField(null=True, blank=True)
    recurrence = RecurrenceField()  # รูปแบบ RRULE

    # ฟิลด์เทมเพลต (คัดลอกไปยังแต่ละอินสแตนซ์)
    departure_time = models.TimeField()
    arrival_time = models.TimeField()
    origin = PointField()
    destination = PointField()
    route = models.JSONField()
    difficulty = models.CharField(choices=DIFFICULTY_CHOICES)
    # ... แอตทริบิวต์ปาร์ตี้อื่นๆ

    is_active = models.BooleanField(default=True)

แต่ละอินสแตนซ์ Party ที่สร้างขึ้นเชื่อมโยงกลับไปยังพาเรนต์:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Party(models.Model):
    # ฟิลด์ที่มีอยู่...

    # รองรับปาร์ตี้ที่เกิดซ้ำ
    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)

การสร้างอินสแตนซ์

เมื่อปาร์ตี้ที่เกิดซ้ำถูกสร้างหรือตารางเปลี่ยนแปลง เราสร้างอินสแตนซ์สำหรับช่วงเวลาที่จะมาถึง:

 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]:
    """สร้างอินสแตนซ์ปาร์ตี้สำหรับการเกิดขึ้นที่จะมาถึง"""
    occurrences = self.recurrence.between(
        datetime.now().date(),
        datetime.now().date() + timedelta(days=lookahead_days),
    )

    created = []
    for date in occurrences:
        # ข้ามถ้าอินสแตนซ์มีอยู่แล้ว
        if self.instances.filter(scheduled_date=date).exists():
            continue

        # สร้างอินสแตนซ์ปาร์ตี้ใหม่จากเทมเพลต
        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,
            # ... คัดลอกฟิลด์เทมเพลตอื่นๆ
        )
        created.append(party)

    return created

การสร้างอัตโนมัติด้วย Celery Beat

งาน Celery รายวันรักษาอินสแตนซ์ให้เป็นปัจจุบัน:

1
2
3
4
5
6
7
8
9
@shared_task
def generate_recurring_party_instances():
    """สร้างอินสแตนซ์ปาร์ตี้สำหรับปาร์ตี้ที่เกิดซ้ำที่ใช้งานอยู่ทั้งหมด"""
    for recurring_party in RecurringParty.objects.filter(is_active=True):
        created = recurring_party.generate_instances(lookahead_days=30)
        if created:
            logger.info(
                f"สร้าง {len(created)} อินสแตนซ์สำหรับ {recurring_party.name}"
            )

ตาราง Celery Beat รันสิ่งนี้ตอนเที่ยงคืน:

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),  # ทุกวันตอนเที่ยงคืน
    },
}

อินเทอร์เฟซผู้ใช้

การสร้างปาร์ตี้ที่เกิดซ้ำ

ฟอร์มใช้ django-recurrence widget สำหรับสร้างรูปแบบ RRULE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class RecurringPartyForm(forms.ModelForm):
    recurrence = RecurrenceField(
        label="รูปแบบการเกิดซ้ำ",
        help_text="ตั้งค่าว่าปาร์ตี้นี้ควรเกิดซ้ำเมื่อใด",
        required=True,
    )

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

Widget ให้อินเทอร์เฟซที่ใช้งานง่ายสำหรับรูปแบบทั่วไป:

  • รายวัน / รายสัปดาห์ / รายเดือน / รายปี
  • วันเฉพาะในสัปดาห์
  • รูปแบบลำดับ (แรก ที่สอง สุดท้าย)
  • ช่วงเวลาที่กำหนดเอง (ทุก 2 สัปดาห์)
  • เงื่อนไขสิ้นสุด (จนถึงวันที่ หลังจาก N ครั้ง หรือไม่มี)

การจัดการอินสแตนซ์

ผู้จัดสามารถดูอินสแตนซ์ที่สร้างขึ้นทั้งหมดและ:

  • แก้ไขอินสแตนซ์แต่ละรายการ - เปลี่ยนเส้นทางหรือเวลาสำหรับหนึ่งครั้ง
  • ยกเลิกอินสแตนซ์ - ทำเครื่องหมายวันที่เฉพาะว่ายกเลิกแล้ว
  • สร้างอินสแตนซ์ใหม่ - ทริกเกอร์การสร้างด้วยตนเองสำหรับ 30 วันถัดไป

การจัดการกรณีพิเศษ

การรับรู้เขตเวลา

วันที่ทั้งหมดจะถูกเก็บในเขตเวลาที่กำหนดค่าของผู้จัดและแสดงในเวลาท้องถิ่นของผู้ชม:

1
2
3
# สร้างอินสแตนซ์โดยใช้วันที่ที่รับรู้เขตเวลา
local_tz = get_current_timezone()
today = timezone.now().astimezone(local_tz).date()

การจัดการวันหยุดและข้อยกเว้น

ผู้ใช้สามารถยกเลิกอินสแตนซ์แต่ละรายการโดยไม่ส่งผลกระทบต่อรูปแบบที่เกิดซ้ำ:

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

อินสแตนซ์ที่ยกเลิกยังคงปรากฏในรายการพร้อมตัวบ่งชี้ภาพ ป้องกันความสับสน

อินสแตนซ์กำพร้า

หากปาร์ตี้ที่เกิดซ้ำถูกลบ อินสแตนซ์ที่สร้างขึ้นสามารถเก็บรักษาไว้ได้:

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

    recurring_party.delete()

ประสบการณ์ผู้ใช้

การสร้างการปั่นรายสัปดาห์ใช้เพียงไม่กี่ขั้นตอน:

  1. ไปที่ แดชบอร์ด → ปาร์ตี้ที่เกิดซ้ำ → สร้าง
  2. ตั้งค่ารายละเอียดปาร์ตี้ของคุณ (เส้นทาง เวลา ความยาก)
  3. กำหนดค่ารูปแบบการเกิดซ้ำ (เช่น “ทุกวันเสาร์ เวลา 8 โมงเช้า”)
  4. ตั้งวันที่เริ่มต้นและวันที่สิ้นสุด (ถ้ามี)
  5. บันทึก - อินสแตนซ์จะถูกสร้างโดยอัตโนมัติ!

ระบบสร้างอินสแตนซ์ปาร์ตี้ล่วงหน้า 30 วัน และ Celery ยังคงสร้างใหม่เมื่อเวลาผ่านไป

ประโยชน์สำหรับชุมชนนักปั่น

  • ตั้งค่าแล้วลืม - ไม่ต้องสร้างกิจกรรมรายสัปดาห์อีกต่อไป
  • ความสอดคล้อง - เส้นทางเดียวกัน เวลาเดียวกัน การตั้งค่าเดียวกัน
  • ความยืดหยุ่น - แก้ไขหรือยกเลิกแต่ละครั้งได้
  • การค้นพบ - นักปั่นสามารถค้นหาการปั่นกลุ่มประจำได้ง่าย
  • การรวมปฏิทิน - สมัครรับกิจกรรมที่เกิดซ้ำในแอปปฏิทินของคุณ

พร้อมที่จะตั้งค่าการปั่นประจำของคุณหรือยัง? สร้างปาร์ตี้ที่เกิดซ้ำ และให้เราจัดการการจัดตาราง!