กลุ่มนักปั่นจักรยานหลายกลุ่มมีการปั่นประจำ - ทีมปั่นเช้าวันจันทร์ การปั่นผจญภัยวันเสาร์แรกของเดือน หรือการปั่นสังสรรค์เย็นวันพุธ จนถึงตอนนี้ ผู้จัดต้องสร้างแต่ละกิจกรรมด้วยตนเอง ด้วยฟีเจอร์ปาร์ตี้ที่เกิดซ้ำใหม่ของเรา คุณสามารถตั้งค่าตารางเวลาครั้งเดียวและให้ 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()
|
ประสบการณ์ผู้ใช้
การสร้างการปั่นรายสัปดาห์ใช้เพียงไม่กี่ขั้นตอน:
- ไปที่ แดชบอร์ด → ปาร์ตี้ที่เกิดซ้ำ → สร้าง
- ตั้งค่ารายละเอียดปาร์ตี้ของคุณ (เส้นทาง เวลา ความยาก)
- กำหนดค่ารูปแบบการเกิดซ้ำ (เช่น “ทุกวันเสาร์ เวลา 8 โมงเช้า”)
- ตั้งวันที่เริ่มต้นและวันที่สิ้นสุด (ถ้ามี)
- บันทึก - อินสแตนซ์จะถูกสร้างโดยอัตโนมัติ!
ระบบสร้างอินสแตนซ์ปาร์ตี้ล่วงหน้า 30 วัน และ Celery ยังคงสร้างใหม่เมื่อเวลาผ่านไป
ประโยชน์สำหรับชุมชนนักปั่น
- ตั้งค่าแล้วลืม - ไม่ต้องสร้างกิจกรรมรายสัปดาห์อีกต่อไป
- ความสอดคล้อง - เส้นทางเดียวกัน เวลาเดียวกัน การตั้งค่าเดียวกัน
- ความยืดหยุ่น - แก้ไขหรือยกเลิกแต่ละครั้งได้
- การค้นพบ - นักปั่นสามารถค้นหาการปั่นกลุ่มประจำได้ง่าย
- การรวมปฏิทิน - สมัครรับกิจกรรมที่เกิดซ้ำในแอปปฏิทินของคุณ
พร้อมที่จะตั้งค่าการปั่นประจำของคุณหรือยัง? สร้างปาร์ตี้ที่เกิดซ้ำ และให้เราจัดการการจัดตาราง!