Nhiều nhóm đạp xe có các chuyến đi thường xuyên - đội đi làm sáng thứ Hai, chuyến phiêu lưu thứ Bảy đầu tháng, hoặc vòng giao lưu tối thứ Tư. Cho đến nay, người tổ chức phải tạo thủ công từng sự kiện. Với tính năng Bữa tiệc Định kỳ mới, bạn có thể thiết lập lịch một lần và để Party Onbici tự động tạo các phiên bản.

Vấn đề với các sự kiện một lần

Các nhóm đạp xe cộng đồng thường có lịch trình có thể dự đoán được:

  • “Mỗi thứ Ba và thứ Năm lúc 6:30 sáng”
  • “Chủ nhật đầu tiên mỗi tháng”
  • “Cách tuần vào sáng thứ Bảy”

Việc tạo thủ công các sự kiện này rất tẻ nhạt, dễ xảy ra lỗi và khó duy trì các tuyến đường và cài đặt nhất quán qua các lần.

Giới thiệu iCalendar RRULE

Chúng tôi xây dựng hệ thống sự kiện định kỳ dựa trên tiêu chuẩn iCalendar RRULE (RFC 5545). Đây là cùng định dạng được sử dụng bởi Google Calendar, Apple Calendar và Outlook - đảm bảo khả năng tương thích với hệ sinh thái lịch rộng hơn.

Ví dụ về mẫu lặp lại

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
  → Mỗi thứ Hai, thứ Tư và thứ Sáu

RRULE:FREQ=MONTHLY;BYDAY=1SA
  → Thứ Bảy đầu tiên mỗi tháng

RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
  → Cách tuần vào Chủ nhật

RRULE:FREQ=DAILY;COUNT=10
  → Hàng ngày trong 10 lần

Mô hình dữ liệu

Một RecurringParty đóng vai trò như một mẫu tạo ra các phiên bản 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)

    # Cấu hình lịch trình
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    max_occurrences = models.PositiveIntegerField(null=True, blank=True)
    recurrence = RecurrenceField()  # mẫu RRULE

    # Các trường mẫu (được sao chép vào mỗi phiên bản)
    departure_time = models.TimeField()
    arrival_time = models.TimeField()
    origin = PointField()
    destination = PointField()
    route = models.JSONField()
    difficulty = models.CharField(choices=DIFFICULTY_CHOICES)
    # ... các thuộc tính bữa tiệc khác

    is_active = models.BooleanField(default=True)

Mỗi phiên bản Party được tạo liên kết ngược về cha mẹ của nó:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Party(models.Model):
    # Các trường hiện có...

    # Hỗ trợ bữa tiệc định kỳ
    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)

Tạo phiên bản

Khi một bữa tiệc định kỳ được tạo hoặc lịch trình thay đổi, chúng tôi tạo các phiên bản cho khoảng thời gian sắp tới:

 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]:
    """Tạo các phiên bản bữa tiệc cho các lần xuất hiện sắp tới."""
    occurrences = self.recurrence.between(
        datetime.now().date(),
        datetime.now().date() + timedelta(days=lookahead_days),
    )

    created = []
    for date in occurrences:
        # Bỏ qua nếu phiên bản đã tồn tại
        if self.instances.filter(scheduled_date=date).exists():
            continue

        # Tạo phiên bản bữa tiệc mới từ mẫu
        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,
            # ... sao chép các trường mẫu khác
        )
        created.append(party)

    return created

Tạo tự động với Celery Beat

Một tác vụ Celery hàng ngày giữ các phiên bản được cập nhật:

1
2
3
4
5
6
7
8
9
@shared_task
def generate_recurring_party_instances():
    """Tạo các phiên bản bữa tiệc cho tất cả các bữa tiệc định kỳ đang hoạt động."""
    for recurring_party in RecurringParty.objects.filter(is_active=True):
        created = recurring_party.generate_instances(lookahead_days=30)
        if created:
            logger.info(
                f"Đã tạo {len(created)} phiên bản cho {recurring_party.name}"
            )

Lịch Celery Beat chạy điều này vào lúc nửa đêm:

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),  # Hàng ngày lúc nửa đêm
    },
}

Giao diện người dùng

Tạo bữa tiệc định kỳ

Biểu mẫu sử dụng widget django-recurrence để xây dựng các mẫu RRULE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class RecurringPartyForm(forms.ModelForm):
    recurrence = RecurrenceField(
        label="Mẫu lặp lại",
        help_text="Thiết lập khi nào bữa tiệc này nên lặp lại.",
        required=True,
    )

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

Widget cung cấp giao diện trực quan cho các mẫu phổ biến:

  • Hàng ngày / Hàng tuần / Hàng tháng / Hàng năm
  • Các ngày cụ thể trong tuần
  • Mẫu thứ tự (đầu tiên, thứ hai, cuối cùng)
  • Khoảng cách tùy chỉnh (mỗi 2 tuần)
  • Điều kiện kết thúc (đến ngày, sau N lần, hoặc không bao giờ)

Quản lý phiên bản

Người tổ chức có thể xem tất cả các phiên bản đã tạo và:

  • Chỉnh sửa phiên bản riêng lẻ - Thay đổi tuyến đường hoặc thời gian cho một lần
  • Hủy phiên bản - Đánh dấu các ngày cụ thể là đã hủy
  • Tạo lại phiên bản - Kích hoạt thủ công việc tạo cho 30 ngày tiếp theo

Xử lý các trường hợp đặc biệt

Nhận thức múi giờ

Tất cả các ngày được lưu trữ trong múi giờ đã cấu hình của người tổ chức và hiển thị theo giờ địa phương của người xem:

1
2
3
# Tạo phiên bản sử dụng ngày có nhận thức múi giờ
local_tz = get_current_timezone()
today = timezone.now().astimezone(local_tz).date()

Xử lý ngày lễ và ngoại lệ

Người dùng có thể hủy các phiên bản riêng lẻ mà không ảnh hưởng đến mẫu định kỳ:

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

Các phiên bản đã hủy vẫn xuất hiện trong danh sách với chỉ báo trực quan, ngăn ngừa nhầm lẫn.

Phiên bản mồ côi

Nếu một bữa tiệc định kỳ bị xóa, các phiên bản đã tạo có thể được giữ lại tùy chọn:

 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:
        # Xóa mềm các phiên bản trong tương lai
        recurring_party.instances.filter(
            scheduled_date__gte=date.today(),
            deleted_at__isnull=True,
        ).update(deleted_at=timezone.now())

    recurring_party.delete()

Trải nghiệm người dùng

Tạo một chuyến đi hàng tuần chỉ mất vài bước:

  1. Đi đến Bảng điều khiển → Bữa tiệc định kỳ → Tạo
  2. Thiết lập chi tiết bữa tiệc của bạn (tuyến đường, thời gian, độ khó)
  3. Cấu hình mẫu lặp lại (ví dụ: “Mỗi thứ Bảy lúc 8 giờ sáng”)
  4. Đặt ngày bắt đầu và ngày kết thúc tùy chọn
  5. Lưu - các phiên bản được tạo tự động!

Hệ thống tạo các phiên bản bữa tiệc trước 30 ngày, và Celery tiếp tục tạo mới khi thời gian trôi qua.

Lợi ích cho cộng đồng đạp xe

  • Thiết lập và quên - Không còn phải tạo sự kiện hàng tuần
  • Nhất quán - Cùng tuyến đường, cùng thời gian, cùng cài đặt
  • Linh hoạt - Chỉnh sửa hoặc hủy các lần riêng lẻ
  • Khám phá - Người đi xe có thể dễ dàng tìm thấy các chuyến đi nhóm thường xuyên
  • Tích hợp lịch - Đăng ký các sự kiện định kỳ trong ứng dụng lịch của bạn

Sẵn sàng thiết lập chuyến đi thường xuyên của bạn? Tạo bữa tiệc định kỳ và để chúng tôi xử lý việc lên lịch!