العديد من مجموعات ركوب الدراجات لديها رحلات منتظمة - فريق التنقل الصباحي يوم الإثنين، رحلة المغامرة في أول سبت من الشهر، أو الجولة الاجتماعية مساء الأربعاء. حتى الآن، كان على المنظمين إنشاء كل حدث يدوياً. مع ميزة الحفلات المتكررة الجديدة، يمكنك إعداد جدول زمني مرة واحدة وترك 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)

    # Schedule configuration
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    max_occurrences = models.PositiveIntegerField(null=True, blank=True)
    recurrence = RecurrenceField()  # RRULE pattern

    # Template fields (copied to each instance)
    departure_time = models.TimeField()
    arrival_time = models.TimeField()
    origin = PointField()
    destination = PointField()
    route = models.JSONField()
    difficulty = models.CharField(choices=DIFFICULTY_CHOICES)
    # ... other party attributes

    is_active = models.BooleanField(default=True)

كل حالة Party مُنشأة ترتبط بالأصل:

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

    # Recurring party support
    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]:
    """Generate party instances for upcoming occurrences."""
    occurrences = self.recurrence.between(
        datetime.now().date(),
        datetime.now().date() + timedelta(days=lookahead_days),
    )

    created = []
    for date in occurrences:
        # Skip if instance already exists
        if self.instances.filter(scheduled_date=date).exists():
            continue

        # Create new party instance from 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,
            # ... copy other template fields
        )
        created.append(party)

    return created

الإنشاء التلقائي مع Celery Beat

مهمة Celery يومية تحافظ على الحالات محدّثة:

1
2
3
4
5
6
7
8
9
@shared_task
def generate_recurring_party_instances():
    """Generate party instances for all active recurring parties."""
    for recurring_party in RecurringParty.objects.filter(is_active=True):
        created = recurring_party.generate_instances(lookahead_days=30)
        if created:
            logger.info(
                f"Generated {len(created)} instances for {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),  # Daily at midnight
    },
}

واجهة المستخدم

إنشاء حفلة متكررة

النموذج يستخدم أداة django-recurrence لبناء أنماط RRULE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class RecurringPartyForm(forms.ModelForm):
    recurrence = RecurrenceField(
        label="Recurrence pattern",
        help_text="Set up when this party should repeat.",
        required=True,
    )

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

الأداة توفر واجهة بديهية للأنماط الشائعة:

  • يومي / أسبوعي / شهري / سنوي
  • أيام محددة من الأسبوع
  • أنماط ترتيبية (الأول، الثاني، الأخير)
  • فترات مخصصة (كل أسبوعين)
  • شروط الانتهاء (حتى تاريخ معين، بعد N مرة، أو أبداً)

إدارة الحالات

يمكن للمنظمين عرض جميع الحالات المُنشأة و:

  • تعديل حالات فردية - تغيير المسار أو الوقت لمرة واحدة
  • إلغاء حالات - تحديد تواريخ معينة كملغاة
  • إعادة إنشاء الحالات - تشغيل الإنشاء يدوياً للـ 30 يوماً القادمة

التعامل مع الحالات الخاصة

الوعي بالمناطق الزمنية

جميع التواريخ تُخزن في المنطقة الزمنية المُعدة للمنظم وتُعرض بالتوقيت المحلي للمشاهد:

1
2
3
# Generate instances using timezone-aware dates
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 future instances
        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 يستمر في إنشاء حالات جديدة مع مرور الوقت.

الفوائد لمجتمعات ركوب الدراجات

  • اضبطها وانساها - لا مزيد من إنشاء الأحداث الأسبوعية
  • الاتساق - نفس المسار، نفس الوقت، نفس الإعدادات
  • المرونة - تعديل أو إلغاء مرات فردية
  • الاكتشاف - يمكن للراكبين العثور بسهولة على رحلات المجموعات المنتظمة
  • تكامل التقويم - اشترك في الأحداث المتكررة في تطبيق التقويم الخاص بك

مستعد لإعداد رحلتك المنتظمة؟ أنشئ حفلة متكررة ودعنا نتولى الجدولة!